diff options
Diffstat (limited to 'gr-wxgui/src')
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 () |