summaryrefslogtreecommitdiff
path: root/gr-wxgui/src/python
diff options
context:
space:
mode:
Diffstat (limited to 'gr-wxgui/src/python')
-rw-r--r--gr-wxgui/src/python/CMakeLists.txt88
-rw-r--r--gr-wxgui/src/python/__init__.py25
-rw-r--r--gr-wxgui/src/python/common.py252
-rw-r--r--gr-wxgui/src/python/const_window.py207
-rw-r--r--gr-wxgui/src/python/constants.py76
-rw-r--r--gr-wxgui/src/python/constsink_gl.py140
-rw-r--r--gr-wxgui/src/python/fft_window.py408
-rw-r--r--gr-wxgui/src/python/fftsink2.py41
-rw-r--r--gr-wxgui/src/python/fftsink_gl.py201
-rw-r--r--gr-wxgui/src/python/fftsink_nongl.py647
-rw-r--r--gr-wxgui/src/python/form.py391
-rw-r--r--gr-wxgui/src/python/forms/__init__.py103
-rw-r--r--gr-wxgui/src/python/forms/converters.py154
-rw-r--r--gr-wxgui/src/python/forms/forms.py653
-rw-r--r--gr-wxgui/src/python/gui.py135
-rw-r--r--gr-wxgui/src/python/histo_window.py164
-rw-r--r--gr-wxgui/src/python/histosink_gl.py110
-rw-r--r--gr-wxgui/src/python/number_window.py213
-rw-r--r--gr-wxgui/src/python/numbersink2.py171
-rw-r--r--gr-wxgui/src/python/plot.py1834
-rw-r--r--gr-wxgui/src/python/plotter/__init__.py24
-rw-r--r--gr-wxgui/src/python/plotter/bar_plotter.py144
-rw-r--r--gr-wxgui/src/python/plotter/channel_plotter.py236
-rw-r--r--gr-wxgui/src/python/plotter/common.py133
-rw-r--r--gr-wxgui/src/python/plotter/gltext.py503
-rw-r--r--gr-wxgui/src/python/plotter/grid_plotter_base.py419
-rw-r--r--gr-wxgui/src/python/plotter/plotter_base.py214
-rw-r--r--gr-wxgui/src/python/plotter/waterfall_plotter.py278
-rw-r--r--gr-wxgui/src/python/powermate.py448
-rw-r--r--gr-wxgui/src/python/pubsub.py153
-rw-r--r--gr-wxgui/src/python/scope_window.py686
-rw-r--r--gr-wxgui/src/python/scopesink2.py41
-rw-r--r--gr-wxgui/src/python/scopesink_gl.py233
-rw-r--r--gr-wxgui/src/python/scopesink_nongl.py651
-rw-r--r--gr-wxgui/src/python/slider.py49
-rw-r--r--gr-wxgui/src/python/stdgui2.py100
-rw-r--r--gr-wxgui/src/python/termsink.py77
-rw-r--r--gr-wxgui/src/python/waterfall_window.py324
-rw-r--r--gr-wxgui/src/python/waterfallsink2.py41
-rw-r--r--gr-wxgui/src/python/waterfallsink_gl.py173
-rw-r--r--gr-wxgui/src/python/waterfallsink_nongl.py431
41 files changed, 11371 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/CMakeLists.txt b/gr-wxgui/src/python/CMakeLists.txt
new file mode 100644
index 000000000..24e06acd5
--- /dev/null
+++ b/gr-wxgui/src/python/CMakeLists.txt
@@ -0,0 +1,88 @@
+# Copyright 2011 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+########################################################################
+include(GrPython)
+
+########################################################################
+# Install python files into wxgui module
+########################################################################
+GR_PYTHON_INSTALL(
+ FILES
+ __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
+ gui.py
+ histosink_gl.py
+ histo_window.py
+ numbersink2.py
+ number_window.py
+ plot.py
+ powermate.py
+ pubsub.py
+ scopesink2.py
+ scopesink_nongl.py
+ scopesink_gl.py
+ scope_window.py
+ termsink.py
+ waterfallsink2.py
+ waterfallsink_nongl.py
+ waterfallsink_gl.py
+ waterfall_window.py
+ slider.py
+ stdgui2.py
+ DESTINATION ${GR_PYTHON_DIR}/gnuradio/wxgui
+ COMPONENT "wxgui"
+)
+
+########################################################################
+# Install python files into wxgui forms sub-module
+########################################################################
+GR_PYTHON_INSTALL(
+ FILES
+ forms/__init__.py
+ forms/forms.py
+ forms/converters.py
+ DESTINATION ${GR_PYTHON_DIR}/gnuradio/wxgui/forms
+ COMPONENT "wxgui"
+)
+
+########################################################################
+# Install python files into wxgui plotter sub-module
+########################################################################
+GR_PYTHON_INSTALL(
+ FILES
+ plotter/__init__.py
+ plotter/bar_plotter.py
+ plotter/channel_plotter.py
+ plotter/common.py
+ plotter/gltext.py
+ plotter/grid_plotter_base.py
+ plotter/plotter_base.py
+ plotter/waterfall_plotter.py
+ DESTINATION ${GR_PYTHON_DIR}/gnuradio/wxgui/plotter
+ COMPONENT "wxgui"
+)
diff --git a/gr-wxgui/src/python/__init__.py b/gr-wxgui/src/python/__init__.py
new file mode 100644
index 000000000..68f8f4b5e
--- /dev/null
+++ b/gr-wxgui/src/python/__init__.py
@@ -0,0 +1,25 @@
+#
+# Copyright 2011 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+'''
+This is the gr-wxgui package. This package provides a GUI interface
+using the Wx backend.
+'''
diff --git a/gr-wxgui/src/python/common.py b/gr-wxgui/src/python/common.py
new file mode 100644
index 000000000..1410d29df
--- /dev/null
+++ b/gr-wxgui/src/python/common.py
@@ -0,0 +1,252 @@
+#
+# Copyright 2008, 2009 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.
+#
+
+##################################################
+# conditional disconnections of wx flow graph
+##################################################
+import wx
+from gnuradio import gr
+
+RUN_ALWAYS = gr.prefs().get_bool ('wxgui', 'run_always', False)
+
+class wxgui_hb(object):
+ """
+ The wxgui hier block helper/wrapper class:
+ A hier block should inherit from this class to make use of the wxgui connect method.
+ To use, call wxgui_connect in place of regular connect; self.win must be defined.
+ The implementation will conditionally enable the copy block after the source (self).
+ This condition depends on weather or not the window is visible with the parent notebooks.
+ This condition will be re-checked on every ui update event.
+ """
+
+ def wxgui_connect(self, *points):
+ """
+ Use wxgui connect when the first point is the self source of the hb.
+ The win property of this object should be set to the wx window.
+ When this method tries to connect self to the next point,
+ it will conditionally make this connection based on the visibility state.
+ All other points will be connected normally.
+ """
+ try:
+ assert points[0] == self or points[0][0] == self
+ copy = gr.copy(self._hb.input_signature().sizeof_stream_item(0))
+ handler = self._handler_factory(copy.set_enabled)
+ if RUN_ALWAYS == False:
+ handler(False) #initially disable the copy block
+ else:
+ handler(True) #initially enable the copy block
+ self._bind_to_visible_event(win=self.win, handler=handler)
+ points = list(points)
+ points.insert(1, copy) #insert the copy block into the chain
+ except (AssertionError, IndexError): pass
+ self.connect(*points) #actually connect the blocks
+
+ @staticmethod
+ def _handler_factory(handler):
+ """
+ Create a function that will cache the visibility flag,
+ and only call the handler when that flag changes.
+ @param handler the function to call on a change
+ @return a function of 1 argument
+ """
+ cache = [None]
+ def callback(visible):
+ if cache[0] == visible: return
+ cache[0] = visible
+ #print visible, handler
+ if RUN_ALWAYS == False:
+ handler(visible)
+ else:
+ handler(True)
+ return callback
+
+ @staticmethod
+ def _bind_to_visible_event(win, handler):
+ """
+ Bind a handler to a window when its visibility changes.
+ Specifically, call the handler when the window visibility changes.
+ This condition is checked on every update ui event.
+ @param win the wx window
+ @param handler a function of 1 param
+ """
+ #is the window visible in the hierarchy
+ def is_wx_window_visible(my_win):
+ while True:
+ parent = my_win.GetParent()
+ if not parent: return True #reached the top of the hierarchy
+ #if we are hidden, then finish, otherwise keep traversing up
+ if isinstance(parent, wx.Notebook) and parent.GetCurrentPage() != my_win: return False
+ my_win = parent
+ #call the handler, the arg is shown or not
+ def handler_factory(my_win, my_handler):
+ def callback(evt):
+ my_handler(is_wx_window_visible(my_win))
+ evt.Skip() #skip so all bound handlers are called
+ return callback
+ handler = handler_factory(win, handler)
+ #bind the handler to all the parent notebooks
+ win.Bind(wx.EVT_UPDATE_UI, handler)
+
+##################################################
+# Helpful Functions
+##################################################
+
+#A macro to apply an index to a key
+index_key = lambda key, i: "%s_%d"%(key, i+1)
+
+def _register_access_method(destination, controller, key):
+ """
+ Helper function for register access methods.
+ This helper creates distinct set and get methods for each key
+ and adds them to the destination object.
+ """
+ def set(value): controller[key] = value
+ setattr(destination, 'set_'+key, set)
+ def get(): return controller[key]
+ setattr(destination, 'get_'+key, get)
+
+def register_access_methods(destination, controller):
+ """
+ Register setter and getter functions in the destination object for all keys in the controller.
+ @param destination the object to get new setter and getter methods
+ @param controller the pubsub controller
+ """
+ for key in controller.keys(): _register_access_method(destination, controller, key)
+
+##################################################
+# Input Watcher Thread
+##################################################
+from gnuradio import gru
+
+class input_watcher(gru.msgq_runner):
+ """
+ Input watcher thread runs forever.
+ Read messages from the message queue.
+ Forward messages to the message handler.
+ """
+ def __init__ (self, msgq, controller, msg_key, arg1_key='', arg2_key=''):
+ self._controller = controller
+ self._msg_key = msg_key
+ self._arg1_key = arg1_key
+ self._arg2_key = arg2_key
+ gru.msgq_runner.__init__(self, msgq, self.handle_msg)
+
+ def handle_msg(self, msg):
+ if self._arg1_key: self._controller[self._arg1_key] = msg.arg1()
+ if self._arg2_key: self._controller[self._arg2_key] = msg.arg2()
+ self._controller[self._msg_key] = msg.to_string()
+
+
+##################################################
+# Shared Functions
+##################################################
+import numpy
+import math
+
+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
+ sign = num > 0 and 1 or -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
+ """
+ factor = 2.0
+ mean = numpy.average(samples)
+ std = numpy.std(samples)
+ fft = numpy.abs(numpy.fft.fft(samples - mean))
+ envelope = 2*numpy.max(fft)/len(samples)
+ ampl = max(std, envelope) or 0.1
+ return mean - factor*ampl, mean + factor*ampl
+
+def get_min_max_fft(fft_samps):
+ """
+ Get the minimum and maximum bounds for an array of fft samples.
+ @param samples the array of real values
+ @return a tuple of min, max
+ """
+ #get the peak level (max of the samples)
+ peak_level = numpy.max(fft_samps)
+ #separate noise samples
+ noise_samps = numpy.sort(fft_samps)[:len(fft_samps)/2]
+ #get the noise floor
+ noise_floor = numpy.average(noise_samps)
+ #get the noise deviation
+ noise_dev = numpy.std(noise_samps)
+ #determine the maximum and minimum levels
+ max_level = peak_level
+ min_level = noise_floor - abs(2*noise_dev)
+ return min_level, max_level
diff --git a/gr-wxgui/src/python/const_window.py b/gr-wxgui/src/python/const_window.py
new file mode 100644
index 000000000..a7ff2e5f3
--- /dev/null
+++ b/gr-wxgui/src/python/const_window.py
@@ -0,0 +1,207 @@
+#
+# 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 *
+from gnuradio import gr #for gr.prefs
+import forms
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 200
+LOOP_BW_MIN_EXP, LOOP_BW_MAX_EXP = -6, 0.0
+GAIN_MU_MIN_EXP, GAIN_MU_MAX_EXP = -6, -0.301
+DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'const_rate', 5)
+DEFAULT_WIN_SIZE = (500, 400)
+DEFAULT_CONST_SIZE = gr.prefs().get_long('wxgui', '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 = 2.0
+
+##################################################
+# 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, style=wx.SUNKEN_BORDER)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = forms.static_box_sizer(
+ parent=self, label='Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ #loop_bw
+ control_box.AddStretchSpacer()
+ forms.text_box(
+ sizer=control_box, parent=self, label='Loop Bandwidth',
+ converter=forms.float_converter(),
+ ps=parent, key=LOOP_BW_KEY,
+ )
+ forms.log_slider(
+ sizer=control_box, parent=self,
+ min_exp=LOOP_BW_MIN_EXP,
+ max_exp=LOOP_BW_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=LOOP_BW_KEY,
+ )
+ #gain_mu
+ control_box.AddStretchSpacer()
+ forms.text_box(
+ sizer=control_box, parent=self, label='Gain Mu',
+ converter=forms.float_converter(),
+ ps=parent, key=GAIN_MU_KEY,
+ )
+ forms.log_slider(
+ sizer=control_box, parent=self,
+ min_exp=GAIN_MU_MIN_EXP,
+ max_exp=GAIN_MU_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=GAIN_MU_KEY,
+ )
+ #marker
+ control_box.AddStretchSpacer()
+ forms.drop_down(
+ sizer=control_box, parent=self,
+ ps=parent, key=MARKER_KEY, label='Marker',
+ choices=map(lambda x: x[1], MARKER_TYPES),
+ labels=map(lambda x: x[0], MARKER_TYPES),
+ )
+ #run/stop
+ control_box.AddStretchSpacer()
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+##################################################
+# Constellation window with plotter and control panel
+##################################################
+class const_window(wx.Panel, pubsub.pubsub):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ msg_key,
+ loop_bw_key,
+ gain_mu_key,
+ gain_omega_key,
+ omega_key,
+ sample_rate_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #proxy the keys
+ self.proxy(MSG_KEY, controller, msg_key)
+ self.proxy(LOOP_BW_KEY, controller, loop_bw_key)
+ self.proxy(GAIN_MU_KEY, controller, gain_mu_key)
+ self.proxy(GAIN_OMEGA_KEY, controller, gain_omega_key)
+ self.proxy(OMEGA_KEY, controller, omega_key)
+ self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
+ #initialize values
+ self[RUNNING_KEY] = True
+ self[X_DIVS_KEY] = 8
+ self[Y_DIVS_KEY] = 8
+ self[MARKER_KEY] = DEFAULT_MARKER_TYPE
+ #init panel and plot
+ wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.SetSizeHints(*size)
+ self.plotter.set_title(title)
+ self.plotter.set_x_label('Inphase')
+ self.plotter.set_y_label('Quadrature')
+ self.plotter.enable_point_label(True)
+ self.plotter.enable_grid_lines(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_gain_omega(gain_mu): self[GAIN_OMEGA_KEY] = .25*gain_mu**2
+ self.subscribe(GAIN_MU_KEY, set_gain_omega)
+ #register events
+ self.subscribe(MSG_KEY, self.handle_msg)
+ self.subscribe(X_DIVS_KEY, self.update_grid)
+ self.subscribe(Y_DIVS_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):
+ #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/self[X_DIVS_KEY]))
+ #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/self[Y_DIVS_KEY]))
+ #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..08cc6a634
--- /dev/null
+++ b/gr-wxgui/src/python/constants.py
@@ -0,0 +1,76 @@
+#
+# Copyright 2008,2010 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'
+USE_PERSISTENCE_KEY = 'use_persistence'
+PERSIST_ALPHA_KEY = 'persist_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'
+XY_MARKER_KEY = 'xy_marker'
+MSG_KEY = 'msg'
+NUM_LINES_KEY = 'num_lines'
+OMEGA_KEY = 'omega'
+PEAK_HOLD_KEY = 'peak_hold'
+TRACE_STORE_KEY = 'trace_store'
+TRACE_SHOW_KEY = 'trace_show'
+REF_LEVEL_KEY = 'ref_level'
+RUNNING_KEY = 'running'
+SAMPLE_RATE_KEY = 'sample_rate'
+TRIGGER_CHANNEL_KEY = 'trigger_channel'
+TRIGGER_LEVEL_KEY = 'trigger_level'
+TRIGGER_MODE_KEY = 'trigger_mode'
+TRIGGER_SLOPE_KEY = 'trigger_slope'
+TRIGGER_SHOW_KEY = 'trigger_show'
+XY_MODE_KEY = 'xy_mode'
+X_CHANNEL_KEY = 'x_channel'
+Y_CHANNEL_KEY = 'y_channel'
+T_FRAC_OFF_KEY = 't_frac_off'
+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'
+Y_AXIS_LABEL = 'y_axis_label'
+MAXIMUM_KEY = 'maximum'
+MINIMUM_KEY = 'minimum'
+NUM_BINS_KEY = 'num_bins'
+FRAME_SIZE_KEY = 'frame_size'
+CHANNEL_OPTIONS_KEY = 'channel_options'
+SHOW_CONTROL_PANEL_KEY = 'show_control_panel'
+LOOP_BW_KEY = 'loop_bw'
diff --git a/gr-wxgui/src/python/constsink_gl.py b/gr-wxgui/src/python/constsink_gl.py
new file mode 100644
index 000000000..51434df68
--- /dev/null
+++ b/gr-wxgui/src/python/constsink_gl.py
@@ -0,0 +1,140 @@
+#
+# 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 *
+import sys
+try:
+ from gnuradio import digital
+except ImportError:
+ sys.stderr.write("Error: could not import gnuradio.digital, please install gr-digitial.\n")
+ sys.exit(1)
+
+##################################################
+# Constellation sink block (wrapper for old wxgui)
+##################################################
+class const_sink_c(gr.hier_block2, common.wxgui_hb):
+ """
+ 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,
+ loop_bw=6.28/100.0,
+ 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,
+ )
+ 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 = digital.costas_loop_cc(loop_bw, M)
+ # Timing recovery loop
+ # Critically damped 2nd order DLL
+ self._retime = digital.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)
+ #controller
+ def setter(p, k, x): p[k] = x
+ self.controller = pubsub()
+ self.controller.subscribe(LOOP_BW_KEY, self._costas.set_loop_bandwidth)
+ self.controller.publish(LOOP_BW_KEY, self._costas.get_loop_bandwidth)
+ 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, self.controller, MSG_KEY)
+ #create window
+ self.win = const_window.const_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ msg_key=MSG_KEY,
+ loop_bw_key=LOOP_BW_KEY,
+ gain_mu_key=GAIN_MU_KEY,
+ gain_omega_key=GAIN_OMEGA_KEY,
+ omega_key=OMEGA_KEY,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ )
+ common.register_access_methods(self, self.win)
+ #connect
+ self.wxgui_connect(self, self._costas, self._retime, agc, sd, sink)
+
+
diff --git a/gr-wxgui/src/python/fft_window.py b/gr-wxgui/src/python/fft_window.py
new file mode 100644
index 000000000..cf21b893a
--- /dev/null
+++ b/gr-wxgui/src/python/fft_window.py
@@ -0,0 +1,408 @@
+#
+# Copyright 2008, 2009, 2010 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 *
+from gnuradio import gr #for gr.prefs
+import forms
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+PERSIST_ALPHA_MIN_EXP, PERSIST_ALPHA_MAX_EXP = -2, 0
+DEFAULT_WIN_SIZE = (600, 300)
+DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
+DB_DIV_MIN, DB_DIV_MAX = 1, 20
+FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
+PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
+EMPTY_TRACE = list()
+TRACES = ('A', 'B')
+TRACES_COLOR_SPEC = {
+ 'A': (1.0, 0.0, 0.0),
+ 'B': (0.8, 0.0, 0.8),
+}
+
+##################################################
+# 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, style=wx.SUNKEN_BORDER)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ control_box.AddStretchSpacer()
+ #checkboxes for average and peak hold
+ options_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Trace Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ forms.check_box(
+ sizer=options_box, parent=self, label='Peak Hold',
+ ps=parent, key=PEAK_HOLD_KEY,
+ )
+ forms.check_box(
+ sizer=options_box, parent=self, label='Average',
+ ps=parent, key=AVERAGE_KEY,
+ )
+ #static text and slider for averaging
+ avg_alpha_text = forms.static_text(
+ sizer=options_box, parent=self, label='Avg Alpha',
+ converter=forms.float_converter(lambda x: '%.4f'%x),
+ ps=parent, key=AVG_ALPHA_KEY, width=50,
+ )
+ avg_alpha_slider = forms.log_slider(
+ sizer=options_box, parent=self,
+ min_exp=AVG_ALPHA_MIN_EXP,
+ max_exp=AVG_ALPHA_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=AVG_ALPHA_KEY,
+ )
+ for widget in (avg_alpha_text, avg_alpha_slider):
+ parent.subscribe(AVERAGE_KEY, widget.Enable)
+ widget.Enable(parent[AVERAGE_KEY])
+ parent.subscribe(AVERAGE_KEY, widget.ShowItems)
+ #allways show initially, so room is reserved for them
+ widget.ShowItems(True) # (parent[AVERAGE_KEY])
+
+ parent.subscribe(AVERAGE_KEY, self._update_layout)
+
+ forms.check_box(
+ sizer=options_box, parent=self, label='Persistence',
+ ps=parent, key=USE_PERSISTENCE_KEY,
+ )
+ #static text and slider for persist alpha
+ persist_alpha_text = forms.static_text(
+ sizer=options_box, parent=self, label='Persist Alpha',
+ converter=forms.float_converter(lambda x: '%.4f'%x),
+ ps=parent, key=PERSIST_ALPHA_KEY, width=50,
+ )
+ persist_alpha_slider = forms.log_slider(
+ sizer=options_box, parent=self,
+ min_exp=PERSIST_ALPHA_MIN_EXP,
+ max_exp=PERSIST_ALPHA_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=PERSIST_ALPHA_KEY,
+ )
+ for widget in (persist_alpha_text, persist_alpha_slider):
+ parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable)
+ widget.Enable(parent[USE_PERSISTENCE_KEY])
+ parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems)
+ #allways show initially, so room is reserved for them
+ widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY])
+
+ parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout)
+
+ #trace menu
+ for trace in TRACES:
+ trace_box = wx.BoxSizer(wx.HORIZONTAL)
+ options_box.Add(trace_box, 0, wx.EXPAND)
+ forms.check_box(
+ sizer=trace_box, parent=self,
+ ps=parent, key=TRACE_SHOW_KEY+trace,
+ label='Trace %s'%trace,
+ )
+ trace_box.AddSpacer(10)
+ forms.single_button(
+ sizer=trace_box, parent=self,
+ ps=parent, key=TRACE_STORE_KEY+trace,
+ label='Store', style=wx.BU_EXACTFIT,
+ )
+ trace_box.AddSpacer(10)
+ #radio buttons for div size
+ control_box.AddStretchSpacer()
+ y_ctrl_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Axis Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ forms.incr_decr_buttons(
+ parent=self, sizer=y_ctrl_box, label='dB/Div',
+ on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
+ )
+ #ref lvl buttons
+ forms.incr_decr_buttons(
+ parent=self, sizer=y_ctrl_box, label='Ref Level',
+ on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
+ )
+ y_ctrl_box.AddSpacer(2)
+ #autoscale
+ forms.single_button(
+ sizer=y_ctrl_box, parent=self, label='Autoscale',
+ callback=self.parent.autoscale,
+ )
+ #run/stop
+ control_box.AddStretchSpacer()
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+ #mouse wheel event
+ def on_mouse_wheel(event):
+ if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
+ else: self._on_decr_ref_level(event)
+ parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ def _on_incr_ref_level(self, event):
+ self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
+ def _on_decr_ref_level(self, event):
+ self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
+ def _on_incr_db_div(self, event):
+ self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
+ def _on_decr_db_div(self, event):
+ self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
+ ##################################################
+ # subscriber handlers
+ ##################################################
+ def _update_layout(self,key):
+ # Just ignore the key value we get
+ # we only need to now that the visability or size of something has changed
+ self.parent.Layout()
+ #self.parent.Fit()
+
+##################################################
+# FFT window with plotter and control panel
+##################################################
+class fft_window(wx.Panel, pubsub.pubsub):
+ 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,
+ use_persistence,
+ persist_alpha,
+ ):
+
+ pubsub.pubsub.__init__(self)
+ #setup
+ self.samples = EMPTY_TRACE
+ self.real = real
+ self.fft_size = fft_size
+ self._reset_peak_vals()
+ self._traces = dict()
+ #proxy the keys
+ self.proxy(MSG_KEY, controller, msg_key)
+ self.proxy(AVERAGE_KEY, controller, average_key)
+ self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
+ self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
+ #initialize values
+ self[PEAK_HOLD_KEY] = peak_hold
+ self[Y_PER_DIV_KEY] = y_per_div
+ self[Y_DIVS_KEY] = y_divs
+ self[X_DIVS_KEY] = 8 #approximate
+ self[REF_LEVEL_KEY] = ref_level
+ self[BASEBAND_FREQ_KEY] = baseband_freq
+ self[RUNNING_KEY] = True
+ self[USE_PERSISTENCE_KEY] = use_persistence
+ self[PERSIST_ALPHA_KEY] = persist_alpha
+ for trace in TRACES:
+ #a function that returns a function
+ #so the function wont use local trace
+ def new_store_trace(my_trace):
+ def store_trace(*args):
+ self._traces[my_trace] = self.samples
+ self.update_grid()
+ return store_trace
+ def new_toggle_trace(my_trace):
+ def toggle_trace(toggle):
+ #do an automatic store if toggled on and empty trace
+ if toggle and not len(self._traces[my_trace]):
+ self._traces[my_trace] = self.samples
+ self.update_grid()
+ return toggle_trace
+ self._traces[trace] = EMPTY_TRACE
+ self[TRACE_STORE_KEY+trace] = False
+ self[TRACE_SHOW_KEY+trace] = False
+ self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
+ self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
+ #init panel and plot
+ wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.SetSizeHints(*size)
+ self.plotter.set_title(title)
+ self.plotter.enable_legend(True)
+ self.plotter.enable_point_label(True)
+ self.plotter.enable_grid_lines(True)
+ self.plotter.set_use_persistence(use_persistence)
+ self.plotter.set_persist_alpha(persist_alpha)
+ #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)
+ #register events
+ self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
+ self.subscribe(MSG_KEY, self.handle_msg)
+ self.subscribe(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)
+ self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
+ self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
+ #initial update
+ self.update_grid()
+
+ def set_callback(self,callb):
+ self.plotter.set_callback(callb)
+
+ def autoscale(self, *args):
+ """
+ Autoscale the fft plot to the last frame.
+ Set the dynamic range and reference level.
+ """
+ if not len(self.samples): return
+ min_level, max_level = common.get_min_max_fft(self.samples)
+ #set the range to a clean number of the dynamic range
+ self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
+ #set the reference level to a multiple of y per div
+ self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
+
+ def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
+
+ 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+1)/2]
+ else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/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)
+ #plot the peak hold
+ self.plotter.set_waveform(
+ channel='Peak',
+ samples=self.peak_vals,
+ color_spec=PEAK_VALS_COLOR_SPEC,
+ )
+ else:
+ self._reset_peak_vals()
+ self.plotter.clear_waveform(channel='Peak')
+ #plot the fft
+ self.plotter.set_waveform(
+ channel='FFT',
+ samples=samples,
+ color_spec=FFT_PLOT_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.
+ """
+ for trace in TRACES:
+ channel = '%s'%trace.upper()
+ if self[TRACE_SHOW_KEY+trace]:
+ self.plotter.set_waveform(
+ channel=channel,
+ samples=self._traces[trace],
+ color_spec=TRACES_COLOR_SPEC[trace],
+ )
+ else: self.plotter.clear_waveform(channel=channel)
+ #grid parameters
+ sample_rate = 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)
+ #update the x grid
+ if self.real:
+ self.plotter.set_x_grid(
+ baseband_freq,
+ baseband_freq + sample_rate/2.0,
+ x_per_div, True,
+ )
+ else:
+ self.plotter.set_x_grid(
+ baseband_freq - sample_rate/2.0,
+ baseband_freq + sample_rate/2.0,
+ x_per_div, True,
+ )
+ #update x units
+ self.plotter.set_x_label('Frequency', '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
new file mode 100644
index 000000000..3277cd3ff
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink2.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2008,2009 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
+
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
+
+if style == 'auto' or style == 'gl':
+ try:
+ import wx.glcanvas
+ from OpenGL.GL import *
+ from fftsink_gl import fft_sink_f, fft_sink_c
+ except ImportError:
+ if style == 'gl':
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
+ else:
+ # Fall backto non-gl sinks
+ from fftsink_nongl import fft_sink_f, fft_sink_c
+elif style == 'nongl':
+ from fftsink_nongl import fft_sink_f, fft_sink_c
+else:
+ raise RuntimeError("Unknown wxgui style")
diff --git a/gr-wxgui/src/python/fftsink_gl.py b/gr-wxgui/src/python/fftsink_gl.py
new file mode 100644
index 000000000..dc31e84a1
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink_gl.py
@@ -0,0 +1,201 @@
+#
+# Copyright 2008,2009,2010 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 __future__ import division
+
+##################################################
+# Imports
+##################################################
+import fft_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+import math
+
+##################################################
+# FFT sink block (wrapper for old wxgui)
+##################################################
+class _fft_sink_base(gr.hier_block2, common.wxgui_hb):
+ """
+ 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=fft_window.DEFAULT_FRAME_RATE,
+ average=False,
+ avg_alpha=None,
+ title='',
+ size=fft_window.DEFAULT_WIN_SIZE,
+ peak_hold=False,
+ win=None,
+ use_persistence=False,
+ persist_alpha=None,
+ **kwargs #do not end with a comma
+ ):
+ #ensure avg alpha
+ if avg_alpha is None: avg_alpha = 2.0/fft_rate
+ #ensure analog alpha
+ if persist_alpha is None:
+ actual_fft_rate=float(sample_rate/fft_size)/float(max(1,int(float((sample_rate/fft_size)/fft_rate))))
+ #print "requested_fft_rate ",fft_rate
+ #print "actual_fft_rate ",actual_fft_rate
+ analog_cutoff_freq=0.5 # Hertz
+ #calculate alpha from wanted cutoff freq
+ persist_alpha = 1.0 - math.exp(-2.0*math.pi*analog_cutoff_freq/actual_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
+ 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,
+ win=win,
+ )
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(gr.sizeof_float*fft_size, msgq, True)
+
+
+ #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
+ common.input_watcher(msgq, self.controller, MSG_KEY)
+ #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,
+ use_persistence=use_persistence,
+ persist_alpha=persist_alpha,
+ )
+ common.register_access_methods(self, self.win)
+ setattr(self.win, 'set_baseband_freq', getattr(self, 'set_baseband_freq')) #BACKWARDS
+ setattr(self.win, 'set_peak_hold', getattr(self, 'set_peak_hold')) #BACKWARDS
+ #connect
+ self.wxgui_connect(self, fft, sink)
+
+ def set_callback(self,callb):
+ self.win.set_callback(callb)
+
+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 = 2048.0e3
+
+ #Generate some noise
+ noise =gr.noise_source_c(gr.GR_UNIFORM, 1.0/10)
+
+ # 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, 57.50e3, 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)
+
+ combine1=gr.add_cc()
+ self.connect(src1, (combine1,0))
+ self.connect(noise,(combine1,1))
+ self.connect(combine1,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, 57.50e3, 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)
+
+ combine2=gr.add_ff()
+ c2f2=gr.complex_to_float()
+
+ self.connect(src2, (combine2,0))
+ self.connect(noise,c2f2,(combine2,1))
+ self.connect(combine2, 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..473cc424c
--- /dev/null
+++ b/gr-wxgui/src/python/fftsink_nongl.py
@@ -0,0 +1,647 @@
+#!/usr/bin/env python
+#
+# Copyright 2003-2007,2009,2010 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 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,use_persistence=False,persist_alpha=0.2):
+
+ # 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.use_persistence = use_persistence
+ self.persist_alpha = persist_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_use_persistence(self, enable):
+ self.use_persistence = enable
+ self.win.set_use_persistence(enable)
+
+ def set_persist_alpha(self, persist_alpha):
+ self.persist_alpha = persist_alpha
+ self.win.set_persist_alpha(persist_alpha)
+
+ 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)))
+
+ def set_callback(self, callb):
+ return
+
+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, use_persistence=False,persist_alpha=0.2, **kwargs):
+
+ 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,use_persistence=use_persistence,persist_alpha=persist_alpha)
+
+ 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,
+ -20*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)
+ self.set_use_persistence(self.use_persistence)
+ self.set_persist_alpha(self.persist_alpha)
+
+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, use_persistence=False,persist_alpha=0.2, **kwargs):
+
+ 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, use_persistence=use_persistence,persist_alpha=persist_alpha)
+
+ 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,
+ -20*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_use_persistence(self.use_persistence)
+ self.set_persist_alpha(self.persist_alpha)
+ 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 (gru.msgq_runner):
+ def __init__ (self, msgq, fft_size, event_receiver, **kwds):
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ gru.msgq_runner.__init__(self, msgq, self.handle_msg)
+
+ def handle_msg(self, msg):
+ 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.use_persistence_check_box = wx.CheckBox(parent=self, style=wx.CHK_2STATE, label="Persistence")
+ self.use_persistence_check_box.Bind(wx.EVT_CHECKBOX, parent.on_use_persistence)
+ control_box.Add(self.use_persistence_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.use_persistence_check_box.SetValue(self.parent.fftsink.use_persistence)
+ 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.use_persistence=False
+ self.persist_alpha=0.2
+
+
+ self.plot.SetEnableGrid (True)
+ # self.SetEnableZoom (True)
+ # self.SetBackgroundColour ('black')
+
+ self.build_popup_menu()
+ self.set_baseband_freq(self.fftsink.baseband_freq)
+
+ 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_use_persistence(self, enable):
+ self.use_persistence = enable
+ self.plot.set_use_persistence( enable)
+
+ def set_persist_alpha(self, persist_alpha):
+ self.persist_alpha = persist_alpha
+ self.plot.set_persist_alpha(persist_alpha)
+
+ 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_use_persistence(self, evt):
+ # print "on_analog"
+ self.fftsink.set_use_persistence(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_use_persistence = 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_use_persistence, id=self.id_use_persistence)
+ 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_use_persistence, "Persistence")
+ 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_use_persistence : lambda : self.fftsink.use_persistence,
+ 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 = 100*20.48e3
+
+ # Generate a complex sinusoid
+ #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 100*2e3, 1)
+ src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 100*5.75e3, 1)
+ noise1 = analog.noise_source_c(analog.GR_UNIFORM, 1.0/10)
+ add1 = blocks.add_cc()
+
+ # 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, (add1,0))
+ self.connect(noise1, (add1,1))
+ self.connect(add1, thr1, sink1)
+
+ #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 100*2e3, 1)
+ src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 100*5.75e3, 1)
+ noise2 = analog.noise_source_f(analog.GR_UNIFORM, 1.0/10)
+ add2 = blocks.add_ff()
+
+ 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, (add2,0))
+ self.connect(noise2, (add2,1))
+ self.connect(add2, 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
new file mode 100644
index 000000000..0442e49c8
--- /dev/null
+++ b/gr-wxgui/src/python/form.py
@@ -0,0 +1,391 @@
+#!/usr/bin/env python
+#
+# Copyright 2005 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 gnuradio import eng_notation
+
+# ----------------------------------------------------------------
+# Wrappers for certain widgets
+# ----------------------------------------------------------------
+
+def button_with_callback(parent, label, callback):
+ new_id = wx.NewId()
+ btn = wx.Button(parent, new_id, label)
+ wx.EVT_BUTTON(parent, new_id, lambda evt: callback())
+ return btn
+
+
+# ----------------------------------------------------------------
+# Format converters
+# ----------------------------------------------------------------
+
+class abstract_converter(object):
+ def value_to_prim(self, v):
+ """
+ Convert from user specified value to value acceptable to underlying primitive.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def prim_to_value(self, s):
+ """
+ Convert from underlying primitive value to user specified value.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def help(self):
+ return "Any string is acceptable"
+
+class identity_converter(abstract_converter):
+ def value_to_prim(self,v):
+ return v
+ def prim_to_value(self, s):
+ return s
+
+class int_converter(abstract_converter):
+ def value_to_prim(self, v):
+ return str(v)
+ def prim_to_value(self, s):
+ return int(s, 0)
+ def help(self):
+ return "Enter an integer. Leading 0x indicates hex"
+
+class float_converter(abstract_converter):
+ def value_to_prim(self, v):
+ return eng_notation.num_to_str(v)
+ def prim_to_value(self, s):
+ return eng_notation.str_to_num(s)
+ def help(self):
+ return "Enter a float with optional scale suffix. E.g., 100.1M"
+
+
+# ----------------------------------------------------------------
+# Various types of data entry fields
+# ----------------------------------------------------------------
+
+class field(object):
+ """
+ A field in a form.
+ """
+ def __init__(self, converter, value):
+ self.converter = converter
+ if value is not None:
+ self.set_value(value)
+
+ def set_value(self, v):
+ self._set_prim_value(self.converter.value_to_prim(v))
+
+ def get_value(self):
+ return self.converter.prim_to_value(self._get_prim_value())
+
+ def get_value_with_check(self):
+ """
+ Returns (value, error_msg), where error_msg is not None if there was problem
+ """
+ try:
+ return (self.get_value(), None)
+ except:
+ return (None, self._error_msg())
+
+ def _set_prim_value(self, v):
+ raise NotImplementedError
+
+ def _get_prim_value(self):
+ raise NotImplementedError
+
+ def _pair_with_label(self, widget, parent=None, sizer=None, label=None, weight=1):
+ self.label = label
+ if label is None:
+ sizer.Add (widget, weight, wx.EXPAND)
+ return widget
+ elif 0:
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ label_widget = wx.StaticText(parent, -1, label + ': ')
+ hbox.Add(label_widget, 0, wx.EXPAND)
+ hbox.Add(widget, 1, wx.EXPAND)
+ sizer.Add(hbox, weight, wx.EXPAND)
+ return widget
+ else:
+ label_widget = wx.StaticText(parent, -1, label + ': ')
+ sizer.Add(label_widget, 0, wx.EXPAND)
+ sizer.Add(widget, weight, wx.EXPAND)
+ return widget
+
+ def _error_msg(self):
+ prefix = ''
+ if self.label:
+ prefix = self.label + ': '
+ return "%s%s is invalid. %s" % (prefix, self._get_prim_value(),
+ self.converter.help())
+
+# static (display-only) text fields
+
+class static_text_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), weight=0):
+ self.f = self._pair_with_label(wx.StaticText(parent, -1, ""),
+ parent=parent, sizer=sizer, label=label, weight=weight)
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetLabel()
+
+ def _set_prim_value(self, v):
+ self.f.SetLabel(v)
+
+
+class static_int_field(static_text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None, weight=0):
+ static_text_field.__init__(self, parent, sizer, label, value, int_converter(), weight)
+
+class static_float_field(static_text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None, weight=0):
+ static_text_field.__init__(self, parent, sizer, label, value, float_converter(), weight)
+
+
+# editable text fields
+
+class text_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, weight=1):
+ style = 0
+ if callback:
+ style = wx.TE_PROCESS_ENTER
+
+ new_id = wx.NewId()
+ w = wx.TextCtrl(parent, new_id, "", style=style)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_TEXT_ENTER(w, new_id, lambda evt: callback())
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(v)
+
+
+class int_field(text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ callback=None, weight=1):
+ text_field.__init__(self, parent, sizer, label, value, int_converter(), callback, weight)
+
+class float_field(text_field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ callback=None, weight=1):
+ text_field.__init__(self, parent, sizer, label, value, float_converter(), callback, weight)
+
+# other fields
+
+class slider_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, min=0, max=100, weight=1):
+ new_id = wx.NewId()
+ w = wx.Slider(parent, new_id, (max+min)/2, min, max,
+ size=wx.Size(250, -1), style=wx.SL_HORIZONTAL | wx.SL_LABELS)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_COMMAND_SCROLL(w, new_id, lambda evt: callback(evt.GetInt()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(int(v))
+
+class quantized_slider_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, range=None, weight=1):
+ if not isinstance(range, (tuple, list)) or len(range) != 3:
+ raise ValueError, range
+
+ self.min = range[0]
+ self.max = range[1]
+ self.step_size = float(range[2])
+ nsteps = int((self.max-self.min)/self.step_size)
+
+ new_id = wx.NewId()
+ w = wx.Slider(parent, new_id, 0, 0, nsteps,
+ size=wx.Size(250, -1), style=wx.SL_HORIZONTAL)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=label, weight=weight)
+ if callback:
+ wx.EVT_COMMAND_SCROLL(w, new_id,
+ lambda evt: callback(self._map_out(evt.GetInt())))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self._map_out(self.f.GetValue())
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(self._map_in(v))
+
+ def _map_in(self, x):
+ return int((x-self.min) / self.step_size)
+
+ def _map_out(self, x):
+ return x * self.step_size + self.min
+
+class checkbox_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, weight=1):
+ new_id = wx.NewId()
+ w = wx.CheckBox(parent, new_id, label, style=wx.CHK_2STATE)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=None, weight=weight)
+ if callback:
+ wx.EVT_CHECKBOX(w, new_id, lambda evt: callback(evt.GetInt()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetValue()
+
+ def _set_prim_value(self, v):
+ self.f.SetValue(int(v))
+
+
+class radiobox_field(field):
+ def __init__(self, parent=None, sizer=None, label=None, value=None,
+ converter=identity_converter(), callback=None, weight=1,
+ choices=None, major_dimension=1, specify_rows=False):
+ new_id = wx.NewId()
+
+ if specify_rows:
+ style=wx.RA_SPECIFY_ROWS | wx.RA_HORIZONTAL
+ else:
+ style=wx.RA_SPECIFY_COLS | wx.RA_HORIZONTAL
+
+ w = wx.RadioBox(parent, new_id, label=label, style=style, majorDimension=major_dimension,
+ choices=choices)
+ self.f = self._pair_with_label(w, parent=parent, sizer=sizer, label=None, weight=weight)
+ if callback:
+ wx.EVT_RADIOBOX(w, new_id, lambda evt: callback(evt.GetString()))
+ field.__init__(self, converter, value)
+
+ def _get_prim_value(self):
+ return self.f.GetStringSelection()
+
+ def _set_prim_value(self, v):
+ self.f.SetStringSelection(str(v))
+
+# ----------------------------------------------------------------
+# the form class
+# ----------------------------------------------------------------
+
+class form(dict):
+ def __init__(self):
+ dict.__init__(self)
+
+ def check_input_for_errors(self):
+ """
+ Returns list of error messages if there's trouble,
+ else empty list.
+ """
+ vals = [f.get_value_with_check() for f in self.values()]
+ return [t[1] for t in vals if t[1] is not None]
+
+ def get_key_vals(self):
+ d = {}
+ for (key, f) in self.items():
+ d[key] = f.get_value()
+ return d
+
+
+ def _nop(*args): pass
+
+ def check_input_and_call(self, callback, status_handler=_nop):
+ """
+ Return a function that checks the form for errors, and then if it's OK,
+ invokes the user specified callback, passing it the form key/value dictionary.
+ status_handler is called with a string indicating results.
+ """
+ def doit_callback(*ignore):
+ errors = self.check_input_for_errors()
+ if errors:
+ status_handler(errors[0])
+ #print '\n'.join(tuple(errors))
+ else:
+ kv = self.get_key_vals()
+ if callback(kv):
+ status_handler("OK")
+ else:
+ status_handler("Failed")
+
+ return doit_callback
+
+
+
+# ----------------------------------------------------------------
+# Stand-alone example code
+# ----------------------------------------------------------------
+
+import sys
+from gnuradio.wxgui import stdgui2
+
+class demo_app_flow_graph (stdgui2.std_top_block):
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
+
+ self.frame = frame
+ self.panel = panel
+
+ def _print_kv(kv):
+ print "kv =", kv
+ return True
+
+ self.form = form()
+
+ self.form['static1'] = \
+ static_text_field(parent=panel, sizer=vbox,
+ label="Static Text",
+ value="The Static Value")
+
+ self.form['text1'] = \
+ text_field(parent=panel, sizer=vbox,
+ label="TextCtrl",
+ value="The Editable Value")
+
+ self.form['int1'] = \
+ int_field(parent=panel, sizer=vbox,
+ label="Int Field",
+ value=1234)
+
+ self.form['float1'] = \
+ float_field(parent=panel, sizer=vbox,
+ label="Float Field",
+ value=3.14159)
+
+ self.doit = button_with_callback(
+ panel, "Do It!",
+ self.form.check_input_and_call(_print_kv, self._set_status_msg))
+
+ vbox.Add(self.doit, 0, wx.CENTER)
+
+ def _set_status_msg(self, msg):
+ self.frame.GetStatusBar().SetStatusText(msg, 0)
+
+
+def main ():
+ app = stdgui2.stdapp(demo_app_flow_graph, "wxgui form demo", nstatus=1)
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/forms/__init__.py b/gr-wxgui/src/python/forms/__init__.py
new file mode 100644
index 000000000..3068b18fe
--- /dev/null
+++ b/gr-wxgui/src/python/forms/__init__.py
@@ -0,0 +1,103 @@
+#
+# Copyright 2009 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.
+#
+
+"""
+The following classes will be available through gnuradio.wxgui.forms:
+"""
+
+########################################################################
+# External Converters
+########################################################################
+from converters import \
+ eval_converter, str_converter, \
+ float_converter, int_converter
+
+########################################################################
+# External Forms
+########################################################################
+from forms import \
+ radio_buttons, drop_down, notebook, \
+ button, toggle_button, single_button, \
+ check_box, text_box, static_text, \
+ slider, log_slider, gauge, \
+ make_bold, DataEvent, EVT_DATA
+
+########################################################################
+# Helpful widgets
+########################################################################
+import wx
+
+class static_box_sizer(wx.StaticBoxSizer):
+ """
+ A box sizer with label and border.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param label title label for this widget (optional)
+ @param bold true to boldify the label
+ @param orient the sizer orientation wx.VERTICAL or wx.HORIZONTAL (default=wx.VERTICAL)
+ """
+ def __init__(self, parent, label='', bold=False, sizer=None, orient=wx.VERTICAL, proportion=0, flag=wx.EXPAND):
+ box = wx.StaticBox(parent=parent, label=label)
+ if bold: make_bold(box)
+ wx.StaticBoxSizer.__init__(self, box=box, orient=orient)
+ if sizer: sizer.Add(self, proportion, flag)
+
+class incr_decr_buttons(wx.BoxSizer):
+ """
+ A horizontal box sizer with a increment and a decrement button.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param label title label for this widget (optional)
+ @param on_incr the callback for pressing the + button
+ @param on_decr the callback for pressing the - button
+ """
+ def __init__(self, parent, on_incr, on_decr, label='', sizer=None, proportion=0, flag=wx.EXPAND):
+ """
+ @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)
+ buttons_box = wx.BoxSizer(wx.HORIZONTAL)
+ self._incr_button = wx.Button(parent, label='+', style=wx.BU_EXACTFIT)
+ self._incr_button.Bind(wx.EVT_BUTTON, on_incr)
+ buttons_box.Add(self._incr_button, 0, wx.ALIGN_CENTER_VERTICAL)
+ self._decr_button = wx.Button(parent, label=' - ', style=wx.BU_EXACTFIT)
+ self._decr_button.Bind(wx.EVT_BUTTON, on_decr)
+ buttons_box.Add(self._decr_button, 0, wx.ALIGN_CENTER_VERTICAL)
+ if label: #add label
+ self.Add(wx.StaticText(parent, label='%s: '%label), 1, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT)
+ self.Add(buttons_box, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT)
+ else: self.Add(buttons_box, 0, wx.ALIGN_CENTER_VERTICAL)
+ if sizer: sizer.Add(self, proportion, flag)
+
+ 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()
diff --git a/gr-wxgui/src/python/forms/converters.py b/gr-wxgui/src/python/forms/converters.py
new file mode 100644
index 000000000..db14d2752
--- /dev/null
+++ b/gr-wxgui/src/python/forms/converters.py
@@ -0,0 +1,154 @@
+#
+# Copyright 2009 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 eng_notation
+import math
+
+class abstract_converter(object):
+ def external_to_internal(self, v):
+ """
+ Convert from user specified value to value acceptable to underlying primitive.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def internal_to_external(self, s):
+ """
+ Convert from underlying primitive value to user specified value.
+ The underlying primitive usually expects strings.
+ """
+ raise NotImplementedError
+ def help(self):
+ return "Any string is acceptable"
+
+class identity_converter(abstract_converter):
+ def external_to_internal(self,v):
+ return v
+ def internal_to_external(self, s):
+ return s
+
+########################################################################
+# Commonly used converters
+########################################################################
+class chooser_converter(abstract_converter):
+ """
+ Convert between a set of possible choices and an index.
+ Used in the chooser base and all sub-classes.
+ """
+ def __init__(self, choices):
+ #choices must be a list because tuple does not have .index() in python2.5
+ self._choices = list(choices)
+ def external_to_internal(self, choice):
+ return self._choices.index(choice)
+ def internal_to_external(self, index):
+ return self._choices[index]
+ def help(self):
+ return 'Enter a possible value in choices: "%s"'%str(self._choices)
+
+class bool_converter(abstract_converter):
+ """
+ The internal representation is boolean.
+ The external representation is specified.
+ Used in the check box form.
+ """
+ def __init__(self, true, false):
+ self._true = true
+ self._false = false
+ def external_to_internal(self, v):
+ if v == self._true: return True
+ if v == self._false: return False
+ raise Exception, 'Value "%s" is not a possible option.'%v
+ def internal_to_external(self, v):
+ if v: return self._true
+ else: return self._false
+ def help(self):
+ return "Value must be in (%s, %s)."%(self._true, self._false)
+
+class eval_converter(abstract_converter):
+ """
+ A catchall converter when int and float are not enough.
+ Evaluate the internal representation with python's eval().
+ Possible uses, set a complex number, constellation points.
+ Used in text box.
+ """
+ def __init__(self, formatter=lambda x: '%s'%(x)):
+ self._formatter = formatter
+ def external_to_internal(self, v):
+ return self._formatter(v)
+ def internal_to_external(self, s):
+ return eval(s)
+ def help(self):
+ return "Value must be evaluatable by python's eval."
+
+class str_converter(abstract_converter):
+ def __init__(self, formatter=lambda x: '%s'%(x)):
+ self._formatter = formatter
+ def external_to_internal(self, v):
+ return self._formatter(v)
+ def internal_to_external(self, s):
+ return str(s)
+
+class int_converter(abstract_converter):
+ def __init__(self, formatter=lambda x: '%d'%round(x)):
+ self._formatter = formatter
+ def external_to_internal(self, v):
+ return self._formatter(v)
+ def internal_to_external(self, s):
+ return int(s, 0)
+ def help(self):
+ return "Enter an integer. Leading 0x indicates hex"
+
+class float_converter(abstract_converter):
+ def __init__(self, formatter=eng_notation.num_to_str):
+ self._formatter = formatter
+ def external_to_internal(self, v):
+ return self._formatter(v)
+ def internal_to_external(self, s):
+ return eng_notation.str_to_num(s)
+ def help(self):
+ return "Enter a float with optional scale suffix. E.g., 100.1M"
+
+class slider_converter(abstract_converter):
+ """
+ Scale values to and from the slider.
+ """
+ def __init__(self, minimum, maximum, num_steps, cast):
+ assert minimum < maximum
+ assert num_steps > 0
+ self._offset = minimum
+ self._scaler = float(maximum - minimum)/num_steps
+ self._cast = cast
+ def external_to_internal(self, v):
+ return (v - self._offset)/self._scaler
+ def internal_to_external(self, v):
+ return self._cast(v*self._scaler + self._offset)
+ def help(self):
+ return "Value should be within slider range"
+
+class log_slider_converter(slider_converter):
+ def __init__(self, min_exp, max_exp, num_steps, base):
+ assert min_exp < max_exp
+ assert num_steps > 0
+ self._base = base
+ slider_converter.__init__(self, minimum=min_exp, maximum=max_exp, num_steps=num_steps, cast=float)
+ def external_to_internal(self, v):
+ return slider_converter.external_to_internal(self, math.log(v, self._base))
+ def internal_to_external(self, v):
+ return self._base**slider_converter.internal_to_external(self, v)
diff --git a/gr-wxgui/src/python/forms/forms.py b/gr-wxgui/src/python/forms/forms.py
new file mode 100644
index 000000000..cabc5860b
--- /dev/null
+++ b/gr-wxgui/src/python/forms/forms.py
@@ -0,0 +1,653 @@
+#
+# Copyright 2009 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.
+#
+
+"""
+The forms module contains general purpose wx-gui forms for gnuradio apps.
+
+The forms follow a layered model:
+ * internal layer
+ * deals with the wxgui objects directly
+ * implemented in event handler and update methods
+ * translation layer
+ * translates the between the external and internal layers
+ * handles parsing errors between layers
+ * external layer
+ * provided external access to the user
+ * set_value, get_value, and optional callback
+ * set and get through optional pubsub and key
+
+Known problems:
+ * An empty label in the radio box still consumes space.
+ * The static text cannot resize the parent at runtime.
+"""
+
+EXT_KEY = 'external'
+INT_KEY = 'internal'
+
+import wx
+import sys
+from gnuradio.gr.pubsub import pubsub
+import converters
+
+EVT_DATA = wx.PyEventBinder(wx.NewEventType())
+class DataEvent(wx.PyEvent):
+ def __init__(self, data):
+ wx.PyEvent.__init__(self, wx.NewId(), EVT_DATA.typeId)
+ self.data = data
+
+def make_bold(widget):
+ font = widget.GetFont()
+ font.SetWeight(wx.FONTWEIGHT_BOLD)
+ widget.SetFont(font)
+
+########################################################################
+# Base Class Form
+########################################################################
+class _form_base(pubsub, wx.BoxSizer):
+ def __init__(self, parent=None, sizer=None, proportion=0, flag=wx.EXPAND, ps=None, key='', value=None, callback=None, converter=converters.identity_converter()):
+ pubsub.__init__(self)
+ wx.BoxSizer.__init__(self, wx.HORIZONTAL)
+ self._parent = parent
+ self._key = key
+ self._converter = converter
+ self._callback = callback
+ self._widgets = list()
+ #add to the sizer if provided
+ if sizer: sizer.Add(self, proportion, flag)
+ #proxy the pubsub and key into this form
+ if ps is not None:
+ assert key
+ self.proxy(EXT_KEY, ps, key)
+ #no pubsub passed, must set initial value
+ else: self.set_value(value)
+
+ def __str__(self):
+ return "Form: %s -> %s"%(self.__class__, self._key)
+
+ def _add_widget(self, widget, label='', flag=0, label_prop=0, widget_prop=1):
+ """
+ Add the main widget to this object sizer.
+ If label is passed, add a label as well.
+ Register the widget and the label in the widgets list (for enable/disable).
+ Bind the update handler to the widget for data events.
+ This ensures that the gui thread handles updating widgets.
+ Setup the pusub triggers for external and internal.
+ @param widget the main widget
+ @param label the optional label
+ @param flag additional flags for widget
+ @param label_prop the proportion for the label
+ @param widget_prop the proportion for the widget
+ """
+ #setup data event
+ widget.Bind(EVT_DATA, lambda x: self._update(x.data))
+ update = lambda x: wx.PostEvent(widget, DataEvent(x))
+ #register widget
+ self._widgets.append(widget)
+ #create optional label
+ if not label: self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | flag)
+ else:
+ label_text = wx.StaticText(self._parent, label='%s: '%label)
+ self._widgets.append(label_text)
+ self.Add(label_text, label_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT)
+ self.Add(widget, widget_prop, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | flag)
+ #initialize without triggering pubsubs
+ self._translate_external_to_internal(self[EXT_KEY])
+ update(self[INT_KEY])
+ #subscribe all the functions
+ self.subscribe(INT_KEY, update)
+ self.subscribe(INT_KEY, self._translate_internal_to_external)
+ self.subscribe(EXT_KEY, self._translate_external_to_internal)
+
+ def _translate_external_to_internal(self, external):
+ try:
+ internal = self._converter.external_to_internal(external)
+ #prevent infinite loop between internal and external pubsub keys by only setting if changed
+ if self[INT_KEY] != internal: self[INT_KEY] = internal
+ except Exception, e:
+ self._err_msg(external, e)
+ self[INT_KEY] = self[INT_KEY] #reset to last good setting
+
+ def _translate_internal_to_external(self, internal):
+ try:
+ external = self._converter.internal_to_external(internal)
+ #prevent infinite loop between internal and external pubsub keys by only setting if changed
+ if self[EXT_KEY] != external: self[EXT_KEY] = external
+ except Exception, e:
+ self._err_msg(internal, e)
+ self[EXT_KEY] = self[EXT_KEY] #reset to last good setting
+ if self._callback: self._callback(self[EXT_KEY])
+
+ def _err_msg(self, value, e):
+ print >> sys.stderr, self, 'Error translating value: "%s"\n\t%s\n\t%s'%(value, e, self._converter.help())
+
+ #override in subclasses to handle the wxgui object
+ def _update(self, value): raise NotImplementedError
+ def _handle(self, event): raise NotImplementedError
+
+ #provide a set/get interface for this form
+ def get_value(self): return self[EXT_KEY]
+ def set_value(self, value): self[EXT_KEY] = value
+
+ def Disable(self, disable=True): self.Enable(not disable)
+ def Enable(self, enable=True):
+ if enable:
+ for widget in self._widgets: widget.Enable()
+ else:
+ for widget in self._widgets: widget.Disable()
+
+########################################################################
+# Base Class Chooser Form
+########################################################################
+class _chooser_base(_form_base):
+ def __init__(self, choices=[], labels=None, **kwargs):
+ _form_base.__init__(self, converter=converters.chooser_converter(choices), **kwargs)
+ self._choices = choices
+ self._labels = map(str, labels or choices)
+
+########################################################################
+# Base Class Slider Form
+########################################################################
+class _slider_base(_form_base):
+ def __init__(self, label='', length=-1, converter=None, num_steps=100, style=wx.SL_HORIZONTAL, **kwargs):
+ _form_base.__init__(self, converter=converter, **kwargs)
+ if style & wx.SL_HORIZONTAL: slider_size = wx.Size(length, -1)
+ elif style & wx.SL_VERTICAL: slider_size = wx.Size(-1, length)
+ else: raise NotImplementedError
+ self._slider = wx.Slider(self._parent, minValue=0, maxValue=num_steps, size=slider_size, style=style)
+ self._slider.Bind(wx.EVT_SCROLL, self._handle)
+ self._add_widget(self._slider, label, flag=wx.EXPAND)
+
+ def _handle(self, event): self[INT_KEY] = self._slider.GetValue()
+ def _update(self, value): self._slider.SetValue(int(round(value)))
+
+########################################################################
+# Static Text Form
+########################################################################
+class static_text(_form_base):
+ """
+ A text box form.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param label title label for this widget (optional)
+ @param width the width of the form in px
+ @param bold true to bold-ify the text (default=False)
+ @param units a suffix to add after the text
+ @param converter forms.str_converter(), int_converter(), float_converter()...
+ """
+ def __init__(self, label='', width=-1, bold=False, units='', converter=converters.str_converter(), **kwargs):
+ self._units = units
+ _form_base.__init__(self, converter=converter, **kwargs)
+ self._static_text = wx.StaticText(self._parent, size=wx.Size(width, -1))
+ if bold: make_bold(self._static_text)
+ self._add_widget(self._static_text, label)
+
+ def _update(self, label):
+ if self._units: label += ' ' + self._units
+ self._static_text.SetLabel(label); self._parent.Layout()
+
+########################################################################
+# Text Box Form
+########################################################################
+class text_box(_form_base):
+ """
+ A text box form.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param label title label for this widget (optional)
+ @param width the width of the form in px
+ @param converter forms.str_converter(), int_converter(), float_converter()...
+ """
+ def __init__(self, label='', width=-1, converter=converters.eval_converter(), **kwargs):
+ _form_base.__init__(self, converter=converter, **kwargs)
+ self._text_box = wx.TextCtrl(self._parent, size=wx.Size(width, -1), style=wx.TE_PROCESS_ENTER)
+ self._default_bg_colour = self._text_box.GetBackgroundColour()
+ self._text_box.Bind(wx.EVT_TEXT_ENTER, self._handle)
+ self._text_box.Bind(wx.EVT_TEXT, self._update_color)
+ self._add_widget(self._text_box, label)
+
+ def _update_color(self, *args):
+ if self._text_box.GetValue() == self[INT_KEY]:
+ self._text_box.SetBackgroundColour(self._default_bg_colour)
+ else: self._text_box.SetBackgroundColour('#EEDDDD')
+
+ def _handle(self, event): self[INT_KEY] = self._text_box.GetValue()
+ def _update(self, value): self._text_box.SetValue(value); self._update_color()
+
+########################################################################
+# Slider Form
+# Linear Slider
+# Logarithmic Slider
+########################################################################
+class slider(_slider_base):
+ """
+ A generic linear slider.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param label title label for this widget (optional)
+ @param length the length of the slider in px (optional)
+ @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
+ @param minimum the minimum value
+ @param maximum the maximum value
+ @param num_steps the number of slider steps (or specify step_size)
+ @param step_size the step between slider jumps (or specify num_steps)
+ @param cast a cast function, int, or float (default=float)
+ """
+ def __init__(self, minimum=-100, maximum=100, num_steps=100, step_size=None, cast=float, **kwargs):
+ assert step_size or num_steps
+ if step_size is not None: num_steps = (maximum - minimum)/step_size
+ converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=cast)
+ _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
+
+class log_slider(_slider_base):
+ """
+ A generic logarithmic slider.
+ The sliders min and max values are base**min_exp and base**max_exp.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param label title label for this widget (optional)
+ @param length the length of the slider in px (optional)
+ @param style wx.SL_HORIZONTAL or wx.SL_VERTICAL (default=horizontal)
+ @param min_exp the minimum exponent
+ @param max_exp the maximum exponent
+ @param base the exponent base in base**exp
+ @param num_steps the number of slider steps (or specify step_size)
+ @param step_size the exponent step size (or specify num_steps)
+ """
+ def __init__(self, min_exp=0, max_exp=1, base=10, num_steps=100, step_size=None, **kwargs):
+ assert step_size or num_steps
+ if step_size is not None: num_steps = (max_exp - min_exp)/step_size
+ converter = converters.log_slider_converter(min_exp=min_exp, max_exp=max_exp, num_steps=num_steps, base=base)
+ _slider_base.__init__(self, converter=converter, num_steps=num_steps, **kwargs)
+
+########################################################################
+# Gauge Form
+########################################################################
+class gauge(_form_base):
+ """
+ A gauge bar.
+ The gauge displays floating point values between the minimum and maximum.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param label title label for this widget (optional)
+ @param length the length of the slider in px (optional)
+ @param style wx.GA_HORIZONTAL or wx.GA_VERTICAL (default=horizontal)
+ @param minimum the minimum value
+ @param maximum the maximum value
+ @param num_steps the number of slider steps (or specify step_size)
+ @param step_size the step between slider jumps (or specify num_steps)
+ """
+ def __init__(self, label='', length=-1, minimum=-100, maximum=100, num_steps=100, step_size=None, style=wx.GA_HORIZONTAL, **kwargs):
+ assert step_size or num_steps
+ if step_size is not None: num_steps = (maximum - minimum)/step_size
+ converter = converters.slider_converter(minimum=minimum, maximum=maximum, num_steps=num_steps, cast=float)
+ _form_base.__init__(self, converter=converter, **kwargs)
+ if style & wx.SL_HORIZONTAL: gauge_size = wx.Size(length, -1)
+ elif style & wx.SL_VERTICAL: gauge_size = wx.Size(-1, length)
+ else: raise NotImplementedError
+ self._gauge = wx.Gauge(self._parent, range=num_steps, size=gauge_size, style=style)
+ self._add_widget(self._gauge, label, flag=wx.EXPAND)
+
+ def _update(self, value): self._gauge.SetValue(value)
+
+########################################################################
+# Check Box Form
+########################################################################
+class check_box(_form_base):
+ """
+ Create a check box form.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param true the value for form when checked (default=True)
+ @param false the value for form when unchecked (default=False)
+ @param label title label for this widget (optional)
+ """
+ def __init__(self, label='', true=True, false=False, **kwargs):
+ _form_base.__init__(self, converter=converters.bool_converter(true=true, false=false), **kwargs)
+ self._check_box = wx.CheckBox(self._parent, style=wx.CHK_2STATE, label=label)
+ self._check_box.Bind(wx.EVT_CHECKBOX, self._handle)
+ self._add_widget(self._check_box)
+
+ def _handle(self, event): self[INT_KEY] = self._check_box.IsChecked()
+ def _update(self, checked): self._check_box.SetValue(checked)
+
+########################################################################
+# Drop Down Chooser Form
+########################################################################
+class drop_down(_chooser_base):
+ """
+ Create a drop down menu form.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param choices list of possible values
+ @param labels list of labels for each choice (default=choices)
+ @param label title label for this widget (optional)
+ @param width the form width in px (optional)
+ """
+ def __init__(self, label='', width=-1, **kwargs):
+ _chooser_base.__init__(self, **kwargs)
+ self._drop_down = wx.Choice(self._parent, choices=self._labels, size=wx.Size(width, -1))
+ self._drop_down.Bind(wx.EVT_CHOICE, self._handle)
+ self._add_widget(self._drop_down, label, widget_prop=0, label_prop=1)
+
+ def _handle(self, event): self[INT_KEY] = self._drop_down.GetSelection()
+ def _update(self, i): self._drop_down.SetSelection(i)
+
+########################################################################
+# Button Chooser Form
+# Circularly move through the choices with each click.
+# Can be a single-click button with one choice.
+# Can be a 2-state button with two choices.
+########################################################################
+class button(_chooser_base):
+ """
+ Create a multi-state button.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param choices list of possible values
+ @param labels list of labels for each choice (default=choices)
+ @param width the width of the button in pixels (optional)
+ @param style style arguments (optional)
+ @param label title label for this widget (optional)
+ """
+ def __init__(self, label='', style=0, width=-1, **kwargs):
+ _chooser_base.__init__(self, **kwargs)
+ self._button = wx.Button(self._parent, size=wx.Size(width, -1), style=style)
+ self._button.Bind(wx.EVT_BUTTON, self._handle)
+ self._add_widget(self._button, label, widget_prop=((not style&wx.BU_EXACTFIT) and 1 or 0))
+
+ def _handle(self, event): self[INT_KEY] = (self[INT_KEY] + 1)%len(self._choices) #circularly increment index
+ def _update(self, i): self._button.SetLabel(self._labels[i]); self.Layout()
+
+class toggle_button(button):
+ """
+ Create a dual-state button.
+ This button will alternate between True and False when clicked.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param width the width of the button in pixels (optional)
+ @param style style arguments (optional)
+ @param true_label the button's label in the true state
+ @param false_label the button's label in the false state
+ """
+ def __init__(self, true_label='On (click to stop)', false_label='Off (click to start)', **kwargs):
+ button.__init__(self, choices=[True, False], labels=[true_label, false_label], **kwargs)
+
+class single_button(toggle_button):
+ """
+ Create a single state button.
+ This button will callback() when clicked.
+ For use when state holding is not important.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param width the width of the button in pixels (optional)
+ @param style style arguments (optional)
+ @param label the button's label
+ """
+ def __init__(self, label='click for callback', **kwargs):
+ toggle_button.__init__(self, true_label=label, false_label=label, value=True, **kwargs)
+
+########################################################################
+# Radio Buttons Chooser Form
+########################################################################
+class radio_buttons(_chooser_base):
+ """
+ Create a radio button form.
+ @param parent the parent widget
+ @param sizer add this widget to sizer if provided (optional)
+ @param proportion the proportion when added to the sizer (default=0)
+ @param flag the flag argument when added to the sizer (default=wx.EXPAND)
+ @param ps the pubsub object (optional)
+ @param key the pubsub key (optional)
+ @param value the default value (optional)
+ @param choices list of possible values
+ @param labels list of labels for each choice (default=choices)
+ @param major_dimension the number of rows/cols (default=auto)
+ @param label title label for this widget (optional)
+ @param style useful style args: wx.RA_HORIZONTAL, wx.RA_VERTICAL, wx.NO_BORDER (default=wx.RA_HORIZONTAL)
+ """
+ def __init__(self, style=wx.RA_HORIZONTAL, label='', major_dimension=0, **kwargs):
+ _chooser_base.__init__(self, **kwargs)
+ #create radio buttons
+ self._radio_buttons = wx.RadioBox(self._parent, choices=self._labels, style=style, label=label, majorDimension=major_dimension)
+ self._radio_buttons.Bind(wx.EVT_RADIOBOX, self._handle)
+ self._add_widget(self._radio_buttons)
+
+ def _handle(self, event): self[INT_KEY] = self._radio_buttons.GetSelection()
+ def _update(self, i): self._radio_buttons.SetSelection(i)
+
+########################################################################
+# Notebook Chooser Form
+# The notebook pages/tabs are for selecting between choices.
+# A page must be added to the notebook for each choice.
+########################################################################
+class notebook(_chooser_base):
+ def __init__(self, pages, notebook, **kwargs):
+ _chooser_base.__init__(self, **kwargs)
+ assert len(pages) == len(self._choices)
+ self._notebook = notebook
+ self._notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, self._handle)
+ #add pages, setting the label on each tab
+ for i, page in enumerate(pages):
+ self._notebook.AddPage(page, self._labels[i])
+ self._add_widget(self._notebook)
+
+ def _handle(self, event): self[INT_KEY] = self._notebook.GetSelection()
+ # SetSelection triggers a page change event (deprecated, breaks on Windows) and ChangeSelection does not
+ def _update(self, i): self._notebook.ChangeSelection(i)
+
+# ----------------------------------------------------------------
+# Stand-alone test application
+# ----------------------------------------------------------------
+
+import wx
+from gnuradio.wxgui import gui
+
+class app_gui (object):
+ def __init__(self, frame, panel, vbox, top_block, options, args):
+
+ def callback(v): print v
+
+ radio_buttons(
+ sizer=vbox,
+ parent=panel,
+ choices=[2, 4, 8, 16],
+ labels=['two', 'four', 'eight', 'sixteen'],
+ value=4,
+ style=wx.RA_HORIZONTAL,
+ label='test radio long string',
+ callback=callback,
+ #major_dimension = 2,
+ )
+
+ radio_buttons(
+ sizer=vbox,
+ parent=panel,
+ choices=[2, 4, 8, 16],
+ labels=['two', 'four', 'eight', 'sixteen'],
+ value=4,
+ style=wx.RA_VERTICAL,
+ label='test radio long string',
+ callback=callback,
+ #major_dimension = 2,
+ )
+
+ radio_buttons(
+ sizer=vbox,
+ parent=panel,
+ choices=[2, 4, 8, 16],
+ labels=['two', 'four', 'eight', 'sixteen'],
+ value=4,
+ style=wx.RA_VERTICAL | wx.NO_BORDER,
+ callback=callback,
+ #major_dimension = 2,
+ )
+
+ button(
+ sizer=vbox,
+ parent=panel,
+ choices=[2, 4, 8, 16],
+ labels=['two', 'four', 'eight', 'sixteen'],
+ value=2,
+ label='button value',
+ callback=callback,
+ #width=100,
+ )
+
+
+ drop_down(
+ sizer=vbox,
+ parent=panel,
+ choices=[2, 4, 8, 16],
+ value=2,
+ label='Choose One',
+ callback=callback,
+ )
+ check_box(
+ sizer=vbox,
+ parent=panel,
+ value=False,
+ label='check me',
+ callback=callback,
+ )
+ text_box(
+ sizer=vbox,
+ parent=panel,
+ value=3,
+ label='text box',
+ callback=callback,
+ width=200,
+ )
+
+ static_text(
+ sizer=vbox,
+ parent=panel,
+ value='bob',
+ label='static text',
+ width=-1,
+ bold=True,
+ )
+
+ slider(
+ sizer=vbox,
+ parent=panel,
+ value=12,
+ label='slider',
+ callback=callback,
+ )
+
+ log_slider(
+ sizer=vbox,
+ parent=panel,
+ value=12,
+ label='slider',
+ callback=callback,
+ )
+
+ slider(
+ sizer=vbox,
+ parent=panel,
+ value=12,
+ label='slider',
+ callback=callback,
+ style=wx.SL_VERTICAL,
+ length=30,
+ )
+
+ toggle_button(
+ sizer=vbox,
+ parent=panel,
+ value=True,
+ label='toggle it',
+ callback=callback,
+ )
+
+ single_button(
+ sizer=vbox,
+ parent=panel,
+ label='sig test',
+ callback=callback,
+ )
+
+if __name__ == "__main__":
+ try:
+
+ # Create the GUI application
+ app = gui.app(
+ gui=app_gui, # User interface class
+ title="Test Forms", # Top window title
+ )
+
+ # And run it
+ app.MainLoop()
+
+ except RuntimeError, e:
+ print e
+ sys.exit(1)
diff --git a/gr-wxgui/src/python/gui.py b/gr-wxgui/src/python/gui.py
new file mode 100644
index 000000000..ccc773eab
--- /dev/null
+++ b/gr-wxgui/src/python/gui.py
@@ -0,0 +1,135 @@
+#
+# Copyright 2009 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 gnuradio import gr
+
+#
+# Top-level display panel with vertical box sizer. User does not create or
+# subclass this class; rather, the user supplies his own class constructor
+# that gets invoked with needed parameters.
+#
+class top_panel(wx.Panel):
+ def __init__(self, frame, top_block, gui, options, args):
+ wx.Panel.__init__(self, frame, -1)
+ vbox = wx.BoxSizer(wx.VERTICAL)
+
+ # Create the user's GUI class
+ if gui is not None:
+ self.gui = gui(frame, # Top-level window frame
+ self, # Parent class for user created windows
+ vbox, # Sizer for user to add windows to
+ top_block, # GUI-unaware flowgraph to manipulate
+ options, # Command-line options
+ args) # Command-line arguments
+
+ else:
+ # User hasn't made their own GUI, create our default
+ # We don't have a default GUI yet either :)
+ p = wx.Panel(self)
+ p.SetSize((640,480))
+ vbox.Add(p, 1, wx.EXPAND)
+
+ self.SetSizer(vbox)
+ self.SetAutoLayout(True)
+ vbox.Fit(self)
+
+ def shutdown(self):
+ try:
+ self.gui.shutdown()
+ except AttributeError:
+ pass
+
+#
+# Top-level window frame with menu and status bars.
+#
+class top_frame(wx.Frame):
+ def __init__ (self, top_block, gui, options, args,
+ title, nstatus, start, realtime):
+
+ wx.Frame.__init__(self, None, -1, title)
+ self.top_block = top_block
+
+ self.CreateStatusBar(nstatus)
+ mainmenu = wx.MenuBar()
+ self.SetMenuBar(mainmenu)
+
+ menu = wx.Menu()
+
+ item = menu.Append(200, 'E&xit', 'Exit Application') # FIXME magic ID
+ self.Bind(wx.EVT_MENU, self.OnCloseWindow, item)
+ mainmenu.Append(menu, "&File")
+ self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
+
+ # Create main panel, creates user GUI class with supplied parameters
+ self.panel = top_panel(self, top_block, gui, options, args)
+
+ vbox = wx.BoxSizer(wx.VERTICAL)
+ vbox.Add(self.panel, 1, wx.EXPAND)
+ self.SetSizer(vbox)
+ self.SetAutoLayout(True)
+ vbox.Fit(self)
+
+ if realtime:
+ if gr.enable_realtime_scheduling() != gr.RT_OK:
+ self.SetStatusText("Failed to enable realtime scheduling")
+
+ if start and self.top_block is not None:
+ self.top_block.start()
+
+ def OnCloseWindow(self, event):
+ # Give user API a chance to do something
+ self.panel.shutdown()
+
+ # Stop flowgraph as a convenience
+ self.SetStatusText("Ensuring flowgraph has completed before exiting...")
+ if self.top_block is not None:
+ self.top_block.stop()
+ self.top_block.wait()
+
+ self.Destroy()
+
+
+#
+# Top-level wxPython application object. User creates or subclasses this
+# in their GUI script.
+#
+class app(wx.App):
+ def __init__ (self, top_block=None, gui=None, options=None, args=None,
+ title="GNU Radio", nstatus=1, start=False, realtime=False):
+ self.top_block = top_block
+ self.gui = gui
+ self.options = options
+ self.args = args
+ self.title = title
+ self.nstatus = nstatus
+ self.start = start
+ self.realtime = realtime
+
+ wx.App.__init__ (self, redirect=False)
+
+ def OnInit(self):
+ # Pass user parameters to top window frame
+ frame = top_frame(self.top_block, self.gui, self.options, self.args,
+ self.title, self.nstatus, self.start, self.realtime)
+ frame.Show(True)
+ self.SetTopWindow(frame)
+ return True
diff --git a/gr-wxgui/src/python/histo_window.py b/gr-wxgui/src/python/histo_window.py
new file mode 100644
index 000000000..e87e97825
--- /dev/null
+++ b/gr-wxgui/src/python/histo_window.py
@@ -0,0 +1,164 @@
+#
+# Copyright 2009 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 *
+from gnuradio import gr #for gr.prefs
+import forms
+
+##################################################
+# Constants
+##################################################
+DEFAULT_WIN_SIZE = (600, 300)
+
+##################################################
+# histo window control panel
+##################################################
+class control_panel(wx.Panel):
+ """
+ A control panel with wx widgits to control the plotter and histo sink.
+ """
+
+ 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)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ SIZE = (100, -1)
+ control_box = forms.static_box_sizer(
+ parent=self, label='Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ #num bins
+ control_box.AddStretchSpacer()
+ forms.text_box(
+ sizer=control_box, parent=self, label='Num Bins',
+ converter=forms.int_converter(),
+ ps=parent, key=NUM_BINS_KEY,
+ )
+ #frame size
+ control_box.AddStretchSpacer()
+ forms.text_box(
+ sizer=control_box, parent=self, label='Frame Size',
+ converter=forms.int_converter(),
+ ps=parent, key=FRAME_SIZE_KEY,
+ )
+ #run/stop
+ control_box.AddStretchSpacer()
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+##################################################
+# histo window with plotter and control panel
+##################################################
+class histo_window(wx.Panel, pubsub.pubsub):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ maximum_key,
+ minimum_key,
+ num_bins_key,
+ frame_size_key,
+ msg_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ #setup
+ self.samples = list()
+ #proxy the keys
+ self.proxy(MAXIMUM_KEY, controller, maximum_key)
+ self.proxy(MINIMUM_KEY, controller, minimum_key)
+ self.proxy(NUM_BINS_KEY, controller, num_bins_key)
+ self.proxy(FRAME_SIZE_KEY, controller, frame_size_key)
+ self.proxy(MSG_KEY, controller, msg_key)
+ #initialize values
+ self[RUNNING_KEY] = True
+ self[X_DIVS_KEY] = 8
+ self[Y_DIVS_KEY] = 4
+ #init panel and plot
+ wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.bar_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.SetSizeHints(*size)
+ self.plotter.set_title(title)
+ self.plotter.enable_point_label(True)
+ self.plotter.enable_grid_lines(False)
+ #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)
+ #register events
+ self.subscribe(MSG_KEY, self.handle_msg)
+ self.subscribe(X_DIVS_KEY, self.update_grid)
+ self.subscribe(Y_DIVS_KEY, self.update_grid)
+
+ def handle_msg(self, msg):
+ """
+ Handle the message from the fft sink message queue.
+ @param msg the frame as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ #convert to floating point numbers
+ self.samples = 100*numpy.fromstring(msg, numpy.float32)[:self[NUM_BINS_KEY]] #only take first frame
+ self.plotter.set_bars(
+ bars=self.samples,
+ bar_width=0.6,
+ color_spec=(0, 0, 1),
+ )
+ self.update_grid()
+
+ def update_grid(self):
+ if not len(self.samples): return
+ #calculate the maximum y value
+ y_off = math.ceil(numpy.max(self.samples))
+ y_off = min(max(y_off, 1.0), 100.0) #between 1% and 100%
+ #update the x grid
+ self.plotter.set_x_grid(
+ self[MINIMUM_KEY], self[MAXIMUM_KEY],
+ common.get_clean_num((self[MAXIMUM_KEY] - self[MINIMUM_KEY])/self[X_DIVS_KEY]),
+ )
+ self.plotter.set_x_label('Counts')
+ #update the y grid
+ self.plotter.set_y_grid(0, y_off, y_off/self[Y_DIVS_KEY])
+ self.plotter.set_y_label('Frequency', '%')
+ self.plotter.update()
diff --git a/gr-wxgui/src/python/histosink_gl.py b/gr-wxgui/src/python/histosink_gl.py
new file mode 100644
index 000000000..509f746be
--- /dev/null
+++ b/gr-wxgui/src/python/histosink_gl.py
@@ -0,0 +1,110 @@
+#
+# Copyright 2009 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 histo_window
+import common
+from gnuradio import gr, blks2
+from pubsub import pubsub
+from constants import *
+
+##################################################
+# histo sink block (wrapper for old wxgui)
+##################################################
+class histo_sink_f(gr.hier_block2, common.wxgui_hb):
+ """
+ A histogram block and a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ size=histo_window.DEFAULT_WIN_SIZE,
+ title='',
+ num_bins=11,
+ frame_size=1000,
+ ):
+ #init
+ gr.hier_block2.__init__(
+ self,
+ "histo_sink",
+ gr.io_signature(1, 1, gr.sizeof_float),
+ gr.io_signature(0, 0, 0),
+ )
+ #blocks
+ msgq = gr.msg_queue(2)
+ histo = gr.histo_sink_f(msgq)
+ histo.set_num_bins(num_bins)
+ histo.set_frame_size(frame_size)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(NUM_BINS_KEY, histo.set_num_bins)
+ self.controller.publish(NUM_BINS_KEY, histo.get_num_bins)
+ self.controller.subscribe(FRAME_SIZE_KEY, histo.set_frame_size)
+ self.controller.publish(FRAME_SIZE_KEY, histo.get_frame_size)
+ #start input watcher
+ common.input_watcher(msgq, self.controller, MSG_KEY, arg1_key=MINIMUM_KEY, arg2_key=MAXIMUM_KEY)
+ #create window
+ self.win = histo_window.histo_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ maximum_key=MAXIMUM_KEY,
+ minimum_key=MINIMUM_KEY,
+ num_bins_key=NUM_BINS_KEY,
+ frame_size_key=FRAME_SIZE_KEY,
+ msg_key=MSG_KEY,
+ )
+ common.register_access_methods(self, self.win)
+ #connect
+ self.wxgui_connect(self, histo)
+
+# ----------------------------------------------------------------
+# 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)
+
+ # build our flow graph
+ input_rate = 20.48e3
+
+ 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 = histo_sink_f (panel, title="Data", num_bins=31, frame_size=1000)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+ self.connect(src2, thr2, sink2)
+
+def main ():
+ app = stdgui2.stdapp (test_app_block, "Histo Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
diff --git a/gr-wxgui/src/python/number_window.py b/gr-wxgui/src/python/number_window.py
new file mode 100644
index 000000000..ab9d1ebc0
--- /dev/null
+++ b/gr-wxgui/src/python/number_window.py
@@ -0,0 +1,213 @@
+#
+# 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 *
+from gnuradio import gr #for gr.prefs
+import forms
+
+##################################################
+# Constants
+##################################################
+NEG_INF = float('-inf')
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+DEFAULT_NUMBER_RATE = gr.prefs().get_long('wxgui', 'number_rate', 5)
+DEFAULT_WIN_SIZE = (300, 300)
+DEFAULT_GAUGE_RANGE = 1000
+VALUE_REPR_KEY = 'value_repr'
+VALUE_REAL_KEY = 'value_real'
+VALUE_IMAG_KEY = 'value_imag'
+
+##################################################
+# 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)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ #checkboxes for average and peak hold
+ control_box.AddStretchSpacer()
+ options_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ forms.check_box(
+ sizer=options_box, parent=self, label='Peak Hold',
+ ps=parent, key=PEAK_HOLD_KEY,
+ )
+ forms.check_box(
+ sizer=options_box, parent=self, label='Average',
+ ps=parent, key=AVERAGE_KEY,
+ )
+ #static text and slider for averaging
+ avg_alpha_text = forms.static_text(
+ sizer=options_box, parent=self, label='Avg Alpha',
+ converter=forms.float_converter(lambda x: '%.4f'%x),
+ ps=parent, key=AVG_ALPHA_KEY, width=50,
+ )
+ avg_alpha_slider = forms.log_slider(
+ sizer=options_box, parent=self,
+ min_exp=AVG_ALPHA_MIN_EXP,
+ max_exp=AVG_ALPHA_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=AVG_ALPHA_KEY,
+ )
+ for widget in (avg_alpha_text, avg_alpha_slider):
+ parent.subscribe(AVERAGE_KEY, widget.Enable)
+ widget.Enable(parent[AVERAGE_KEY])
+ #run/stop
+ control_box.AddStretchSpacer()
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+##################################################
+# Numbersink window with label and gauges
+##################################################
+class number_window(wx.Panel, pubsub.pubsub):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ units,
+ show_gauge,
+ real,
+ minval,
+ maxval,
+ decimal_places,
+ average_key,
+ avg_alpha_key,
+ peak_hold,
+ msg_key,
+ sample_rate_key,
+ ):
+ pubsub.pubsub.__init__(self)
+ wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
+ #setup
+ self.peak_val_real = NEG_INF
+ self.peak_val_imag = NEG_INF
+ self.real = real
+ self.units = units
+ self.decimal_places = decimal_places
+ #proxy the keys
+ self.proxy(MSG_KEY, controller, msg_key)
+ self.proxy(AVERAGE_KEY, controller, average_key)
+ self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
+ self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
+ #initialize values
+ self[PEAK_HOLD_KEY] = peak_hold
+ self[RUNNING_KEY] = True
+ self[VALUE_REAL_KEY] = minval
+ self[VALUE_IMAG_KEY] = minval
+ #setup the box with display and controls
+ self.control_panel = control_panel(self)
+ main_box = wx.BoxSizer(wx.HORIZONTAL)
+ sizer = forms.static_box_sizer(
+ parent=self, sizer=main_box, label=title,
+ bold=True, orient=wx.VERTICAL, proportion=1,
+ )
+ main_box.Add(self.control_panel, 0, wx.EXPAND)
+ sizer.AddStretchSpacer()
+ forms.static_text(
+ parent=self, sizer=sizer,
+ ps=self, key=VALUE_REPR_KEY, width=size[0],
+ converter=forms.str_converter(),
+ )
+ sizer.AddStretchSpacer()
+ self.gauge_real = forms.gauge(
+ parent=self, sizer=sizer, style=wx.GA_HORIZONTAL,
+ ps=self, key=VALUE_REAL_KEY, length=size[0],
+ minimum=minval, maximum=maxval, num_steps=DEFAULT_GAUGE_RANGE,
+ )
+ self.gauge_imag = forms.gauge(
+ parent=self, sizer=sizer, style=wx.GA_HORIZONTAL,
+ ps=self, key=VALUE_IMAG_KEY, length=size[0],
+ minimum=minval, maximum=maxval, num_steps=DEFAULT_GAUGE_RANGE,
+ )
+ #hide/show gauges
+ self.show_gauges(show_gauge)
+ self.SetSizerAndFit(main_box)
+ #register events
+ self.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
+ """
+ self.gauge_real.ShowItems(show_gauge)
+ self.gauge_imag.ShowItems(show_gauge and not self.real)
+
+ 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 event event.data is the number sample as a character array
+ """
+ if not self[RUNNING_KEY]: return
+ 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)
+ self[VALUE_REAL_KEY] = 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)
+ self[VALUE_REAL_KEY] = sample.real
+ self[VALUE_IMAG_KEY] = sample.imag
+ #set label text
+ self[VALUE_REPR_KEY] = 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
new file mode 100644
index 000000000..011acdfd5
--- /dev/null
+++ b/gr-wxgui/src/python/numbersink2.py
@@ -0,0 +1,171 @@
+#
+# 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 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.wxgui_hb):
+ """
+ An decimator block with a number window display
+ """
+
+ def __init__(
+ self,
+ parent,
+ unit='units',
+ 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,
+ **kwargs #catchall for backwards compatibility
+ ):
+ #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)
+ avg = gr.single_pole_iir_filter_ff(1.0)
+ else:
+ mult = gr.multiply_const_cc(factor)
+ add = gr.add_const_cc(ref_level)
+ avg = gr.single_pole_iir_filter_cc(1.0)
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(self._item_size, msgq, True)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(SAMPLE_RATE_KEY, sd.set_sample_rate)
+ self.controller.publish(SAMPLE_RATE_KEY, sd.sample_rate)
+ self.controller[AVERAGE_KEY] = average
+ self.controller[AVG_ALPHA_KEY] = avg_alpha
+ def update_avg(*args):
+ if self.controller[AVERAGE_KEY]: avg.set_taps(self.controller[AVG_ALPHA_KEY])
+ else: avg.set_taps(1.0)
+ update_avg()
+ self.controller.subscribe(AVERAGE_KEY, update_avg)
+ self.controller.subscribe(AVG_ALPHA_KEY, update_avg)
+ #start input watcher
+ common.input_watcher(msgq, self.controller, MSG_KEY)
+ #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,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ )
+ common.register_access_methods(self, self.controller)
+ #backwards compadibility
+ self.set_show_gauge = self.win.show_gauges
+ #connect
+ self.wxgui_connect(self, sd, mult, add, avg, sink)
+
+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)
+
+ # build our flow graph
+ input_rate = 20.48e3
+
+ # Generate a real and complex sinusoids
+ 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='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, minval=-1, maxval=1,
+ ref_level=0, decimal_places=3)
+ vbox.Add (sink2.win, 1, wx.EXPAND)
+
+ self.connect (src1, thr1, sink1)
+ self.connect (src2, thr2, sink2)
+
+def main ():
+ app = stdgui2.stdapp (test_app_flow_graph, "Number Sink Test App")
+ app.MainLoop ()
+
+if __name__ == '__main__':
+ main ()
+
diff --git a/gr-wxgui/src/python/plot.py b/gr-wxgui/src/python/plot.py
new file mode 100644
index 000000000..041a2a7a5
--- /dev/null
+++ b/gr-wxgui/src/python/plot.py
@@ -0,0 +1,1834 @@
+#-----------------------------------------------------------------------------
+# Name: wx.lib.plot.py
+# Purpose: Line, Bar and Scatter Graphs
+#
+# Author: Gordon Williams
+#
+# Created: 2003/11/03
+# RCS-ID: $Id$
+# Copyright: (c) 2002,2007,2010
+# Licence: Use as you wish.
+#-----------------------------------------------------------------------------
+# 12/15/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o 2.5 compatability update.
+# o Renamed to plot.py in the wx.lib directory.
+# o Reworked test frame to work with wx demo framework. This saves a bit
+# of tedious cut and paste, and the test app is excellent.
+#
+# 12/18/2003 - Jeff Grimmett (grimmtooth@softhome.net)
+#
+# o wxScrolledMessageDialog -> ScrolledMessageDialog
+#
+# Oct 6, 2004 Gordon Williams (g_will@cyberus.ca)
+# - Added bar graph demo
+# - Modified line end shape from round to square.
+# - Removed FloatDCWrapper for conversion to ints and ints in arguments
+#
+# Oct 15, 2004 Gordon Williams (g_will@cyberus.ca)
+# - Imported modules given leading underscore to name.
+# - Added Cursor Line Tracking and User Point Labels.
+# - Demo for Cursor Line Tracking and Point Labels.
+# - Size of plot preview frame adjusted to show page better.
+# - Added helper functions PositionUserToScreen and PositionScreenToUser in PlotCanvas.
+# - Added functions GetClosestPoints (all curves) and GetClosestPoint (only closest curve)
+# can be in either user coords or screen coords.
+#
+# May 27, 2007 Johnathan Corgan (jcorgan@corganenterprises.com)
+# - Converted from numarray to numpy
+#
+# Apr 23, 2010 Martin Dudok van Heel (http://www.olifantasia.com/gnuradio/contact_olifantasia.gif)
+# - Added Persistence option (emulate after glow of an analog CRT display using IIR)
+
+"""
+This is a simple light weight plotting module that can be used with
+Boa or easily integrated into your own wxPython application. The
+emphasis is on small size and fast plotting for large data sets. It
+has a reasonable number of features to do line and scatter graphs
+easily as well as simple bar graphs. It is not as sophisticated or
+as powerful as SciPy Plt or Chaco. Both of these are great packages
+but consume huge amounts of computer resources for simple plots.
+They can be found at http://scipy.com
+
+This file contains two parts; first the re-usable library stuff, then,
+after a "if __name__=='__main__'" test, a simple frame and a few default
+plots for examples and testing.
+
+Based on wxPlotCanvas
+Written by K.Hinsen, R. Srinivasan;
+Ported to wxPython Harm van der Heijden, feb 1999
+
+Major Additions Gordon Williams Feb. 2003 (g_will@cyberus.ca)
+ -More style options
+ -Zooming using mouse 'rubber band'
+ -Scroll left, right
+ -Grid(graticule)
+ -Printing, preview, and page set up (margins)
+ -Axis and title labels
+ -Cursor xy axis values
+ -Doc strings and lots of comments
+ -Optimizations for large number of points
+ -Legends
+
+Did a lot of work here to speed markers up. Only a factor of 4
+improvement though. Lines are much faster than markers, especially
+filled markers. Stay away from circles and triangles unless you
+only have a few thousand points.
+
+Times for 25,000 points
+Line - 0.078 sec
+Markers
+Square - 0.22 sec
+dot - 0.10
+circle - 0.87
+cross,plus - 0.28
+triangle, triangle_down - 0.90
+
+Thanks to Chris Barker for getting this version working on Linux.
+
+Zooming controls with mouse (when enabled):
+ Left mouse drag - Zoom box.
+ Left mouse double click - reset zoom.
+ Right mouse click - zoom out centred on click location.
+"""
+
+import string as _string
+import time as _time
+import wx
+
+# Needs numpy or numarray
+try:
+ import numpy as _numpy
+except:
+ try:
+ import numarray as _numpy #if numarray is used it is renamed numpy
+ except:
+ msg= """
+ This module requires the numpy or numarray module,
+ which could not be imported. It probably is not installed
+ (it's not part of the standard Python distribution). See the
+ Python site (http://www.python.org) for information on
+ downloading source or binaries."""
+ raise ImportError, "numpy or numarray not found. \n" + msg
+
+
+
+#
+# Plotting classes...
+#
+class PolyPoints:
+ """Base Class for lines and markers
+ - All methods are private.
+ """
+
+ def __init__(self, points, attr):
+ self.points = _numpy.array(points)
+ self.currentScale= (1,1)
+ self.currentShift= (0,0)
+ self.scaled = self.points
+ self.attributes = {}
+ self.attributes.update(self._attributes)
+ for name, value in attr.items():
+ if name not in self._attributes.keys():
+ raise KeyError, "Style attribute incorrect. Should be one of %s" % self._attributes.keys()
+ self.attributes[name] = value
+
+ def boundingBox(self):
+ if len(self.points) == 0:
+ # no curves to draw
+ # defaults to (-1,-1) and (1,1) but axis can be set in Draw
+ minXY= _numpy.array([-1,-1])
+ maxXY= _numpy.array([ 1, 1])
+ else:
+ minXY= _numpy.minimum.reduce(self.points)
+ maxXY= _numpy.maximum.reduce(self.points)
+ return minXY, maxXY
+
+ def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+ if len(self.points) == 0:
+ # no curves to draw
+ return
+ if (scale is not self.currentScale) or (shift is not self.currentShift):
+ # update point scaling
+ self.scaled = scale*self.points+shift
+ self.currentScale= scale
+ self.currentShift= shift
+ # else unchanged use the current scaling
+
+ def getLegend(self):
+ return self.attributes['legend']
+
+ def getClosestPoint(self, pntXY, pointScaled= True):
+ """Returns the index of closest point on the curve, pointXY, scaledXY, distance
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ if pointScaled == True:
+ #Using screen coords
+ p = self.scaled
+ pxy = self.currentScale * _numpy.array(pntXY)+ self.currentShift
+ else:
+ #Using user coords
+ p = self.points
+ pxy = _numpy.array(pntXY)
+ #determine distance for each point
+ d= _numpy.sqrt(_numpy.add.reduce((p-pxy)**2,1)) #sqrt(dx^2+dy^2)
+ pntIndex = _numpy.argmin(d)
+ dist = d[pntIndex]
+ return [pntIndex, self.points[pntIndex], self.scaled[pntIndex], dist]
+
+
+class PolyLine(PolyPoints):
+ """Class to define line type and style
+ - All methods except __init__ are private.
+ """
+
+ _attributes = {'colour': 'black',
+ 'width': 1,
+ 'style': wx.SOLID,
+ 'legend': ''}
+
+ def __init__(self, points, **attr):
+ """Creates PolyLine object
+ points - sequence (array, tuple or list) of (x,y) points making up line
+ **attr - key word attributes
+ Defaults:
+ 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
+ 'width'= 1, - Pen width
+ 'style'= wx.SOLID, - wx.Pen style
+ 'legend'= '' - Line Legend to display
+ """
+ PolyPoints.__init__(self, points, attr)
+
+ def draw(self, dc, printerScale, coord= None):
+ colour = self.attributes['colour']
+ width = self.attributes['width'] * printerScale
+ style= self.attributes['style']
+ pen = wx.Pen(wx.NamedColour(colour), width, style)
+ pen.SetCap(wx.CAP_BUTT)
+ dc.SetPen(pen)
+ if coord == None:
+ dc.DrawLines(self.scaled)
+ else:
+ dc.DrawLines(coord) # draw legend line
+
+ def getSymExtent(self, printerScale):
+ """Width and Height of Marker"""
+ h= self.attributes['width'] * printerScale
+ w= 5 * h
+ return (w,h)
+
+
+class PolyMarker(PolyPoints):
+ """Class to define marker type and style
+ - All methods except __init__ are private.
+ """
+
+ _attributes = {'colour': 'black',
+ 'width': 1,
+ 'size': 2,
+ 'fillcolour': None,
+ 'fillstyle': wx.SOLID,
+ 'marker': 'circle',
+ 'legend': ''}
+
+ def __init__(self, points, **attr):
+ """Creates PolyMarker object
+ points - sequence (array, tuple or list) of (x,y) points
+ **attr - key word attributes
+ Defaults:
+ 'colour'= 'black', - wx.Pen Colour any wx.NamedColour
+ 'width'= 1, - Pen width
+ 'size'= 2, - Marker size
+ 'fillcolour'= same as colour, - wx.Brush Colour any wx.NamedColour
+ 'fillstyle'= wx.SOLID, - wx.Brush fill style (use wx.TRANSPARENT for no fill)
+ 'marker'= 'circle' - Marker shape
+ 'legend'= '' - Marker Legend to display
+
+ Marker Shapes:
+ - 'circle'
+ - 'dot'
+ - 'square'
+ - 'triangle'
+ - 'triangle_down'
+ - 'cross'
+ - 'plus'
+ """
+
+ PolyPoints.__init__(self, points, attr)
+
+ def draw(self, dc, printerScale, coord= None):
+ colour = self.attributes['colour']
+ width = self.attributes['width'] * printerScale
+ size = self.attributes['size'] * printerScale
+ fillcolour = self.attributes['fillcolour']
+ fillstyle = self.attributes['fillstyle']
+ marker = self.attributes['marker']
+
+ dc.SetPen(wx.Pen(wx.NamedColour(colour), width))
+ if fillcolour:
+ dc.SetBrush(wx.Brush(wx.NamedColour(fillcolour),fillstyle))
+ else:
+ dc.SetBrush(wx.Brush(wx.NamedColour(colour), fillstyle))
+ if coord == None:
+ self._drawmarkers(dc, self.scaled, marker, size)
+ else:
+ self._drawmarkers(dc, coord, marker, size) # draw legend marker
+
+ def getSymExtent(self, printerScale):
+ """Width and Height of Marker"""
+ s= 5*self.attributes['size'] * printerScale
+ return (s,s)
+
+ def _drawmarkers(self, dc, coords, marker,size=1):
+ f = eval('self._' +marker)
+ f(dc, coords, size)
+
+ def _circle(self, dc, coords, size=1):
+ fact= 2.5*size
+ wh= 5.0*size
+ rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
+ rect[:,0:2]= coords-[fact,fact]
+ dc.DrawEllipseList(rect.astype(_numpy.int32))
+
+ def _dot(self, dc, coords, size=1):
+ dc.DrawPointList(coords)
+
+ def _square(self, dc, coords, size=1):
+ fact= 2.5*size
+ wh= 5.0*size
+ rect= _numpy.zeros((len(coords),4),_numpy.float)+[0.0,0.0,wh,wh]
+ rect[:,0:2]= coords-[fact,fact]
+ dc.DrawRectangleList(rect.astype(_numpy.int32))
+
+ def _triangle(self, dc, coords, size=1):
+ shape= [(-2.5*size,1.44*size), (2.5*size,1.44*size), (0.0,-2.88*size)]
+ poly= _numpy.repeat(coords,3)
+ poly.shape= (len(coords),3,2)
+ poly += shape
+ dc.DrawPolygonList(poly.astype(_numpy.int32))
+
+ def _triangle_down(self, dc, coords, size=1):
+ shape= [(-2.5*size,-1.44*size), (2.5*size,-1.44*size), (0.0,2.88*size)]
+ poly= _numpy.repeat(coords,3)
+ poly.shape= (len(coords),3,2)
+ poly += shape
+ dc.DrawPolygonList(poly.astype(_numpy.int32))
+
+ def _cross(self, dc, coords, size=1):
+ fact= 2.5*size
+ for f in [[-fact,-fact,fact,fact],[-fact,fact,fact,-fact]]:
+ lines= _numpy.concatenate((coords,coords),axis=1)+f
+ dc.DrawLineList(lines.astype(_numpy.int32))
+
+ def _plus(self, dc, coords, size=1):
+ fact= 2.5*size
+ for f in [[-fact,0,fact,0],[0,-fact,0,fact]]:
+ lines= _numpy.concatenate((coords,coords),axis=1)+f
+ dc.DrawLineList(lines.astype(_numpy.int32))
+
+class PlotGraphics:
+ """Container to hold PolyXXX objects and graph labels
+ - All methods except __init__ are private.
+ """
+
+ def __init__(self, objects, title='', xLabel='', yLabel= ''):
+ """Creates PlotGraphics object
+ objects - list of PolyXXX objects to make graph
+ title - title shown at top of graph
+ xLabel - label shown on x-axis
+ yLabel - label shown on y-axis
+ """
+ if type(objects) not in [list,tuple]:
+ raise TypeError, "objects argument should be list or tuple"
+ self.objects = objects
+ self.title= title
+ self.xLabel= xLabel
+ self.yLabel= yLabel
+
+ def boundingBox(self):
+ p1, p2 = self.objects[0].boundingBox()
+ for o in self.objects[1:]:
+ p1o, p2o = o.boundingBox()
+ p1 = _numpy.minimum(p1, p1o)
+ p2 = _numpy.maximum(p2, p2o)
+ return p1, p2
+
+ def scaleAndShift(self, scale=(1,1), shift=(0,0)):
+ for o in self.objects:
+ o.scaleAndShift(scale, shift)
+
+ def setPrinterScale(self, scale):
+ """Thickens up lines and markers only for printing"""
+ self.printerScale= scale
+
+ def setXLabel(self, xLabel= ''):
+ """Set the X axis label on the graph"""
+ self.xLabel= xLabel
+
+ def setYLabel(self, yLabel= ''):
+ """Set the Y axis label on the graph"""
+ self.yLabel= yLabel
+
+ def setTitle(self, title= ''):
+ """Set the title at the top of graph"""
+ self.title= title
+
+ def getXLabel(self):
+ """Get x axis label string"""
+ return self.xLabel
+
+ def getYLabel(self):
+ """Get y axis label string"""
+ return self.yLabel
+
+ def getTitle(self, title= ''):
+ """Get the title at the top of graph"""
+ return self.title
+
+ def draw(self, dc):
+ for o in self.objects:
+ #t=_time.clock() # profile info
+ o.draw(dc, self.printerScale)
+ #dt= _time.clock()-t
+ #print o, "time=", dt
+
+ def getSymExtent(self, printerScale):
+ """Get max width and height of lines and markers symbols for legend"""
+ symExt = self.objects[0].getSymExtent(printerScale)
+ for o in self.objects[1:]:
+ oSymExt = o.getSymExtent(printerScale)
+ symExt = _numpy.maximum(symExt, oSymExt)
+ return symExt
+
+ def getLegendNames(self):
+ """Returns list of legend names"""
+ lst = [None]*len(self)
+ for i in range(len(self)):
+ lst[i]= self.objects[i].getLegend()
+ return lst
+
+ def __len__(self):
+ return len(self.objects)
+
+ def __getitem__(self, item):
+ return self.objects[item]
+
+
+#-------------------------------------------------------------------------------
+# Main window that you will want to import into your application.
+
+class PlotCanvas(wx.Window):
+ """Subclass of a wx.Window to allow simple general plotting
+ of data with zoom, labels, and automatic axis scaling."""
+
+ def __init__(self, parent, id = -1, pos=wx.DefaultPosition,
+ size=wx.DefaultSize, style= wx.DEFAULT_FRAME_STYLE, name= ""):
+
+ self.use_persistence=False
+ self.alpha=0.3
+ self.decimation=10
+ self.decim_counter=0
+ """Constucts a window, which can be a child of a frame, dialog or
+ any other non-control window"""
+
+ wx.Window.__init__(self, parent, id, pos, size, style, name)
+ self.border = (1,1)
+
+ self.SetBackgroundColour("white")
+
+ # Create some mouse events for zooming
+ self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+ self.Bind(wx.EVT_LEFT_UP, self.OnMouseLeftUp)
+ self.Bind(wx.EVT_MOTION, self.OnMotion)
+ self.Bind(wx.EVT_LEFT_DCLICK, self.OnMouseDoubleClick)
+ self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseRightDown)
+
+ # set curser as cross-hairs
+ self.SetCursor(wx.CROSS_CURSOR)
+
+ # Things for printing
+ self.print_data = wx.PrintData()
+ self.print_data.SetPaperId(wx.PAPER_LETTER)
+ self.print_data.SetOrientation(wx.LANDSCAPE)
+ self.pageSetupData= wx.PageSetupDialogData()
+ self.pageSetupData.SetMarginBottomRight((25,25))
+ self.pageSetupData.SetMarginTopLeft((25,25))
+ self.pageSetupData.SetPrintData(self.print_data)
+ self.printerScale = 1
+ self.parent= parent
+
+ # Zooming variables
+ self._zoomInFactor = 0.5
+ self._zoomOutFactor = 2
+ self._zoomCorner1= _numpy.array([0.0, 0.0]) # left mouse down corner
+ self._zoomCorner2= _numpy.array([0.0, 0.0]) # left mouse up corner
+ self._zoomEnabled= False
+ self._hasDragged= False
+
+ # Drawing Variables
+ self.last_draw = None
+ self._pointScale= 1
+ self._pointShift= 0
+ self._xSpec= 'auto'
+ self._ySpec= 'auto'
+ self._gridEnabled= False
+ self._legendEnabled= False
+ self._xUseScopeTicks= False
+
+ # Fonts
+ self._fontCache = {}
+ self._fontSizeAxis= 10
+ self._fontSizeTitle= 15
+ self._fontSizeLegend= 7
+
+ # pointLabels
+ self._pointLabelEnabled= False
+ self.last_PointLabel= None
+ self._pointLabelFunc= None
+ self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
+
+ self.Bind(wx.EVT_PAINT, self.OnPaint)
+ self.Bind(wx.EVT_SIZE, self.OnSize)
+ # OnSize called to make sure the buffer is initialized.
+ # This might result in OnSize getting called twice on some
+ # platforms at initialization, but little harm done.
+ self.OnSize(None) # sets the initial size based on client size
+ # UNCONDITIONAL, needed to create self._Buffer
+
+
+ def set_use_persistence(self, enable):
+ self.use_persistence = enable
+
+ def set_persist_alpha(self, persist_alpha):
+ self.alpha = persist_alpha
+
+
+ # SaveFile
+ def SaveFile(self, fileName= ''):
+ """Saves the file to the type specified in the extension. If no file
+ name is specified a dialog box is provided. Returns True if sucessful,
+ otherwise False.
+
+ .bmp Save a Windows bitmap file.
+ .xbm Save an X bitmap file.
+ .xpm Save an XPM bitmap file.
+ .png Save a Portable Network Graphics file.
+ .jpg Save a Joint Photographic Experts Group file.
+ """
+ if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+ dlg1 = wx.FileDialog(
+ self,
+ "Choose a file with extension bmp, gif, xbm, xpm, png, or jpg", ".", "",
+ "BMP files (*.bmp)|*.bmp|XBM files (*.xbm)|*.xbm|XPM file (*.xpm)|*.xpm|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg",
+ wx.SAVE|wx.OVERWRITE_PROMPT
+ )
+ try:
+ while 1:
+ if dlg1.ShowModal() == wx.ID_OK:
+ fileName = dlg1.GetPath()
+ # Check for proper exension
+ if _string.lower(fileName[-3:]) not in ['bmp','xbm','xpm','png','jpg']:
+ dlg2 = wx.MessageDialog(self, 'File name extension\n'
+ 'must be one of\n'
+ 'bmp, xbm, xpm, png, or jpg',
+ 'File Name Error', wx.OK | wx.ICON_ERROR)
+ try:
+ dlg2.ShowModal()
+ finally:
+ dlg2.Destroy()
+ else:
+ break # now save file
+ else: # exit without saving
+ return False
+ finally:
+ dlg1.Destroy()
+
+ # File name has required extension
+ fType = _string.lower(fileName[-3:])
+ if fType == "bmp":
+ tp= wx.BITMAP_TYPE_BMP # Save a Windows bitmap file.
+ elif fType == "xbm":
+ tp= wx.BITMAP_TYPE_XBM # Save an X bitmap file.
+ elif fType == "xpm":
+ tp= wx.BITMAP_TYPE_XPM # Save an XPM bitmap file.
+ elif fType == "jpg":
+ tp= wx.BITMAP_TYPE_JPEG # Save a JPG file.
+ else:
+ tp= wx.BITMAP_TYPE_PNG # Save a PNG file.
+ # Save Bitmap
+ res= self._Buffer.SaveFile(fileName, tp)
+ return res
+
+ def PageSetup(self):
+ """Brings up the page setup dialog"""
+ data = self.pageSetupData
+ data.SetPrintData(self.print_data)
+ dlg = wx.PageSetupDialog(self.parent, data)
+ try:
+ if dlg.ShowModal() == wx.ID_OK:
+ data = dlg.GetPageSetupData() # returns wx.PageSetupDialogData
+ # updates page parameters from dialog
+ self.pageSetupData.SetMarginBottomRight(data.GetMarginBottomRight())
+ self.pageSetupData.SetMarginTopLeft(data.GetMarginTopLeft())
+ self.pageSetupData.SetPrintData(data.GetPrintData())
+ self.print_data=data.GetPrintData() # updates print_data
+ finally:
+ dlg.Destroy()
+
+ def Printout(self, paper=None):
+ """Print current plot."""
+ if paper != None:
+ self.print_data.SetPaperId(paper)
+ pdd = wx.PrintDialogData()
+ pdd.SetPrintData(self.print_data)
+ printer = wx.Printer(pdd)
+ out = PlotPrintout(self)
+ print_ok = printer.Print(self.parent, out)
+ if print_ok:
+ self.print_data = printer.GetPrintDialogData().GetPrintData()
+ out.Destroy()
+
+ def PrintPreview(self):
+ """Print-preview current plot."""
+ printout = PlotPrintout(self)
+ printout2 = PlotPrintout(self)
+ self.preview = wx.PrintPreview(printout, printout2, self.print_data)
+ if not self.preview.Ok():
+ wx.MessageDialog(self, "Print Preview failed.\n" \
+ "Check that default printer is configured\n", \
+ "Print error", wx.OK|wx.CENTRE).ShowModal()
+ self.preview.SetZoom(40)
+ # search up tree to find frame instance
+ frameInst= self
+ while not isinstance(frameInst, wx.Frame):
+ frameInst= frameInst.GetParent()
+ frame = wx.PreviewFrame(self.preview, frameInst, "Preview")
+ frame.Initialize()
+ frame.SetPosition(self.GetPosition())
+ frame.SetSize((600,550))
+ frame.Centre(wx.BOTH)
+ frame.Show(True)
+
+ def SetFontSizeAxis(self, point= 10):
+ """Set the tick and axis label font size (default is 10 point)"""
+ self._fontSizeAxis= point
+
+ def GetFontSizeAxis(self):
+ """Get current tick and axis label font size in points"""
+ return self._fontSizeAxis
+
+ def SetFontSizeTitle(self, point= 15):
+ """Set Title font size (default is 15 point)"""
+ self._fontSizeTitle= point
+
+ def GetFontSizeTitle(self):
+ """Get current Title font size in points"""
+ return self._fontSizeTitle
+
+ def SetFontSizeLegend(self, point= 7):
+ """Set Legend font size (default is 7 point)"""
+ self._fontSizeLegend= point
+
+ def GetFontSizeLegend(self):
+ """Get current Legend font size in points"""
+ return self._fontSizeLegend
+
+ def SetEnableZoom(self, value):
+ """Set True to enable zooming."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._zoomEnabled= value
+
+ def GetEnableZoom(self):
+ """True if zooming enabled."""
+ return self._zoomEnabled
+
+ def SetEnableGrid(self, value):
+ """Set True to enable grid."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._gridEnabled= value
+ self.Redraw()
+
+ def GetEnableGrid(self):
+ """True if grid enabled."""
+ return self._gridEnabled
+
+ def SetEnableLegend(self, value):
+ """Set True to enable legend."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._legendEnabled= value
+ self.Redraw()
+
+ def GetEnableLegend(self):
+ """True if Legend enabled."""
+ return self._legendEnabled
+
+ def SetEnablePointLabel(self, value):
+ """Set True to enable pointLabel."""
+ if value not in [True,False]:
+ raise TypeError, "Value should be True or False"
+ self._pointLabelEnabled= value
+ self.Redraw() #will erase existing pointLabel if present
+ self.last_PointLabel = None
+
+ def GetEnablePointLabel(self):
+ """True if pointLabel enabled."""
+ return self._pointLabelEnabled
+
+ def SetPointLabelFunc(self, func):
+ """Sets the function with custom code for pointLabel drawing
+ ******** more info needed ***************
+ """
+ self._pointLabelFunc= func
+
+ def GetPointLabelFunc(self):
+ """Returns pointLabel Drawing Function"""
+ return self._pointLabelFunc
+
+ def Reset(self):
+ """Unzoom the plot."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ self.Draw(self.last_draw[0])
+
+ def ScrollRight(self, units):
+ """Move view right number of axis units."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ xAxis= (xAxis[0]+units, xAxis[1]+units)
+ self.Draw(graphics,xAxis,yAxis)
+
+ def ScrollUp(self, units):
+ """Move view up number of axis units."""
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ yAxis= (yAxis[0]+units, yAxis[1]+units)
+ self.Draw(graphics,xAxis,yAxis)
+
+ def GetXY(self,event):
+ """Takes a mouse event and returns the XY user axis values."""
+ x,y= self.PositionScreenToUser(event.GetPosition())
+ return x,y
+
+ def PositionUserToScreen(self, pntXY):
+ """Converts User position to Screen Coordinates"""
+ userPos= _numpy.array(pntXY)
+ x,y= userPos * self._pointScale + self._pointShift
+ return x,y
+
+ def PositionScreenToUser(self, pntXY):
+ """Converts Screen position to User Coordinates"""
+ screenPos= _numpy.array(pntXY)
+ x,y= (screenPos-self._pointShift)/self._pointScale
+ return x,y
+
+ def SetXSpec(self, type= 'auto'):
+ """xSpec- defines x axis type. Can be 'none', 'min' or 'auto'
+ where:
+ 'none' - shows no axis or tick mark values
+ 'min' - shows min bounding box values
+ 'auto' - rounds axis range to sensible values
+ """
+ self._xSpec= type
+
+ def SetYSpec(self, type= 'auto'):
+ """ySpec- defines x axis type. Can be 'none', 'min' or 'auto'
+ where:
+ 'none' - shows no axis or tick mark values
+ 'min' - shows min bounding box values
+ 'auto' - rounds axis range to sensible values
+ """
+ self._ySpec= type
+
+ def GetXSpec(self):
+ """Returns current XSpec for axis"""
+ return self._xSpec
+
+ def GetYSpec(self):
+ """Returns current YSpec for axis"""
+ return self._ySpec
+
+ def GetXMaxRange(self):
+ """Returns (minX, maxX) x-axis range for displayed graph"""
+ graphics= self.last_draw[0]
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+ return xAxis
+
+ def GetYMaxRange(self):
+ """Returns (minY, maxY) y-axis range for displayed graph"""
+ graphics= self.last_draw[0]
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+ return yAxis
+
+ def GetXCurrentRange(self):
+ """Returns (minX, maxX) x-axis for currently displayed portion of graph"""
+ return self.last_draw[1]
+
+ def GetYCurrentRange(self):
+ """Returns (minY, maxY) y-axis for currently displayed portion of graph"""
+ return self.last_draw[2]
+
+ def SetXUseScopeTicks(self, v=False):
+ """Always 10 divisions, no labels"""
+ self._xUseScopeTicks = v
+
+ def GetXUseScopeTicks(self):
+ return self._xUseScopeTicks
+
+ def Draw(self, graphics, xAxis = None, yAxis = None, dc = None, step=None):
+ """Draw objects in graphics with specified x and y axis.
+ graphics- instance of PlotGraphics with list of PolyXXX objects
+ xAxis - tuple with (min, max) axis range to view
+ yAxis - same as xAxis
+ dc - drawing context - doesn't have to be specified.
+ If it's not, the offscreen buffer is used
+ """
+ # check Axis is either tuple or none
+ if type(xAxis) not in [type(None),tuple]:
+ raise TypeError, "xAxis should be None or (minX,maxX)"
+ if type(yAxis) not in [type(None),tuple]:
+ raise TypeError, "yAxis should be None or (minY,maxY)"
+
+ # check case for axis = (a,b) where a==b caused by improper zooms
+ if xAxis != None:
+ if xAxis[0] == xAxis[1]:
+ return
+ if yAxis != None:
+ if yAxis[0] == yAxis[1]:
+ return
+
+ if dc == None:
+ # sets new dc and clears it
+ if self.use_persistence:
+ dc = wx.MemoryDC()
+ dc.SelectObject(self._Buffer)
+ dc.Clear()
+ else:
+ dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
+ dc.Clear()
+
+ dc.BeginDrawing()
+ # dc.Clear()
+
+
+
+ # set font size for every thing but title and legend
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+
+ # sizes axis to axis type, create lower left and upper right corners of plot
+ if xAxis == None or yAxis == None:
+ # One or both axis not specified in Draw
+ p1, p2 = graphics.boundingBox() # min, max points of graphics
+ if xAxis == None:
+ xAxis = self._axisInterval(self._xSpec, p1[0], p2[0]) # in user units
+ if yAxis == None:
+ yAxis = self._axisInterval(self._ySpec, p1[1], p2[1])
+ # Adjust bounding box for axis spec
+ p1[0],p1[1] = xAxis[0], yAxis[0] # lower left corner user scale (xmin,ymin)
+ p2[0],p2[1] = xAxis[1], yAxis[1] # upper right corner user scale (xmax,ymax)
+ else:
+ # Both axis specified in Draw
+ p1= _numpy.array([xAxis[0], yAxis[0]]) # lower left corner user scale (xmin,ymin)
+ p2= _numpy.array([xAxis[1], yAxis[1]]) # upper right corner user scale (xmax,ymax)
+
+ self.last_draw = (graphics, xAxis, yAxis) # saves most recient values
+
+ if False:
+ ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
+ #dc.SetPen(wx.Pen(wx.BLACK))
+ dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) ) #wx.SOLID wx.TRANSPARENT ) )
+ #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT
+ dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
+ #dc.SetBrush(wx.Brush( wx.WHITE, wx.SOLID ) )
+ #dc.SetLogicalFunction(wx.COPY)
+
+ # Get ticks and textExtents for axis if required
+ if self._xSpec is not 'none':
+ if self._xUseScopeTicks:
+ xticks = self._scope_ticks(xAxis[0], xAxis[1])
+ else:
+ xticks = self._ticks(xAxis[0], xAxis[1])
+ xTextExtent = dc.GetTextExtent(xticks[-1][1])# w h of x axis text last number on axis
+ else:
+ xticks = None
+ xTextExtent= (0,0) # No text for ticks
+ if self._ySpec is not 'none':
+ yticks = self._ticks(yAxis[0], yAxis[1], step)
+ yTextExtentBottom= dc.GetTextExtent(yticks[0][1])
+ yTextExtentTop = dc.GetTextExtent(yticks[-1][1])
+ yTextExtent= (max(yTextExtentBottom[0],yTextExtentTop[0]),
+ max(yTextExtentBottom[1],yTextExtentTop[1]))
+ else:
+ yticks = None
+ yTextExtent= (0,0) # No text for ticks
+
+ # TextExtents for Title and Axis Labels
+ titleWH, xLabelWH, yLabelWH= self._titleLablesWH(dc, graphics)
+
+ # TextExtents for Legend
+ legendBoxWH, legendSymExt, legendTextExt = self._legendWH(dc, graphics)
+
+ # room around graph area
+ rhsW= max(xTextExtent[0], legendBoxWH[0]) # use larger of number width or legend width
+ lhsW= yTextExtent[0]+ yLabelWH[1]
+ bottomH= max(xTextExtent[1], yTextExtent[1]/2.)+ xLabelWH[1]
+ topH= yTextExtent[1]/2. + titleWH[1]
+ textSize_scale= _numpy.array([rhsW+lhsW,bottomH+topH]) # make plot area smaller by text size
+ textSize_shift= _numpy.array([lhsW, bottomH]) # shift plot area by this amount
+
+ # drawing title and labels text
+ dc.SetFont(self._getFont(self._fontSizeTitle))
+ titlePos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- titleWH[0]/2.,
+ self.plotbox_origin[1]- self.plotbox_size[1])
+ dc.DrawText(graphics.getTitle(),titlePos[0],titlePos[1])
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ xLabelPos= (self.plotbox_origin[0]+ lhsW + (self.plotbox_size[0]-lhsW-rhsW)/2.- xLabelWH[0]/2.,
+ self.plotbox_origin[1]- xLabelWH[1])
+ dc.DrawText(graphics.getXLabel(),xLabelPos[0],xLabelPos[1])
+ yLabelPos= (self.plotbox_origin[0],
+ self.plotbox_origin[1]- bottomH- (self.plotbox_size[1]-bottomH-topH)/2.+ yLabelWH[0]/2.)
+ if graphics.getYLabel(): # bug fix for Linux
+ dc.DrawRotatedText(graphics.getYLabel(),yLabelPos[0],yLabelPos[1],90)
+
+ # drawing legend makers and text
+ if self._legendEnabled:
+ self._drawLegend(dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt)
+
+ # allow for scaling and shifting plotted points
+ scale = (self.plotbox_size-textSize_scale) / (p2-p1)* _numpy.array((1,-1))
+ shift = -p1*scale + self.plotbox_origin + textSize_shift * _numpy.array((1,-1))
+ self._pointScale= scale # make available for mouse events
+ self._pointShift= shift
+
+ #dc.SetLogicalFunction(wx.INVERT) #wx.XOR wx.INVERT
+ self._drawAxes(dc, p1, p2, scale, shift, xticks, yticks)
+ #dc.SetLogicalFunction(wx.COPY)
+
+ graphics.scaleAndShift(scale, shift)
+ graphics.setPrinterScale(self.printerScale) # thicken up lines and markers if printing
+
+ # set clipping area so drawing does not occur outside axis box
+ ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(p1, p2)
+ dc.SetClippingRegion(ptx,pty,rectWidth,rectHeight)
+ # Draw the lines and markers
+ #start = _time.clock()
+
+ graphics.draw(dc)
+ # print "entire graphics drawing took: %f second"%(_time.clock() - start)
+ # remove the clipping region
+ dc.DestroyClippingRegion()
+ dc.EndDrawing()
+
+
+ if self.use_persistence:
+ dc=None
+ self._Buffer.CopyToBuffer(self._Bufferarray) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
+ ## do the IIR filter
+ alpha_int=int(float(self.alpha*256))
+ if True:
+ _numpy.add(self._Bufferarray,0,self._Buffer3array)
+ _numpy.multiply(self._Buffer3array,alpha_int,self._Buffer3array)
+ _numpy.multiply(self._Buffer2array,(256-alpha_int),self._Buffer2array)
+ _numpy.add(self._Buffer3array,self._Buffer2array,self._Buffer2array)
+ _numpy.right_shift(self._Buffer2array,8,self._Buffer2array)
+ elif False:
+ self._Buffer2array=(self._Bufferarray.astype(_numpy.uint32) *alpha_int + self._Buffer2array*(256-alpha_int)).__rshift__(8)
+ elif False:
+ self._Buffer2array *=(256-alpha_int)
+ self._Buffer2array +=self._Bufferarray.astype(_numpy.uint32)*alpha_int
+ self._Buffer2array /=256
+
+ ##copy back to image buffer
+ self._Buffer2.CopyFromBuffer(self._Buffer2array.astype(_numpy.uint8)) #, format=wx.BitmapBufferFormat_RGB, stride=-1)
+
+ #draw to the screen
+ #self.decim_counter=self.decim_counter+1
+ if True: #self.decim_counter>self.decimation:
+ #self.decim_counter=0
+ dc2 = wx.ClientDC( self )
+ dc2.BeginDrawing()
+ dc2.DrawBitmap(self._Buffer2, 0, 0, False)
+ #dc2.DrawBitmap(self._Buffer, 0, 0, False)
+ dc2.EndDrawing()
+
+ def Redraw(self, dc= None):
+ """Redraw the existing plot."""
+ if self.last_draw is not None:
+ graphics, xAxis, yAxis= self.last_draw
+ self.Draw(graphics,xAxis,yAxis,dc)
+
+ def Clear(self):
+ """Erase the window."""
+ self.last_PointLabel = None #reset pointLabel
+ dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
+ dc.Clear()
+ self.last_draw = None
+
+ def Zoom(self, Center, Ratio):
+ """ Zoom on the plot
+ Centers on the X,Y coords given in Center
+ Zooms by the Ratio = (Xratio, Yratio) given
+ """
+ self.last_PointLabel = None #reset maker
+ x,y = Center
+ if self.last_draw != None:
+ (graphics, xAxis, yAxis) = self.last_draw
+ w = (xAxis[1] - xAxis[0]) * Ratio[0]
+ h = (yAxis[1] - yAxis[0]) * Ratio[1]
+ xAxis = ( x - w/2, x + w/2 )
+ yAxis = ( y - h/2, y + h/2 )
+ self.Draw(graphics, xAxis, yAxis)
+
+ def GetClosestPoints(self, pntXY, pointScaled= True):
+ """Returns list with
+ [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ list for each curve.
+ Returns [] if no curves are being plotted.
+
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ if self.last_draw == None:
+ #no graph available
+ return []
+ graphics, xAxis, yAxis= self.last_draw
+ l = []
+ for curveNum,obj in enumerate(graphics):
+ #check there are points in the curve
+ if len(obj.points) == 0:
+ continue #go to next obj
+ #[curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ cn = [curveNum]+ [obj.getLegend()]+ obj.getClosestPoint( pntXY, pointScaled)
+ l.append(cn)
+ return l
+
+ def GetClosetPoint(self, pntXY, pointScaled= True):
+ """Returns list with
+ [curveNumber, legend, index of closest point, pointXY, scaledXY, distance]
+ list for only the closest curve.
+ Returns [] if no curves are being plotted.
+
+ x, y in user coords
+ if pointScaled == True based on screen coords
+ if pointScaled == False based on user coords
+ """
+ #closest points on screen based on screen scaling (pointScaled= True)
+ #list [curveNumber, index, pointXY, scaledXY, distance] for each curve
+ closestPts= self.GetClosestPoints(pntXY, pointScaled)
+ if closestPts == []:
+ return [] #no graph present
+ #find one with least distance
+ dists = [c[-1] for c in closestPts]
+ mdist = min(dists) #Min dist
+ i = dists.index(mdist) #index for min dist
+ return closestPts[i] #this is the closest point on closest curve
+
+ def UpdatePointLabel(self, mDataDict):
+ """Updates the pointLabel point on screen with data contained in
+ mDataDict.
+
+ mDataDict will be passed to your function set by
+ SetPointLabelFunc. It can contain anything you
+ want to display on the screen at the scaledXY point
+ you specify.
+
+ This function can be called from parent window with onClick,
+ onMotion events etc.
+ """
+ if self.last_PointLabel != None:
+ #compare pointXY
+ if mDataDict["pointXY"] != self.last_PointLabel["pointXY"]:
+ #closest changed
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self._drawPointLabel(mDataDict) #plot new
+ else:
+ #just plot new with no erase
+ self._drawPointLabel(mDataDict) #plot new
+ #save for next erase
+ self.last_PointLabel = mDataDict
+
+ # event handlers **********************************
+ def OnMotion(self, event):
+ if self._zoomEnabled and event.LeftIsDown():
+ if self._hasDragged:
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+ else:
+ self._hasDragged= True
+ self._zoomCorner2[0], self._zoomCorner2[1] = self.GetXY(event)
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # add new
+
+ def OnMouseLeftDown(self,event):
+ self._zoomCorner1[0], self._zoomCorner1[1]= self.GetXY(event)
+
+ def OnMouseLeftUp(self, event):
+ if self._zoomEnabled:
+ if self._hasDragged == True:
+ self._drawRubberBand(self._zoomCorner1, self._zoomCorner2) # remove old
+ self._zoomCorner2[0], self._zoomCorner2[1]= self.GetXY(event)
+ self._hasDragged = False # reset flag
+ minX, minY= _numpy.minimum( self._zoomCorner1, self._zoomCorner2)
+ maxX, maxY= _numpy.maximum( self._zoomCorner1, self._zoomCorner2)
+ self.last_PointLabel = None #reset pointLabel
+ if self.last_draw != None:
+ self.Draw(self.last_draw[0], xAxis = (minX,maxX), yAxis = (minY,maxY), dc = None)
+ #else: # A box has not been drawn, zoom in on a point
+ ## this interfered with the double click, so I've disables it.
+ # X,Y = self.GetXY(event)
+ # self.Zoom( (X,Y), (self._zoomInFactor,self._zoomInFactor) )
+
+ def OnMouseDoubleClick(self,event):
+ if self._zoomEnabled:
+ self.Reset()
+
+ def OnMouseRightDown(self,event):
+ if self._zoomEnabled:
+ X,Y = self.GetXY(event)
+ self.Zoom( (X,Y), (self._zoomOutFactor, self._zoomOutFactor) )
+
+ def OnPaint(self, event):
+ # All that is needed here is to draw the buffer to screen
+ if self.last_PointLabel != None:
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self.last_PointLabel = None
+
+ #paint current buffer to screen
+ dc = wx.BufferedPaintDC(self, self._Buffer)
+
+ def OnSize(self,event):
+ # The Buffer init is done here, to make sure the buffer is always
+ # the same size as the Window
+ Size = self.GetClientSize()
+
+ # Make new offscreen bitmap: this bitmap will always have the
+ # current drawing in it, so it can be used to save the image to
+ # a file, or whatever.
+ self._Buffer = wx.EmptyBitmap(Size[0],Size[1],24)
+
+
+ if True: #self.use_persistence:
+ #self._Bufferarray = _numpy.zeros((Size[0], Size[1],3), dtype=_numpy.uint8)
+ self._Bufferarray = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint8)
+
+ # Make new second offscreen bitmap: this bitmap will always have the
+ # last drawing in it, so it can be used to do display time dependent processing
+ # like averaging (IIR) or show differences between updates
+ self._Buffer2 = wx.EmptyBitmap(Size[0],Size[1],24)
+ # now the extra buffers for the IIR processing
+ # note the different datatype uint32
+ self._Buffer2array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
+ self._Buffer3array = _numpy.zeros((Size[0]* Size[1]*3), dtype=_numpy.uint32) #dtype=_numpy.float
+ # optional you can set the ufunct buffer size to improve speed
+ #_numpy.setbufsize(16*((Size[0]* Size[1]*3)/16 +1))
+ self._setSize()
+
+ self.last_PointLabel = None #reset pointLabel
+
+ if self.last_draw is None:
+ self.Clear()
+ else:
+ graphics, xSpec, ySpec = self.last_draw
+ self.Draw(graphics,xSpec,ySpec)
+
+ def OnLeave(self, event):
+ """Used to erase pointLabel when mouse outside window"""
+ if self.last_PointLabel != None:
+ self._drawPointLabel(self.last_PointLabel) #erase old
+ self.last_PointLabel = None
+
+
+ # Private Methods **************************************************
+ def _setSize(self, width=None, height=None):
+ """DC width and height."""
+ if width == None:
+ (self.width,self.height) = self.GetClientSize()
+ else:
+ self.width, self.height= width,height
+ self.plotbox_size = 0.97*_numpy.array([self.width, self.height])
+ xo = 0.5*(self.width-self.plotbox_size[0])
+ yo = self.height-0.5*(self.height-self.plotbox_size[1])
+ self.plotbox_origin = _numpy.array([xo, yo])
+
+ def _setPrinterScale(self, scale):
+ """Used to thicken lines and increase marker size for print out."""
+ # line thickness on printer is very thin at 600 dot/in. Markers small
+ self.printerScale= scale
+
+ def _printDraw(self, printDC):
+ """Used for printing."""
+ if self.last_draw != None:
+ graphics, xSpec, ySpec= self.last_draw
+ self.Draw(graphics,xSpec,ySpec,printDC)
+
+ def _drawPointLabel(self, mDataDict):
+ """Draws and erases pointLabels"""
+ width = self._Buffer.GetWidth()
+ height = self._Buffer.GetHeight()
+ tmp_Buffer = wx.EmptyBitmap(width,height)
+ dcs = wx.MemoryDC()
+ dcs.SelectObject(tmp_Buffer)
+ dcs.Clear()
+ dcs.BeginDrawing()
+ self._pointLabelFunc(dcs,mDataDict) #custom user pointLabel function
+ dcs.EndDrawing()
+
+ dc = wx.ClientDC( self )
+ #this will erase if called twice
+ dc.Blit(0, 0, width, height, dcs, 0, 0, wx.EQUIV) #(NOT src) XOR dst
+
+
+ def _drawLegend(self,dc,graphics,rhsW,topH,legendBoxWH, legendSymExt, legendTextExt):
+ """Draws legend symbols and text"""
+ # top right hand corner of graph box is ref corner
+ trhc= self.plotbox_origin+ (self.plotbox_size-[rhsW,topH])*[1,-1]
+ legendLHS= .091* legendBoxWH[0] # border space between legend sym and graph box
+ lineHeight= max(legendSymExt[1], legendTextExt[1]) * 1.1 #1.1 used as space between lines
+ dc.SetFont(self._getFont(self._fontSizeLegend))
+ for i in range(len(graphics)):
+ o = graphics[i]
+ s= i*lineHeight
+ if isinstance(o,PolyMarker):
+ # draw marker with legend
+ pnt= (trhc[0]+legendLHS+legendSymExt[0]/2., trhc[1]+s+lineHeight/2.)
+ o.draw(dc, self.printerScale, coord= _numpy.array([pnt]))
+ elif isinstance(o,PolyLine):
+ # draw line with legend
+ pnt1= (trhc[0]+legendLHS, trhc[1]+s+lineHeight/2.)
+ pnt2= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.)
+ o.draw(dc, self.printerScale, coord= _numpy.array([pnt1,pnt2]))
+ else:
+ raise TypeError, "object is neither PolyMarker or PolyLine instance"
+ # draw legend txt
+ pnt= (trhc[0]+legendLHS+legendSymExt[0], trhc[1]+s+lineHeight/2.-legendTextExt[1]/2)
+ dc.DrawText(o.getLegend(),pnt[0],pnt[1])
+ dc.SetFont(self._getFont(self._fontSizeAxis)) # reset
+
+ def _titleLablesWH(self, dc, graphics):
+ """Draws Title and labels and returns width and height for each"""
+ # TextExtents for Title and Axis Labels
+ dc.SetFont(self._getFont(self._fontSizeTitle))
+ title= graphics.getTitle()
+ titleWH= dc.GetTextExtent(title)
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ xLabel, yLabel= graphics.getXLabel(),graphics.getYLabel()
+ xLabelWH= dc.GetTextExtent(xLabel)
+ yLabelWH= dc.GetTextExtent(yLabel)
+ return titleWH, xLabelWH, yLabelWH
+
+ def _legendWH(self, dc, graphics):
+ """Returns the size in screen units for legend box"""
+ if self._legendEnabled != True:
+ legendBoxWH= symExt= txtExt= (0,0)
+ else:
+ # find max symbol size
+ symExt= graphics.getSymExtent(self.printerScale)
+ # find max legend text extent
+ dc.SetFont(self._getFont(self._fontSizeLegend))
+ txtList= graphics.getLegendNames()
+ txtExt= dc.GetTextExtent(txtList[0])
+ for txt in graphics.getLegendNames()[1:]:
+ txtExt= _numpy.maximum(txtExt,dc.GetTextExtent(txt))
+ maxW= symExt[0]+txtExt[0]
+ maxH= max(symExt[1],txtExt[1])
+ # padding .1 for lhs of legend box and space between lines
+ maxW= maxW* 1.1
+ maxH= maxH* 1.1 * len(txtList)
+ dc.SetFont(self._getFont(self._fontSizeAxis))
+ legendBoxWH= (maxW,maxH)
+ return (legendBoxWH, symExt, txtExt)
+
+ def _drawRubberBand(self, corner1, corner2):
+ """Draws/erases rect box from corner1 to corner2"""
+ ptx,pty,rectWidth,rectHeight= self._point2ClientCoord(corner1, corner2)
+ # draw rectangle
+ dc = wx.ClientDC( self )
+ dc.BeginDrawing()
+ dc.SetPen(wx.Pen(wx.BLACK))
+ dc.SetBrush(wx.Brush( wx.WHITE, wx.TRANSPARENT ) )
+ dc.SetLogicalFunction(wx.INVERT)
+ dc.DrawRectangle( ptx,pty, rectWidth,rectHeight)
+ dc.SetLogicalFunction(wx.COPY)
+ dc.EndDrawing()
+
+ def _getFont(self,size):
+ """Take font size, adjusts if printing and returns wx.Font"""
+ s = size*self.printerScale
+ of = self.GetFont()
+ # Linux speed up to get font from cache rather than X font server
+ key = (int(s), of.GetFamily (), of.GetStyle (), of.GetWeight ())
+ font = self._fontCache.get (key, None)
+ if font:
+ return font # yeah! cache hit
+ else:
+ font = wx.Font(int(s), of.GetFamily(), of.GetStyle(), of.GetWeight())
+ self._fontCache[key] = font
+ return font
+
+
+ def _point2ClientCoord(self, corner1, corner2):
+ """Converts user point coords to client screen int coords x,y,width,height"""
+ c1= _numpy.array(corner1)
+ c2= _numpy.array(corner2)
+ # convert to screen coords
+ pt1= c1*self._pointScale+self._pointShift
+ pt2= c2*self._pointScale+self._pointShift
+ # make height and width positive
+ pul= _numpy.minimum(pt1,pt2) # Upper left corner
+ plr= _numpy.maximum(pt1,pt2) # Lower right corner
+ rectWidth, rectHeight= plr-pul
+ ptx,pty= pul
+ return ptx, pty, rectWidth, rectHeight
+
+ def _axisInterval(self, spec, lower, upper):
+ """Returns sensible axis range for given spec"""
+ if spec == 'none' or spec == 'min':
+ if lower == upper:
+ return lower-0.5, upper+0.5
+ else:
+ return lower, upper
+ elif spec == 'auto':
+ range = upper-lower
+ # if range == 0.:
+ if abs(range) < 1e-36:
+ return lower-0.5, upper+0.5
+ log = _numpy.log10(range)
+ power = _numpy.floor(log)
+ fraction = log-power
+ if fraction <= 0.05:
+ power = power-1
+ grid = 10.**power
+ lower = lower - lower % grid
+ mod = upper % grid
+ if mod != 0:
+ upper = upper - mod + grid
+ return lower, upper
+ elif type(spec) == type(()):
+ lower, upper = spec
+ if lower <= upper:
+ return lower, upper
+ else:
+ return upper, lower
+ else:
+ raise ValueError, str(spec) + ': illegal axis specification'
+
+ def _drawAxes(self, dc, p1, p2, scale, shift, xticks, yticks):
+
+ penWidth= self.printerScale # increases thickness for printing only
+ dc.SetPen(wx.Pen(wx.NamedColour('BLACK'), penWidth))
+
+ # set length of tick marks--long ones make grid
+ if self._gridEnabled:
+ x,y,width,height= self._point2ClientCoord(p1,p2)
+ yTickLength= width/2.0 +1
+ xTickLength= height/2.0 +1
+ else:
+ yTickLength= 3 * self.printerScale # lengthens lines for printing
+ xTickLength= 3 * self.printerScale
+
+ if self._xSpec is not 'none':
+ lower, upper = p1[0],p2[0]
+ text = 1
+ for y, d in [(p1[1], -xTickLength), (p2[1], xTickLength)]: # miny, maxy and tick lengths
+ a1 = scale*_numpy.array([lower, y])+shift
+ a2 = scale*_numpy.array([upper, y])+shift
+ dc.DrawLine(a1[0],a1[1],a2[0],a2[1]) # draws upper and lower axis line
+ for x, label in xticks:
+ pt = scale*_numpy.array([x, y])+shift
+ dc.DrawLine(pt[0],pt[1],pt[0],pt[1] + d) # draws tick mark d units
+ if text:
+ dc.DrawText(label,pt[0],pt[1])
+ text = 0 # axis values not drawn on top side
+
+ if self._ySpec is not 'none':
+ lower, upper = p1[1],p2[1]
+ text = 1
+ h = dc.GetCharHeight()
+ for x, d in [(p1[0], -yTickLength), (p2[0], yTickLength)]:
+ a1 = scale*_numpy.array([x, lower])+shift
+ a2 = scale*_numpy.array([x, upper])+shift
+ dc.DrawLine(a1[0],a1[1],a2[0],a2[1])
+ for y, label in yticks:
+ pt = scale*_numpy.array([x, y])+shift
+ dc.DrawLine(pt[0],pt[1],pt[0]-d,pt[1])
+ if text:
+ dc.DrawText(label,pt[0]-dc.GetTextExtent(label)[0],
+ pt[1]-0.5*h)
+ text = 0 # axis values not drawn on right side
+
+ def _ticks(self, lower, upper, step=None):
+ ideal = (upper-lower)/7.
+ log = _numpy.log10(ideal)
+ power = _numpy.floor(log)
+ fraction = log-power
+ factor = 1.
+ error = fraction
+ for f, lf in self._multiples:
+ e = _numpy.fabs(fraction-lf)
+ if e < error:
+ error = e
+ factor = f
+ grid = factor * 10.**power
+ if power > 4 or power < -4:
+ format = '%+7.1e'
+ elif power >= 0:
+ digits = max(1, int(power))
+ format = '%' + `digits`+'.0f'
+ else:
+ digits = -int(power)
+ format = '%'+`digits+2`+'.'+`digits`+'f'
+ #force grid when step is not None
+ if step is not None: grid = step
+ ticks = []
+ t = -grid*_numpy.floor(-lower/grid)
+ while t <= upper:
+ if t == -0: t = 0 #remove neg zero condition
+ ticks.append( (t, format % (t,)) )
+ t = t + grid
+ return ticks
+
+ def _scope_ticks (self, lower, upper):
+ '''Always 10 divisions, no labels'''
+ grid = (upper - lower) / 10.0
+ ticks = []
+ t = lower
+ while t <= upper:
+ ticks.append( (t, ""))
+ t = t + grid
+ return ticks
+
+ _multiples = [(2., _numpy.log10(2.)), (5., _numpy.log10(5.))]
+
+
+#-------------------------------------------------------------------------------
+# Used to layout the printer page
+
+class PlotPrintout(wx.Printout):
+ """Controls how the plot is made in printing and previewing"""
+ # Do not change method names in this class,
+ # we have to override wx.Printout methods here!
+ def __init__(self, graph):
+ """graph is instance of plotCanvas to be printed or previewed"""
+ wx.Printout.__init__(self)
+ self.graph = graph
+
+ def HasPage(self, page):
+ if page == 1:
+ return True
+ else:
+ return False
+
+ def GetPageInfo(self):
+ return (1, 1, 1, 1) # disable page numbers
+
+ def OnPrintPage(self, page):
+ dc = self.GetDC() # allows using floats for certain functions
+## print "PPI Printer",self.GetPPIPrinter()
+## print "PPI Screen", self.GetPPIScreen()
+## print "DC GetSize", dc.GetSize()
+## print "GetPageSizePixels", self.GetPageSizePixels()
+ # Note PPIScreen does not give the correct number
+ # Calulate everything for printer and then scale for preview
+ PPIPrinter= self.GetPPIPrinter() # printer dots/inch (w,h)
+ #PPIScreen= self.GetPPIScreen() # screen dots/inch (w,h)
+ dcSize= dc.GetSize() # DC size
+ pageSize= self.GetPageSizePixels() # page size in terms of pixcels
+ clientDcSize= self.graph.GetClientSize()
+
+ # find what the margins are (mm)
+ margLeftSize,margTopSize= self.graph.pageSetupData.GetMarginTopLeft()
+ margRightSize, margBottomSize= self.graph.pageSetupData.GetMarginBottomRight()
+
+ # calculate offset and scale for dc
+ pixLeft= margLeftSize*PPIPrinter[0]/25.4 # mm*(dots/in)/(mm/in)
+ pixRight= margRightSize*PPIPrinter[0]/25.4
+ pixTop= margTopSize*PPIPrinter[1]/25.4
+ pixBottom= margBottomSize*PPIPrinter[1]/25.4
+
+ plotAreaW= pageSize[0]-(pixLeft+pixRight)
+ plotAreaH= pageSize[1]-(pixTop+pixBottom)
+
+ # ratio offset and scale to screen size if preview
+ if self.IsPreview():
+ ratioW= float(dcSize[0])/pageSize[0]
+ ratioH= float(dcSize[1])/pageSize[1]
+ pixLeft *= ratioW
+ pixTop *= ratioH
+ plotAreaW *= ratioW
+ plotAreaH *= ratioH
+
+ # rescale plot to page or preview plot area
+ self.graph._setSize(plotAreaW,plotAreaH)
+
+ # Set offset and scale
+ dc.SetDeviceOrigin(pixLeft,pixTop)
+
+ # Thicken up pens and increase marker size for printing
+ ratioW= float(plotAreaW)/clientDcSize[0]
+ ratioH= float(plotAreaH)/clientDcSize[1]
+ aveScale= (ratioW+ratioH)/2
+ self.graph._setPrinterScale(aveScale) # tickens up pens for printing
+
+ self.graph._printDraw(dc)
+ # rescale back to original
+ self.graph._setSize()
+ self.graph._setPrinterScale(1)
+ self.graph.Redraw() #to get point label scale and shift correct
+
+ return True
+
+
+
+
+#---------------------------------------------------------------------------
+# if running standalone...
+#
+# ...a sample implementation using the above
+#
+
+def _draw1Objects():
+ # 100 points sin function, plotted as green circles
+ data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
+ data1.shape = (100, 2)
+ data1[:,1] = _numpy.sin(data1[:,0])
+ markers1 = PolyMarker(data1, legend='Green Markers', colour='green', marker='circle',size=1)
+
+ # 50 points cos function, plotted as red line
+ data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
+ data1.shape = (50,2)
+ data1[:,1] = _numpy.cos(data1[:,0])
+ lines = PolyLine(data1, legend= 'Red Line', colour='red')
+
+ # A few more points...
+ pi = _numpy.pi
+ markers2 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+ (3.*pi/4., -1)], legend='Cross Legend', colour='blue',
+ marker='cross')
+
+ return PlotGraphics([markers1, lines, markers2],"Graph Title", "X Axis", "Y Axis")
+
+def _draw2Objects():
+ # 100 points sin function, plotted as green dots
+ data1 = 2.*_numpy.pi*_numpy.arange(200)/200.
+ data1.shape = (100, 2)
+ data1[:,1] = _numpy.sin(data1[:,0])
+ line1 = PolyLine(data1, legend='Green Line', colour='green', width=6, style=wx.DOT)
+
+ # 50 points cos function, plotted as red dot-dash
+ data1 = 2.*_numpy.pi*_numpy.arange(100)/100.
+ data1.shape = (50,2)
+ data1[:,1] = _numpy.cos(data1[:,0])
+ line2 = PolyLine(data1, legend='Red Line', colour='red', width=3, style= wx.DOT_DASH)
+
+ # A few more points...
+ pi = _numpy.pi
+ markers1 = PolyMarker([(0., 0.), (pi/4., 1.), (pi/2, 0.),
+ (3.*pi/4., -1)], legend='Cross Hatch Square', colour='blue', width= 3, size= 6,
+ fillcolour= 'red', fillstyle= wx.CROSSDIAG_HATCH,
+ marker='square')
+
+ return PlotGraphics([markers1, line1, line2], "Big Markers with Different Line Styles")
+
+def _draw3Objects():
+ markerList= ['circle', 'dot', 'square', 'triangle', 'triangle_down',
+ 'cross', 'plus', 'circle']
+ m=[]
+ for i in range(len(markerList)):
+ m.append(PolyMarker([(2*i+.5,i+.5)], legend=markerList[i], colour='blue',
+ marker=markerList[i]))
+ return PlotGraphics(m, "Selection of Markers", "Minimal Axis", "No Axis")
+
+def _draw4Objects():
+ # 25,000 point line
+ data1 = _numpy.arange(5e5,1e6,10)
+ data1.shape = (25000, 2)
+ line1 = PolyLine(data1, legend='Wide Line', colour='green', width=5)
+
+ # A few more points...
+ markers2 = PolyMarker(data1, legend='Square', colour='blue',
+ marker='square')
+ return PlotGraphics([line1, markers2], "25,000 Points", "Value X", "")
+
+def _draw5Objects():
+ # Empty graph with axis defined but no points/lines
+ points=[]
+ line1 = PolyLine(points, legend='Wide Line', colour='green', width=5)
+ return PlotGraphics([line1], "Empty Plot With Just Axes", "Value X", "Value Y")
+
+def _draw6Objects():
+ # Bar graph
+ points1=[(1,0), (1,10)]
+ line1 = PolyLine(points1, colour='green', legend='Feb.', width=10)
+ points1g=[(2,0), (2,4)]
+ line1g = PolyLine(points1g, colour='red', legend='Mar.', width=10)
+ points1b=[(3,0), (3,6)]
+ line1b = PolyLine(points1b, colour='blue', legend='Apr.', width=10)
+
+ points2=[(4,0), (4,12)]
+ line2 = PolyLine(points2, colour='Yellow', legend='May', width=10)
+ points2g=[(5,0), (5,8)]
+ line2g = PolyLine(points2g, colour='orange', legend='June', width=10)
+ points2b=[(6,0), (6,4)]
+ line2b = PolyLine(points2b, colour='brown', legend='July', width=10)
+
+ return PlotGraphics([line1, line1g, line1b, line2, line2g, line2b],
+ "Bar Graph - (Turn on Grid, Legend)", "Months", "Number of Students")
+
+
+class TestFrame(wx.Frame):
+ def __init__(self, parent, id, title):
+ wx.Frame.__init__(self, parent, id, title,
+ wx.DefaultPosition, (600, 400))
+
+ # Now Create the menu bar and items
+ self.mainmenu = wx.MenuBar()
+
+ menu = wx.Menu()
+ menu.Append(200, 'Page Setup...', 'Setup the printer page')
+ self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)
+
+ menu.Append(201, 'Print Preview...', 'Show the current plot on page')
+ self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)
+
+ menu.Append(202, 'Print...', 'Print the current plot')
+ self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)
+
+ menu.Append(203, 'Save Plot...', 'Save current plot')
+ self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)
+
+ menu.Append(205, 'E&xit', 'Enough of this already!')
+ self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
+ self.mainmenu.Append(menu, '&File')
+
+ menu = wx.Menu()
+ menu.Append(206, 'Draw1', 'Draw plots1')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw1, id=206)
+ menu.Append(207, 'Draw2', 'Draw plots2')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw2, id=207)
+ menu.Append(208, 'Draw3', 'Draw plots3')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw3, id=208)
+ menu.Append(209, 'Draw4', 'Draw plots4')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw4, id=209)
+ menu.Append(210, 'Draw5', 'Draw plots5')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw5, id=210)
+ menu.Append(260, 'Draw6', 'Draw plots6')
+ self.Bind(wx.EVT_MENU,self.OnPlotDraw6, id=260)
+
+
+ menu.Append(211, '&Redraw', 'Redraw plots')
+ self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
+ menu.Append(212, '&Clear', 'Clear canvas')
+ self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
+ menu.Append(213, '&Scale', 'Scale canvas')
+ self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
+ menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
+ menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
+ menu.Append(220, 'Enable &Legend', 'Turn on Legend', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
+ menu.Append(222, 'Enable &Point Label', 'Show Closest Point', kind=wx.ITEM_CHECK)
+ self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
+
+ menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
+ self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
+ menu.Append(230, 'Scroll Rt 2', 'Move View Right 2 Units')
+ self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
+ menu.Append(235, '&Plot Reset', 'Reset to original plot')
+ self.Bind(wx.EVT_MENU,self.OnReset, id=235)
+
+ self.mainmenu.Append(menu, '&Plot')
+
+ menu = wx.Menu()
+ menu.Append(300, '&About', 'About this thing...')
+ self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
+ self.mainmenu.Append(menu, '&Help')
+
+ self.SetMenuBar(self.mainmenu)
+
+ # A status bar to tell people what's happening
+ self.CreateStatusBar(1)
+
+ self.client = PlotCanvas(self)
+ #define the function for drawing pointLabels
+ self.client.SetPointLabelFunc(self.DrawPointLabel)
+ # Create mouse event for showing cursor coords in status bar
+ self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
+ # Show closest point when enabled
+ self.client.Bind(wx.EVT_MOTION, self.OnMotion)
+
+ self.Show(True)
+
+ def DrawPointLabel(self, dc, mDataDict):
+ """This is the fuction that defines how the pointLabels are plotted
+ dc - DC that will be passed
+ mDataDict - Dictionary of data that you want to use for the pointLabel
+
+ As an example I have decided I want a box at the curve point
+ with some text information about the curve plotted below.
+ Any wxDC method can be used.
+ """
+ # ----------
+ dc.SetPen(wx.Pen(wx.BLACK))
+ dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )
+
+ sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
+ dc.DrawRectangle( sx-5,sy-5, 10, 10) #10by10 square centered on point
+ px,py = mDataDict["pointXY"]
+ cNum = mDataDict["curveNum"]
+ pntIn = mDataDict["pIndex"]
+ legend = mDataDict["legend"]
+ #make a string to display
+ s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px, py, pntIn)
+ dc.DrawText(s, sx , sy+1)
+ # -----------
+
+ def OnMouseLeftDown(self,event):
+ s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
+ self.SetStatusText(s)
+ event.Skip() #allows plotCanvas OnMouseLeftDown to be called
+
+ def OnMotion(self, event):
+ #show closest point (when enbled)
+ if self.client.GetEnablePointLabel() == True:
+ #make up dict with info for the pointLabel
+ #I've decided to mark the closest point on the closest curve
+ dlst= self.client.GetClosetPoint( self.client.GetXY(event), pointScaled= True)
+ if dlst != []: #returns [] if none
+ curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
+ #make up dictionary to pass to my user function (see DrawPointLabel)
+ mDataDict= {"curveNum":curveNum, "legend":legend, "pIndex":pIndex,\
+ "pointXY":pointXY, "scaledXY":scaledXY}
+ #pass dict to update the pointLabel
+ self.client.UpdatePointLabel(mDataDict)
+ event.Skip() #go to next handler
+
+ def OnFilePageSetup(self, event):
+ self.client.PageSetup()
+
+ def OnFilePrintPreview(self, event):
+ self.client.PrintPreview()
+
+ def OnFilePrint(self, event):
+ self.client.Printout()
+
+ def OnSaveFile(self, event):
+ self.client.SaveFile()
+
+ def OnFileExit(self, event):
+ self.Close()
+
+ def OnPlotDraw1(self, event):
+ self.resetDefaults()
+ self.client.Draw(_draw1Objects())
+
+ def OnPlotDraw2(self, event):
+ self.resetDefaults()
+ self.client.Draw(_draw2Objects())
+
+ def OnPlotDraw3(self, event):
+ self.resetDefaults()
+ self.client.SetFont(wx.Font(10,wx.SCRIPT,wx.NORMAL,wx.NORMAL))
+ self.client.SetFontSizeAxis(20)
+ self.client.SetFontSizeLegend(12)
+ self.client.SetXSpec('min')
+ self.client.SetYSpec('none')
+ self.client.Draw(_draw3Objects())
+
+ def OnPlotDraw4(self, event):
+ self.resetDefaults()
+ drawObj= _draw4Objects()
+ self.client.Draw(drawObj)
+## # profile
+## start = _time.clock()
+## for x in range(10):
+## self.client.Draw(drawObj)
+## print "10 plots of Draw4 took: %f sec."%(_time.clock() - start)
+## # profile end
+
+ def OnPlotDraw5(self, event):
+ # Empty plot with just axes
+ self.resetDefaults()
+ drawObj= _draw5Objects()
+ # make the axis X= (0,5), Y=(0,10)
+ # (default with None is X= (-1,1), Y= (-1,1))
+ self.client.Draw(drawObj, xAxis= (0,5), yAxis= (0,10))
+
+ def OnPlotDraw6(self, event):
+ #Bar Graph Example
+ self.resetDefaults()
+ #self.client.SetEnableLegend(True) #turn on Legend
+ #self.client.SetEnableGrid(True) #turn on Grid
+ self.client.SetXSpec('none') #turns off x-axis scale
+ self.client.SetYSpec('auto')
+ self.client.Draw(_draw6Objects(), xAxis= (0,7))
+
+ def OnPlotRedraw(self,event):
+ self.client.Redraw()
+
+ def OnPlotClear(self,event):
+ self.client.Clear()
+
+ def OnPlotScale(self, event):
+ if self.client.last_draw != None:
+ graphics, xAxis, yAxis= self.client.last_draw
+ self.client.Draw(graphics,(1,3.05),(0,1))
+
+ def OnEnableZoom(self, event):
+ self.client.SetEnableZoom(event.IsChecked())
+
+ def OnEnableGrid(self, event):
+ self.client.SetEnableGrid(event.IsChecked())
+
+ def OnEnableLegend(self, event):
+ self.client.SetEnableLegend(event.IsChecked())
+
+ def OnEnablePointLabel(self, event):
+ self.client.SetEnablePointLabel(event.IsChecked())
+
+ def OnScrUp(self, event):
+ self.client.ScrollUp(1)
+
+ def OnScrRt(self,event):
+ self.client.ScrollRight(2)
+
+ def OnReset(self,event):
+ self.client.Reset()
+
+ def OnHelpAbout(self, event):
+ from wx.lib.dialogs import ScrolledMessageDialog
+ about = ScrolledMessageDialog(self, __doc__, "About...")
+ about.ShowModal()
+
+ def resetDefaults(self):
+ """Just to reset the fonts back to the PlotCanvas defaults"""
+ self.client.SetFont(wx.Font(10,wx.SWISS,wx.NORMAL,wx.NORMAL))
+ self.client.SetFontSizeAxis(10)
+ self.client.SetFontSizeLegend(7)
+ self.client.SetXSpec('auto')
+ self.client.SetYSpec('auto')
+
+
+def __test():
+
+ class MyApp(wx.App):
+ def OnInit(self):
+ wx.InitAllImageHandlers()
+ frame = TestFrame(None, -1, "PlotCanvas")
+ #frame.Show(True)
+ self.SetTopWindow(frame)
+ return True
+
+
+ app = MyApp(0)
+ app.MainLoop()
+
+if __name__ == '__main__':
+ __test()
diff --git a/gr-wxgui/src/python/plotter/__init__.py b/gr-wxgui/src/python/plotter/__init__.py
new file mode 100644
index 000000000..616492a3e
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/__init__.py
@@ -0,0 +1,24 @@
+#
+# 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
+from bar_plotter import bar_plotter
diff --git a/gr-wxgui/src/python/plotter/bar_plotter.py b/gr-wxgui/src/python/plotter/bar_plotter.py
new file mode 100644
index 000000000..3f9259e9d
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/bar_plotter.py
@@ -0,0 +1,144 @@
+#
+# Copyright 2009 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 grid_plotter_base import grid_plotter_base
+from OpenGL import GL
+import common
+import numpy
+
+LEGEND_TEXT_FONT_SIZE = 8
+LEGEND_BOX_PADDING = 3
+MIN_PADDING = 0, 0, 0, 70 #top, right, bottom, left
+#constants for the waveform storage
+SAMPLES_KEY = 'samples'
+COLOR_SPEC_KEY = 'color_spec'
+MARKERY_KEY = 'marker'
+TRIG_OFF_KEY = 'trig_off'
+
+##################################################
+# Bar Plotter for histogram waveforms
+##################################################
+class bar_plotter(grid_plotter_base):
+
+ def __init__(self, parent):
+ """
+ Create a new bar plotter.
+ """
+ #init
+ grid_plotter_base.__init__(self, parent, MIN_PADDING)
+ self._bars = list()
+ self._bar_width = .5
+ self._color_spec = (0, 0, 0)
+ #setup bar cache
+ self._bar_cache = self.new_gl_cache(self._draw_bars)
+ #setup bar plotter
+ self.register_init(self._init_bar_plotter)
+
+ def _init_bar_plotter(self):
+ """
+ Run gl initialization tasks.
+ """
+ GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
+
+ def _draw_bars(self):
+ """
+ Draw the vertical bars.
+ """
+ bars = self._bars
+ num_bars = len(bars)
+ if num_bars == 0: return
+ #use scissor to prevent drawing outside grid
+ GL.glEnable(GL.GL_SCISSOR_TEST)
+ GL.glScissor(
+ self.padding_left,
+ self.padding_bottom+1,
+ self.width-self.padding_left-self.padding_right-1,
+ self.height-self.padding_top-self.padding_bottom-1,
+ )
+ #load the points
+ points = list()
+ width = self._bar_width/2
+ for i, bar in enumerate(bars):
+ points.extend([
+ (i-width, 0),
+ (i+width, 0),
+ (i+width, bar),
+ (i-width, bar),
+ ]
+ )
+ GL.glColor3f(*self._color_spec)
+ #matrix transforms
+ GL.glPushMatrix()
+ GL.glTranslatef(self.padding_left, self.padding_top, 0)
+ GL.glScalef(
+ (self.width-self.padding_left-self.padding_right),
+ (self.height-self.padding_top-self.padding_bottom),
+ 1,
+ )
+ GL.glTranslatef(0, 1, 0)
+ GL.glScalef(1.0/(num_bars-1), -1.0/(self.y_max-self.y_min), 1)
+ GL.glTranslatef(0, -self.y_min, 0)
+ #draw the bars
+ GL.glVertexPointerf(points)
+ GL.glDrawArrays(GL.GL_QUADS, 0, len(points))
+ GL.glPopMatrix()
+ GL.glDisable(GL.GL_SCISSOR_TEST)
+
+ 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
+ """
+ if len(self._bars) == 0: return ''
+ scalar = float(len(self._bars)-1)/(self.x_max - self.x_min)
+ #convert x val to bar #
+ bar_index = scalar*(x_val - self.x_min)
+ #if abs(bar_index - round(bar_index)) > self._bar_width/2: return ''
+ bar_index = int(round(bar_index))
+ bar_start = (bar_index - self._bar_width/2)/scalar + self.x_min
+ bar_end = (bar_index + self._bar_width/2)/scalar + self.x_min
+ bar_value = self._bars[bar_index]
+ return '%s to %s\n%s: %s'%(
+ common.eng_format(bar_start, self.x_units),
+ common.eng_format(bar_end, self.x_units),
+ self.y_label, common.eng_format(bar_value, self.y_units),
+ )
+
+ def set_bars(self, bars, bar_width, color_spec):
+ """
+ Set the bars.
+ @param bars a list of bars
+ @param bar_width the fractional width of the bar, between 0 and 1
+ @param color_spec the color tuple
+ """
+ self.lock()
+ self._bars = bars
+ self._bar_width = float(bar_width)
+ self._color_spec = color_spec
+ self._bar_cache.changed(True)
+ self.unlock()
+
+
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..4bcc36fd4
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/channel_plotter.py
@@ -0,0 +1,236 @@
+#
+# Copyright 2008, 2009, 2010 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 grid_plotter_base import grid_plotter_base
+from OpenGL import GL
+import common
+import numpy
+import gltext
+import math
+
+LEGEND_TEXT_FONT_SIZE = 8
+LEGEND_BOX_PADDING = 3
+MIN_PADDING = 35, 10, 0, 0 #top, right, bottom, left
+#constants for the waveform storage
+SAMPLES_KEY = 'samples'
+COLOR_SPEC_KEY = 'color_spec'
+MARKERY_KEY = 'marker'
+TRIG_OFF_KEY = 'trig_off'
+
+##################################################
+# 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, MIN_PADDING)
+ self.set_use_persistence(False)
+ #setup legend cache
+ self._legend_cache = self.new_gl_cache(self._draw_legend, 50)
+ self.enable_legend(False)
+ #setup waveform cache
+ self._waveform_cache = self.new_gl_cache(self._draw_waveforms, 50)
+ self._channels = dict()
+ #init channel plotter
+ self.register_init(self._init_channel_plotter)
+ self.callback = None
+
+ def _init_channel_plotter(self):
+ """
+ Run gl initialization tasks.
+ """
+ GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
+
+ 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._legend_cache.changed(True)
+ self.unlock()
+
+ def _draw_waveforms(self):
+ """
+ Draw the waveforms for each channel.
+ Scale the waveform data to the grid using gl matrix operations.
+ """
+ #use scissor to prevent drawing outside grid
+ GL.glEnable(GL.GL_SCISSOR_TEST)
+ GL.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,
+ )
+ for channel in reversed(sorted(self._channels.keys())):
+ samples = self._channels[channel][SAMPLES_KEY]
+ num_samps = len(samples)
+ if not num_samps: continue
+ #use opengl to scale the waveform
+ GL.glPushMatrix()
+ GL.glTranslatef(self.padding_left, self.padding_top, 0)
+ GL.glScalef(
+ (self.width-self.padding_left-self.padding_right),
+ (self.height-self.padding_top-self.padding_bottom),
+ 1,
+ )
+ GL.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), -self._channels[channel][TRIG_OFF_KEY]
+ points = zip(numpy.arange(0, num_samps), samples)
+ GL.glScalef(x_scale, -1.0/(self.y_max-self.y_min), 1)
+ GL.glTranslatef(x_trans, -self.y_min, 0)
+ #draw the points/lines
+ GL.glColor3f(*self._channels[channel][COLOR_SPEC_KEY])
+ marker = self._channels[channel][MARKERY_KEY]
+ if marker is None:
+ GL.glVertexPointerf(points)
+ GL.glDrawArrays(GL.GL_LINE_STRIP, 0, len(points))
+ elif isinstance(marker, (int, float)) and marker > 0:
+ GL.glPointSize(marker)
+ GL.glVertexPointerf(points)
+ GL.glDrawArrays(GL.GL_POINTS, 0, len(points))
+ GL.glPopMatrix()
+ GL.glDisable(GL.GL_SCISSOR_TEST)
+
+ 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\n%s: %s'%(
+ self.x_label, common.eng_format(x_val, self.x_units),
+ self.y_label, common.eng_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_min)/(self.x_max-self.x_min)
+ x_index_low = int(math.floor(x_index))
+ x_index_high = int(math.ceil(x_index))
+ scale = x_index - x_index_low + self._channels[channel][TRIG_OFF_KEY]
+ y_value = (samples[x_index_high] - samples[x_index_low])*scale + samples[x_index_low]
+ label_str += '\n%s: %s'%(channel, common.eng_format(y_value, self.y_units))
+ return label_str
+
+ def _call_callback (self, x_val, y_val):
+ if self.callback != None:
+ self.callback(x_val, y_val)
+
+ def set_callback (self, callback):
+ self.callback = callback
+
+ 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
+ GL.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 clear_waveform(self, channel):
+ """
+ Remove a waveform from the list of waveforms.
+ @param channel the channel key
+ """
+ self.lock()
+ if channel in self._channels.keys():
+ self._channels.pop(channel)
+ self._legend_cache.changed(True)
+ self._waveform_cache.changed(True)
+ self.unlock()
+
+ def set_waveform(self, channel, samples=[], color_spec=(0, 0, 0), marker=None, trig_off=0):
+ """
+ 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
+ @param trig_off fraction of sample for trigger offset
+ """
+ self.lock()
+ if channel not in self._channels.keys(): self._legend_cache.changed(True)
+ self._channels[channel] = {
+ SAMPLES_KEY: samples,
+ COLOR_SPEC_KEY: color_spec,
+ MARKERY_KEY: marker,
+ TRIG_OFF_KEY: trig_off,
+ }
+ self._waveform_cache.changed(True)
+ 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/common.py b/gr-wxgui/src/python/plotter/common.py
new file mode 100644
index 000000000..88215e039
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/common.py
@@ -0,0 +1,133 @@
+#
+# Copyright 2009 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 time
+import math
+import wx
+
+##################################################
+# Number formatting
+##################################################
+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_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
+ """
+ num = float(num)
+ 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 sci_format(num):
+ """
+ Format a floating point number into scientific notation.
+ @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 '%.3ge%d'%(coeff, exp)
+
+def eng_format(num, units=''):
+ """
+ Format a floating point number into engineering notation.
+ @param num the number to format
+ @param units the units to append
+ @return a label string
+ """
+ coeff, exp, prefix = get_si_components(num)
+ if -3 <= exp < 3: return '%g'%num
+ return '%g%s%s%s'%(coeff, units and ' ' or '', prefix, units)
+
+##################################################
+# Interface with thread safe lock/unlock
+##################################################
+class mutex(object):
+ _lock = threading.Lock()
+ def lock(self): self._lock.acquire()
+ def unlock(self): self._lock.release()
+
+##################################################
+# Periodic update thread for point label
+##################################################
+class point_label_thread(threading.Thread, mutex):
+
+ def __init__(self, plotter):
+ self._plotter = plotter
+ self._coor_queue = list()
+ #bind plotter mouse events
+ self._plotter.Bind(wx.EVT_MOTION, lambda evt: self.enqueue(evt.GetPosition()))
+ self._plotter.Bind(wx.EVT_LEAVE_WINDOW, lambda evt: self.enqueue(None))
+ self._plotter.Bind(wx.EVT_RIGHT_DOWN, lambda evt: plotter.enable_point_label(not plotter.enable_point_label()))
+ self._plotter.Bind(wx.EVT_LEFT_DOWN, lambda evt: plotter.call_freq_callback(evt.GetPosition()))
+ #start the thread
+ threading.Thread.__init__(self)
+ self.start()
+
+ def enqueue(self, coor):
+ self.lock()
+ self._coor_queue.append(coor)
+ self.unlock()
+
+ def run(self):
+ last_ts = time.time()
+ last_coor = coor = None
+ try:
+ while True:
+ time.sleep(1.0/30.0)
+ self.lock()
+ #get most recent coor change
+ if self._coor_queue:
+ coor = self._coor_queue[-1]
+ self._coor_queue = list()
+ self.unlock()
+ #update if coor change, or enough time expired
+ if last_coor != coor or (time.time() - last_ts) > (1.0/2.0):
+ self._plotter.set_point_label_coordinate(coor)
+ last_coor = coor
+ last_ts = time.time()
+ except wx.PyDeadObjectError: pass
diff --git a/gr-wxgui/src/python/plotter/gltext.py b/gr-wxgui/src/python/plotter/gltext.py
new file mode 100644
index 000000000..0b6e3f55b
--- /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.Colour):
+ """
+ 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/grid_plotter_base.py b/gr-wxgui/src/python/plotter/grid_plotter_base.py
new file mode 100644
index 000000000..f1bc8f546
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/grid_plotter_base.py
@@ -0,0 +1,419 @@
+#
+# Copyright 2009 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 import GL
+import common
+from plotter_base import plotter_base
+import gltext
+import math
+
+GRID_LINE_COLOR_SPEC = (.7, .7, .7) #gray
+GRID_BORDER_COLOR_SPEC = (0, 0, 0) #black
+TICK_TEXT_FONT_SIZE = 9
+TITLE_TEXT_FONT_SIZE = 13
+UNITS_TEXT_FONT_SIZE = 9
+AXIS_LABEL_PADDING = 5
+TICK_LABEL_PADDING = 5
+TITLE_LABEL_PADDING = 7
+POINT_LABEL_FONT_SIZE = 8
+POINT_LABEL_COLOR_SPEC = (1, 1, 0.5, 0.75)
+POINT_LABEL_PADDING = 3
+POINT_LABEL_OFFSET = 10
+GRID_LINE_DASH_LEN = 4
+
+##################################################
+# Grid Plotter Base Class
+##################################################
+class grid_plotter_base(plotter_base):
+
+ def __init__(self, parent, min_padding=(0, 0, 0, 0)):
+ plotter_base.__init__(self, parent)
+ #setup grid cache
+ self._grid_cache = self.new_gl_cache(self._draw_grid, 25)
+ self.enable_grid_lines(True)
+ #setup padding
+ self.padding_top_min, self.padding_right_min, self.padding_bottom_min, self.padding_left_min = min_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 cache
+ self._point_label_cache = self.new_gl_cache(self._draw_point_label, 75)
+ self.enable_point_label(False)
+ self.enable_grid_aspect_ratio(False)
+ self.set_point_label_coordinate(None)
+ common.point_label_thread(self)
+ #init grid plotter
+ self.register_init(self._init_grid_plotter)
+
+ def _init_grid_plotter(self):
+ """
+ Run gl initialization tasks.
+ """
+ GL.glEnableClientState(GL.GL_VERTEX_ARRAY)
+
+ def set_point_label_coordinate(self, coor):
+ """
+ Set the point label coordinate.
+ @param coor the coordinate x, y tuple or None
+ """
+ self.lock()
+ self._point_label_coordinate = coor
+ self._point_label_cache.changed(True)
+ self.update()
+ self.unlock()
+
+ def call_freq_callback(self, coor):
+ x, y = self._point_label_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 = x_win_scalar*(self.x_max-self.x_min) + self.x_min
+ y_val = y_win_scalar*(self.y_max-self.y_min) + self.y_min
+ self._call_callback(x_val, y_val)
+
+ def enable_grid_aspect_ratio(self, enable=None):
+ """
+ Enable/disable the grid aspect ratio.
+ If enabled, enforce the aspect ratio on the padding:
+ horizontal_padding:vertical_padding == width:height
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_grid_aspect_ratio
+ self.lock()
+ self._enable_grid_aspect_ratio = enable
+ for cache in self._gl_caches: cache.changed(True)
+ 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._point_label_cache.changed(True)
+ self.unlock()
+
+ def set_title(self, title):
+ """
+ Set the title.
+ @param title the title string
+ """
+ self.lock()
+ self.title = title
+ self._grid_cache.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._grid_cache.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._grid_cache.changed(True)
+ self.unlock()
+
+ def set_x_grid(self, minimum, maximum, step, scale=False):
+ """
+ Set the x grid parameters.
+ @param minimum the left-most value
+ @param maximum the right-most value
+ @param step the grid spacing
+ @param scale true to scale the x grid
+ """
+ self.lock()
+ self.x_min = float(minimum)
+ self.x_max = float(maximum)
+ self.x_step = float(step)
+ if scale:
+ coeff, exp, prefix = common.get_si_components(max(abs(self.x_min), abs(self.x_max)))
+ self.x_scalar = 10**(-exp)
+ self.x_prefix = prefix
+ else:
+ self.x_scalar = 1.0
+ self.x_prefix = ''
+ for cache in self._gl_caches: cache.changed(True)
+ self.unlock()
+
+ def set_y_grid(self, minimum, maximum, step, scale=False):
+ """
+ Set the y grid parameters.
+ @param minimum the bottom-most value
+ @param maximum the top-most value
+ @param step the grid spacing
+ @param scale true to scale the y grid
+ """
+ self.lock()
+ self.y_min = float(minimum)
+ self.y_max = float(maximum)
+ self.y_step = float(step)
+ if scale:
+ coeff, exp, prefix = common.get_si_components(max(abs(self.y_min), abs(self.y_max)))
+ self.y_scalar = 10**(-exp)
+ self.y_prefix = prefix
+ else:
+ self.y_scalar = 1.0
+ self.y_prefix = ''
+ for cache in self._gl_caches: cache.changed(True)
+ self.unlock()
+
+ def _draw_grid(self):
+ """
+ Create the x, y, tick, and title labels.
+ Resize the padding for the labels.
+ Draw the border, grid, title, and labels.
+ """
+ ##################################################
+ # Create GL text labels
+ ##################################################
+ #create x tick labels
+ x_tick_labels = [(tick, self._get_tick_label(tick, self.x_units))
+ for tick in self._get_ticks(self.x_min, self.x_max, self.x_step, self.x_scalar)]
+ #create x tick labels
+ y_tick_labels = [(tick, self._get_tick_label(tick, self.y_units))
+ for tick in self._get_ticks(self.y_min, self.y_max, self.y_step, self.y_scalar)]
+ #create x label
+ x_label_str = self.x_units and "%s (%s%s)"%(self.x_label, self.x_prefix, self.x_units) or self.x_label
+ x_label = gltext.Text(x_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True)
+ #create y label
+ y_label_str = self.y_units and "%s (%s%s)"%(self.y_label, self.y_prefix, self.y_units) or self.y_label
+ y_label = gltext.Text(y_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True)
+ #create title
+ title_label = gltext.Text(self.title, bold=True, font_size=TITLE_TEXT_FONT_SIZE, centered=True)
+ ##################################################
+ # Resize the padding
+ ##################################################
+ self.padding_top = max(2*TITLE_LABEL_PADDING + title_label.get_size()[1], self.padding_top_min)
+ self.padding_right = max(2*TICK_LABEL_PADDING, self.padding_right_min)
+ self.padding_bottom = max(2*AXIS_LABEL_PADDING + TICK_LABEL_PADDING + x_label.get_size()[1] + max([label.get_size()[1] for tick, label in x_tick_labels]), self.padding_bottom_min)
+ self.padding_left = max(2*AXIS_LABEL_PADDING + TICK_LABEL_PADDING + y_label.get_size()[1] + max([label.get_size()[0] for tick, label in y_tick_labels]), self.padding_left_min)
+ #enforce padding aspect ratio if enabled
+ if self.enable_grid_aspect_ratio():
+ w_over_h_ratio = float(self.width)/float(self.height)
+ horizontal_padding = float(self.padding_right + self.padding_left)
+ veritical_padding = float(self.padding_top + self.padding_bottom)
+ if w_over_h_ratio > horizontal_padding/veritical_padding:
+ #increase the horizontal padding
+ new_padding = veritical_padding*w_over_h_ratio - horizontal_padding
+ #distribute the padding to left and right
+ self.padding_left += int(round(new_padding/2))
+ self.padding_right += int(round(new_padding/2))
+ else:
+ #increase the vertical padding
+ new_padding = horizontal_padding/w_over_h_ratio - veritical_padding
+ #distribute the padding to top and bottom
+ self.padding_top += int(round(new_padding/2))
+ self.padding_bottom += int(round(new_padding/2))
+ ##################################################
+ # Draw Grid X
+ ##################################################
+ for tick, label in x_tick_labels:
+ 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
+ self._draw_grid_line(
+ (scaled_tick, self.padding_top),
+ (scaled_tick, self.height-self.padding_bottom),
+ )
+ w, h = label.get_size()
+ label.draw_text(wx.Point(scaled_tick-w/2, self.height-self.padding_bottom+TICK_LABEL_PADDING))
+ ##################################################
+ # Draw Grid Y
+ ##################################################
+ for tick, label in y_tick_labels:
+ 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
+ self._draw_grid_line(
+ (self.padding_left, scaled_tick),
+ (self.width-self.padding_right, scaled_tick),
+ )
+ w, h = label.get_size()
+ label.draw_text(wx.Point(self.padding_left-w-TICK_LABEL_PADDING, scaled_tick-h/2))
+ ##################################################
+ # Draw Border
+ ##################################################
+ GL.glColor3f(*GRID_BORDER_COLOR_SPEC)
+ self._draw_rect(
+ self.padding_left,
+ self.padding_top,
+ self.width - self.padding_right - self.padding_left,
+ self.height - self.padding_top - self.padding_bottom,
+ fill=False,
+ )
+ ##################################################
+ # Draw Labels
+ ##################################################
+ #draw title label
+ title_label.draw_text(wx.Point(self.width/2.0, TITLE_LABEL_PADDING + title_label.get_size()[1]/2))
+ #draw x labels
+ x_label.draw_text(wx.Point(
+ (self.width-self.padding_left-self.padding_right)/2.0 + self.padding_left,
+ self.height-(AXIS_LABEL_PADDING + x_label.get_size()[1]/2),
+ )
+ )
+ #draw y labels
+ y_label.draw_text(wx.Point(
+ AXIS_LABEL_PADDING + y_label.get_size()[1]/2,
+ (self.height-self.padding_top-self.padding_bottom)/2.0 + self.padding_top,
+ ), rotation=90,
+ )
+
+ def _get_tick_label(self, tick, unit):
+ """
+ Format the tick value and create a gl text.
+ @param tick the floating point tick value
+ @param unit the axis unit
+ @return the tick label text
+ """
+ if unit: tick_str = common.sci_format(tick)
+ else: tick_str = common.eng_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
+ try:
+ assert step > 0
+ assert max > min
+ assert max - min > step
+ except AssertionError: return [-1, 1]
+ #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 enable_grid_lines(self, enable=None):
+ """
+ Enable/disable the grid lines.
+ @param enable true to enable
+ @return the enable state when None
+ """
+ if enable is None: return self._enable_grid_lines
+ self.lock()
+ self._enable_grid_lines = enable
+ self._grid_cache.changed(True)
+ self.unlock()
+
+ def _draw_grid_line(self, coor1, coor2):
+ """
+ Draw a dashed line from coor1 to coor2.
+ @param corr1 a tuple of x, y
+ @param corr2 a tuple of x, y
+ """
+ if not self.enable_grid_lines(): return
+ length = math.sqrt((coor1[0] - coor2[0])**2 + (coor1[1] - coor2[1])**2)
+ num_points = int(length/GRID_LINE_DASH_LEN)
+ #calculate points array
+ points = [(
+ coor1[0] + i*(coor2[0]-coor1[0])/(num_points - 1),
+ coor1[1] + i*(coor2[1]-coor1[1])/(num_points - 1)
+ ) for i in range(num_points)]
+ #set color and draw
+ GL.glColor3f(*GRID_LINE_COLOR_SPEC)
+ GL.glVertexPointerf(points)
+ GL.glDrawArrays(GL.GL_LINES, 0, len(points))
+
+ 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
+ """
+ GL.glBegin(fill and GL.GL_QUADS or GL.GL_LINE_LOOP)
+ GL.glVertex2f(x, y)
+ GL.glVertex2f(x+width, y)
+ GL.glVertex2f(x+width, y+height)
+ GL.glVertex2f(x, y+height)
+ GL.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._point_label_coordinate: return
+ x, y = self._point_label_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 = x_win_scalar*(self.x_max-self.x_min) + self.x_min
+ y_val = y_win_scalar*(self.y_max-self.y_min) + self.y_min
+ #create text
+ label_str = self._populate_point_label(x_val, y_val)
+ if not label_str: return
+ txt = gltext.Text(label_str, font_size=POINT_LABEL_FONT_SIZE)
+ w, h = txt.get_size()
+ #enable transparency
+ GL.glEnable(GL.GL_BLEND)
+ GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
+ #draw rect + text
+ GL.glColor4f(*POINT_LABEL_COLOR_SPEC)
+ if x > self.width/2: x -= w+2*POINT_LABEL_PADDING + POINT_LABEL_OFFSET
+ else: x += POINT_LABEL_OFFSET
+ 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/plotter_base.py b/gr-wxgui/src/python/plotter/plotter_base.py
new file mode 100644
index 000000000..6d9463458
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/plotter_base.py
@@ -0,0 +1,214 @@
+#
+# Copyright 2008, 2009, 2010 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 import GL
+import common
+
+BACKGROUND_COLOR_SPEC = (1, 0.976, 1, 1) #creamy white
+
+##################################################
+# GL caching interface
+##################################################
+class gl_cache(object):
+ """
+ Cache a set of gl drawing routines in a compiled list.
+ """
+
+ def __init__(self, draw):
+ """
+ Create a new cache.
+ @param draw a function to draw gl stuff
+ """
+ self.changed(True)
+ self._draw = draw
+
+ def init(self):
+ """
+ To be called when gl initializes.
+ Create a new compiled list.
+ """
+ self._grid_compiled_list_id = GL.glGenLists(1)
+
+ def draw(self):
+ """
+ Draw the gl stuff using a compiled list.
+ If changed, reload the compiled list.
+ """
+ if self.changed():
+ GL.glNewList(self._grid_compiled_list_id, GL.GL_COMPILE)
+ self._draw()
+ GL.glEndList()
+ self.changed(False)
+ #draw the grid
+ GL.glCallList(self._grid_compiled_list_id)
+
+ def changed(self, state=None):
+ """
+ Set the changed flag if state is not None.
+ Otherwise return the changed flag.
+ """
+ if state is None: return self._changed
+ self._changed = state
+
+##################################################
+# OpenGL WX Plotter Canvas
+##################################################
+class plotter_base(wx.glcanvas.GLCanvas, common.mutex):
+ """
+ Plotter base class for all plot types.
+ """
+
+ def __init__(self, parent):
+ """
+ Create a new plotter base.
+ Initialize the GLCanvas with double buffering.
+ Initialize various plotter flags.
+ Bind the paint and size events.
+ @param parent the parent widgit
+ """
+ attribList = (wx.glcanvas.WX_GL_DOUBLEBUFFER, wx.glcanvas.WX_GL_RGBA)
+ wx.glcanvas.GLCanvas.__init__(self, parent, wx.ID_ANY, attribList); # Specifically use the CTOR which does NOT create an implicit GL context
+ self._gl_ctx = wx.glcanvas.GLContext(self) # Create the explicit GL context
+ self.use_persistence=False
+ self.persist_alpha=2.0/15
+ self.clear_accum=True
+ self._gl_init_flag = False
+ self._resized_flag = True
+ self._init_fcns = list()
+ self._draw_fcns = list()
+ self._gl_caches = list()
+ self.Bind(wx.EVT_PAINT, self._on_paint)
+ self.Bind(wx.EVT_SIZE, self._on_size)
+ self.Bind(wx.EVT_ERASE_BACKGROUND, lambda e: None)
+
+ def set_use_persistence(self,enable):
+ self.use_persistence=enable
+ self.clear_accum=True
+
+ def set_persist_alpha(self,analog_alpha):
+ self.persist_alpha=analog_alpha
+
+ def new_gl_cache(self, draw_fcn, draw_pri=50):
+ """
+ Create a new gl cache.
+ Register its draw and init function.
+ @return the new cache object
+ """
+ cache = gl_cache(draw_fcn)
+ self.register_init(cache.init)
+ self.register_draw(cache.draw, draw_pri)
+ self._gl_caches.append(cache)
+ return cache
+
+ def register_init(self, init_fcn):
+ self._init_fcns.append(init_fcn)
+
+ def register_draw(self, draw_fcn, draw_pri=50):
+ """
+ Register a draw function with a layer priority.
+ Large pri values are drawn last.
+ Small pri values are drawn first.
+ """
+ for i in range(len(self._draw_fcns)):
+ if draw_pri < self._draw_fcns[i][0]:
+ self._draw_fcns.insert(i, (draw_pri, draw_fcn))
+ return
+ self._draw_fcns.append((draw_pri, draw_fcn))
+
+ def _on_size(self, event):
+ """
+ Flag the resize event.
+ The paint event will handle the actual resizing.
+ """
+ self.lock()
+ self._resized_flag = True
+ self.clear_accum=True
+ self.unlock()
+
+ def _on_paint(self, event):
+ """
+ Respond to paint events.
+ Initialize GL if this is the first paint event.
+ Resize the view port if the width or height changed.
+ Redraw the screen, calling the draw functions.
+ """
+ if not self.IsShownOnScreen(): # Cannot realise a GL context on OS X if window is not yet shown
+ return
+ # create device context (needed on Windows, noop on X)
+ dc = None
+ if event.GetEventObject(): # Only create DC if paint triggered by WM message (for OS X)
+ dc = wx.PaintDC(self)
+ self.lock()
+ self.SetCurrent(self._gl_ctx) # Real the explicit GL context
+
+ # check if gl was initialized
+ if not self._gl_init_flag:
+ GL.glClearColor(*BACKGROUND_COLOR_SPEC)
+ for fcn in self._init_fcns: fcn()
+ self._gl_init_flag = True
+
+ # check for a change in window size
+ if self._resized_flag:
+ self.width, self.height = self.GetSize()
+ GL.glMatrixMode(GL.GL_PROJECTION)
+ GL.glLoadIdentity()
+ GL.glOrtho(0, self.width, self.height, 0, 1, 0)
+ GL.glMatrixMode(GL.GL_MODELVIEW)
+ GL.glLoadIdentity()
+ GL.glViewport(0, 0, self.width, self.height)
+ for cache in self._gl_caches: cache.changed(True)
+ self._resized_flag = False
+
+ # clear buffer if needed
+ if self.clear_accum or not self.use_persistence:
+ GL.glClear(GL.GL_COLOR_BUFFER_BIT)
+ self.clear_accum=False
+
+ # apply fading
+ if self.use_persistence:
+ GL.glEnable(GL.GL_BLEND)
+ GL.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA)
+
+ GL.glBegin(GL.GL_QUADS)
+ GL.glColor4f(1,1,1,self.persist_alpha)
+ GL.glVertex2f(0, self.height)
+ GL.glVertex2f(self.width, self.height)
+ GL.glVertex2f(self.width, 0)
+ GL.glVertex2f(0, 0)
+ GL.glEnd()
+
+ GL.glDisable(GL.GL_BLEND)
+
+ # draw functions
+ for fcn in self._draw_fcns: fcn[1]()
+
+ # show result
+ self.SwapBuffers()
+ self.unlock()
+
+ def update(self):
+ """
+ Force a paint event.
+ """
+ if not self._gl_init_flag: return
+ wx.PostEvent(self, wx.PaintEvent())
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..6a6bf6330
--- /dev/null
+++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py
@@ -0,0 +1,278 @@
+#
+# Copyright 2008, 2009, 2010 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 grid_plotter_base import grid_plotter_base
+from OpenGL import GL
+import common
+import numpy
+import gltext
+import math
+import struct
+
+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
+MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left
+
+ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
+
+pack_color = lambda x: struct.unpack('I', struct.pack('BBBB', *x))[0]
+unpack_color = lambda x: struct.unpack('BBBB', struct.pack('I', int(x)))
+
+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([pack_color(map(
+ lambda pw: int(255*_fcn(i/255.0, pw)),
+ (red_pts, green_pts, blue_pts, alpha_pts),
+ )) for i in range(0, 256)], numpy.uint32)
+
+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, MIN_PADDING)
+ #setup legend cache
+ self._legend_cache = self.new_gl_cache(self._draw_legend)
+ #setup waterfall cache
+ self._waterfall_cache = self.new_gl_cache(self._draw_waterfall, 50)
+ #setup waterfall plotter
+ self.register_init(self._init_waterfall)
+ 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])
+ self.callback = None
+
+ def _init_waterfall(self):
+ """
+ Run gl initialization tasks.
+ """
+ self._waterfall_texture = GL.glGenTextures(1)
+
+ 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.
+ """
+ #resize texture
+ self._resize_texture()
+ #setup texture
+ GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
+ GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
+ GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
+ GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
+ GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE)
+ #write the buffer to the texture
+ while self._buffer:
+ GL.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, self._buffer.pop(0))
+ self._pointer = (self._pointer + 1)%self._num_lines
+ #begin drawing
+ GL.glEnable(GL.GL_TEXTURE_2D)
+ GL.glPushMatrix()
+ #matrix scaling
+ GL.glTranslatef(self.padding_left, self.padding_top, 0)
+ GL.glScalef(
+ float(self.width-self.padding_left-self.padding_right),
+ float(self.height-self.padding_top-self.padding_bottom),
+ 1.0,
+ )
+ #draw texture with wrapping
+ GL.glBegin(GL.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)
+ GL.glTexCoord2f(0, prop_y+1-off)
+ GL.glVertex2f(0, 1)
+ GL.glTexCoord2f(prop_x, prop_y+1-off)
+ GL.glVertex2f(1, 1)
+ GL.glTexCoord2f(prop_x, prop_y)
+ GL.glVertex2f(1, 0)
+ GL.glTexCoord2f(0, prop_y)
+ GL.glVertex2f(0, 0)
+ GL.glEnd()
+ GL.glPopMatrix()
+ GL.glDisable(GL.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'%(self.x_label, common.eng_format(x_val, self.x_units))
+
+ def _call_callback(self, x_val, y_val):
+ if self.callback != None:
+ self.callback(x_val,y_val)
+
+ def set_callback(self,callback):
+ self.callback = callback
+
+ 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 = unpack_color(COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))])
+ GL.glColor4f(*numpy.array(color)/255.0)
+ 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
+ GL.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:
+ GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture)
+ data = numpy.zeros(self._num_lines*ceil_log2(self._fft_size)*4, numpy.uint8).tostring()
+ GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL.GL_RGBA, GL.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._legend_cache.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._legend_cache.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 = COLORS[self._color_mode][samples].tostring()
+ self._buffer.append(data)
+ self._waterfall_cache.changed(True)
+ self.unlock()
diff --git a/gr-wxgui/src/python/powermate.py b/gr-wxgui/src/python/powermate.py
new file mode 100644
index 000000000..7c324c5d9
--- /dev/null
+++ b/gr-wxgui/src/python/powermate.py
@@ -0,0 +1,448 @@
+#!/usr/bin/env python
+#
+# Copyright 2005 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.
+#
+
+"""
+Handler for Griffin PowerMate, Contour ShuttlePro & ShuttleXpress USB knobs
+
+This is Linux and wxPython specific.
+"""
+
+import os
+import sys
+import struct
+import exceptions
+import threading
+import wx
+from gnuradio import gru
+
+imported_ok = True
+
+try:
+ import select
+ import fcntl
+except ImportError:
+ imported_ok = False
+
+
+# First a little bit of background:
+#
+# The Griffin PowerMate has
+# * a single knob which rotates
+# * a single button (pressing the knob)
+#
+# The Contour ShuttleXpress (aka SpaceShuttle) has
+# * "Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 5 buttons
+#
+# The Contour ShuttlePro has
+# * "Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 13 buttons
+#
+# The Contour ShuttlePro V2 has
+# *"Jog Wheel" -- the knob (rotary encoder) on the inside
+# * "Shuttle Ring" -- the spring loaded rubber covered ring
+# * 15 buttons
+
+# We remap all the buttons on the devices so that they start at zero.
+
+# For the ShuttleXpress the buttons are 0 to 4 (left to right)
+
+# For the ShuttlePro, we number the buttons immediately above
+# the ring 0 to 4 (left to right) so that they match our numbering
+# on the ShuttleXpress. The top row is 5, 6, 7, 8. The first row below
+# the ring is 9, 10, and the bottom row is 11, 12.
+
+# For the ShuttlePro V2, buttons 13 & 14 are to the
+# left and right of the wheel respectively.
+
+# We generate 3 kinds of events:
+#
+# button press/release (button_number, press/release)
+# knob rotation (relative_clicks) # typically -1, +1
+# shuttle position (absolute_position) # -7,-6,...,0,...,6,7
+
+# ----------------------------------------------------------------
+# Our ID's for the devices:
+# Not to be confused with anything related to magic hardware numbers.
+
+ID_POWERMATE = 'powermate'
+ID_SHUTTLE_XPRESS = 'shuttle xpress'
+ID_SHUTTLE_PRO = 'shuttle pro'
+ID_SHUTTLE_PRO_V2 = 'shuttle pro v2'
+
+# ------------------------------------------------------------------------
+# format of messages that we read from /dev/input/event*
+# See /usr/include/linux/input.h for more info
+#
+#struct input_event {
+# struct timeval time; = {long seconds, long microseconds}
+# unsigned short type;
+# unsigned short code;
+# unsigned int value;
+#};
+
+input_event_struct = "@llHHi"
+input_event_size = struct.calcsize(input_event_struct)
+
+# ------------------------------------------------------------------------
+# input_event types
+# ------------------------------------------------------------------------
+
+IET_SYN = 0x00 # aka RESET
+IET_KEY = 0x01 # key or button press/release
+IET_REL = 0x02 # relative movement (knob rotation)
+IET_ABS = 0x03 # absolute position (graphics pad, etc)
+IET_MSC = 0x04
+IET_LED = 0x11
+IET_SND = 0x12
+IET_REP = 0x14
+IET_FF = 0x15
+IET_PWR = 0x16
+IET_FF_STATUS = 0x17
+IET_MAX = 0x1f
+
+# ------------------------------------------------------------------------
+# input_event codes (there are a zillion of them, we only define a few)
+# ------------------------------------------------------------------------
+
+# these are valid for IET_KEY
+
+IEC_BTN_0 = 0x100
+IEC_BTN_1 = 0x101
+IEC_BTN_2 = 0x102
+IEC_BTN_3 = 0x103
+IEC_BTN_4 = 0x104
+IEC_BTN_5 = 0x105
+IEC_BTN_6 = 0x106
+IEC_BTN_7 = 0x107
+IEC_BTN_8 = 0x108
+IEC_BTN_9 = 0x109
+IEC_BTN_10 = 0x10a
+IEC_BTN_11 = 0x10b
+IEC_BTN_12 = 0x10c
+IEC_BTN_13 = 0x10d
+IEC_BTN_14 = 0x10e
+IEC_BTN_15 = 0x10f
+
+# these are valid for IET_REL (Relative axes)
+
+IEC_REL_X = 0x00
+IEC_REL_Y = 0x01
+IEC_REL_Z = 0x02
+IEC_REL_HWHEEL = 0x06
+IEC_REL_DIAL = 0x07 # rotating the knob
+IEC_REL_WHEEL = 0x08 # moving the shuttle ring
+IEC_REL_MISC = 0x09
+IEC_REL_MAX = 0x0f
+
+# ------------------------------------------------------------------------
+
+class powermate(threading.Thread):
+ """
+ Interface to Griffin PowerMate and Contour Shuttles
+ """
+ def __init__(self, event_receiver=None, filename=None, **kwargs):
+ self.event_receiver = event_receiver
+ self.handle = -1
+ if not imported_ok:
+ raise exceptions.RuntimeError, 'powermate not supported on this platform'
+
+ if filename:
+ if not self._open_device(filename):
+ raise exceptions.RuntimeError, 'Unable to find powermate'
+ else:
+ ok = False
+ for d in range(0, 16):
+ if self._open_device("/dev/input/event%d" % d):
+ ok = True
+ break
+ if not ok:
+ raise exceptions.RuntimeError, 'Unable to find powermate'
+
+ threading.Thread.__init__(self, **kwargs)
+ self.setDaemon (1)
+ self.keep_running = True
+ self.start ()
+
+ def __del__(self):
+ self.keep_running = False
+ if self.handle >= 0:
+ os.close(self.handle)
+ self.handle = -1
+
+ def _open_device(self, filename):
+ try:
+ self.handle = os.open(filename, os.O_RDWR)
+ if self.handle < 0:
+ return False
+
+ # read event device name
+ name = fcntl.ioctl(self.handle, gru.hexint(0x80ff4506), chr(0) * 256)
+ name = name.replace(chr(0), '')
+
+ # do we see anything we recognize?
+ if name == 'Griffin PowerMate' or name == 'Griffin SoundKnob':
+ self.id = ID_POWERMATE
+ self.mapper = _powermate_remapper()
+ elif name == 'CAVS SpaceShuttle A/V' or name == 'Contour Design ShuttleXpress':
+ self.id = ID_SHUTTLE_XPRESS
+ self.mapper = _contour_remapper()
+ elif name == 'Contour Design ShuttlePRO':
+ self.id = ID_SHUTTLE_PRO
+ self.mapper = _contour_remapper()
+ elif name == 'Contour Design ShuttlePRO v2':
+ self.id = ID_SHUTTLE_PRO_V2
+ self.mapper = _contour_remapper()
+ else:
+ os.close(self.handle)
+ self.handle = -1
+ return False
+
+ # get exclusive control of the device, using ioctl EVIOCGRAB
+ # there may be an issue with this on non x86 platforms and if
+ # the _IOW,_IOC,... macros in <asm/ioctl.h> are changed
+ fcntl.ioctl(self.handle,gru.hexint(0x40044590), 1)
+ return True
+ except exceptions.OSError:
+ return False
+
+
+ def set_event_receiver(self, obj):
+ self.event_receiver = obj
+
+
+ def set_led_state(self, static_brightness, pulse_speed=0,
+ pulse_table=0, pulse_on_sleep=0, pulse_on_wake=0):
+ """
+ What do these magic values mean...
+ """
+ if self.id != ID_POWERMATE:
+ return False
+
+ static_brightness &= 0xff;
+ if pulse_speed < 0:
+ pulse_speed = 0
+ if pulse_speed > 510:
+ pulse_speed = 510
+ if pulse_table < 0:
+ pulse_table = 0
+ if pulse_table > 2:
+ pulse_table = 2
+ pulse_on_sleep = not not pulse_on_sleep # not not = convert to 0/1
+ pulse_on_wake = not not pulse_on_wake
+ magic = (static_brightness
+ | (pulse_speed << 8)
+ | (pulse_table << 17)
+ | (pulse_on_sleep << 19)
+ | (pulse_on_wake << 20))
+ data = struct.pack(input_event_struct, 0, 0, 0x04, 0x01, magic)
+ os.write(self.handle, data)
+ return True
+
+ def run (self):
+ while (self.keep_running):
+ s = os.read (self.handle, input_event_size)
+ if not s:
+ self.keep_running = False
+ break
+
+ raw_input_event = struct.unpack(input_event_struct,s)
+ sec, usec, type, code, val = self.mapper(raw_input_event)
+
+ if self.event_receiver is None:
+ continue
+
+ if type == IET_SYN: # ignore
+ pass
+ elif type == IET_MSC: # ignore (seems to be PowerMate reporting led brightness)
+ pass
+ elif type == IET_REL and code == IEC_REL_DIAL:
+ #print "Dial: %d" % (val,)
+ wx.PostEvent(self.event_receiver, PMRotateEvent(val))
+ elif type == IET_REL and code == IEC_REL_WHEEL:
+ #print "Shuttle: %d" % (val,)
+ wx.PostEvent(self.event_receiver, PMShuttleEvent(val))
+ elif type == IET_KEY:
+ #print "Key: Btn%d %d" % (code - IEC_BTN_0, val)
+ wx.PostEvent(self.event_receiver,
+ PMButtonEvent(code - IEC_BTN_0, val))
+ else:
+ print "powermate: unrecognized event: type = 0x%x code = 0x%x val = %d" % (type, code, val)
+
+
+class _powermate_remapper(object):
+ def __init__(self):
+ pass
+ def __call__(self, event):
+ """
+ Notice how nice and simple this is...
+ """
+ return event
+
+class _contour_remapper(object):
+ def __init__(self):
+ self.prev = None
+ def __call__(self, event):
+ """
+ ...and how screwed up this is
+ """
+ sec, usec, type, code, val = event
+ if type == IET_REL and code == IEC_REL_WHEEL:
+ # === Shuttle ring ===
+ # First off, this really ought to be IET_ABS, not IET_REL!
+ # They never generate a zero value so you can't
+ # tell when the shuttle ring is back in the center.
+ # We kludge around this by calling both -1 and 1 zero.
+ if val == -1 or val == 1:
+ return (sec, usec, type, code, 0)
+ return event
+
+ if type == IET_REL and code == IEC_REL_DIAL:
+ # === Jog knob (rotary encoder) ===
+ # Dim wits got it wrong again! This one should return a
+ # a relative value, e.g., -1, +1. Instead they return
+ # a total that runs modulo 256 (almost!). For some
+ # reason they count like this 253, 254, 255, 1, 2, 3
+
+ if self.prev is None: # first time call
+ self.prev = val
+ return (sec, usec, IET_SYN, 0, 0) # will be ignored above
+
+ diff = val - self.prev
+ if diff == 0: # sometimes it just sends stuff...
+ return (sec, usec, IET_SYN, 0, 0) # will be ignored above
+
+ if abs(diff) > 100: # crossed into the twilight zone
+ if self.prev > val: # we've wrapped going forward
+ self.prev = val
+ return (sec, usec, type, code, +1)
+ else: # we've wrapped going backward
+ self.prev = val
+ return (sec, usec, type, code, -1)
+
+ self.prev = val
+ return (sec, usec, type, code, diff)
+
+ if type == IET_KEY:
+ # remap keys so that all 3 gadgets have buttons 0 to 4 in common
+ return (sec, usec, type,
+ (IEC_BTN_5, IEC_BTN_6, IEC_BTN_7, IEC_BTN_8,
+ IEC_BTN_0, IEC_BTN_1, IEC_BTN_2, IEC_BTN_3, IEC_BTN_4,
+ IEC_BTN_9, IEC_BTN_10,
+ IEC_BTN_11, IEC_BTN_12,
+ IEC_BTN_13, IEC_BTN_14)[code - IEC_BTN_0], val)
+
+ return event
+
+# ------------------------------------------------------------------------
+# new wxPython event classes
+# ------------------------------------------------------------------------
+
+grEVT_POWERMATE_BUTTON = wx.NewEventType()
+grEVT_POWERMATE_ROTATE = wx.NewEventType()
+grEVT_POWERMATE_SHUTTLE = wx.NewEventType()
+
+EVT_POWERMATE_BUTTON = wx.PyEventBinder(grEVT_POWERMATE_BUTTON, 0)
+EVT_POWERMATE_ROTATE = wx.PyEventBinder(grEVT_POWERMATE_ROTATE, 0)
+EVT_POWERMATE_SHUTTLE = wx.PyEventBinder(grEVT_POWERMATE_SHUTTLE, 0)
+
+class PMButtonEvent(wx.PyEvent):
+ def __init__(self, button, value):
+ wx.PyEvent.__init__(self)
+ self.SetEventType(grEVT_POWERMATE_BUTTON)
+ self.button = button
+ self.value = value
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+
+class PMRotateEvent(wx.PyEvent):
+ def __init__(self, delta):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (grEVT_POWERMATE_ROTATE)
+ self.delta = delta
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+
+class PMShuttleEvent(wx.PyEvent):
+ def __init__(self, position):
+ wx.PyEvent.__init__(self)
+ self.SetEventType (grEVT_POWERMATE_SHUTTLE)
+ self.position = position
+
+ def Clone (self):
+ self.__class__(self.GetId())
+
+# ------------------------------------------------------------------------
+# Example usage
+# ------------------------------------------------------------------------
+
+if __name__ == '__main__':
+ class Frame(wx.Frame):
+ def __init__(self,parent=None,id=-1,title='Title',
+ pos=wx.DefaultPosition, size=(400,200)):
+ wx.Frame.__init__(self,parent,id,title,pos,size)
+ EVT_POWERMATE_BUTTON(self, self.on_button)
+ EVT_POWERMATE_ROTATE(self, self.on_rotate)
+ EVT_POWERMATE_SHUTTLE(self, self.on_shuttle)
+ self.brightness = 128
+ self.pulse_speed = 0
+
+ try:
+ self.pm = powermate(self)
+ except:
+ sys.stderr.write("Unable to find PowerMate or Contour Shuttle\n")
+ sys.exit(1)
+
+ self.pm.set_led_state(self.brightness, self.pulse_speed)
+
+
+ def on_button(self, evt):
+ print "Button %d %s" % (evt.button,
+ ("Released", "Pressed")[evt.value])
+
+ def on_rotate(self, evt):
+ print "Rotated %d" % (evt.delta,)
+ if 0:
+ new = max(0, min(255, self.brightness + evt.delta))
+ if new != self.brightness:
+ self.brightness = new
+ self.pm.set_led_state(self.brightness, self.pulse_speed)
+
+ def on_shuttle(self, evt):
+ print "Shuttle %d" % (evt.position,)
+
+ class App(wx.App):
+ def OnInit(self):
+ title='PowerMate Demo'
+ self.frame = Frame(parent=None,id=-1,title=title)
+ self.frame.Show()
+ self.SetTopWindow(self.frame)
+ return True
+
+ app = App()
+ app.MainLoop ()
diff --git a/gr-wxgui/src/python/pubsub.py b/gr-wxgui/src/python/pubsub.py
new file mode 100644
index 000000000..e55d69197
--- /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, pkey) = self._proxies[key]
+ p[pkey] = 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, pkey) = self._proxies[key]
+ return p[pkey]
+ 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, pkey) = self._proxies[key]
+ p.publish(pkey, 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, pkey) = self._proxies[key]
+ p.subscribe(pkey, subscriber)
+ else:
+ self._subscribers[key].append(subscriber)
+
+ def unpublish(self, key):
+ if self._proxies[key] is not None:
+ (p, pkey) = self._proxies[key]
+ p.unpublish(pkey)
+ else:
+ self._publishers[key] = None
+
+ def unsubscribe(self, key, subscriber):
+ if self._proxies[key] is not None:
+ (p, pkey) = self._proxies[key]
+ p.unsubscribe(pkey, subscriber)
+ else:
+ self._subscribers[key].remove(subscriber)
+
+ def proxy(self, key, p, pkey=None):
+ if not self.has_key(key): self.__missing__(key)
+ if pkey is None: pkey = key
+ self._proxies[key] = (p, pkey)
+
+ 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..fa79a986e
--- /dev/null
+++ b/gr-wxgui/src/python/scope_window.py
@@ -0,0 +1,686 @@
+#
+# Copyright 2008,2010 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 *
+from gnuradio import gr #for gr.prefs, trigger modes
+import forms
+
+##################################################
+# Constants
+##################################################
+DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'scope_rate', 30)
+PERSIST_ALPHA_MIN_EXP, PERSIST_ALPHA_MAX_EXP = -2, 0
+SLIDER_STEPS = 100
+DEFAULT_TRIG_MODE = gr.prefs().get_long('wxgui', 'trig_mode', gr.gr_TRIG_MODE_AUTO)
+DEFAULT_WIN_SIZE = (600, 300)
+COUPLING_MODES = (
+ ('DC', False),
+ ('AC', True),
+)
+TRIGGER_MODES = (
+ ('Freerun', gr.gr_TRIG_MODE_FREE),
+ ('Auto', gr.gr_TRIG_MODE_AUTO),
+ ('Normal', gr.gr_TRIG_MODE_NORM),
+ ('Stripchart', gr.gr_TRIG_MODE_STRIPCHART),
+)
+TRIGGER_SLOPES = (
+ ('Pos +', gr.gr_TRIG_SLOPE_POS),
+ ('Neg -', gr.gr_TRIG_SLOPE_NEG),
+)
+CHANNEL_COLOR_SPECS = (
+ (0.3, 0.3, 1.0),
+ (0.0, 0.8, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.8, 0.0, 0.8),
+ (0.7, 0.7, 0.0),
+ (0.15, 0.90, 0.98),
+
+)
+TRIGGER_COLOR_SPEC = (1.0, 0.4, 0.0)
+AUTORANGE_UPDATE_RATE = 0.5 #sec
+MARKER_TYPES = (
+ ('Line Link', None),
+ ('Dot Large', 3.0),
+ ('Dot Med', 2.0),
+ ('Dot Small', 1.0),
+ ('None', 0.0),
+)
+DEFAULT_MARKER_TYPE = None
+
+##################################################
+# 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
+ """
+ WIDTH = 90
+ self.parent = parent
+ wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+
+ ##################################################
+ # Persistence
+ ##################################################
+
+ forms.check_box(
+ sizer=control_box, parent=self, label='Persistence',
+ ps=parent, key=USE_PERSISTENCE_KEY,
+ )
+ #static text and slider for analog alpha
+ persist_alpha_text = forms.static_text(
+ sizer=control_box, parent=self, label='Analog Alpha',
+ converter=forms.float_converter(lambda x: '%.4f'%x),
+ ps=parent, key=PERSIST_ALPHA_KEY, width=50,
+ )
+ persist_alpha_slider = forms.log_slider(
+ sizer=control_box, parent=self,
+ min_exp=PERSIST_ALPHA_MIN_EXP,
+ max_exp=PERSIST_ALPHA_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=PERSIST_ALPHA_KEY,
+ )
+ for widget in (persist_alpha_text, persist_alpha_slider):
+ parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable)
+ widget.Enable(parent[USE_PERSISTENCE_KEY])
+ parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems)
+ #allways show initially, so room is reserved for them
+ widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY])
+
+ parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout)
+
+ ##################################################
+ # Axes Options
+ ##################################################
+ control_box.AddStretchSpacer()
+ axes_options_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Axes Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ ##################################################
+ # Scope Mode Box
+ ##################################################
+ scope_mode_box = wx.BoxSizer(wx.VERTICAL)
+ axes_options_box.Add(scope_mode_box, 0, wx.EXPAND)
+ #x axis divs
+ forms.incr_decr_buttons(
+ parent=self, sizer=scope_mode_box, label='Secs/Div',
+ on_incr=self._on_incr_t_divs, on_decr=self._on_decr_t_divs,
+ )
+ #y axis divs
+ y_buttons_scope = forms.incr_decr_buttons(
+ parent=self, sizer=scope_mode_box, label='Counts/Div',
+ on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
+ )
+ #y axis ref lvl
+ y_off_buttons_scope = forms.incr_decr_buttons(
+ parent=self, sizer=scope_mode_box, label='Y Offset',
+ on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
+ )
+ #t axis ref lvl
+ scope_mode_box.AddSpacer(5)
+ forms.slider(
+ parent=self, sizer=scope_mode_box,
+ ps=parent, key=T_FRAC_OFF_KEY, label='T Offset',
+ minimum=0, maximum=1, num_steps=1000,
+ )
+ scope_mode_box.AddSpacer(5)
+ ##################################################
+ # XY Mode Box
+ ##################################################
+ xy_mode_box = wx.BoxSizer(wx.VERTICAL)
+ axes_options_box.Add(xy_mode_box, 0, wx.EXPAND)
+ #x div controls
+ x_buttons = forms.incr_decr_buttons(
+ parent=self, sizer=xy_mode_box, label='X/Div',
+ on_incr=self._on_incr_x_divs, on_decr=self._on_decr_x_divs,
+ )
+ #y div controls
+ y_buttons = forms.incr_decr_buttons(
+ parent=self, sizer=xy_mode_box, label='Y/Div',
+ on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
+ )
+ #x offset controls
+ x_off_buttons = forms.incr_decr_buttons(
+ parent=self, sizer=xy_mode_box, label='X Off',
+ on_incr=self._on_incr_x_off, on_decr=self._on_decr_x_off,
+ )
+ #y offset controls
+ y_off_buttons = forms.incr_decr_buttons(
+ parent=self, sizer=xy_mode_box, label='Y Off',
+ on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
+ )
+ for widget in (y_buttons_scope, y_off_buttons_scope, x_buttons, y_buttons, x_off_buttons, y_off_buttons):
+ parent.subscribe(AUTORANGE_KEY, widget.Disable)
+ widget.Disable(parent[AUTORANGE_KEY])
+ xy_mode_box.ShowItems(False)
+ #autorange check box
+ forms.check_box(
+ parent=self, sizer=axes_options_box, label='Autorange',
+ ps=parent, key=AUTORANGE_KEY,
+ )
+ ##################################################
+ # Channel Options
+ ##################################################
+ TRIGGER_PAGE_INDEX = parent.num_inputs
+ XY_PAGE_INDEX = parent.num_inputs+1
+ control_box.AddStretchSpacer()
+ chan_options_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Channel Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ options_notebook = wx.Notebook(self)
+ options_notebook_args = list()
+ CHANNELS = [('Ch %d'%(i+1), i) for i in range(parent.num_inputs)]
+ ##################################################
+ # Channel Menu Boxes
+ ##################################################
+ for i in range(parent.num_inputs):
+ channel_menu_panel = wx.Panel(options_notebook)
+ options_notebook_args.append((channel_menu_panel, i, 'Ch%d'%(i+1)))
+ channel_menu_box = wx.BoxSizer(wx.VERTICAL)
+ channel_menu_panel.SetSizer(channel_menu_box)
+ #ac couple check box
+ channel_menu_box.AddStretchSpacer()
+ forms.drop_down(
+ parent=channel_menu_panel, sizer=channel_menu_box,
+ ps=parent, key=common.index_key(AC_COUPLE_KEY, i),
+ choices=map(lambda x: x[1], COUPLING_MODES),
+ labels=map(lambda x: x[0], COUPLING_MODES),
+ label='Coupling', width=WIDTH,
+ )
+ #marker
+ channel_menu_box.AddStretchSpacer()
+ forms.drop_down(
+ parent=channel_menu_panel, sizer=channel_menu_box,
+ ps=parent, key=common.index_key(MARKER_KEY, i),
+ choices=map(lambda x: x[1], MARKER_TYPES),
+ labels=map(lambda x: x[0], MARKER_TYPES),
+ label='Marker', width=WIDTH,
+ )
+ channel_menu_box.AddStretchSpacer()
+ ##################################################
+ # Trigger Menu Box
+ ##################################################
+ trigger_menu_panel = wx.Panel(options_notebook)
+ options_notebook_args.append((trigger_menu_panel, TRIGGER_PAGE_INDEX, 'Trig'))
+ trigger_menu_box = wx.BoxSizer(wx.VERTICAL)
+ trigger_menu_panel.SetSizer(trigger_menu_box)
+ #trigger mode
+ forms.drop_down(
+ parent=trigger_menu_panel, sizer=trigger_menu_box,
+ ps=parent, key=TRIGGER_MODE_KEY,
+ choices=map(lambda x: x[1], TRIGGER_MODES),
+ labels=map(lambda x: x[0], TRIGGER_MODES),
+ label='Mode', width=WIDTH,
+ )
+ #trigger slope
+ trigger_slope_chooser = forms.drop_down(
+ parent=trigger_menu_panel, sizer=trigger_menu_box,
+ ps=parent, key=TRIGGER_SLOPE_KEY,
+ choices=map(lambda x: x[1], TRIGGER_SLOPES),
+ labels=map(lambda x: x[0], TRIGGER_SLOPES),
+ label='Slope', width=WIDTH,
+ )
+ #trigger channel
+ trigger_channel_chooser = forms.drop_down(
+ parent=trigger_menu_panel, sizer=trigger_menu_box,
+ ps=parent, key=TRIGGER_CHANNEL_KEY,
+ choices=map(lambda x: x[1], CHANNELS),
+ labels=map(lambda x: x[0], CHANNELS),
+ label='Channel', width=WIDTH,
+ )
+ #trigger level
+ hbox = wx.BoxSizer(wx.HORIZONTAL)
+ trigger_menu_box.Add(hbox, 0, wx.EXPAND)
+ hbox.Add(wx.StaticText(trigger_menu_panel, label='Level:'), 1, wx.ALIGN_CENTER_VERTICAL)
+ trigger_level_button = forms.single_button(
+ parent=trigger_menu_panel, sizer=hbox, label='50%',
+ callback=parent.set_auto_trigger_level, style=wx.BU_EXACTFIT,
+ )
+ hbox.AddSpacer(WIDTH-60)
+ trigger_level_buttons = forms.incr_decr_buttons(
+ parent=trigger_menu_panel, sizer=hbox,
+ on_incr=self._on_incr_trigger_level, on_decr=self._on_decr_trigger_level,
+ )
+ def disable_all(trigger_mode):
+ for widget in (trigger_slope_chooser, trigger_channel_chooser, trigger_level_buttons, trigger_level_button):
+ widget.Disable(trigger_mode == gr.gr_TRIG_MODE_FREE)
+ parent.subscribe(TRIGGER_MODE_KEY, disable_all)
+ disable_all(parent[TRIGGER_MODE_KEY])
+ ##################################################
+ # XY Menu Box
+ ##################################################
+ if parent.num_inputs > 1:
+ xy_menu_panel = wx.Panel(options_notebook)
+ options_notebook_args.append((xy_menu_panel, XY_PAGE_INDEX, 'XY'))
+ xy_menu_box = wx.BoxSizer(wx.VERTICAL)
+ xy_menu_panel.SetSizer(xy_menu_box)
+ #x and y channel choosers
+ xy_menu_box.AddStretchSpacer()
+ forms.drop_down(
+ parent=xy_menu_panel, sizer=xy_menu_box,
+ ps=parent, key=X_CHANNEL_KEY,
+ choices=map(lambda x: x[1], CHANNELS),
+ labels=map(lambda x: x[0], CHANNELS),
+ label='Channel X', width=WIDTH,
+ )
+ xy_menu_box.AddStretchSpacer()
+ forms.drop_down(
+ parent=xy_menu_panel, sizer=xy_menu_box,
+ ps=parent, key=Y_CHANNEL_KEY,
+ choices=map(lambda x: x[1], CHANNELS),
+ labels=map(lambda x: x[0], CHANNELS),
+ label='Channel Y', width=WIDTH,
+ )
+ #marker
+ xy_menu_box.AddStretchSpacer()
+ forms.drop_down(
+ parent=xy_menu_panel, sizer=xy_menu_box,
+ ps=parent, key=XY_MARKER_KEY,
+ choices=map(lambda x: x[1], MARKER_TYPES),
+ labels=map(lambda x: x[0], MARKER_TYPES),
+ label='Marker', width=WIDTH,
+ )
+ xy_menu_box.AddStretchSpacer()
+ ##################################################
+ # Setup Options Notebook
+ ##################################################
+ forms.notebook(
+ parent=self, sizer=chan_options_box,
+ notebook=options_notebook,
+ ps=parent, key=CHANNEL_OPTIONS_KEY,
+ pages=map(lambda x: x[0], options_notebook_args),
+ choices=map(lambda x: x[1], options_notebook_args),
+ labels=map(lambda x: x[2], options_notebook_args),
+ )
+ #gui handling for channel options changing
+ def options_notebook_changed(chan_opt):
+ try:
+ parent[TRIGGER_SHOW_KEY] = chan_opt == TRIGGER_PAGE_INDEX
+ parent[XY_MODE_KEY] = chan_opt == XY_PAGE_INDEX
+ except wx.PyDeadObjectError: pass
+ parent.subscribe(CHANNEL_OPTIONS_KEY, options_notebook_changed)
+ #gui handling for xy mode changing
+ def xy_mode_changed(mode):
+ #ensure xy tab is selected
+ if mode and parent[CHANNEL_OPTIONS_KEY] != XY_PAGE_INDEX:
+ parent[CHANNEL_OPTIONS_KEY] = XY_PAGE_INDEX
+ #ensure xy tab is not selected
+ elif not mode and parent[CHANNEL_OPTIONS_KEY] == XY_PAGE_INDEX:
+ parent[CHANNEL_OPTIONS_KEY] = 0
+ #show/hide control buttons
+ scope_mode_box.ShowItems(not mode)
+ xy_mode_box.ShowItems(mode)
+ control_box.Layout()
+ parent.subscribe(XY_MODE_KEY, xy_mode_changed)
+ xy_mode_changed(parent[XY_MODE_KEY])
+ ##################################################
+ # Run/Stop Button
+ ##################################################
+ #run/stop
+ control_box.AddStretchSpacer()
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+ #mouse wheel event
+ def on_mouse_wheel(event):
+ if not parent[XY_MODE_KEY]:
+ if event.GetWheelRotation() < 0: self._on_incr_t_divs(event)
+ else: self._on_decr_t_divs(event)
+ parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ #trigger level
+ def _on_incr_trigger_level(self, event):
+ self.parent[TRIGGER_LEVEL_KEY] += self.parent[Y_PER_DIV_KEY]/3.
+ def _on_decr_trigger_level(self, event):
+ self.parent[TRIGGER_LEVEL_KEY] -= self.parent[Y_PER_DIV_KEY]/3.
+ #incr/decr divs
+ def _on_incr_t_divs(self, event):
+ self.parent[T_PER_DIV_KEY] = common.get_clean_incr(self.parent[T_PER_DIV_KEY])
+ def _on_decr_t_divs(self, event):
+ self.parent[T_PER_DIV_KEY] = common.get_clean_decr(self.parent[T_PER_DIV_KEY])
+ def _on_incr_x_divs(self, event):
+ self.parent[X_PER_DIV_KEY] = common.get_clean_incr(self.parent[X_PER_DIV_KEY])
+ def _on_decr_x_divs(self, event):
+ self.parent[X_PER_DIV_KEY] = common.get_clean_decr(self.parent[X_PER_DIV_KEY])
+ def _on_incr_y_divs(self, event):
+ self.parent[Y_PER_DIV_KEY] = common.get_clean_incr(self.parent[Y_PER_DIV_KEY])
+ def _on_decr_y_divs(self, event):
+ self.parent[Y_PER_DIV_KEY] = common.get_clean_decr(self.parent[Y_PER_DIV_KEY])
+ #incr/decr offset
+ def _on_incr_x_off(self, event):
+ self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY]
+ def _on_decr_x_off(self, event):
+ self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY]
+ def _on_incr_y_off(self, event):
+ self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY]
+ def _on_decr_y_off(self, event):
+ self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]
+
+ ##################################################
+ # subscriber handlers
+ ##################################################
+ def _update_layout(self,key):
+ # Just ignore the key value we get
+ # we only need to now that the visability or size of something has changed
+ self.parent.Layout()
+ #self.parent.Fit()
+
+##################################################
+# Scope window with plotter and control panel
+##################################################
+class scope_window(wx.Panel, pubsub.pubsub):
+ def __init__(
+ self,
+ parent,
+ controller,
+ size,
+ title,
+ frame_rate,
+ num_inputs,
+ sample_rate_key,
+ t_scale,
+ v_scale,
+ v_offset,
+ xy_mode,
+ ac_couple_key,
+ trigger_level_key,
+ trigger_mode_key,
+ trigger_slope_key,
+ trigger_channel_key,
+ decimation_key,
+ msg_key,
+ use_persistence,
+ persist_alpha,
+ trig_mode,
+ y_axis_label,
+ ):
+ pubsub.pubsub.__init__(self)
+ #check num inputs
+ assert num_inputs <= len(CHANNEL_COLOR_SPECS)
+ #setup
+ self.sampleses = None
+ self.num_inputs = num_inputs
+ autorange = not v_scale
+ self.autorange_ts = 0
+ v_scale = v_scale or 1
+ self.frame_rate_ts = 0
+ #proxy the keys
+ self.proxy(MSG_KEY, controller, msg_key)
+ self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
+ self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key)
+ self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key)
+ self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key)
+ self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key)
+ self.proxy(DECIMATION_KEY, controller, decimation_key)
+ #initialize values
+ self[RUNNING_KEY] = True
+ self[XY_MARKER_KEY] = 2.0
+ self[CHANNEL_OPTIONS_KEY] = 0
+ self[XY_MODE_KEY] = xy_mode
+ self[X_CHANNEL_KEY] = 0
+ self[Y_CHANNEL_KEY] = self.num_inputs-1
+ self[AUTORANGE_KEY] = autorange
+ self[T_PER_DIV_KEY] = t_scale
+ self[X_PER_DIV_KEY] = v_scale
+ self[Y_PER_DIV_KEY] = v_scale
+ self[T_OFF_KEY] = 0
+ self[X_OFF_KEY] = v_offset
+ self[Y_OFF_KEY] = v_offset
+ self[T_DIVS_KEY] = 8
+ self[X_DIVS_KEY] = 8
+ self[Y_DIVS_KEY] = 8
+ self[Y_AXIS_LABEL] = y_axis_label
+ self[FRAME_RATE_KEY] = frame_rate
+ self[TRIGGER_LEVEL_KEY] = 0
+ self[TRIGGER_CHANNEL_KEY] = 0
+ self[TRIGGER_MODE_KEY] = trig_mode
+
+ self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS
+ self[T_FRAC_OFF_KEY] = 0.5
+ self[USE_PERSISTENCE_KEY] = use_persistence
+ self[PERSIST_ALPHA_KEY] = persist_alpha
+
+ if self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_STRIPCHART:
+ self[T_FRAC_OFF_KEY] = 0.0
+
+ for i in range(num_inputs):
+ self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i))
+ #init panel and plot
+ wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.channel_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.SetSizeHints(*size)
+ self.plotter.set_title(title)
+ self.plotter.enable_legend(True)
+ self.plotter.enable_point_label(True)
+ self.plotter.enable_grid_lines(True)
+ self.plotter.set_use_persistence(use_persistence)
+ self.plotter.set_persist_alpha(persist_alpha)
+ #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)
+ #register events for message
+ self.subscribe(MSG_KEY, self.handle_msg)
+ #register events for grid
+ for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [
+ TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY,
+ 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,
+ XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY,
+ TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY,
+ ]: self.subscribe(key, self.update_grid)
+ #register events for plotter settings
+ self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
+ self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
+ #initial update
+ 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)
+ #extract the trigger offset
+ self.trigger_offset = samples[-1]
+ samples = samples[:-1]
+ 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)]
+ #handle samples
+ self.handle_samples()
+ self.frame_rate_ts = time.time()
+
+ def set_auto_trigger_level(self, *args):
+ """
+ Use the current trigger channel and samples to calculate the 50% level.
+ """
+ if not self.sampleses: return
+ samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]]
+ self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2
+
+ def handle_samples(self):
+ """
+ Handle the cached samples from the scope input.
+ Perform ac coupling, triggering, and auto ranging.
+ """
+ if not self.sampleses: return
+ sampleses = self.sampleses
+ if self[XY_MODE_KEY]:
+ self[DECIMATION_KEY] = 1
+ x_samples = sampleses[self[X_CHANNEL_KEY]]
+ y_samples = sampleses[self[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[X_PER_DIV_KEY] = x_per_div; return
+ #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[X_OFF_KEY] = x_off; return
+ #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[Y_PER_DIV_KEY] = y_per_div; return
+ #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[Y_OFF_KEY] = y_off; return
+ 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],
+ marker=self[XY_MARKER_KEY],
+ )
+ #turn off each waveform
+ for i, samples in enumerate(sampleses):
+ self.plotter.clear_waveform(channel='Ch%d'%(i+1))
+ 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[Y_PER_DIV_KEY] = y_per_div; return
+ #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[Y_OFF_KEY] = y_off; return
+ self.autorange_ts = time.time()
+ #number of samples to scale to the screen
+ actual_rate = self.get_actual_rate()
+ time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY]
+ num_samps = int(round(time_span*actual_rate))
+ #handle the time offset
+ t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span)
+ if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return
+ samps_off = int(round(actual_rate*self[T_OFF_KEY]))
+ #adjust the decim so that we use about half the samps
+ self[DECIMATION_KEY] = int(round(
+ time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0]))
+ )
+ )
+ #num samps too small, auto increment the time
+ if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY])
+ #num samps in bounds, plot each waveform
+ elif num_samps <= len(sampleses[0]):
+ for i, samples in enumerate(sampleses):
+ #plot samples
+ self.plotter.set_waveform(
+ channel='Ch%d'%(i+1),
+ samples=samples[samps_off:num_samps+samps_off],
+ color_spec=CHANNEL_COLOR_SPECS[i],
+ marker=self[common.index_key(MARKER_KEY, i)],
+ trig_off=self.trigger_offset,
+ )
+ #turn XY channel off
+ self.plotter.clear_waveform(channel='XY')
+ #keep trigger level within range
+ if self[TRIGGER_LEVEL_KEY] > self.get_y_max():
+ self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return
+ if self[TRIGGER_LEVEL_KEY] < self.get_y_min():
+ self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return
+ #disable the trigger channel
+ if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE:
+ self.plotter.clear_waveform(channel='Trig')
+ else: #show trigger channel
+ trigger_level = self[TRIGGER_LEVEL_KEY]
+ trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0
+ self.plotter.set_waveform(
+ channel='Trig',
+ samples=(
+ [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()],
+ [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level]
+ ),
+ color_spec=TRIGGER_COLOR_SPEC,
+ )
+ #update the plotter
+ self.plotter.update()
+
+ def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY]
+ def get_t_min(self): return self[T_OFF_KEY]
+ def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY]
+ def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
+ def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
+ def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
+ def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
+
+ def update_grid(self, *args):
+ """
+ Update the grid to reflect the current settings:
+ xy divisions, xy offset, xy mode setting
+ """
+ if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return
+ if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return
+ if self[XY_MODE_KEY]:
+ #update the x axis
+ self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1))
+ self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY])
+ #update the y axis
+ self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1))
+ self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
+ else:
+ #update the t axis
+ self.plotter.set_x_label('Time', 's')
+ self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True)
+ #update the y axis
+ self.plotter.set_y_label(self[Y_AXIS_LABEL])
+ self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
+ #redraw current sample
+ self.handle_samples()
+
diff --git a/gr-wxgui/src/python/scopesink2.py b/gr-wxgui/src/python/scopesink2.py
new file mode 100644
index 000000000..99e268895
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink2.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2008,2009 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
+
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
+
+if style == 'auto' or style == 'gl':
+ try:
+ import wx.glcanvas
+ from OpenGL.GL import *
+ from scopesink_gl import scope_sink_f, scope_sink_c
+ except ImportError:
+ if style == 'gl':
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
+ else:
+ # Fall backto non-gl sinks
+ from scopesink_nongl import scope_sink_f, scope_sink_c
+elif style == 'nongl':
+ from scopesink_nongl import scope_sink_f, scope_sink_c
+else:
+ raise RuntimeError("Unknown wxgui style")
diff --git a/gr-wxgui/src/python/scopesink_gl.py b/gr-wxgui/src/python/scopesink_gl.py
new file mode 100644
index 000000000..e6ff532e7
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink_gl.py
@@ -0,0 +1,233 @@
+#
+# Copyright 2008,2010 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 *
+import math
+
+class ac_couple_block(gr.hier_block2):
+ """
+ AC couple the incoming stream by subtracting out the low pass signal.
+ Mute the low pass filter to disable ac coupling.
+ """
+
+ def __init__(self, controller, ac_couple_key, sample_rate_key):
+ gr.hier_block2.__init__(
+ self,
+ "ac_couple",
+ gr.io_signature(1, 1, gr.sizeof_float),
+ gr.io_signature(1, 1, gr.sizeof_float),
+ )
+ #blocks
+ lpf = gr.single_pole_iir_filter_ff(0.0)
+ sub = gr.sub_ff()
+ mute = gr.mute_ff()
+ #connect
+ self.connect(self, sub, self)
+ self.connect(self, lpf, mute, (sub, 1))
+ #subscribe
+ controller.subscribe(ac_couple_key, lambda x: mute.set_mute(not x))
+ controller.subscribe(sample_rate_key, lambda x: lpf.set_taps(0.05))
+ #initialize
+ controller[ac_couple_key] = controller[ac_couple_key]
+ controller[sample_rate_key] = controller[sample_rate_key]
+
+##################################################
+# Scope sink block (wrapper for old wxgui)
+##################################################
+class _scope_sink_base(gr.hier_block2, common.wxgui_hb):
+ """
+ A scope block with a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ title='',
+ sample_rate=1,
+ size=scope_window.DEFAULT_WIN_SIZE,
+ v_scale=0,
+ t_scale=0,
+ v_offset=0,
+ xy_mode=False,
+ ac_couple=False,
+ num_inputs=1,
+ trig_mode=scope_window.DEFAULT_TRIG_MODE,
+ y_axis_label='Counts',
+ frame_rate=scope_window.DEFAULT_FRAME_RATE,
+ use_persistence=False,
+ persist_alpha=None,
+ **kwargs #do not end with a comma
+ ):
+ #ensure analog alpha
+ if persist_alpha is None:
+ actual_frame_rate=float(frame_rate)
+ analog_cutoff_freq=0.5 # Hertz
+ #calculate alpha from wanted cutoff freq
+ persist_alpha = 1.0 - math.exp(-2.0*math.pi*analog_cutoff_freq/actual_frame_rate)
+
+ if not t_scale: t_scale = 10.0/sample_rate
+ #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)
+ #controller
+ self.controller = pubsub()
+ self.controller.subscribe(SAMPLE_RATE_KEY, scope.set_sample_rate)
+ self.controller.publish(SAMPLE_RATE_KEY, scope.sample_rate)
+ self.controller.subscribe(DECIMATION_KEY, scope.set_decimation_count)
+ self.controller.publish(DECIMATION_KEY, scope.get_decimation_count)
+ self.controller.subscribe(TRIGGER_LEVEL_KEY, scope.set_trigger_level)
+ self.controller.publish(TRIGGER_LEVEL_KEY, scope.get_trigger_level)
+ self.controller.subscribe(TRIGGER_MODE_KEY, scope.set_trigger_mode)
+ self.controller.publish(TRIGGER_MODE_KEY, scope.get_trigger_mode)
+ self.controller.subscribe(TRIGGER_SLOPE_KEY, scope.set_trigger_slope)
+ self.controller.publish(TRIGGER_SLOPE_KEY, scope.get_trigger_slope)
+ self.controller.subscribe(TRIGGER_CHANNEL_KEY, scope.set_trigger_channel)
+ self.controller.publish(TRIGGER_CHANNEL_KEY, scope.get_trigger_channel)
+ actual_num_inputs = self._real and num_inputs or num_inputs*2
+ #init ac couple
+ for i in range(actual_num_inputs):
+ self.controller[common.index_key(AC_COUPLE_KEY, i)] = ac_couple
+ #start input watcher
+ common.input_watcher(msgq, self.controller, MSG_KEY)
+ #create window
+ self.win = scope_window.scope_window(
+ parent=parent,
+ controller=self.controller,
+ size=size,
+ title=title,
+ frame_rate=frame_rate,
+ num_inputs=actual_num_inputs,
+ sample_rate_key=SAMPLE_RATE_KEY,
+ t_scale=t_scale,
+ v_scale=v_scale,
+ v_offset=v_offset,
+ xy_mode=xy_mode,
+ trig_mode=trig_mode,
+ y_axis_label=y_axis_label,
+ ac_couple_key=AC_COUPLE_KEY,
+ trigger_level_key=TRIGGER_LEVEL_KEY,
+ trigger_mode_key=TRIGGER_MODE_KEY,
+ trigger_slope_key=TRIGGER_SLOPE_KEY,
+ trigger_channel_key=TRIGGER_CHANNEL_KEY,
+ decimation_key=DECIMATION_KEY,
+ msg_key=MSG_KEY,
+ use_persistence=use_persistence,
+ persist_alpha=persist_alpha,
+ )
+ common.register_access_methods(self, self.win)
+ #connect
+ if self._real:
+ for i in range(num_inputs):
+ self.wxgui_connect(
+ (self, i),
+ ac_couple_block(self.controller, common.index_key(AC_COUPLE_KEY, i), SAMPLE_RATE_KEY),
+ (scope, i),
+ )
+ else:
+ for i in range(num_inputs):
+ c2f = gr.complex_to_float()
+ self.wxgui_connect((self, i), c2f)
+ for j in range(2):
+ self.connect(
+ (c2f, j),
+ ac_couple_block(self.controller, common.index_key(AC_COUPLE_KEY, 2*i+j), SAMPLE_RATE_KEY),
+ (scope, 2*i+j),
+ )
+
+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
+
+# ----------------------------------------------------------------
+# 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)
+
+ default_input_rate = 1e6
+ if len(argv) > 1:
+ input_rate = int(argv[1])
+ else:
+ input_rate = default_input_rate
+
+ 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*default_input_rate/input_rate # old behavior
+
+ print "input rate %s v_scale %s t_scale %s" % (input_rate,v_scale,t_scale)
+
+
+ # Generate a complex sinusoid
+ ampl=1.0e3
+ self.src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3*input_rate/default_input_rate, ampl)
+ self.noise =gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 11.1*25.1e3*input_rate/default_input_rate, ampl/10)
+ #self.noise =gr.noise_source_c(gr.GR_GAUSSIAN, ampl/10)
+ self.combine=gr.add_cc()
+
+ # 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,
+ 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.combine,0))
+ self.connect(self.noise,(self.combine,1))
+ self.connect(self.combine, 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..d45e79906
--- /dev/null
+++ b/gr-wxgui/src/python/scopesink_nongl.py
@@ -0,0 +1,651 @@
+#!/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 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, **kwargs):
+
+ 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, xy_mode=False, **kwargs):
+
+ 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)
+ self.win.info.xy = xy_mode
+
+ 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
+ self.autorange = not 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 (gru.msgq_runner):
+ def __init__ (self, msgq, event_receiver, frame_decim, **kwds):
+ self.event_receiver = event_receiver
+ self.frame_decim = frame_decim
+ self.iscan = 0
+ gru.msgq_runner.__init__(self, msgq, self.handle_msg)
+
+ def handle_msg(self, msg):
+ 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
+
+ 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 = ['Free', 'Auto', 'Norm'])
+ self.trig_mode_choice.SetSelection(1)
+ 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 == 'Norm':
+ sink.set_trigger_mode (gr.gr_TRIG_MODE_NORM)
+ elif s == 'Auto':
+ sink.set_trigger_mode (gr.gr_TRIG_MODE_AUTO)
+ elif s == 'Free':
+ sink.set_trigger_mode (gr.gr_TRIG_MODE_FREE)
+ 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
new file mode 100644
index 000000000..e8cdcfcac
--- /dev/null
+++ b/gr-wxgui/src/python/slider.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+import wx
+
+def slider(parent, min, max, callback):
+ """
+ Return a wx.Slider object.
+
+ @param min: minimum slider value
+ @type min: float
+ @param max: maximum slider value
+ @type max: float
+ @param callback: function of one arg invoked when slider moves.
+ @rtype: wx.Slider
+ """
+ new_id = wx.NewId()
+ s = wx.Slider(parent, new_id, (max+min)/2, min, max, wx.DefaultPosition,
+ wx.Size(250,-1), wx.SL_HORIZONTAL | wx.SL_LABELS)
+ wx.EVT_COMMAND_SCROLL(parent, new_id,
+ lambda evt : callback(evt.GetInt()))
+ return s
+
+
+# ----------------------------------------------------------------
+# Demo app
+# ----------------------------------------------------------------
+if __name__ == '__main__':
+
+ from gnuradio.wxgui import stdgui
+
+ class demo_graph(stdgui.gui_flow_graph):
+
+ def __init__(self, frame, panel, vbox, argv):
+ stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
+
+ vbox.Add(slider(panel, 23, 47, self.my_callback1), 1, wx.ALIGN_CENTER)
+ vbox.Add(slider(panel, -100, 100, self.my_callback2), 1, wx.ALIGN_CENTER)
+
+ def my_callback1(self, val):
+ print "cb1 = ", val
+
+ def my_callback2(self, val):
+ print "cb2 = ", val
+
+ def main ():
+ app = stdgui.stdapp (demo_graph, "Slider Demo")
+ app.MainLoop ()
+
+ main ()
diff --git a/gr-wxgui/src/python/stdgui2.py b/gr-wxgui/src/python/stdgui2.py
new file mode 100644
index 000000000..71436d72c
--- /dev/null
+++ b/gr-wxgui/src/python/stdgui2.py
@@ -0,0 +1,100 @@
+#
+# Copyright 2004 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.
+#
+
+'''A simple wx gui for GNU Radio applications'''
+
+import wx
+import sys
+from gnuradio import gr
+
+
+class stdapp (wx.App):
+ def __init__ (self, top_block_maker, title="GNU Radio", nstatus=2,
+ max_noutput_items=None):
+ self.top_block_maker = top_block_maker
+ self.title = title
+ self._nstatus = nstatus
+ self._max_noutput_items = max_noutput_items
+ # All our initialization must come before calling wx.App.__init__.
+ # OnInit is called from somewhere in the guts of __init__.
+ wx.App.__init__ (self, redirect=False)
+
+ def OnInit (self):
+ frame = stdframe (self.top_block_maker, self.title, self._nstatus,
+ self._max_noutput_items)
+ frame.Show (True)
+ self.SetTopWindow (frame)
+ return True
+
+
+class stdframe (wx.Frame):
+ def __init__ (self, top_block_maker, title="GNU Radio", nstatus=2,
+ max_nouts=None):
+ # print "stdframe.__init__"
+ wx.Frame.__init__(self, None, -1, title)
+
+ self.CreateStatusBar (nstatus)
+ mainmenu = wx.MenuBar ()
+
+ menu = wx.Menu ()
+ item = menu.Append (200, 'E&xit', 'Exit')
+ self.Bind (wx.EVT_MENU, self.OnCloseWindow, item)
+ mainmenu.Append (menu, "&File")
+ self.SetMenuBar (mainmenu)
+
+ self.Bind (wx.EVT_CLOSE, self.OnCloseWindow)
+ self.panel = stdpanel (self, self, top_block_maker, max_nouts)
+ vbox = wx.BoxSizer(wx.VERTICAL)
+ vbox.Add(self.panel, 1, wx.EXPAND)
+ self.SetSizer(vbox)
+ self.SetAutoLayout(True)
+ vbox.Fit(self)
+
+ def OnCloseWindow (self, event):
+ self.top_block().stop()
+ self.Destroy ()
+
+ def top_block (self):
+ return self.panel.top_block
+
+class stdpanel (wx.Panel):
+ def __init__ (self, parent, frame, top_block_maker,
+ max_nouts=None):
+ # print "stdpanel.__init__"
+ wx.Panel.__init__ (self, parent, -1)
+ self.frame = frame
+
+ vbox = wx.BoxSizer (wx.VERTICAL)
+ self.top_block = top_block_maker (frame, self, vbox, sys.argv)
+ self.SetSizer (vbox)
+ self.SetAutoLayout (True)
+ vbox.Fit (self)
+
+ if(max_nouts is not None):
+ self.top_block.start (max_nouts)
+ else:
+ self.top_block.start ()
+
+class std_top_block (gr.top_block):
+ def __init__ (self, parent, panel, vbox, argv):
+ # Call the hier_block2 constructor
+ # Top blocks have no inputs and outputs
+ gr.top_block.__init__(self, "std_top_block")
diff --git a/gr-wxgui/src/python/termsink.py b/gr-wxgui/src/python/termsink.py
new file mode 100644
index 000000000..a0cfd575d
--- /dev/null
+++ b/gr-wxgui/src/python/termsink.py
@@ -0,0 +1,77 @@
+#
+# Copyright 2009 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 gru
+import wx
+
+DEFAULT_WIN_SIZE = (600, 300)
+APPEND_EVENT = wx.NewEventType()
+EVT_APPEND_EVENT = wx.PyEventBinder(APPEND_EVENT, 0)
+
+class AppendEvent(wx.PyEvent):
+ def __init__(self, text):
+ wx.PyEvent.__init__(self)
+ self.SetEventType(APPEND_EVENT)
+ self.text = text
+
+ def Clone(self):
+ self.__class__(self.GetId())
+
+class termsink(wx.Panel):
+ def __init__(self,
+ parent,
+ msgq,
+ size=DEFAULT_WIN_SIZE,
+ ):
+
+ wx.Panel.__init__(self,
+ parent,
+ size=size,
+ style=wx.SIMPLE_BORDER,
+ )
+
+ self.text_ctrl = wx.TextCtrl(self,
+ wx.ID_ANY,
+ value="",
+ size=size,
+ style=wx.TE_MULTILINE|wx.TE_READONLY,
+ )
+
+ main_sizer = wx.BoxSizer(wx.VERTICAL)
+ main_sizer.Add(self.text_ctrl, 1, wx.EXPAND)
+ self.SetSizerAndFit(main_sizer)
+
+ EVT_APPEND_EVENT(self, self.evt_append)
+ self.runner = gru.msgq_runner(msgq, self.handle_msg)
+
+ def handle_msg(self, msg):
+ # This gets called in the queue runner thread context
+ # For now, just add whatever the user sends to the text control
+ text = msg.to_string()
+
+ # Create a wxPython event and post it to the event queue
+ evt = AppendEvent(text)
+ wx.PostEvent(self, evt)
+ del evt
+
+ def evt_append(self, evt):
+ # This gets called by the wxPython event queue runner
+ self.text_ctrl.AppendText(evt.text)
diff --git a/gr-wxgui/src/python/waterfall_window.py b/gr-wxgui/src/python/waterfall_window.py
new file mode 100644
index 000000000..a190899c3
--- /dev/null
+++ b/gr-wxgui/src/python/waterfall_window.py
@@ -0,0 +1,324 @@
+#
+# 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-1`301, USA.
+#
+
+##################################################
+# Imports
+##################################################
+import plotter
+import common
+import wx
+import numpy
+import math
+import pubsub
+from constants import *
+from gnuradio import gr #for gr.prefs
+import forms
+
+##################################################
+# Constants
+##################################################
+SLIDER_STEPS = 100
+AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
+DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'waterfall_rate', 30)
+DEFAULT_COLOR_MODE = gr.prefs().get_string('wxgui', 'waterfall_color', 'rgb1')
+DEFAULT_WIN_SIZE = (600, 300)
+DIV_LEVELS = (1, 2, 5, 10, 20)
+MIN_DYNAMIC_RANGE, MAX_DYNAMIC_RANGE = 10, 200
+DYNAMIC_RANGE_STEP = 10.
+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, style=wx.SUNKEN_BORDER)
+ parent[SHOW_CONTROL_PANEL_KEY] = True
+ parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
+ control_box = wx.BoxSizer(wx.VERTICAL)
+ control_box.AddStretchSpacer()
+ options_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ #average
+ forms.check_box(
+ sizer=options_box, parent=self, label='Average',
+ ps=parent, key=AVERAGE_KEY,
+ )
+ avg_alpha_text = forms.static_text(
+ sizer=options_box, parent=self, label='Avg Alpha',
+ converter=forms.float_converter(lambda x: '%.4f'%x),
+ ps=parent, key=AVG_ALPHA_KEY, width=50,
+ )
+ avg_alpha_slider = forms.log_slider(
+ sizer=options_box, parent=self,
+ min_exp=AVG_ALPHA_MIN_EXP,
+ max_exp=AVG_ALPHA_MAX_EXP,
+ num_steps=SLIDER_STEPS,
+ ps=parent, key=AVG_ALPHA_KEY,
+ )
+ for widget in (avg_alpha_text, avg_alpha_slider):
+ parent.subscribe(AVERAGE_KEY, widget.Enable)
+ widget.Enable(parent[AVERAGE_KEY])
+ #begin axes box
+ control_box.AddStretchSpacer()
+ axes_box = forms.static_box_sizer(
+ parent=self, sizer=control_box, label='Axes Options',
+ bold=True, orient=wx.VERTICAL,
+ )
+ #num lines buttons
+ forms.incr_decr_buttons(
+ parent=self, sizer=axes_box, label='Time Scale',
+ on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
+ )
+ #dyanmic range buttons
+ forms.incr_decr_buttons(
+ parent=self, sizer=axes_box, label='Dyn Range',
+ on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
+ )
+ #ref lvl buttons
+ forms.incr_decr_buttons(
+ parent=self, sizer=axes_box, label='Ref Level',
+ on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
+ )
+ #color mode
+ forms.drop_down(
+ parent=self, sizer=axes_box, width=100,
+ ps=parent, key=COLOR_MODE_KEY, label='Color',
+ choices=map(lambda x: x[1], COLOR_MODES),
+ labels=map(lambda x: x[0], COLOR_MODES),
+ )
+ #autoscale
+ forms.single_button(
+ parent=self, sizer=axes_box, label='Autoscale',
+ callback=self.parent.autoscale,
+ )
+ #clear
+ control_box.AddStretchSpacer()
+ forms.single_button(
+ parent=self, sizer=control_box, label='Clear',
+ callback=self._on_clear_button,
+ )
+ #run/stop
+ forms.toggle_button(
+ sizer=control_box, parent=self,
+ true_label='Stop', false_label='Run',
+ ps=parent, key=RUNNING_KEY,
+ )
+ #set sizer
+ self.SetSizerAndFit(control_box)
+
+ ##################################################
+ # Event handlers
+ ##################################################
+ def _on_clear_button(self, event):
+ self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
+ def _on_incr_dynamic_range(self, event):
+ self.parent[DYNAMIC_RANGE_KEY] = min(MAX_DYNAMIC_RANGE, common.get_clean_incr(self.parent[DYNAMIC_RANGE_KEY]))
+ def _on_decr_dynamic_range(self, event):
+ self.parent[DYNAMIC_RANGE_KEY] = max(MIN_DYNAMIC_RANGE, common.get_clean_decr(self.parent[DYNAMIC_RANGE_KEY]))
+ def _on_incr_ref_level(self, event):
+ self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
+ def _on_decr_ref_level(self, event):
+ self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
+ def _on_incr_time_scale(self, event):
+ old_rate = self.parent[FRAME_RATE_KEY]
+ self.parent[FRAME_RATE_KEY] *= 0.75
+ if self.parent[FRAME_RATE_KEY] < 1.0:
+ self.parent[FRAME_RATE_KEY] = 1.0
+
+ if self.parent[FRAME_RATE_KEY] == old_rate:
+ self.parent[DECIMATION_KEY] += 1
+ def _on_decr_time_scale(self, event):
+ old_rate = self.parent[FRAME_RATE_KEY]
+ self.parent[FRAME_RATE_KEY] *= 1.25
+ if self.parent[FRAME_RATE_KEY] == old_rate:
+ self.parent[DECIMATION_KEY] -= 1
+
+##################################################
+# Waterfall window with plotter and control panel
+##################################################
+class waterfall_window(wx.Panel, pubsub.pubsub):
+ 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.samples = list()
+ self.real = real
+ self.fft_size = fft_size
+ #proxy the keys
+ self.proxy(MSG_KEY, controller, msg_key)
+ self.proxy(DECIMATION_KEY, controller, decimation_key)
+ self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
+ self.proxy(AVERAGE_KEY, controller, average_key)
+ self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
+ self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
+ #init panel and plot
+ wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
+ self.plotter = plotter.waterfall_plotter(self)
+ self.plotter.SetSize(wx.Size(*size))
+ self.plotter.SetSizeHints(*size)
+ self.plotter.set_title(title)
+ self.plotter.enable_point_label(True)
+ self.plotter.enable_grid_lines(False)
+ #plotter listeners
+ self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
+ self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
+ #initialize values
+ self[DYNAMIC_RANGE_KEY] = dynamic_range
+ self[NUM_LINES_KEY] = num_lines
+ self[Y_DIVS_KEY] = 8
+ self[X_DIVS_KEY] = 8 #approximate
+ self[REF_LEVEL_KEY] = ref_level
+ self[BASEBAND_FREQ_KEY] = baseband_freq
+ self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
+ self[COLOR_MODE_KEY] = DEFAULT_COLOR_MODE
+ self[RUNNING_KEY] = 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)
+ #register events
+ self.subscribe(MSG_KEY, self.handle_msg)
+ for key in (
+ DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
+ BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
+ ): self.subscribe(key, self.update_grid)
+ #initial update
+ self.update_grid()
+
+ def set_callback(self,callb):
+ self.plotter.set_callback(callb)
+
+ 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.
+ """
+ if not len(self.samples): return
+ min_level, max_level = common.get_min_max_fft(self.samples)
+ #set the range and level
+ self[DYNAMIC_RANGE_KEY] = common.get_clean_num(max_level - min_level)
+ self[REF_LEVEL_KEY] = DYNAMIC_RANGE_STEP*round(.5+max_level/DYNAMIC_RANGE_STEP)
+
+ 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+1)/2]
+ else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/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[SAMPLE_RATE_KEY]
+ frame_rate = self[FRAME_RATE_KEY]
+ if frame_rate < 1.0 :
+ frame_rate = 1.0
+ 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)
+ #update the x grid
+ if self.real:
+ self.plotter.set_x_grid(
+ baseband_freq,
+ baseband_freq + sample_rate/2.0,
+ x_per_div, True,
+ )
+ else:
+ self.plotter.set_x_grid(
+ baseband_freq - sample_rate/2.0,
+ baseband_freq + sample_rate/2.0,
+ x_per_div, True,
+ )
+ #update x units
+ self.plotter.set_x_label('Frequency', '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, True)
+ #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
new file mode 100644
index 000000000..0b876fc3e
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink2.py
@@ -0,0 +1,41 @@
+#
+# Copyright 2008,2009 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
+
+p = gr.prefs()
+style = p.get_string('wxgui', 'style', 'auto')
+
+if style == 'auto' or style == 'gl':
+ try:
+ import wx.glcanvas
+ from OpenGL.GL import *
+ from waterfallsink_gl import waterfall_sink_f, waterfall_sink_c
+ except ImportError:
+ if style == 'gl':
+ raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?")
+ else:
+ # Fall backto non-gl sinks
+ from waterfallsink_nongl import waterfall_sink_f, waterfall_sink_c
+elif style == 'nongl':
+ from waterfallsink_nongl import waterfall_sink_f, waterfall_sink_c
+else:
+ raise RuntimeError("Unknown wxgui style")
diff --git a/gr-wxgui/src/python/waterfallsink_gl.py b/gr-wxgui/src/python/waterfallsink_gl.py
new file mode 100644
index 000000000..b69c5dda0
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink_gl.py
@@ -0,0 +1,173 @@
+#
+# Copyright 2008,2009 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.wxgui_hb):
+ """
+ An fft block with real/complex inputs and a gui window.
+ """
+
+ def __init__(
+ self,
+ parent,
+ baseband_freq=0,
+ 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,
+ win=None,
+ **kwargs #do not end with a comma
+ ):
+ #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
+ 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,
+ win=win,
+ )
+ msgq = gr.msg_queue(2)
+ sink = gr.message_sink(gr.sizeof_float*fft_size, msgq, True)
+ #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
+ common.input_watcher(msgq, self.controller, MSG_KEY)
+ #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,
+ )
+ common.register_access_methods(self, self.win)
+ setattr(self.win, 'set_baseband_freq', getattr(self, 'set_baseband_freq')) #BACKWARDS
+ #connect
+ self.wxgui_connect(self, fft, sink)
+
+ def set_callback(self,callb):
+ self.win.set_callback(callb)
+
+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..bf77b4b13
--- /dev/null
+++ b/gr-wxgui/src/python/waterfallsink_nongl.py
@@ -0,0 +1,431 @@
+#!/usr/bin/env python
+#
+# Copyright 2003-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 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)))
+
+ def set_callback(self, callb):
+ return
+
+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, **kwargs):
+
+ 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, **kwargs):
+
+ 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 (gru.msgq_runner):
+ def __init__ (self, msgq, fft_size, event_receiver, **kwds):
+ self.fft_size = fft_size
+ self.event_receiver = event_receiver
+ gru.msgq_runner.__init__(self, msgq, self.handle_msg)
+
+ def handle_msg(self, msg):
+ 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 ()