diff options
Diffstat (limited to 'gr-wxgui/src')
27 files changed, 1918 insertions, 1213 deletions
diff --git a/gr-wxgui/src/python/Makefile.am b/gr-wxgui/src/python/Makefile.am index 58a9f57549..45d75b6055 100644 --- a/gr-wxgui/src/python/Makefile.am +++ b/gr-wxgui/src/python/Makefile.am @@ -42,6 +42,8 @@ ourpython_PYTHON = \ fftsink_gl.py \ fft_window.py \ gui.py \ + histosink_gl.py \ + histo_window.py \ numbersink2.py \ number_window.py \ plot.py \ diff --git a/gr-wxgui/src/python/common.py b/gr-wxgui/src/python/common.py index e7d08891d3..c84827eb8a 100644 --- a/gr-wxgui/src/python/common.py +++ b/gr-wxgui/src/python/common.py @@ -19,60 +19,86 @@ # Boston, MA 02110-1301, USA. # -import threading -import numpy -import math -import wx +#A macro to apply an index to a key +index_key = lambda key, i: "%s_%d"%(key, i+1) -class prop_setter(object): - def _register_set_prop(self, controller, control_key, *args): - def set_method(value): controller[control_key] = value - if args: set_method(args[0]) - setattr(self, 'set_%s'%control_key, set_method) +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) -################################################## -# Custom Data Event -################################################## -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 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 ################################################## +import threading + class input_watcher(threading.Thread): """ Input watcher thread runs forever. Read messages from the message queue. Forward messages to the message handler. """ - def __init__ (self, msgq, handle_msg): + def __init__ (self, msgq, controller, msg_key, arg1_key='', arg2_key=''): threading.Thread.__init__(self) self.setDaemon(1) self.msgq = msgq - self._handle_msg = handle_msg + self._controller = controller + self._msg_key = msg_key + self._arg1_key = arg1_key + self._arg2_key = arg2_key self.keep_running = True self.start() def run(self): - while self.keep_running: self._handle_msg(self.msgq.delete_head().to_string()) + while self.keep_running: + msg = self.msgq.delete_head() + 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() ################################################## # WX Shared Classes ################################################## +import math +import wx + +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 + class LabelText(wx.StaticText): """ Label text to give the wx plots a uniform look. Get the default label text and set the font bold. """ def __init__(self, parent, label): - wx.StaticText.__init__(self, parent, -1, label) + wx.StaticText.__init__(self, parent, label=label) font = self.GetFont() font.SetWeight(wx.FONTWEIGHT_BOLD) self.SetFont(font) +class LabelBox(wx.BoxSizer): + def __init__(self, parent, label, widget): + wx.BoxSizer.__init__(self, wx.HORIZONTAL) + self.Add(wx.StaticText(parent, label=' %s '%label), 1, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT) + self.Add(widget, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT) + class IncrDecrButtons(wx.BoxSizer): """ A horizontal box sizer with a increment and a decrement button. @@ -84,14 +110,14 @@ class IncrDecrButtons(wx.BoxSizer): @param on_decr the event handler for decrement """ wx.BoxSizer.__init__(self, wx.HORIZONTAL) - self._incr_button = wx.Button(parent, -1, '+', style=wx.BU_EXACTFIT) + self._incr_button = wx.Button(parent, label='+', style=wx.BU_EXACTFIT) self._incr_button.Bind(wx.EVT_BUTTON, on_incr) self.Add(self._incr_button, 0, wx.ALIGN_CENTER_VERTICAL) - self._decr_button = wx.Button(parent, -1, ' - ', style=wx.BU_EXACTFIT) + self._decr_button = wx.Button(parent, label=' - ', style=wx.BU_EXACTFIT) self._decr_button.Bind(wx.EVT_BUTTON, on_decr) self.Add(self._decr_button, 0, wx.ALIGN_CENTER_VERTICAL) - def Disable(self, disable=True): self.Enable(not disable) + def Disable(self): self.Enable(False) def Enable(self, enable=True): if enable: self._incr_button.Enable() @@ -104,7 +130,7 @@ class ToggleButtonController(wx.Button): def __init__(self, parent, controller, control_key, true_label, false_label): self._controller = controller self._control_key = control_key - wx.Button.__init__(self, parent, -1, '', style=wx.BU_EXACTFIT) + wx.Button.__init__(self, parent, style=wx.BU_EXACTFIT) self.Bind(wx.EVT_BUTTON, self._evt_button) controller.subscribe(control_key, lambda x: self.SetLabel(x and true_label or false_label)) @@ -122,30 +148,55 @@ class CheckBoxController(wx.CheckBox): def _evt_checkbox(self, e): self._controller[self._control_key] = bool(e.IsChecked()) +from gnuradio import eng_notation + +class TextBoxController(wx.TextCtrl): + def __init__(self, parent, controller, control_key, cast=float): + self._controller = controller + self._control_key = control_key + self._cast = cast + wx.TextCtrl.__init__(self, parent, style=wx.TE_PROCESS_ENTER) + self.Bind(wx.EVT_TEXT_ENTER, self._evt_enter) + controller.subscribe(control_key, lambda x: self.SetValue(eng_notation.num_to_str(x))) + + def _evt_enter(self, e): + try: self._controller[self._control_key] = self._cast(eng_notation.str_to_num(self.GetValue())) + except: self._controller[self._control_key] = self._controller[self._control_key] + class LogSliderController(wx.BoxSizer): """ Log slider controller with display label and slider. Gives logarithmic scaling to slider operation. """ - def __init__(self, parent, label, min_exp, max_exp, slider_steps, controller, control_key, formatter=lambda x: ': %.6f'%x): + def __init__(self, parent, prefix, min_exp, max_exp, slider_steps, controller, control_key, formatter=lambda x: ': %.6f'%x): + self._prefix = prefix + self._min_exp = min_exp + self._max_exp = max_exp + self._controller = controller + self._control_key = control_key + self._formatter = formatter wx.BoxSizer.__init__(self, wx.VERTICAL) - self._label = wx.StaticText(parent, -1, label + formatter(1/3.0)) + self._label = wx.StaticText(parent, label=prefix + formatter(1/3.0)) self.Add(self._label, 0, wx.EXPAND) - self._slider = wx.Slider(parent, -1, 0, 0, slider_steps, style=wx.SL_HORIZONTAL) + self._slider = wx.Slider(parent, minValue=0, maxValue=slider_steps, style=wx.SL_HORIZONTAL) self.Add(self._slider, 0, wx.EXPAND) - def _on_slider_event(event): - controller[control_key] = \ - 10**(float(max_exp-min_exp)*self._slider.GetValue()/slider_steps + min_exp) - self._slider.Bind(wx.EVT_SLIDER, _on_slider_event) - def _on_controller_set(value): - self._label.SetLabel(label + formatter(value)) - slider_value = slider_steps*(math.log10(value)-min_exp)/(max_exp-min_exp) - slider_value = min(max(0, slider_value), slider_steps) - if abs(slider_value - self._slider.GetValue()) > 1: - self._slider.SetValue(slider_value) - controller.subscribe(control_key, _on_controller_set) - - def Disable(self, disable=True): self.Enable(not disable) + self._slider.Bind(wx.EVT_SLIDER, self._on_slider_event) + controller.subscribe(control_key, self._on_controller_set) + + def _get_slope(self): + return float(self._max_exp-self._min_exp)/self._slider.GetMax() + + def _on_slider_event(self, e): + self._controller[self._control_key] = 10**(self._get_slope()*self._slider.GetValue() + self._min_exp) + + def _on_controller_set(self, value): + self._label.SetLabel(self._prefix + self._formatter(value)) + slider_value = (math.log10(value)-self._min_exp)/self._get_slope() + slider_value = min(max(self._slider.GetMin(), slider_value), self._slider.GetMax()) + if abs(slider_value - self._slider.GetValue()) > 1: + self._slider.SetValue(slider_value) + + def Disable(self): self.Enable(False) def Enable(self, enable=True): if enable: self._slider.Enable() @@ -154,45 +205,39 @@ class LogSliderController(wx.BoxSizer): self._slider.Disable() self._label.Disable() -class DropDownController(wx.BoxSizer): +class DropDownController(wx.Choice): """ Drop down controller with label and chooser. Srop down selection from a set of choices. """ - def __init__(self, parent, label, choices, controller, control_key, size=(-1, -1)): + def __init__(self, parent, choices, controller, control_key, size=(-1, -1)): """ @param parent the parent window - @param label the label for the drop down @param choices a list of tuples -> (label, value) @param controller the prop val controller @param control_key the prop key for this control """ - wx.BoxSizer.__init__(self, wx.HORIZONTAL) - self._label = wx.StaticText(parent, -1, ' %s '%label) - self.Add(self._label, 1, wx.ALIGN_CENTER_VERTICAL) - self._chooser = wx.Choice(parent, -1, choices=[c[0] for c in choices], size=size) - def _on_chooser_event(event): - controller[control_key] = choices[self._chooser.GetSelection()][1] - self._chooser.Bind(wx.EVT_CHOICE, _on_chooser_event) - self.Add(self._chooser, 0, wx.ALIGN_CENTER_VERTICAL) - def _on_controller_set(value): - #only set the chooser if the value is a possible choice - for i, choice in enumerate(choices): - if value == choice[1]: self._chooser.SetSelection(i) - controller.subscribe(control_key, _on_controller_set) - - def Disable(self, disable=True): self.Enable(not disable) - def Enable(self, enable=True): - if enable: - self._chooser.Enable() - self._label.Enable() - else: - self._chooser.Disable() - self._label.Disable() + self._controller = controller + self._control_key = control_key + self._choices = choices + wx.Choice.__init__(self, parent, choices=[c[0] for c in choices], size=size) + self.Bind(wx.EVT_CHOICE, self._on_chooser_event) + controller.subscribe(control_key, self._on_controller_set) + + def _on_chooser_event(self, e): + self._controller[self._control_key] = self._choices[self.GetSelection()][1] + + def _on_controller_set(self, value): + #only set the chooser if the value is a possible choice + for i, choice in enumerate(self._choices): + if value == choice[1]: self.SetSelection(i) ################################################## # Shared Functions ################################################## +import numpy +import math + def get_exp(num): """ Get the exponent of the number in base 10. @@ -209,8 +254,7 @@ def get_clean_num(num): @return the closest number """ if num == 0: return 0 - if num > 0: sign = 1 - else: sign = -1 + 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)))] @@ -263,49 +307,3 @@ def get_min_max(samples): min = mean - rms max = mean + rms return min, max - -def get_si_components(num): - """ - Get the SI units for the number. - Extract the coeff and exponent of the number. - The exponent will be a multiple of 3. - @param num the floating point number - @return the tuple coeff, exp, prefix - """ - exp = get_exp(num) - exp -= exp%3 - exp = min(max(exp, -24), 24) #bounds on SI table below - prefix = { - 24: 'Y', 21: 'Z', - 18: 'E', 15: 'P', - 12: 'T', 9: 'G', - 6: 'M', 3: 'K', - 0: '', - -3: 'm', -6: 'u', - -9: 'n', -12: 'p', - -15: 'f', -18: 'a', - -21: 'z', -24: 'y', - }[exp] - coeff = num/10**exp - return coeff, exp, prefix - -def label_format(num): - """ - Format a floating point number into a presentable string. - If the number has an small enough exponent, use regular decimal. - Otherwise, format the number with floating point notation. - Exponents are normalized to multiples of 3. - In the case where the exponent was found to be -3, - it is best to display this as a regular decimal, with a 0 to the left. - @param num the number to format - @return a label string - """ - coeff, exp, prefix = get_si_components(num) - if -3 <= exp < 3: return '%g'%num - return '%se%d'%('%.3g'%coeff, exp) - -if __name__ == '__main__': - import random - for i in range(-25, 25): - num = random.random()*10**i - print num, ':', get_si_components(num) diff --git a/gr-wxgui/src/python/const_window.py b/gr-wxgui/src/python/const_window.py index cb7236b492..8e0e61ac3d 100644 --- a/gr-wxgui/src/python/const_window.py +++ b/gr-wxgui/src/python/const_window.py @@ -62,32 +62,31 @@ class control_panel(wx.Panel): @param parent the wx parent window """ self.parent = parent - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) control_box = wx.BoxSizer(wx.VERTICAL) self.marker_index = 2 #begin control box - control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) - #marker - control_box.AddStretchSpacer() - self.marker_chooser = common.DropDownController(self, 'Marker', MARKER_TYPES, parent, MARKER_KEY) - control_box.Add(self.marker_chooser, 0, wx.EXPAND) #alpha control_box.AddStretchSpacer() - self.alpha_slider = common.LogSliderController( + alpha_slider = common.LogSliderController( self, 'Alpha', ALPHA_MIN_EXP, ALPHA_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.alpha_key, + parent, ALPHA_KEY, ) - control_box.Add(self.alpha_slider, 0, wx.EXPAND) + control_box.Add(alpha_slider, 0, wx.EXPAND) #gain_mu control_box.AddStretchSpacer() - self.gain_mu_slider = common.LogSliderController( + gain_mu_slider = common.LogSliderController( self, 'Gain Mu', GAIN_MU_MIN_EXP, GAIN_MU_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.gain_mu_key, + parent, GAIN_MU_KEY, ) - control_box.Add(self.gain_mu_slider, 0, wx.EXPAND) + control_box.Add(gain_mu_slider, 0, wx.EXPAND) + #marker + control_box.AddStretchSpacer() + marker_chooser = common.DropDownController(self, MARKER_TYPES, parent, MARKER_KEY) + control_box.Add(common.LabelBox(self, 'Marker', marker_chooser), 0, wx.EXPAND) #run/stop control_box.AddStretchSpacer() self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') @@ -98,7 +97,7 @@ class control_panel(wx.Panel): ################################################## # Constellation window with plotter and control panel ################################################## -class const_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class const_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -110,22 +109,27 @@ class const_window(wx.Panel, pubsub.pubsub, common.prop_setter): beta_key, gain_mu_key, gain_omega_key, + omega_key, + sample_rate_key, ): pubsub.pubsub.__init__(self) - #setup - self.ext_controller = controller - self.alpha_key = alpha_key - self.beta_key = beta_key - self.gain_mu_key = gain_mu_key - self.gain_omega_key = gain_omega_key + #proxy the keys + self.proxy(MSG_KEY, controller, msg_key) + self.proxy(ALPHA_KEY, controller, alpha_key) + self.proxy(BETA_KEY, controller, beta_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) #init panel and plot - wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER) + wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER) self.plotter = plotter.channel_plotter(self) self.plotter.SetSize(wx.Size(*size)) self.plotter.set_title(title) self.plotter.set_x_label('Inphase') self.plotter.set_y_label('Quadrature') self.plotter.enable_point_label(True) + 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) @@ -133,22 +137,21 @@ class const_window(wx.Panel, pubsub.pubsub, common.prop_setter): main_box.Add(self.control_panel, 0, wx.EXPAND) self.SetSizerAndFit(main_box) #alpha and gain mu 2nd orders - def set_beta(alpha): self.ext_controller[self.beta_key] = .25*alpha**2 - self.ext_controller.subscribe(self.alpha_key, set_beta) - def set_gain_omega(gain_mu): self.ext_controller[self.gain_omega_key] = .25*gain_mu**2 - self.ext_controller.subscribe(self.gain_mu_key, set_gain_omega) - #initial setup - self.ext_controller[self.alpha_key] = self.ext_controller[self.alpha_key] - self.ext_controller[self.gain_mu_key] = self.ext_controller[self.gain_mu_key] - self._register_set_prop(self, RUNNING_KEY, True) - self._register_set_prop(self, X_DIVS_KEY, 8) - self._register_set_prop(self, Y_DIVS_KEY, 8) - self._register_set_prop(self, MARKER_KEY, DEFAULT_MARKER_TYPE) + def set_beta(alpha): self[BETA_KEY] = .25*alpha**2 + self.subscribe(ALPHA_KEY, set_beta) + def set_gain_omega(gain_mu): self[GAIN_OMEGA_KEY] = .25*gain_mu**2 + self.subscribe(GAIN_MU_KEY, set_gain_omega) + #initialize values + self[ALPHA_KEY] = self[ALPHA_KEY] + self[GAIN_MU_KEY] = self[GAIN_MU_KEY] + self[RUNNING_KEY] = True + self[X_DIVS_KEY] = 8 + self[Y_DIVS_KEY] = 8 + self[MARKER_KEY] = DEFAULT_MARKER_TYPE #register events - self.ext_controller.subscribe(msg_key, self.handle_msg) - for key in ( - X_DIVS_KEY, Y_DIVS_KEY, - ): self.subscribe(key, self.update_grid) + 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() @@ -173,15 +176,12 @@ class const_window(wx.Panel, pubsub.pubsub, common.prop_setter): self.plotter.update() def update_grid(self): - #grid parameters - x_divs = self[X_DIVS_KEY] - y_divs = self[Y_DIVS_KEY] #update the x axis x_max = 2.0 - self.plotter.set_x_grid(-x_max, x_max, common.get_clean_num(2.0*x_max/x_divs)) + 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/y_divs)) + 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 index 06c5a44f10..a4ccdca6df 100644 --- a/gr-wxgui/src/python/constants.py +++ b/gr-wxgui/src/python/constants.py @@ -36,6 +36,7 @@ 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' @@ -43,15 +44,15 @@ PEAK_HOLD_KEY = 'peak_hold' REF_LEVEL_KEY = 'ref_level' RUNNING_KEY = 'running' SAMPLE_RATE_KEY = 'sample_rate' -SCOPE_TRIGGER_CHANNEL_KEY = 'scope_trigger_channel' -SCOPE_TRIGGER_LEVEL_KEY = 'scope_trigger_level' -SCOPE_TRIGGER_MODE_KEY = 'scope_trigger_mode' -SCOPE_X_CHANNEL_KEY = 'scope_x_channel' -SCOPE_Y_CHANNEL_KEY = 'scope_y_channel' -SCOPE_XY_MODE_KEY = 'scope_xy_mode' TRIGGER_CHANNEL_KEY = 'trigger_channel' TRIGGER_LEVEL_KEY = 'trigger_level' TRIGGER_MODE_KEY = 'trigger_mode' +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' @@ -61,4 +62,7 @@ X_PER_DIV_KEY = 'x_per_div' Y_DIVS_KEY = 'y_divs' Y_OFF_KEY = 'y_off' Y_PER_DIV_KEY = 'y_per_div' - +MAXIMUM_KEY = 'maximum' +MINIMUM_KEY = 'minimum' +NUM_BINS_KEY = 'num_bins' +FRAME_SIZE_KEY = 'frame_size' diff --git a/gr-wxgui/src/python/constsink_gl.py b/gr-wxgui/src/python/constsink_gl.py index 64666e462c..b3a1625b09 100644 --- a/gr-wxgui/src/python/constsink_gl.py +++ b/gr-wxgui/src/python/constsink_gl.py @@ -31,7 +31,7 @@ from constants import * ################################################## # Constellation sink block (wrapper for old wxgui) ################################################## -class const_sink_c(gr.hier_block2, common.prop_setter): +class const_sink_c(gr.hier_block2): """ A constellation block with a gui window. """ @@ -97,8 +97,7 @@ class const_sink_c(gr.hier_block2, common.prop_setter): #connect self.connect(self, self._costas, self._retime, agc, sd, sink) #controller - def setter(p, k, x): # lambdas can't have assignments :( - p[k] = x + def setter(p, k, x): p[k] = x self.controller = pubsub() self.controller.subscribe(ALPHA_KEY, self._costas.set_alpha) self.controller.publish(ALPHA_KEY, self._costas.alpha) @@ -116,7 +115,7 @@ class const_sink_c(gr.hier_block2, common.prop_setter): #initial update self.controller[SAMPLE_RATE_KEY] = sample_rate #start input watcher - common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x)) + common.input_watcher(msgq, self.controller, MSG_KEY) #create window self.win = const_window.const_window( parent=parent, @@ -128,15 +127,9 @@ class const_sink_c(gr.hier_block2, common.prop_setter): beta_key=BETA_KEY, gain_mu_key=GAIN_MU_KEY, gain_omega_key=GAIN_OMEGA_KEY, + omega_key=OMEGA_KEY, + sample_rate_key=SAMPLE_RATE_KEY, ) - #register callbacks from window for external use - for attr in filter(lambda a: a.startswith('set_'), dir(self.win)): - setattr(self, attr, getattr(self.win, attr)) - self._register_set_prop(self.controller, ALPHA_KEY) - self._register_set_prop(self.controller, BETA_KEY) - self._register_set_prop(self.controller, GAIN_MU_KEY) - self._register_set_prop(self.controller, OMEGA_KEY) - self._register_set_prop(self.controller, GAIN_OMEGA_KEY) - self._register_set_prop(self.controller, SAMPLE_RATE_KEY) + common.register_access_methods(self, self.win) diff --git a/gr-wxgui/src/python/fft_window.py b/gr-wxgui/src/python/fft_window.py index 6e54aec872..fdd5562dc2 100644 --- a/gr-wxgui/src/python/fft_window.py +++ b/gr-wxgui/src/python/fft_window.py @@ -39,8 +39,8 @@ AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0 DEFAULT_WIN_SIZE = (600, 300) DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30) DIV_LEVELS = (1, 2, 5, 10, 20) -FFT_PLOT_COLOR_SPEC = (0, 0, 1) -PEAK_VALS_COLOR_SPEC = (0, 1, 0) +FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0) +PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0) NO_PEAK_VALS = list() ################################################## @@ -57,31 +57,31 @@ class control_panel(wx.Panel): @param parent the wx parent window """ self.parent = parent - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) control_box = wx.BoxSizer(wx.VERTICAL) #checkboxes for average and peak hold control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) - self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key) - control_box.Add(self.average_check_box, 0, wx.EXPAND) - self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY) - control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND) + peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY) + control_box.Add(peak_hold_check_box, 0, wx.EXPAND) + average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY) + control_box.Add(average_check_box, 0, wx.EXPAND) control_box.AddSpacer(2) - self.avg_alpha_slider = common.LogSliderController( + avg_alpha_slider = common.LogSliderController( self, 'Avg Alpha', AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.avg_alpha_key, + parent, AVG_ALPHA_KEY, formatter=lambda x: ': %.4f'%x, ) - parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable) - control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND) + parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable) + control_box.Add(avg_alpha_slider, 0, wx.EXPAND) #radio buttons for div size control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER) radio_box = wx.BoxSizer(wx.VERTICAL) self.radio_buttons = list() for y_per_div in DIV_LEVELS: - radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div) + radio_button = wx.RadioButton(self, label="%d dB/div"%y_per_div) radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div) self.radio_buttons.append(radio_button) radio_box.Add(radio_button, 0, wx.ALIGN_LEFT) @@ -91,18 +91,23 @@ class control_panel(wx.Panel): control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER) control_box.AddSpacer(2) - self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level) - control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER) + _ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level) + control_box.Add(_ref_lvl_buttons, 0, wx.ALIGN_CENTER) #autoscale control_box.AddStretchSpacer() - self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT) - self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale) - control_box.Add(self.autoscale_button, 0, wx.EXPAND) + autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT) + autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale) + control_box.Add(autoscale_button, 0, wx.EXPAND) #run/stop - self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') - control_box.Add(self.run_button, 0, wx.EXPAND) + run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') + control_box.Add(run_button, 0, wx.EXPAND) #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 @@ -117,16 +122,14 @@ class control_panel(wx.Panel): index = self.radio_buttons.index(selected_radio_button) self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index] def _on_incr_ref_level(self, event): - self.parent.set_ref_level( - self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]) + 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.set_ref_level( - self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]) + self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY] ################################################## # FFT window with plotter and control panel ################################################## -class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class fft_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -150,47 +153,48 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0] #setup self.samples = list() - self.ext_controller = controller self.real = real self.fft_size = fft_size - self.sample_rate_key = sample_rate_key - self.average_key = average_key - self.avg_alpha_key = avg_alpha_key self._reset_peak_vals() + #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) #init panel and plot - wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER) + wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER) self.plotter = plotter.channel_plotter(self) self.plotter.SetSize(wx.Size(*size)) self.plotter.set_title(title) + self.plotter.enable_legend(True) self.plotter.enable_point_label(True) + 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) - #initial setup - self.ext_controller[self.average_key] = self.ext_controller[self.average_key] - self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key] - self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold) - self._register_set_prop(self, Y_PER_DIV_KEY, y_per_div) - self._register_set_prop(self, Y_DIVS_KEY, y_divs) - self._register_set_prop(self, X_DIVS_KEY, 8) #approximate - self._register_set_prop(self, REF_LEVEL_KEY, ref_level) - self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq) - self._register_set_prop(self, RUNNING_KEY, True) + #initialize values + self[AVERAGE_KEY] = self[AVERAGE_KEY] + self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY] + 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 #register events - self.subscribe(PEAK_HOLD_KEY, self.plotter.enable_legend) - self.ext_controller.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals()) - self.ext_controller.subscribe(msg_key, self.handle_msg) - self.ext_controller.subscribe(self.sample_rate_key, self.update_grid) + self.subscribe(AVERAGE_KEY, lambda x: 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) #initial update - self.plotter.enable_legend(self[PEAK_HOLD_KEY]) self.update_grid() def autoscale(self, *args): @@ -207,9 +211,9 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): noise_floor -= abs(noise_floor)*.5 peak_level += abs(peak_level)*.1 #set the reference level to a multiple of y divs - self.set_ref_level(self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])) + self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY]) #set the range to a clean number of the dynamic range - self.set_y_per_div(common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])) + self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY]) def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS @@ -234,19 +238,21 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): if self[PEAK_HOLD_KEY]: if len(self.peak_vals) != len(samples): self.peak_vals = samples self.peak_vals = numpy.maximum(samples, self.peak_vals) - else: self._reset_peak_vals() + #plot the 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, ) - #plot the peak hold - self.plotter.set_waveform( - channel='Peak', - samples=self.peak_vals, - color_spec=PEAK_VALS_COLOR_SPEC, - ) #update the plotter self.plotter.update() @@ -259,7 +265,7 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): The y axis depends on y per div, y divs, and ref level. """ #grid parameters - sample_rate = self.ext_controller[self.sample_rate_key] + 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] @@ -269,24 +275,21 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): if self.real: x_width = sample_rate/2.0 else: x_width = sample_rate/1.0 x_per_div = common.get_clean_num(x_width/x_divs) - coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0)) #update the x grid if self.real: self.plotter.set_x_grid( baseband_freq, baseband_freq + sample_rate/2.0, - x_per_div, - 10**(-exp), + 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, - 10**(-exp), + x_per_div, True, ) #update x units - self.plotter.set_x_label('Frequency', prefix+'Hz') + 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 diff --git a/gr-wxgui/src/python/fftsink_gl.py b/gr-wxgui/src/python/fftsink_gl.py index 0c6f38dc02..30ebd3fde6 100644 --- a/gr-wxgui/src/python/fftsink_gl.py +++ b/gr-wxgui/src/python/fftsink_gl.py @@ -31,7 +31,7 @@ from constants import * ################################################## # FFT sink block (wrapper for old wxgui) ################################################## -class _fft_sink_base(gr.hier_block2, common.prop_setter): +class _fft_sink_base(gr.hier_block2): """ An fft block with real/complex inputs and a gui window. """ @@ -85,9 +85,7 @@ class _fft_sink_base(gr.hier_block2, common.prop_setter): self.controller.subscribe(SAMPLE_RATE_KEY, fft.set_sample_rate) self.controller.publish(SAMPLE_RATE_KEY, fft.sample_rate) #start input watcher - def setter(p, k, x): # lambdas can't have assignments :( - p[k] = x - common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x)) + common.input_watcher(msgq, self.controller, MSG_KEY) #create window self.win = fft_window.fft_window( parent=parent, @@ -106,12 +104,9 @@ class _fft_sink_base(gr.hier_block2, common.prop_setter): peak_hold=peak_hold, msg_key=MSG_KEY, ) - #register callbacks from window for external use - for attr in filter(lambda a: a.startswith('set_'), dir(self.win)): - setattr(self, attr, getattr(self.win, attr)) - self._register_set_prop(self.controller, SAMPLE_RATE_KEY) - self._register_set_prop(self.controller, AVERAGE_KEY) - self._register_set_prop(self.controller, AVG_ALPHA_KEY) + 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 class fft_sink_f(_fft_sink_base): _fft_chain = blks2.logpwrfft_f diff --git a/gr-wxgui/src/python/fftsink_nongl.py b/gr-wxgui/src/python/fftsink_nongl.py index d455ddc270..a1033e8182 100644 --- a/gr-wxgui/src/python/fftsink_nongl.py +++ b/gr-wxgui/src/python/fftsink_nongl.py @@ -319,7 +319,7 @@ class fft_window (wx.Panel): # self.SetBackgroundColour ('black') self.build_popup_menu() - self.set_baseband_freq(0.0) + self.set_baseband_freq(self.fftsink.baseband_freq) EVT_DATA_EVENT (self, self.set_data) wx.EVT_CLOSE (self, self.on_close_window) diff --git a/gr-wxgui/src/python/histo_window.py b/gr-wxgui/src/python/histo_window.py new file mode 100644 index 0000000000..dce52ff9bf --- /dev/null +++ b/gr-wxgui/src/python/histo_window.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. +# + +################################################## +# Imports +################################################## +import plotter +import common +import wx +import numpy +import math +import pubsub +from constants import * +from gnuradio import gr #for gr.prefs + +################################################## +# 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) + control_box = wx.BoxSizer(wx.VERTICAL) + SIZE = (100, -1) + control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) + control_box.AddStretchSpacer() + #num bins + def num_bins_cast(num): + num = int(num) + assert num > 1 + return num + num_bins_ctrl = common.TextBoxController(self, parent, NUM_BINS_KEY, cast=num_bins_cast) + control_box.Add(common.LabelBox(self, ' Num Bins ', num_bins_ctrl), 0, wx.EXPAND) + control_box.AddStretchSpacer() + #frame size + frame_size_ctrl = common.TextBoxController(self, parent, FRAME_SIZE_KEY, cast=num_bins_cast) + control_box.Add(common.LabelBox(self, ' Frame Size ', frame_size_ctrl), 0, wx.EXPAND) + control_box.AddStretchSpacer() + #run/stop + self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') + control_box.Add(self.run_button, 0, wx.EXPAND) + #set sizer + self.SetSizerAndFit(control_box) + +################################################## +# 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) + #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.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) + #initialize values + self[NUM_BINS_KEY] = self[NUM_BINS_KEY] + self[FRAME_SIZE_KEY] = self[FRAME_SIZE_KEY] + self[RUNNING_KEY] = True + self[X_DIVS_KEY] = 8 + self[Y_DIVS_KEY] = 4 + #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 0000000000..db6606e413 --- /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): + """ + 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) + #connect + self.connect(self, histo) + #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) + +# ---------------------------------------------------------------- +# 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 index e766c68c1d..f12a182483 100644 --- a/gr-wxgui/src/python/number_window.py +++ b/gr-wxgui/src/python/number_window.py @@ -53,23 +53,23 @@ class control_panel(wx.Panel): @param parent the wx parent window """ self.parent = parent - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) control_box = wx.BoxSizer(wx.VERTICAL) #checkboxes for average and peak hold control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) - self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key) - control_box.Add(self.average_check_box, 0, wx.EXPAND) self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY) control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND) + self.average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY) + control_box.Add(self.average_check_box, 0, wx.EXPAND) control_box.AddSpacer(2) self.avg_alpha_slider = common.LogSliderController( self, 'Avg Alpha', AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.avg_alpha_key, + parent, AVG_ALPHA_KEY, formatter=lambda x: ': %.4f'%x, ) - parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable) + parent.subscribe(AVERAGE_KEY, self.avg_alpha_slider.Enable) control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND) #run/stop control_box.AddStretchSpacer() @@ -81,7 +81,7 @@ class control_panel(wx.Panel): ################################################## # Numbersink window with label and gauges ################################################## -class number_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class number_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -98,20 +98,23 @@ class number_window(wx.Panel, pubsub.pubsub, common.prop_setter): avg_alpha_key, peak_hold, msg_key, + sample_rate_key, ): pubsub.pubsub.__init__(self) - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) #setup self.peak_val_real = NEG_INF self.peak_val_imag = NEG_INF - self.ext_controller = controller self.real = real self.units = units self.minval = minval self.maxval = maxval self.decimal_places = decimal_places - self.average_key = average_key - self.avg_alpha_key = avg_alpha_key + #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) #setup the box with display and controls self.control_panel = control_panel(self) main_box = wx.BoxSizer(wx.HORIZONTAL) @@ -128,13 +131,13 @@ class number_window(wx.Panel, pubsub.pubsub, common.prop_setter): sizer.Add(self.gauge_real, 1, wx.EXPAND) sizer.Add(self.gauge_imag, 1, wx.EXPAND) self.SetSizerAndFit(main_box) - #initial setup - self.ext_controller[self.average_key] = self.ext_controller[self.average_key] - self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key] - self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold) - self._register_set_prop(self, RUNNING_KEY, True) + #initialize values + self[PEAK_HOLD_KEY] = peak_hold + self[RUNNING_KEY] = True + self[AVERAGE_KEY] = self[AVERAGE_KEY] + self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY] #register events - self.ext_controller.subscribe(msg_key, self.handle_msg) + self.subscribe(MSG_KEY, self.handle_msg) self.Bind(common.EVT_DATA, self.update) def show_gauges(self, show_gauge): diff --git a/gr-wxgui/src/python/numbersink2.py b/gr-wxgui/src/python/numbersink2.py index 9e52745d81..5fa9e3aefe 100644 --- a/gr-wxgui/src/python/numbersink2.py +++ b/gr-wxgui/src/python/numbersink2.py @@ -31,7 +31,7 @@ from constants import * ################################################## # Number sink block (wrapper for old wxgui) ################################################## -class _number_sink_base(gr.hier_block2, common.prop_setter): +class _number_sink_base(gr.hier_block2): """ An decimator block with a number window display """ @@ -74,29 +74,28 @@ class _number_sink_base(gr.hier_block2, common.prop_setter): if self._real: mult = gr.multiply_const_ff(factor) add = gr.add_const_ff(ref_level) - self._avg = gr.single_pole_iir_filter_ff(1.0) + avg = gr.single_pole_iir_filter_ff(1.0) else: mult = gr.multiply_const_cc(factor) add = gr.add_const_cc(ref_level) - self._avg = gr.single_pole_iir_filter_cc(1.0) + avg = gr.single_pole_iir_filter_cc(1.0) msgq = gr.msg_queue(2) sink = gr.message_sink(self._item_size, msgq, True) #connect - self.connect(self, sd, mult, add, self._avg, sink) - #setup averaging - self._avg_alpha = avg_alpha - self.set_average(average) - self.set_avg_alpha(avg_alpha) + self.connect(self, sd, mult, add, avg, sink) #controller self.controller = pubsub() self.controller.subscribe(SAMPLE_RATE_KEY, sd.set_sample_rate) - self.controller.subscribe(AVERAGE_KEY, self.set_average) - self.controller.publish(AVERAGE_KEY, self.get_average) - self.controller.subscribe(AVG_ALPHA_KEY, self.set_avg_alpha) - self.controller.publish(AVG_ALPHA_KEY, self.get_avg_alpha) + self.controller.publish(SAMPLE_RATE_KEY, sd.sample_rate) + def update_avg(*args): + if self.controller[AVERAGE_KEY]: avg.set_taps(self.controller[AVG_ALPHA_KEY]) + else: avg.set_taps(1.0) + self.controller.subscribe(AVERAGE_KEY, update_avg) + self.controller.subscribe(AVG_ALPHA_KEY, update_avg) + self.controller[AVERAGE_KEY] = average + self.controller[AVG_ALPHA_KEY] = avg_alpha #start input watcher - def set_msg(msg): self.controller[MSG_KEY] = msg - common.input_watcher(msgq, set_msg) + common.input_watcher(msgq, self.controller, MSG_KEY) #create window self.win = number_window.number_window( parent=parent, @@ -113,25 +112,12 @@ class _number_sink_base(gr.hier_block2, common.prop_setter): avg_alpha_key=AVG_ALPHA_KEY, peak_hold=peak_hold, msg_key=MSG_KEY, + sample_rate_key=SAMPLE_RATE_KEY, ) - #register callbacks from window for external use - for attr in filter(lambda a: a.startswith('set_'), dir(self.win)): - setattr(self, attr, getattr(self.win, attr)) - self._register_set_prop(self.controller, SAMPLE_RATE_KEY) + common.register_access_methods(self, self.controller) #backwards compadibility self.set_show_gauge = self.win.show_gauges - def get_average(self): return self._average - def set_average(self, average): - self._average = average - if self.get_average(): self._avg.set_taps(self.get_avg_alpha()) - else: self._avg.set_taps(1.0) - - def get_avg_alpha(self): return self._avg_alpha - def set_avg_alpha(self, avg_alpha): - self._avg_alpha = avg_alpha - self.set_average(self.get_average()) - class number_sink_f(_number_sink_base): _item_size = gr.sizeof_float _real = True diff --git a/gr-wxgui/src/python/plotter/Makefile.am b/gr-wxgui/src/python/plotter/Makefile.am index ada5067947..d00f0a4256 100644 --- a/gr-wxgui/src/python/plotter/Makefile.am +++ b/gr-wxgui/src/python/plotter/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2004,2005,2008 Free Software Foundation, Inc. +# Copyright 2004,2005,2008,2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -30,8 +30,11 @@ ourlibdir = $(grpyexecdir)/wxgui/plotter ourpython_PYTHON = \ __init__.py \ + bar_plotter.py \ channel_plotter.py \ + common.py \ gltext.py \ + grid_plotter_base.py \ plotter_base.py \ waterfall_plotter.py diff --git a/gr-wxgui/src/python/plotter/__init__.py b/gr-wxgui/src/python/plotter/__init__.py index 12f8b34507..616492a3e6 100644 --- a/gr-wxgui/src/python/plotter/__init__.py +++ b/gr-wxgui/src/python/plotter/__init__.py @@ -21,3 +21,4 @@ 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 0000000000..3f9259e9df --- /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 index 8fa28410b3..ff0a3a160d 100644 --- a/gr-wxgui/src/python/plotter/channel_plotter.py +++ b/gr-wxgui/src/python/plotter/channel_plotter.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -20,20 +20,21 @@ # import wx -from plotter_base import grid_plotter_base -from OpenGL.GL import * -from gnuradio.wxgui import common +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 -PADDING = 35, 15, 40, 60 #top, right, bottom, left +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 @@ -45,16 +46,21 @@ class channel_plotter(grid_plotter_base): Create a new channel plotter. """ #init - grid_plotter_base.__init__(self, parent, PADDING) - self._channels = dict() + grid_plotter_base.__init__(self, parent, MIN_PADDING) + #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) - def _gl_init(self): + def _init_channel_plotter(self): """ Run gl initialization tasks. """ - glEnableClientState(GL_VERTEX_ARRAY) - self._grid_compiled_list_id = glGenLists(1) + GL.glEnableClientState(GL.GL_VERTEX_ARRAY) def enable_legend(self, enable=None): """ @@ -65,73 +71,55 @@ class channel_plotter(grid_plotter_base): if enable is None: return self._enable_legend self.lock() self._enable_legend = enable - self.changed(True) + self._legend_cache.changed(True) self.unlock() - def draw(self): + def _draw_waveforms(self): """ - Draw the grid and waveforms. + Draw the waveforms for each channel. + Scale the waveform data to the grid using gl matrix operations. """ - self.lock() - self.clear() - #store the grid drawing operations - if self.changed(): - glNewList(self._grid_compiled_list_id, GL_COMPILE) - self._draw_grid() - self._draw_legend() - glEndList() - self.changed(False) - #draw the grid - glCallList(self._grid_compiled_list_id) #use scissor to prevent drawing outside grid - glEnable(GL_SCISSOR_TEST) - glScissor( + 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, ) - #draw the waveforms - self._draw_waveforms() - glDisable(GL_SCISSOR_TEST) - self._draw_point_label() - #swap buffer into display - self.SwapBuffers() - self.unlock() - - def _draw_waveforms(self): - """ - Draw the waveforms for each channel. - Scale the waveform data to the grid using gl matrix operations. - """ for channel in reversed(sorted(self._channels.keys())): samples = self._channels[channel][SAMPLES_KEY] num_samps = len(samples) if not num_samps: continue #use opengl to scale the waveform - glPushMatrix() - glTranslatef(self.padding_left, self.padding_top, 0) - glScalef( + 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, ) - glTranslatef(0, 1, 0) + 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), 0 + x_scale, x_trans = 1.0/(num_samps-1), -self._channels[channel][TRIG_OFF_KEY] points = zip(numpy.arange(0, num_samps), samples) - glScalef(x_scale, -1.0/(self.y_max-self.y_min), 1) - glTranslatef(x_trans, -self.y_min, 0) + 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 - glColor3f(*self._channels[channel][COLOR_SPEC_KEY]) + GL.glColor3f(*self._channels[channel][COLOR_SPEC_KEY]) marker = self._channels[channel][MARKERY_KEY] - if marker: glPointSize(marker) - glVertexPointerf(points) - glDrawArrays(marker is None and GL_LINE_STRIP or GL_POINTS, 0, len(points)) - glPopMatrix() + 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): """ @@ -143,12 +131,9 @@ class channel_plotter(grid_plotter_base): @return a string with newlines """ #create text - label_str = '%s: %s %s\n%s: %s %s'%( - self.x_label, - common.label_format(x_val), - self.x_units, self.y_label, - common.label_format(y_val), - self.y_units, + 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] @@ -156,11 +141,12 @@ class channel_plotter(grid_plotter_base): if not num_samps: continue if isinstance(samples, tuple): continue #linear interpolation - x_index = (num_samps-1)*(x_val/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + x_index = (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)) - y_value = (samples[x_index_high] - samples[x_index_low])*(x_index - x_index_low) + samples[x_index_low] - label_str += '\n%s: %s %s'%(channel, common.label_format(y_value), self.y_units) + 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 _draw_legend(self): @@ -178,7 +164,7 @@ class channel_plotter(grid_plotter_base): txt = gltext.Text(channel, font_size=LEGEND_TEXT_FONT_SIZE) w, h = txt.get_size() #draw rect + text - glColor3f(*color_spec) + GL.glColor3f(*color_spec) self._draw_rect( x_off - w - LEGEND_BOX_PADDING, self.padding_top/2 - h/2 - LEGEND_BOX_PADDING, @@ -188,21 +174,36 @@ class channel_plotter(grid_plotter_base): txt.draw_text(wx.Point(x_off - w, self.padding_top/2 - h/2)) x_off -= w + 4*LEGEND_BOX_PADDING - def set_waveform(self, channel, samples, color_spec, marker=None): + 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.changed(True) + 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__': diff --git a/gr-wxgui/src/python/plotter/common.py b/gr-wxgui/src/python/plotter/common.py new file mode 100644 index 0000000000..7699986aad --- /dev/null +++ b/gr-wxgui/src/python/plotter/common.py @@ -0,0 +1,131 @@ +# +# 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)) + #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/grid_plotter_base.py b/gr-wxgui/src/python/plotter/grid_plotter_base.py new file mode 100644 index 0000000000..fd318ffa06 --- /dev/null +++ b/gr-wxgui/src/python/plotter/grid_plotter_base.py @@ -0,0 +1,370 @@ +# +# 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, .5) +POINT_LABEL_PADDING = 3 +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.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 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) + ################################################## + # 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() + #draw rect + text + GL.glColor3f(*POINT_LABEL_COLOR_SPEC) + if x > self.width/2: x -= w+2*POINT_LABEL_PADDING + self._draw_rect(x, y-h-2*POINT_LABEL_PADDING, w+2*POINT_LABEL_PADDING, h+2*POINT_LABEL_PADDING) + txt.draw_text(wx.Point(x+POINT_LABEL_PADDING, y-h-POINT_LABEL_PADDING)) diff --git a/gr-wxgui/src/python/plotter/plotter_base.py b/gr-wxgui/src/python/plotter/plotter_base.py index 6d5349a5f7..662365a37f 100644 --- a/gr-wxgui/src/python/plotter/plotter_base.py +++ b/gr-wxgui/src/python/plotter/plotter_base.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -21,27 +21,59 @@ import wx import wx.glcanvas -from OpenGL.GL import * -from gnuradio.wxgui import common -import threading -import gltext -import math -import time +from OpenGL import GL +import common BACKGROUND_COLOR_SPEC = (1, 0.976, 1, 1) #creamy white -GRID_LINE_COLOR_SPEC = (0, 0, 0) #black -TICK_TEXT_FONT_SIZE = 9 -TITLE_TEXT_FONT_SIZE = 13 -UNITS_TEXT_FONT_SIZE = 9 -TICK_LABEL_PADDING = 5 -POINT_LABEL_FONT_SIZE = 8 -POINT_LABEL_COLOR_SPEC = (1, 1, .5) -POINT_LABEL_PADDING = 3 + +################################################## +# 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): +class plotter_base(wx.glcanvas.GLCanvas, common.mutex): """ Plotter base class for all plot types. """ @@ -54,340 +86,86 @@ class _plotter_base(wx.glcanvas.GLCanvas): Bind the paint and size events. @param parent the parent widgit """ - self._global_lock = threading.Lock() attribList = (wx.glcanvas.WX_GL_DOUBLEBUFFER, wx.glcanvas.WX_GL_RGBA) wx.glcanvas.GLCanvas.__init__(self, parent, attribList=attribList) - self.changed(False) self._gl_init_flag = False self._resized_flag = True - self._update_ts = 0 + 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 lock(self): self._global_lock.acquire() - def unlock(self): self._global_lock.release() + 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.unlock() def _on_paint(self, event): """ - Respond to paint events, call update. + 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. """ + self.lock() self.SetCurrent() #check if gl was initialized if not self._gl_init_flag: - glClearColor(*BACKGROUND_COLOR_SPEC) - self._gl_init() + 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.lock() self.width, self.height = self.GetSize() - glMatrixMode(GL_PROJECTION) - glLoadIdentity() - glOrtho(0, self.width, self.height, 0, 1, 0) - glMatrixMode(GL_MODELVIEW) - glLoadIdentity() - glViewport(0, 0, self.width, self.height) + 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 - self.changed(True) - self.unlock() - self.draw() + #clear, draw functions, swap + GL.glClear(GL.GL_COLOR_BUFFER_BIT) + for fcn in self._draw_fcns: fcn[1]() + self.SwapBuffers() + self.unlock() def update(self): """ Force a paint event. - Record the timestamp. """ wx.PostEvent(self, wx.PaintEvent()) - self._update_ts = time.time() - - def clear(self): glClear(GL_COLOR_BUFFER_BIT) - - def changed(self, state=None): - """ - Set the changed flag if state is not None. - Otherwise return the changed flag. - """ - if state is not None: self._changed = state - else: return self._changed - -################################################## -# Grid Plotter Base Class -################################################## -class grid_plotter_base(_plotter_base): - - def __init__(self, parent, padding): - _plotter_base.__init__(self, parent) - self.padding_top, self.padding_right, self.padding_bottom, self.padding_left = padding - #store title and unit strings - self.set_title('Title') - self.set_x_label('X Label') - self.set_y_label('Y Label') - #init the grid to some value - self.set_x_grid(-1, 1, 1) - self.set_y_grid(-1, 1, 1) - #setup point label - self.enable_point_label(False) - self._mouse_coordinate = None - self.Bind(wx.EVT_MOTION, self._on_motion) - self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave_window) - - def _on_motion(self, event): - """ - Mouse motion, record the position X, Y. - """ - self.lock() - self._mouse_coordinate = event.GetPosition() - #update based on last known update time - if time.time() - self._update_ts > 0.03: self.update() - self.unlock() - - def _on_leave_window(self, event): - """ - Mouse leave window, set the position to None. - """ - self.lock() - self._mouse_coordinate = None - self.update() - self.unlock() - - def enable_point_label(self, enable=None): - """ - Enable/disable the point label. - @param enable true to enable - @return the enable state when None - """ - if enable is None: return self._enable_point_label - self.lock() - self._enable_point_label = enable - self.changed(True) - self.unlock() - - def set_title(self, title): - """ - Set the title. - @param title the title string - """ - self.lock() - self.title = title - self.changed(True) - self.unlock() - - def set_x_label(self, x_label, x_units=''): - """ - Set the x label and units. - @param x_label the x label string - @param x_units the x units string - """ - self.lock() - self.x_label = x_label - self.x_units = x_units - self.changed(True) - self.unlock() - - def set_y_label(self, y_label, y_units=''): - """ - Set the y label and units. - @param y_label the y label string - @param y_units the y units string - """ - self.lock() - self.y_label = y_label - self.y_units = y_units - self.changed(True) - self.unlock() - - def set_x_grid(self, x_min, x_max, x_step, x_scalar=1.0): - """ - Set the x grid parameters. - @param x_min the left-most value - @param x_max the right-most value - @param x_step the grid spacing - @param x_scalar the scalar factor - """ - self.lock() - self.x_min = float(x_min) - self.x_max = float(x_max) - self.x_step = float(x_step) - self.x_scalar = float(x_scalar) - self.changed(True) - self.unlock() - - def set_y_grid(self, y_min, y_max, y_step, y_scalar=1.0): - """ - Set the y grid parameters. - @param y_min the bottom-most value - @param y_max the top-most value - @param y_step the grid spacing - @param y_scalar the scalar factor - """ - self.lock() - self.y_min = float(y_min) - self.y_max = float(y_max) - self.y_step = float(y_step) - self.y_scalar = float(y_scalar) - self.changed(True) - self.unlock() - - def _draw_grid(self): - """ - Draw the border, grid, title, and units. - """ - ################################################## - # Draw Border - ################################################## - glColor3f(*GRID_LINE_COLOR_SPEC) - 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 Grid X - ################################################## - for tick in self._get_ticks(self.x_min, self.x_max, self.x_step, self.x_scalar): - scaled_tick = (self.width-self.padding_left-self.padding_right)*\ - (tick/self.x_scalar-self.x_min)/(self.x_max-self.x_min) + self.padding_left - glColor3f(*GRID_LINE_COLOR_SPEC) - self._draw_line( - (scaled_tick, self.padding_top, 0), - (scaled_tick, self.height-self.padding_bottom, 0), - ) - txt = self._get_tick_label(tick) - w, h = txt.get_size() - txt.draw_text(wx.Point(scaled_tick-w/2, self.height-self.padding_bottom+TICK_LABEL_PADDING)) - ################################################## - # Draw Grid Y - ################################################## - for tick in self._get_ticks(self.y_min, self.y_max, self.y_step, self.y_scalar): - scaled_tick = (self.height-self.padding_top-self.padding_bottom)*\ - (1 - (tick/self.y_scalar-self.y_min)/(self.y_max-self.y_min)) + self.padding_top - glColor3f(*GRID_LINE_COLOR_SPEC) - self._draw_line( - (self.padding_left, scaled_tick, 0), - (self.width-self.padding_right, scaled_tick, 0), - ) - txt = self._get_tick_label(tick) - w, h = txt.get_size() - txt.draw_text(wx.Point(self.padding_left-w-TICK_LABEL_PADDING, scaled_tick-h/2)) - ################################################## - # Draw Title - ################################################## - #draw x units - txt = gltext.Text(self.title, bold=True, font_size=TITLE_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point(self.width/2.0, .5*self.padding_top)) - ################################################## - # Draw Labels - ################################################## - #draw x labels - x_label_str = self.x_units and "%s (%s)"%(self.x_label, self.x_units) or self.x_label - txt = gltext.Text(x_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point( - (self.width-self.padding_left-self.padding_right)/2.0 + self.padding_left, - self.height-.25*self.padding_bottom, - ) - ) - #draw y labels - y_label_str = self.y_units and "%s (%s)"%(self.y_label, self.y_units) or self.y_label - txt = gltext.Text(y_label_str, bold=True, font_size=UNITS_TEXT_FONT_SIZE, centered=True) - txt.draw_text(wx.Point( - .25*self.padding_left, - (self.height-self.padding_top-self.padding_bottom)/2.0 + self.padding_top, - ), rotation=90, - ) - - def _get_tick_label(self, tick): - """ - Format the tick value and create a gl text. - @param tick the floating point tick value - @return the tick label text - """ - tick_str = common.label_format(tick) - return gltext.Text(tick_str, font_size=TICK_TEXT_FONT_SIZE) - - def _get_ticks(self, min, max, step, scalar): - """ - Determine the positions for the ticks. - @param min the lower bound - @param max the upper bound - @param step the grid spacing - @param scalar the grid scaling - @return a list of tick positions between min and max - """ - #cast to float - min = float(min) - max = float(max) - step = float(step) - #check for valid numbers - assert step > 0 - assert max > min - assert max - min > step - #determine the start and stop value - start = int(math.ceil(min/step)) - stop = int(math.floor(max/step)) - return [i*step*scalar for i in range(start, stop+1)] - - def _draw_line(self, coor1, coor2): - """ - Draw a line from coor1 to coor2. - @param corr1 a tuple of x, y, z - @param corr2 a tuple of x, y, z - """ - glBegin(GL_LINES) - glVertex3f(*coor1) - glVertex3f(*coor2) - glEnd() - - def _draw_rect(self, x, y, width, height, fill=True): - """ - Draw a rectangle on the x, y plane. - X and Y are the top-left corner. - @param x the left position of the rectangle - @param y the top position of the rectangle - @param width the width of the rectangle - @param height the height of the rectangle - @param fill true to color inside of rectangle - """ - glBegin(fill and GL_QUADS or GL_LINE_LOOP) - glVertex2f(x, y) - glVertex2f(x+width, y) - glVertex2f(x+width, y+height) - glVertex2f(x, y+height) - glEnd() - - def _draw_point_label(self): - """ - Draw the point label for the last mouse motion coordinate. - The mouse coordinate must be an X, Y tuple. - The label will be drawn at the X, Y coordinate. - The values of the X, Y coordinate will be scaled to the current X, Y bounds. - """ - if not self.enable_point_label(): return - if not self._mouse_coordinate: return - x, y = self._mouse_coordinate - if x < self.padding_left or x > self.width-self.padding_right: return - if y < self.padding_top or y > self.height-self.padding_bottom: return - #scale to window bounds - x_win_scalar = float(x - self.padding_left)/(self.width-self.padding_left-self.padding_right) - y_win_scalar = float((self.height - y) - self.padding_bottom)/(self.height-self.padding_top-self.padding_bottom) - #scale to grid bounds - x_val = self.x_scalar*(x_win_scalar*(self.x_max-self.x_min) + self.x_min) - y_val = self.y_scalar*(y_win_scalar*(self.y_max-self.y_min) + self.y_min) - #create text - label_str = self._populate_point_label(x_val, y_val) - txt = gltext.Text(label_str, font_size=POINT_LABEL_FONT_SIZE) - w, h = txt.get_size() - #draw rect + text - glColor3f(*POINT_LABEL_COLOR_SPEC) - if x > self.width/2: x -= w+2*POINT_LABEL_PADDING - self._draw_rect(x, y-h-2*POINT_LABEL_PADDING, w+2*POINT_LABEL_PADDING, h+2*POINT_LABEL_PADDING) - txt.draw_text(wx.Point(x+POINT_LABEL_PADDING, y-h-POINT_LABEL_PADDING)) diff --git a/gr-wxgui/src/python/plotter/waterfall_plotter.py b/gr-wxgui/src/python/plotter/waterfall_plotter.py index 4dc19f672f..2e0669961b 100644 --- a/gr-wxgui/src/python/plotter/waterfall_plotter.py +++ b/gr-wxgui/src/python/plotter/waterfall_plotter.py @@ -1,5 +1,5 @@ # -# Copyright 2008 Free Software Foundation, Inc. +# Copyright 2008, 2009 Free Software Foundation, Inc. # # This file is part of GNU Radio # @@ -20,9 +20,9 @@ # import wx -from plotter_base import grid_plotter_base -from OpenGL.GL import * -from gnuradio.wxgui import common +from grid_plotter_base import grid_plotter_base +from OpenGL import GL +import common import numpy import gltext import math @@ -33,7 +33,7 @@ LEGEND_NUM_LABELS = 9 LEGEND_WIDTH = 8 LEGEND_FONT_SIZE = 8 LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black -PADDING = 35, 60, 40, 60 #top, right, bottom, left +MIN_PADDING = 0, 60, 0, 0 #top, right, bottom, left ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2))) @@ -91,7 +91,13 @@ class waterfall_plotter(grid_plotter_base): Create a new channel plotter. """ #init - grid_plotter_base.__init__(self, parent, PADDING) + 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 @@ -102,35 +108,11 @@ class waterfall_plotter(grid_plotter_base): self.set_num_lines(0) self.set_color_mode(COLORS.keys()[0]) - def _gl_init(self): + def _init_waterfall(self): """ Run gl initialization tasks. """ - self._grid_compiled_list_id = glGenLists(1) - self._waterfall_texture = glGenTextures(1) - - def draw(self): - """ - Draw the grid and waveforms. - """ - self.lock() - #resize texture - self._resize_texture() - #store the grid drawing operations - if self.changed(): - glNewList(self._grid_compiled_list_id, GL_COMPILE) - self._draw_grid() - self._draw_legend() - glEndList() - self.changed(False) - self.clear() - #draw the grid - glCallList(self._grid_compiled_list_id) - self._draw_waterfall() - self._draw_point_label() - #swap buffer into display - self.SwapBuffers() - self.unlock() + self._waterfall_texture = GL.glGenTextures(1) def _draw_waterfall(self): """ @@ -138,42 +120,44 @@ class waterfall_plotter(grid_plotter_base): 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 - glBindTexture(GL_TEXTURE_2D, self._waterfall_texture) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) - glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) + 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: - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL_RGBA, GL_UNSIGNED_BYTE, self._buffer.pop(0)) + 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 - glEnable(GL_TEXTURE_2D) - glPushMatrix() + GL.glEnable(GL.GL_TEXTURE_2D) + GL.glPushMatrix() #matrix scaling - glTranslatef(self.padding_left, self.padding_top, 0) - glScalef( + 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 - glBegin(GL_QUADS) + 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) - glTexCoord2f(0, prop_y+1-off) - glVertex2f(0, 1) - glTexCoord2f(prop_x, prop_y+1-off) - glVertex2f(1, 1) - glTexCoord2f(prop_x, prop_y) - glVertex2f(1, 0) - glTexCoord2f(0, prop_y) - glVertex2f(0, 0) - glEnd() - glPopMatrix() - glDisable(GL_TEXTURE_2D) + 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): """ @@ -183,7 +167,7 @@ class waterfall_plotter(grid_plotter_base): @param y_val the current y value @return a value string with units """ - return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units) + return '%s: %s'%(self.x_label, common.eng_format(x_val, self.x_units)) def _draw_legend(self): """ @@ -196,11 +180,11 @@ class waterfall_plotter(grid_plotter_base): x = self.width - self.padding_right + LEGEND_LEFT_PAD for i in range(LEGEND_NUM_BLOCKS): color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))] - glColor4f(*map(lambda c: ord(c)/255.0, color)) + GL.glColor4f(*map(lambda c: ord(c)/255.0, color)) y = self.height - (i+1)*block_height - self.padding_bottom self._draw_rect(x, y, LEGEND_WIDTH, block_height) #draw rectangle around color scale border - glColor3f(*LEGEND_BORDER_COLOR_SPEC) + 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) @@ -224,9 +208,9 @@ class waterfall_plotter(grid_plotter_base): self._buffer = list() self._pointer = 0 if self._num_lines and self._fft_size: - glBindTexture(GL_TEXTURE_2D, self._waterfall_texture) + GL.glBindTexture(GL.GL_TEXTURE_2D, self._waterfall_texture) data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring() - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL_RGBA, GL_UNSIGNED_BYTE, data) + 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): @@ -239,7 +223,7 @@ class waterfall_plotter(grid_plotter_base): self.lock() if color_mode in COLORS.keys(): self._color_mode = color_mode - self.changed(True) + self._legend_cache.changed(True) self.update() self.unlock() @@ -268,7 +252,7 @@ class waterfall_plotter(grid_plotter_base): if self._minimum != minimum or self._maximum != maximum: self._minimum = minimum self._maximum = maximum - self.changed(True) + self._legend_cache.changed(True) if self._fft_size != len(samples): self._fft_size = len(samples) self._resize_texture(True) @@ -279,4 +263,5 @@ class waterfall_plotter(grid_plotter_base): #convert the samples to RGBA data data = numpy.choose(samples, COLORS[self._color_mode]).tostring() self._buffer.append(data) + self._waterfall_cache.changed(True) self.unlock() diff --git a/gr-wxgui/src/python/pubsub.py b/gr-wxgui/src/python/pubsub.py index cc8ea5ccc6..e55d691978 100644 --- a/gr-wxgui/src/python/pubsub.py +++ b/gr-wxgui/src/python/pubsub.py @@ -28,73 +28,73 @@ This is a proof of concept implementation, will likely change significantly. class pubsub(dict): def __init__(self): - self._publishers = { } - self._subscribers = { } - self._proxies = { } - + 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 - + dict.__setitem__(self, key, value) + self._publishers[key] = None + self._subscribers[key] = [] + self._proxies[key] = None + def __setitem__(self, key, val): - if not self.has_key(key): - self.__missing__(key, val) - elif self._proxies[key] is not None: - (p, newkey) = self._proxies[key] - p[newkey] = val - else: - dict.__setitem__(self, key, val) - for sub in self._subscribers[key]: - # Note this means subscribers will get called in the thread - # context of the 'set' caller. - sub(val) + 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, newkey) = self._proxies[key] - return p[newkey] - elif self._publishers[key] is not None: - return self._publishers[key]() - else: - return dict.__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 not self.has_key(key): self.__missing__(key) if self._proxies[key] is not None: - (p, newkey) = self._proxies[key] - p.publish(newkey, publisher) + (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 not self.has_key(key): self.__missing__(key) if self._proxies[key] is not None: - (p, newkey) = self._proxies[key] - p.subscribe(newkey, subscriber) + (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, newkey) = self._proxies[key] - p.unpublish(newkey) + (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, newkey) = self._proxies[key] - p.unsubscribe(newkey, subscriber) + (p, pkey) = self._proxies[key] + p.unsubscribe(pkey, subscriber) else: self._subscribers[key].remove(subscriber) - def proxy(self, key, p, newkey=None): - if not self.has_key(key): self.__missing__(key) - if newkey is None: newkey = key - self._proxies[key] = (p, newkey) + def 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 @@ -110,22 +110,22 @@ if __name__ == "__main__": # Add some subscribers # First is a bare function def print_len(x): - print "len=%i" % (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` + 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 + # Update key 'foo', will notify subscribers print "Updating 'foo' with three subscribers:" o['foo'] = 'bar'; @@ -135,7 +135,7 @@ if __name__ == "__main__": # 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'] @@ -145,7 +145,7 @@ if __name__ == "__main__": # 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' diff --git a/gr-wxgui/src/python/scope_window.py b/gr-wxgui/src/python/scope_window.py index 7d4f97113e..bbc66426a4 100644 --- a/gr-wxgui/src/python/scope_window.py +++ b/gr-wxgui/src/python/scope_window.py @@ -29,41 +29,40 @@ import numpy import time import pubsub from constants import * -from gnuradio import gr #for gr.prefs +from gnuradio import gr #for gr.prefs, trigger modes ################################################## # Constants ################################################## DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'scope_rate', 30) DEFAULT_WIN_SIZE = (600, 300) -DEFAULT_V_SCALE = 1000 +COUPLING_MODES = ( + ('DC', False), + ('AC', True), +) TRIGGER_MODES = ( - ('Off', 0), - ('Neg', -1), - ('Pos', +1), + ('Freerun', gr.gr_TRIG_MODE_FREE), + ('Automatic', gr.gr_TRIG_MODE_AUTO), + ('Normal', gr.gr_TRIG_MODE_NORM), ) -TRIGGER_LEVELS = ( - ('Auto', None), - ('+High', 0.75), - ('+Med', 0.5), - ('+Low', 0.25), - ('Zero', 0.0), - ('-Low', -0.25), - ('-Med', -0.5), - ('-High', -0.75), +TRIGGER_SLOPES = ( + ('Positive +', gr.gr_TRIG_SLOPE_POS), + ('Negative -', gr.gr_TRIG_SLOPE_NEG), ) CHANNEL_COLOR_SPECS = ( - (0, 0, 1), - (0, 1, 0), - (1, 0, 0), - (1, 0, 1), + (0.3, 0.3, 1.0), + (0.0, 0.8, 0.0), + (1.0, 0.0, 0.0), + (0.8, 0.0, 0.8), ) +TRIGGER_COLOR_SPEC = (1.0, 0.4, 0.0) AUTORANGE_UPDATE_RATE = 0.5 #sec MARKER_TYPES = ( - ('Dot Small', 1.0), - ('Dot Medium', 2.0), - ('Dot Large', 3.0), ('Line Link', None), + ('Dot Large', 3.0), + ('Dot Med', 2.0), + ('Dot Small', 1.0), + ('None', 0.0), ) DEFAULT_MARKER_TYPE = None @@ -79,175 +78,213 @@ class control_panel(wx.Panel): Create a new control panel. @param parent the wx parent window """ + SIZE = (100, -1) self.parent = parent wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) - self.control_box = control_box = wx.BoxSizer(wx.VERTICAL) - #trigger options - control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Trigger Options'), 0, wx.ALIGN_CENTER) - control_box.AddSpacer(2) - #trigger mode - self.trigger_mode_chooser = common.DropDownController(self, 'Mode', TRIGGER_MODES, parent, TRIGGER_MODE_KEY) - control_box.Add(self.trigger_mode_chooser, 0, wx.EXPAND) - #trigger level - self.trigger_level_chooser = common.DropDownController(self, 'Level', TRIGGER_LEVELS, parent, TRIGGER_LEVEL_KEY) - parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_level_chooser.Disable(x==0)) - control_box.Add(self.trigger_level_chooser, 0, wx.EXPAND) - #trigger channel - choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)] - self.trigger_channel_chooser = common.DropDownController(self, 'Channel', choices, parent, TRIGGER_CHANNEL_KEY) - parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_channel_chooser.Disable(x==0)) - control_box.Add(self.trigger_channel_chooser, 0, wx.EXPAND) - #axes options - SPACING = 15 + control_box = wx.BoxSizer(wx.VERTICAL) + ################################################## + # Axes Options + ################################################## control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Axes Options'), 0, wx.ALIGN_CENTER) control_box.AddSpacer(2) ################################################## # Scope Mode Box ################################################## - self.scope_mode_box = wx.BoxSizer(wx.VERTICAL) - control_box.Add(self.scope_mode_box, 0, wx.EXPAND) + scope_mode_box = wx.BoxSizer(wx.VERTICAL) + control_box.Add(scope_mode_box, 0, wx.EXPAND) #x axis divs - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Secs/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - x_buttons = common.IncrDecrButtons(self, self._on_incr_t_divs, self._on_decr_t_divs) - hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + x_buttons_scope = common.IncrDecrButtons(self, self._on_incr_t_divs, self._on_decr_t_divs) + scope_mode_box.Add(common.LabelBox(self, 'Secs/Div', x_buttons_scope), 0, wx.EXPAND) #y axis divs - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Units/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs) - parent.subscribe(AUTORANGE_KEY, y_buttons.Disable) - hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + y_buttons_scope = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs) + parent.subscribe(AUTORANGE_KEY, lambda x: y_buttons_scope.Enable(not x)) + scope_mode_box.Add(common.LabelBox(self, 'Counts/Div', y_buttons_scope), 0, wx.EXPAND) #y axis ref lvl - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Y Offset '), 1, wx.ALIGN_CENTER_VERTICAL) - y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off) - parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable) - hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + y_off_buttons_scope = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off) + parent.subscribe(AUTORANGE_KEY, lambda x: y_off_buttons_scope.Enable(not x)) + scope_mode_box.Add(common.LabelBox(self, 'Y Offset', y_off_buttons_scope), 0, wx.EXPAND) + #t axis ref lvl + scope_mode_box.AddSpacer(5) + t_off_slider = wx.Slider(self, size=SIZE, style=wx.SL_HORIZONTAL) + t_off_slider.SetRange(0, 1000) + def t_off_slider_changed(evt): parent[T_FRAC_OFF_KEY] = float(t_off_slider.GetValue())/t_off_slider.GetMax() + t_off_slider.Bind(wx.EVT_SLIDER, t_off_slider_changed) + parent.subscribe(T_FRAC_OFF_KEY, lambda x: t_off_slider.SetValue(int(round(x*t_off_slider.GetMax())))) + scope_mode_box.Add(common.LabelBox(self, 'T Offset', t_off_slider), 0, wx.EXPAND) + scope_mode_box.AddSpacer(5) ################################################## # XY Mode Box ################################################## - self.xy_mode_box = wx.BoxSizer(wx.VERTICAL) - control_box.Add(self.xy_mode_box, 0, wx.EXPAND) - #x and y channel - CHOOSER_WIDTH = 60 - CENTER_SPACING = 10 - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)] - self.channel_x_chooser = common.DropDownController(self, 'X Ch', choices, parent, SCOPE_X_CHANNEL_KEY, (CHOOSER_WIDTH, -1)) - hbox.Add(self.channel_x_chooser, 0, wx.EXPAND) - hbox.AddSpacer(CENTER_SPACING) - self.channel_y_chooser = common.DropDownController(self, 'Y Ch', choices, parent, SCOPE_Y_CHANNEL_KEY, (CHOOSER_WIDTH, -1)) - hbox.Add(self.channel_y_chooser, 0, wx.EXPAND) - #div controls - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' X/Div '), 1, wx.ALIGN_CENTER_VERTICAL) + xy_mode_box = wx.BoxSizer(wx.VERTICAL) + control_box.Add(xy_mode_box, 0, wx.EXPAND) + #x div controls x_buttons = common.IncrDecrButtons(self, self._on_incr_x_divs, self._on_decr_x_divs) - parent.subscribe(AUTORANGE_KEY, x_buttons.Disable) - hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(CENTER_SPACING) - hbox.Add(wx.StaticText(self, -1, ' Y/Div '), 1, wx.ALIGN_CENTER_VERTICAL) + parent.subscribe(AUTORANGE_KEY, lambda x: x_buttons.Enable(not x)) + xy_mode_box.Add(common.LabelBox(self, 'X/Div', x_buttons), 0, wx.EXPAND) + #y div controls y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs) - parent.subscribe(AUTORANGE_KEY, y_buttons.Disable) - hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - #offset controls - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' X Off '), 1, wx.ALIGN_CENTER_VERTICAL) + parent.subscribe(AUTORANGE_KEY, lambda x: y_buttons.Enable(not x)) + xy_mode_box.Add(common.LabelBox(self, 'Y/Div', y_buttons), 0, wx.EXPAND) + #x offset controls x_off_buttons = common.IncrDecrButtons(self, self._on_incr_x_off, self._on_decr_x_off) - parent.subscribe(AUTORANGE_KEY, x_off_buttons.Disable) - hbox.Add(x_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(CENTER_SPACING) - hbox.Add(wx.StaticText(self, -1, ' Y Off '), 1, wx.ALIGN_CENTER_VERTICAL) + parent.subscribe(AUTORANGE_KEY, lambda x: x_off_buttons.Enable(not x)) + xy_mode_box.Add(common.LabelBox(self, 'X Off', x_off_buttons), 0, wx.EXPAND) + #y offset controls y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off) - parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable) - hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - ################################################## - # End Special Boxes - ################################################## - #misc options - control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Range Options'), 0, wx.ALIGN_CENTER) - #ac couple check box - self.ac_couple_check_box = common.CheckBoxController(self, 'AC Couple', parent, AC_COUPLE_KEY) - control_box.Add(self.ac_couple_check_box, 0, wx.ALIGN_LEFT) + parent.subscribe(AUTORANGE_KEY, lambda x: y_off_buttons.Enable(not x)) + xy_mode_box.Add(common.LabelBox(self, 'Y Off', y_off_buttons), 0, wx.EXPAND) + xy_mode_box.ShowItems(False) #autorange check box self.autorange_check_box = common.CheckBoxController(self, 'Autorange', parent, AUTORANGE_KEY) control_box.Add(self.autorange_check_box, 0, wx.ALIGN_LEFT) - #marker control_box.AddStretchSpacer() - self.marker_chooser = common.DropDownController(self, 'Marker', MARKER_TYPES, parent, MARKER_KEY) - control_box.Add(self.marker_chooser, 0, wx.EXPAND) - #xy mode - control_box.AddStretchSpacer() - self.scope_xy_mode_button = common.ToggleButtonController(self, parent, SCOPE_XY_MODE_KEY, 'Scope Mode', 'X:Y Mode') - parent.subscribe(SCOPE_XY_MODE_KEY, self._on_scope_xy_mode) - control_box.Add(self.scope_xy_mode_button, 0, wx.EXPAND) + ################################################## + # Channel Options + ################################################## + TRIGGER_PAGE_INDEX = parent.num_inputs + XY_PAGE_INDEX = parent.num_inputs+1 + control_box.Add(common.LabelText(self, 'Channel Options'), 0, wx.ALIGN_CENTER) + control_box.AddSpacer(2) + options_notebook = wx.Notebook(self) + control_box.Add(options_notebook, 0, wx.EXPAND) + def options_notebook_changed(evt): + try: + parent[TRIGGER_SHOW_KEY] = options_notebook.GetSelection() == TRIGGER_PAGE_INDEX + parent[XY_MODE_KEY] = options_notebook.GetSelection() == XY_PAGE_INDEX + except wx.PyDeadObjectError: pass + options_notebook.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGED, options_notebook_changed) + def xy_mode_changed(mode): + #ensure xy tab is selected + if mode and options_notebook.GetSelection() != XY_PAGE_INDEX: + options_notebook.SetSelection(XY_PAGE_INDEX) + #ensure xy tab is not selected + elif not mode and options_notebook.GetSelection() == XY_PAGE_INDEX: + options_notebook.SetSelection(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) + ################################################## + # Channel Menu Boxes + ################################################## + for i in range(parent.num_inputs): + channel_menu_panel = wx.Panel(options_notebook) + options_notebook.AddPage(channel_menu_panel, '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() + coupling_chooser = common.DropDownController(channel_menu_panel, COUPLING_MODES, parent, common.index_key(AC_COUPLE_KEY, i), SIZE) + channel_menu_box.Add(common.LabelBox(channel_menu_panel, 'Coupling', coupling_chooser), 0, wx.EXPAND) + #marker + channel_menu_box.AddStretchSpacer() + marker_chooser = common.DropDownController(channel_menu_panel, MARKER_TYPES, parent, common.index_key(MARKER_KEY, i), SIZE) + channel_menu_box.Add(common.LabelBox(channel_menu_panel, 'Marker', marker_chooser), 0, wx.EXPAND) + channel_menu_box.AddStretchSpacer() + ################################################## + # Trigger Menu Box + ################################################## + trigger_menu_panel = wx.Panel(options_notebook) + options_notebook.AddPage(trigger_menu_panel, 'Trig') + trigger_menu_box = wx.BoxSizer(wx.VERTICAL) + trigger_menu_panel.SetSizer(trigger_menu_box) + #trigger mode + trigger_mode_chooser = common.DropDownController(trigger_menu_panel, TRIGGER_MODES, parent, TRIGGER_MODE_KEY, SIZE) + trigger_menu_box.Add(common.LabelBox(trigger_menu_panel, 'Mode', trigger_mode_chooser), 0, wx.EXPAND) + #trigger slope + trigger_slope_chooser = common.DropDownController(trigger_menu_panel, TRIGGER_SLOPES, parent, TRIGGER_SLOPE_KEY, SIZE) + parent.subscribe(TRIGGER_MODE_KEY, lambda x: trigger_slope_chooser.Enable(x!=gr.gr_TRIG_MODE_FREE)) + trigger_menu_box.Add(common.LabelBox(trigger_menu_panel, 'Slope', trigger_slope_chooser), 0, wx.EXPAND) + #trigger channel + choices = [('Channel %d'%(i+1), i) for i in range(parent.num_inputs)] + trigger_channel_chooser = common.DropDownController(trigger_menu_panel, choices, parent, TRIGGER_CHANNEL_KEY, SIZE) + parent.subscribe(TRIGGER_MODE_KEY, lambda x: trigger_channel_chooser.Enable(x!=gr.gr_TRIG_MODE_FREE)) + trigger_menu_box.Add(common.LabelBox(trigger_menu_panel, 'Channel', trigger_channel_chooser), 0, wx.EXPAND) + #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 = wx.Button(trigger_menu_panel, label='50%', style=wx.BU_EXACTFIT) + parent.subscribe(TRIGGER_MODE_KEY, lambda x: trigger_level_button.Enable(x!=gr.gr_TRIG_MODE_FREE)) + trigger_level_button.Bind(wx.EVT_BUTTON, self.parent.set_auto_trigger_level) + hbox.Add(trigger_level_button, 0, wx.ALIGN_CENTER_VERTICAL) + hbox.AddSpacer(10) + trigger_level_buttons = common.IncrDecrButtons(trigger_menu_panel, self._on_incr_trigger_level, self._on_decr_trigger_level) + parent.subscribe(TRIGGER_MODE_KEY, lambda x: trigger_level_buttons.Enable(x!=gr.gr_TRIG_MODE_FREE)) + hbox.Add(trigger_level_buttons, 0, wx.ALIGN_CENTER_VERTICAL) + ################################################## + # XY Menu Box + ################################################## + if parent.num_inputs > 1: + xy_menu_panel = wx.Panel(options_notebook) + options_notebook.AddPage(xy_menu_panel, 'XY') + xy_menu_box = wx.BoxSizer(wx.VERTICAL) + xy_menu_panel.SetSizer(xy_menu_box) + #x and y channel choosers + xy_menu_box.AddStretchSpacer() + choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)] + x_channel_chooser = common.DropDownController(xy_menu_panel, choices, parent, X_CHANNEL_KEY, SIZE) + xy_menu_box.Add(common.LabelBox(xy_menu_panel, 'Ch X', x_channel_chooser), 0, wx.EXPAND) + xy_menu_box.AddStretchSpacer() + y_channel_chooser = common.DropDownController(xy_menu_panel, choices, parent, Y_CHANNEL_KEY, SIZE) + xy_menu_box.Add(common.LabelBox(xy_menu_panel, 'Ch Y', y_channel_chooser), 0, wx.EXPAND) + #marker + xy_menu_box.AddStretchSpacer() + marker_chooser = common.DropDownController(xy_menu_panel, MARKER_TYPES, parent, XY_MARKER_KEY, SIZE) + xy_menu_box.Add(common.LabelBox(xy_menu_panel, 'Marker', marker_chooser), 0, wx.EXPAND) + xy_menu_box.AddStretchSpacer() + ################################################## + # Run/Stop Button + ################################################## #run/stop self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') control_box.Add(self.run_button, 0, wx.EXPAND) #set sizer self.SetSizerAndFit(control_box) + #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 ################################################## - def _on_scope_xy_mode(self, mode): - self.scope_mode_box.ShowItems(not mode) - self.xy_mode_box.ShowItems(mode) - self.control_box.Layout() + #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.set_t_per_div( - common.get_clean_incr(self.parent[T_PER_DIV_KEY])) + 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.set_t_per_div( - common.get_clean_decr(self.parent[T_PER_DIV_KEY])) + 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.set_x_per_div( - common.get_clean_incr(self.parent[X_PER_DIV_KEY])) + 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.set_x_per_div( - common.get_clean_decr(self.parent[X_PER_DIV_KEY])) + 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.set_y_per_div( - common.get_clean_incr(self.parent[Y_PER_DIV_KEY])) + 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.set_y_per_div( - common.get_clean_decr(self.parent[Y_PER_DIV_KEY])) + self.parent[Y_PER_DIV_KEY] = common.get_clean_decr(self.parent[Y_PER_DIV_KEY]) #incr/decr offset - def _on_incr_t_off(self, event): - self.parent.set_t_off( - self.parent[T_OFF_KEY] + self.parent[T_PER_DIV_KEY]) - def _on_decr_t_off(self, event): - self.parent.set_t_off( - self.parent[T_OFF_KEY] - self.parent[T_PER_DIV_KEY]) def _on_incr_x_off(self, event): - self.parent.set_x_off( - self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY]) + 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.set_x_off( - self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY]) + 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.set_y_off( - self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY]) + 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.set_y_off( - self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]) + self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY] ################################################## # Scope window with plotter and control panel ################################################## -class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class scope_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -259,11 +296,13 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): sample_rate_key, t_scale, v_scale, - ac_couple, xy_mode, - scope_trigger_level_key, - scope_trigger_mode_key, - scope_trigger_channel_key, + ac_couple_key, + trigger_level_key, + trigger_mode_key, + trigger_slope_key, + trigger_channel_key, + decimation_key, msg_key, ): pubsub.pubsub.__init__(self) @@ -271,67 +310,73 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): assert num_inputs <= len(CHANNEL_COLOR_SPECS) #setup self.sampleses = None - self.ext_controller = controller self.num_inputs = num_inputs - self.sample_rate_key = sample_rate_key - autorange = v_scale is None + autorange = not v_scale self.autorange_ts = 0 - if v_scale is None: v_scale = 1 + v_scale = v_scale or 1 self.frame_rate_ts = 0 - self._init = False #HACK - #scope keys - self.scope_trigger_level_key = scope_trigger_level_key - self.scope_trigger_mode_key = scope_trigger_mode_key - self.scope_trigger_channel_key = scope_trigger_channel_key + #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) + 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, -1, style=wx.SIMPLE_BORDER) + wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER) self.plotter = plotter.channel_plotter(self) self.plotter.SetSize(wx.Size(*size)) self.plotter.set_title(title) self.plotter.enable_legend(True) self.plotter.enable_point_label(True) + 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) - #initial setup - self._register_set_prop(self, RUNNING_KEY, True) - self._register_set_prop(self, AC_COUPLE_KEY, ac_couple) - self._register_set_prop(self, SCOPE_XY_MODE_KEY, xy_mode) - self._register_set_prop(self, AUTORANGE_KEY, autorange) - self._register_set_prop(self, T_PER_DIV_KEY, t_scale) - self._register_set_prop(self, X_PER_DIV_KEY, v_scale) - self._register_set_prop(self, Y_PER_DIV_KEY, v_scale) - self._register_set_prop(self, T_OFF_KEY, 0) - self._register_set_prop(self, X_OFF_KEY, 0) - self._register_set_prop(self, Y_OFF_KEY, 0) - self._register_set_prop(self, T_DIVS_KEY, 8) - self._register_set_prop(self, X_DIVS_KEY, 8) - self._register_set_prop(self, Y_DIVS_KEY, 8) - self._register_set_prop(self, SCOPE_X_CHANNEL_KEY, 0) - self._register_set_prop(self, SCOPE_Y_CHANNEL_KEY, num_inputs-1) - self._register_set_prop(self, FRAME_RATE_KEY, frame_rate) - self._register_set_prop(self, TRIGGER_CHANNEL_KEY, 0) - self._register_set_prop(self, TRIGGER_MODE_KEY, 1) - self._register_set_prop(self, TRIGGER_LEVEL_KEY, None) - self._register_set_prop(self, MARKER_KEY, DEFAULT_MARKER_TYPE) - #register events - self.ext_controller.subscribe(msg_key, self.handle_msg) - for key in ( + #initialize values + self[RUNNING_KEY] = True + for i in range(self.num_inputs): + self[common.index_key(AC_COUPLE_KEY, i)] = self[common.index_key(AC_COUPLE_KEY, i)] + self[common.index_key(MARKER_KEY, i)] = DEFAULT_MARKER_TYPE + self[XY_MARKER_KEY] = 2.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] = 0 + self[Y_OFF_KEY] = 0 + self[T_DIVS_KEY] = 8 + self[X_DIVS_KEY] = 8 + self[Y_DIVS_KEY] = 8 + self[FRAME_RATE_KEY] = frame_rate + self[TRIGGER_LEVEL_KEY] = 0 + self[TRIGGER_CHANNEL_KEY] = 0 + self[TRIGGER_MODE_KEY] = gr.gr_TRIG_MODE_AUTO + self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS + self[T_FRAC_OFF_KEY] = 0.5 + #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, - SCOPE_XY_MODE_KEY, - SCOPE_X_CHANNEL_KEY, - SCOPE_Y_CHANNEL_KEY, - AUTORANGE_KEY, - AC_COUPLE_KEY, - MARKER_KEY, - ): self.subscribe(key, self.update_grid) - #initial update, dont do this here, wait for handle_msg #HACK - #self.update_grid() + 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) + #initial update + self.update_grid() def handle_msg(self, msg): """ @@ -345,15 +390,23 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): 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)] - if not self._init: #HACK - self._init = True - self.update_grid() #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. @@ -361,52 +414,37 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): """ if not self.sampleses: return sampleses = self.sampleses - #trigger level (must do before ac coupling) - self.ext_controller[self.scope_trigger_channel_key] = self[TRIGGER_CHANNEL_KEY] - self.ext_controller[self.scope_trigger_mode_key] = self[TRIGGER_MODE_KEY] - trigger_level = self[TRIGGER_LEVEL_KEY] - if trigger_level is None: self.ext_controller[self.scope_trigger_level_key] = '' - else: - samples = sampleses[self[TRIGGER_CHANNEL_KEY]] - self.ext_controller[self.scope_trigger_level_key] = \ - trigger_level*(numpy.max(samples)-numpy.min(samples))/2 + numpy.average(samples) - #ac coupling - if self[AC_COUPLE_KEY]: - sampleses = [samples - numpy.average(samples) for samples in sampleses] - if self[SCOPE_XY_MODE_KEY]: - x_samples = sampleses[self[SCOPE_X_CHANNEL_KEY]] - y_samples = sampleses[self[SCOPE_Y_CHANNEL_KEY]] + 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.set_x_per_div(x_per_div) + 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.set_x_off(x_off) + 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.set_y_per_div(y_per_div) + 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.set_y_off(y_off) + 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[MARKER_KEY], + marker=self[XY_MARKER_KEY], ) #turn off each waveform for i, samples in enumerate(sampleses): - self.plotter.set_waveform( - channel='Ch%d'%(i+1), - samples=[], - color_spec=CHANNEL_COLOR_SPECS[i], - ) + self.plotter.clear_waveform(channel='Ch%d'%(i+1)) else: #autorange if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE: @@ -415,86 +453,89 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): y_max = numpy.max([bound[1] for bound in bounds]) #adjust the y per div y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY]) - if y_per_div != self[Y_PER_DIV_KEY]: self.set_y_per_div(y_per_div) + 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.set_y_off(y_off) + if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return self.autorange_ts = time.time() - #plot each waveform - for i, samples in enumerate(sampleses): - #number of samples to scale to the screen - num_samps = int(self[T_PER_DIV_KEY]*self[T_DIVS_KEY]*self.ext_controller[self.sample_rate_key]) - #handle num samps out of bounds - if num_samps > len(samples): - self.set_t_per_div( - common.get_clean_decr(self[T_PER_DIV_KEY])) - elif num_samps < 2: - self.set_t_per_div( - common.get_clean_incr(self[T_PER_DIV_KEY])) - num_samps = 0 - else: + #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[:num_samps], + samples=samples[samps_off:num_samps+samps_off], color_spec=CHANNEL_COLOR_SPECS[i], - marker=self[MARKER_KEY], + 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='XY', - samples=[], - color_spec=CHANNEL_COLOR_SPECS[0], + 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 """ - #grid parameters - t_per_div = self[T_PER_DIV_KEY] - x_per_div = self[X_PER_DIV_KEY] - y_per_div = self[Y_PER_DIV_KEY] - t_off = self[T_OFF_KEY] - x_off = self[X_OFF_KEY] - y_off = self[Y_OFF_KEY] - t_divs = self[T_DIVS_KEY] - x_divs = self[X_DIVS_KEY] - y_divs = self[Y_DIVS_KEY] - if self[SCOPE_XY_MODE_KEY]: + 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[SCOPE_X_CHANNEL_KEY]+1)) - self.plotter.set_x_grid( - -1*x_per_div*x_divs/2.0 + x_off, - x_per_div*x_divs/2.0 + x_off, - x_per_div, - ) + 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[SCOPE_Y_CHANNEL_KEY]+1)) - self.plotter.set_y_grid( - -1*y_per_div*y_divs/2.0 + y_off, - y_per_div*y_divs/2.0 + y_off, - y_per_div, - ) + 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 - coeff, exp, prefix = common.get_si_components(t_per_div*t_divs + t_off) - self.plotter.set_x_label('Time', prefix+'s') - self.plotter.set_x_grid( - t_off, - t_per_div*t_divs + t_off, - t_per_div, - 10**(-exp), - ) + 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('Counts') - self.plotter.set_y_grid( - -1*y_per_div*y_divs/2.0 + y_off, - y_per_div*y_divs/2.0 + y_off, - y_per_div, - ) + 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 index 40f9f3886c..87aa4337fe 100644 --- a/gr-wxgui/src/python/scopesink2.py +++ b/gr-wxgui/src/python/scopesink2.py @@ -38,7 +38,7 @@ if style == 'gl': except ImportError: raise RuntimeError("Unable to import OpenGL. Are Python wrappers for OpenGL installed?") - from scopesink_gl import scope_sink_f, scope_sink_c, constellation_sink + from scopesink_gl import scope_sink_f, scope_sink_c else: - from scopesink_nongl import scope_sink_f, scope_sink_c, constellation_sink + from scopesink_nongl import scope_sink_f, scope_sink_c diff --git a/gr-wxgui/src/python/scopesink_gl.py b/gr-wxgui/src/python/scopesink_gl.py index 36d8d8b8a2..73125c3594 100644 --- a/gr-wxgui/src/python/scopesink_gl.py +++ b/gr-wxgui/src/python/scopesink_gl.py @@ -28,10 +28,38 @@ from gnuradio import gr from pubsub import pubsub from constants import * +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, ac_couple, 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 + copy = gr.kludge_copy(gr.sizeof_float) + lpf = gr.single_pole_iir_filter_ff(0.0) + sub = gr.sub_ff() + mute = gr.mute_ff() + #connect + self.connect(self, copy, sub, self) + self.connect(copy, 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(2.0/x)) + #initialize + controller[ac_couple_key] = ac_couple + controller[sample_rate_key] = controller[sample_rate_key] + ################################################## # Scope sink block (wrapper for old wxgui) ################################################## -class _scope_sink_base(gr.hier_block2, common.prop_setter): +class _scope_sink_base(gr.hier_block2): """ A scope block with a gui window. """ @@ -42,15 +70,14 @@ class _scope_sink_base(gr.hier_block2, common.prop_setter): title='', sample_rate=1, size=scope_window.DEFAULT_WIN_SIZE, - frame_decim=None, #ignore (old wrapper) - v_scale=scope_window.DEFAULT_V_SCALE, - t_scale=None, - num_inputs=1, - ac_couple=False, + v_scale=0, + t_scale=0, xy_mode=False, + ac_couple=False, + num_inputs=1, frame_rate=scope_window.DEFAULT_FRAME_RATE, ): - if t_scale is None: t_scale = 0.001 + if not t_scale: t_scale = 10.0/sample_rate #init gr.hier_block2.__init__( self, @@ -61,37 +88,41 @@ class _scope_sink_base(gr.hier_block2, common.prop_setter): #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) #connect if self._real: for i in range(num_inputs): - self.connect((self, i), (scope, i)) + self.connect( + (self, i), + ac_couple_block(self.controller, common.index_key(AC_COUPLE_KEY, i), ac_couple, SAMPLE_RATE_KEY), + (scope, i), + ) else: for i in range(num_inputs): c2f = gr.complex_to_float() self.connect((self, i), c2f) - self.connect((c2f, 0), (scope, 2*i+0)) - self.connect((c2f, 1), (scope, 2*i+1)) + for j in range(2): + self.connect( + (c2f, j), + ac_couple_block(self.controller, common.index_key(AC_COUPLE_KEY, 2*i+j), ac_couple, SAMPLE_RATE_KEY), + (scope, 2*i+j), + ) num_inputs *= 2 - #controller - self.controller = pubsub() - self.controller.subscribe(SAMPLE_RATE_KEY, scope.set_sample_rate) - self.controller.publish(SAMPLE_RATE_KEY, scope.sample_rate) - def set_trigger_level(level): - if level == '': scope.set_trigger_level_auto() - else: scope.set_trigger_level(level) - self.controller.subscribe(SCOPE_TRIGGER_LEVEL_KEY, set_trigger_level) - def set_trigger_mode(mode): - if mode == 0: mode = gr.gr_TRIG_AUTO - elif mode < 0: mode = gr.gr_TRIG_NEG_SLOPE - elif mode > 0: mode = gr.gr_TRIG_POS_SLOPE - else: return - scope.set_trigger_mode(mode) - self.controller.subscribe(SCOPE_TRIGGER_MODE_KEY, set_trigger_mode) - self.controller.subscribe(SCOPE_TRIGGER_CHANNEL_KEY, scope.set_trigger_channel) #start input watcher - def setter(p, k, x): # lambdas can't have assignments :( - p[k] = x - common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x)) + common.input_watcher(msgq, self.controller, MSG_KEY) #create window self.win = scope_window.scope_window( parent=parent, @@ -103,21 +134,16 @@ class _scope_sink_base(gr.hier_block2, common.prop_setter): sample_rate_key=SAMPLE_RATE_KEY, t_scale=t_scale, v_scale=v_scale, - ac_couple=ac_couple, xy_mode=xy_mode, - scope_trigger_level_key=SCOPE_TRIGGER_LEVEL_KEY, - scope_trigger_mode_key=SCOPE_TRIGGER_MODE_KEY, - scope_trigger_channel_key=SCOPE_TRIGGER_CHANNEL_KEY, + 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, ) - #register callbacks from window for external use - for attr in filter(lambda a: a.startswith('set_'), dir(self.win)): - setattr(self, attr, getattr(self.win, attr)) - self._register_set_prop(self.controller, SAMPLE_RATE_KEY) - #backwards compadibility - self.win.set_format_line = lambda: setter(self.win, MARKER_KEY, None) - self.win.set_format_dot = lambda: setter(self.win, MARKER_KEY, 2.0) - self.win.set_format_plus = lambda: setter(self.win, MARKER_KEY, 3.0) + common.register_access_methods(self, self.win) class scope_sink_f(_scope_sink_base): _item_size = gr.sizeof_float @@ -127,12 +153,6 @@ class scope_sink_c(_scope_sink_base): _item_size = gr.sizeof_gr_complex _real = False -#backwards compadible wrapper (maybe only grc uses this) -class constellation_sink(scope_sink_c): - def __init__(self, *args, **kwargs): - scope_sink_c.__init__(self, *args, **kwargs) - self.set_scope_xy_mode(True) - # ---------------------------------------------------------------- # Stand-alone test application # ---------------------------------------------------------------- @@ -171,7 +191,6 @@ class test_top_block (stdgui2.std_top_block): 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) diff --git a/gr-wxgui/src/python/scopesink_nongl.py b/gr-wxgui/src/python/scopesink_nongl.py index 71fd7e1280..34967dd136 100644 --- a/gr-wxgui/src/python/scopesink_nongl.py +++ b/gr-wxgui/src/python/scopesink_nongl.py @@ -35,7 +35,7 @@ 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): + 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), @@ -56,7 +56,7 @@ class scope_sink_f(gr.hier_block2): 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): + v_scale=default_v_scale, t_scale=None, num_inputs=1, **kwargs): gr.hier_block2.__init__(self, "scope_sink_c", gr.io_signature(num_inputs, num_inputs, gr.sizeof_gr_complex), @@ -167,10 +167,7 @@ class win_info (object): self.marker = 'line' self.xy = xy - if v_scale == None: # 0 and None are both False, but 0 != None - self.autorange = True - else: - self.autorange = False # 0 is a valid v_scale + self.autorange = not v_scale self.running = True def get_time_per_div (self): @@ -320,7 +317,8 @@ class scope_window (wx.Panel): ctrlbox.Add (self.trig_chan_choice, 0, wx.ALIGN_CENTER) self.trig_mode_choice = wx.Choice (self, 1005, - choices = ['Auto', 'Pos', 'Neg']) + 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) @@ -432,12 +430,12 @@ class scope_window (wx.Panel): def trig_mode_choice_event (self, evt): sink = self.info.scopesink s = evt.GetString () - if s == 'Pos': - sink.set_trigger_mode (gr.gr_TRIG_POS_SLOPE) - elif s == 'Neg': - sink.set_trigger_mode (gr.gr_TRIG_NEG_SLOPE) + if s == 'Norm': + sink.set_trigger_mode (gr.gr_TRIG_MODE_NORM) elif s == 'Auto': - sink.set_trigger_mode (gr.gr_TRIG_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" diff --git a/gr-wxgui/src/python/waterfall_window.py b/gr-wxgui/src/python/waterfall_window.py index f24b142a7d..8dcb4b6192 100644 --- a/gr-wxgui/src/python/waterfall_window.py +++ b/gr-wxgui/src/python/waterfall_window.py @@ -16,7 +16,7 @@ # 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. +# Boston, MA 02110-1`301, USA. # ################################################## @@ -61,57 +61,57 @@ class control_panel(wx.Panel): @param parent the wx parent window """ self.parent = parent - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) control_box = wx.BoxSizer(wx.VERTICAL) control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) #color mode control_box.AddStretchSpacer() - self.color_mode_chooser = common.DropDownController(self, 'Color', COLOR_MODES, parent, COLOR_MODE_KEY) - control_box.Add(self.color_mode_chooser, 0, wx.EXPAND) + color_mode_chooser = common.DropDownController(self, COLOR_MODES, parent, COLOR_MODE_KEY) + control_box.Add(common.LabelBox(self, 'Color', color_mode_chooser), 0, wx.EXPAND) #average control_box.AddStretchSpacer() - self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key) - control_box.Add(self.average_check_box, 0, wx.EXPAND) + average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY) + control_box.Add(average_check_box, 0, wx.EXPAND) control_box.AddSpacer(2) - self.avg_alpha_slider = common.LogSliderController( + avg_alpha_slider = common.LogSliderController( self, 'Avg Alpha', AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.avg_alpha_key, + parent, AVG_ALPHA_KEY, formatter=lambda x: ': %.4f'%x, ) - parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable) - control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND) + parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable) + control_box.Add(avg_alpha_slider, 0, wx.EXPAND) #dyanmic range buttons control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Dynamic Range'), 0, wx.ALIGN_CENTER) control_box.AddSpacer(2) - self._dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range) - control_box.Add(self._dynamic_range_buttons, 0, wx.ALIGN_CENTER) + dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range) + control_box.Add(dynamic_range_buttons, 0, wx.ALIGN_CENTER) #ref lvl buttons control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER) control_box.AddSpacer(2) - self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level) - control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER) + ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level) + control_box.Add(ref_lvl_buttons, 0, wx.ALIGN_CENTER) #num lines buttons control_box.AddStretchSpacer() control_box.Add(common.LabelText(self, 'Set Time Scale'), 0, wx.ALIGN_CENTER) control_box.AddSpacer(2) - self._time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale) - control_box.Add(self._time_scale_buttons, 0, wx.ALIGN_CENTER) + time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale) + control_box.Add(time_scale_buttons, 0, wx.ALIGN_CENTER) #autoscale control_box.AddStretchSpacer() - self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT) - self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale) - control_box.Add(self.autoscale_button, 0, wx.EXPAND) + autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT) + autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale) + control_box.Add(autoscale_button, 0, wx.EXPAND) #clear - self.clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT) - self.clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button) - control_box.Add(self.clear_button, 0, wx.EXPAND) + clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT) + clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button) + control_box.Add(clear_button, 0, wx.EXPAND) #run/stop - self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') - control_box.Add(self.run_button, 0, wx.EXPAND) + run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') + control_box.Add(run_button, 0, wx.EXPAND) #set sizer self.SetSizerAndFit(control_box) @@ -119,34 +119,30 @@ class control_panel(wx.Panel): # Event handlers ################################################## def _on_clear_button(self, event): - self.parent.set_num_lines(self.parent[NUM_LINES_KEY]) + self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY] def _on_incr_dynamic_range(self, event): - self.parent.set_dynamic_range( - min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)) + self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE) def _on_decr_dynamic_range(self, event): - self.parent.set_dynamic_range( - max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)) + self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE) def _on_incr_ref_level(self, event): - self.parent.set_ref_level( - self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1) + self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1 def _on_decr_ref_level(self, event): - self.parent.set_ref_level( - self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1) + self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1 def _on_incr_time_scale(self, event): - old_rate = self.parent.ext_controller[self.parent.frame_rate_key] - self.parent.ext_controller[self.parent.frame_rate_key] *= 0.75 - if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate: - self.parent.ext_controller[self.parent.decimation_key] += 1 + old_rate = self.parent[FRAME_RATE_KEY] + self.parent[FRAME_RATE_KEY] *= 0.75 + if self.parent[FRAME_RATE_KEY] == old_rate: + self.parent[DECIMATION_KEY] += 1 def _on_decr_time_scale(self, event): - old_rate = self.parent.ext_controller[self.parent.frame_rate_key] - self.parent.ext_controller[self.parent.frame_rate_key] *= 1.25 - if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate: - self.parent.ext_controller[self.parent.decimation_key] -= 1 + 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, common.prop_setter): +class waterfall_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -169,20 +165,22 @@ class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter): pubsub.pubsub.__init__(self) #setup self.samples = list() - self.ext_controller = controller self.real = real self.fft_size = fft_size - self.decimation_key = decimation_key - self.sample_rate_key = sample_rate_key - self.frame_rate_key = frame_rate_key - self.average_key = average_key - self.avg_alpha_key = avg_alpha_key + #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, -1, style=wx.SIMPLE_BORDER) + wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER) self.plotter = plotter.waterfall_plotter(self) self.plotter.SetSize(wx.Size(*size)) self.plotter.set_title(title) self.plotter.enable_point_label(True) + 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) @@ -192,26 +190,23 @@ class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter): #plotter listeners self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode) self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines) - #initial setup - self.ext_controller[self.average_key] = self.ext_controller[self.average_key] - self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key] - self._register_set_prop(self, DYNAMIC_RANGE_KEY, dynamic_range) - self._register_set_prop(self, NUM_LINES_KEY, num_lines) - self._register_set_prop(self, Y_DIVS_KEY, 8) - self._register_set_prop(self, X_DIVS_KEY, 8) #approximate - self._register_set_prop(self, REF_LEVEL_KEY, ref_level) - self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq) - self._register_set_prop(self, COLOR_MODE_KEY, COLOR_MODES[0][1]) - self._register_set_prop(self, RUNNING_KEY, True) + #initialize values + self[AVERAGE_KEY] = self[AVERAGE_KEY] + self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY] + 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[RUNNING_KEY] = True #register events - self.ext_controller.subscribe(msg_key, self.handle_msg) - self.ext_controller.subscribe(self.decimation_key, self.update_grid) - self.ext_controller.subscribe(self.sample_rate_key, self.update_grid) - self.ext_controller.subscribe(self.frame_rate_key, self.update_grid) - self.subscribe(BASEBAND_FREQ_KEY, self.update_grid) - self.subscribe(NUM_LINES_KEY, self.update_grid) - self.subscribe(Y_DIVS_KEY, self.update_grid) - self.subscribe(X_DIVS_KEY, self.update_grid) + 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() @@ -230,8 +225,8 @@ class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter): noise_floor -= abs(noise_floor)*.5 peak_level += abs(peak_level)*.1 #set the range and level - self.set_ref_level(peak_level) - self.set_dynamic_range(peak_level - noise_floor) + self[REF_LEVEL_KEY] = peak_level + self[DYNAMIC_RANGE_KEY] = peak_level - noise_floor def handle_msg(self, msg): """ @@ -266,8 +261,8 @@ class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter): The y axis depends on y per div, y divs, and ref level. """ #grid parameters - sample_rate = self.ext_controller[self.sample_rate_key] - frame_rate = self.ext_controller[self.frame_rate_key] + sample_rate = self[SAMPLE_RATE_KEY] + frame_rate = self[FRAME_RATE_KEY] baseband_freq = self[BASEBAND_FREQ_KEY] num_lines = self[NUM_LINES_KEY] y_divs = self[Y_DIVS_KEY] @@ -276,28 +271,25 @@ class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter): if self.real: x_width = sample_rate/2.0 else: x_width = sample_rate/1.0 x_per_div = common.get_clean_num(x_width/x_divs) - coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0)) #update the x grid if self.real: self.plotter.set_x_grid( baseband_freq, baseband_freq + sample_rate/2.0, - x_per_div, - 10**(-exp), + 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, - 10**(-exp), + x_per_div, True, ) #update x units - self.plotter.set_x_label('Frequency', prefix+'Hz') + 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) + self.plotter.set_y_grid(0, duration, y_per_div, True) #update y units self.plotter.set_y_label('Time', 's') #update plotter diff --git a/gr-wxgui/src/python/waterfallsink_gl.py b/gr-wxgui/src/python/waterfallsink_gl.py index dd8e457539..344640af08 100644 --- a/gr-wxgui/src/python/waterfallsink_gl.py +++ b/gr-wxgui/src/python/waterfallsink_gl.py @@ -31,7 +31,7 @@ from constants import * ################################################## # Waterfall sink block (wrapper for old wxgui) ################################################## -class _waterfall_sink_base(gr.hier_block2, common.prop_setter): +class _waterfall_sink_base(gr.hier_block2): """ An fft block with real/complex inputs and a gui window. """ @@ -89,9 +89,7 @@ class _waterfall_sink_base(gr.hier_block2, common.prop_setter): self.controller.subscribe(FRAME_RATE_KEY, fft.set_vec_rate) self.controller.publish(FRAME_RATE_KEY, fft.frame_rate) #start input watcher - def setter(p, k, x): # lambdas can't have assignments :( - p[k] = x - common.input_watcher(msgq, lambda x: setter(self.controller, MSG_KEY, x)) + common.input_watcher(msgq, self.controller, MSG_KEY) #create window self.win = waterfall_window.waterfall_window( parent=parent, @@ -111,12 +109,8 @@ class _waterfall_sink_base(gr.hier_block2, common.prop_setter): avg_alpha_key=AVG_ALPHA_KEY, msg_key=MSG_KEY, ) - #register callbacks from window for external use - for attr in filter(lambda a: a.startswith('set_'), dir(self.win)): - setattr(self, attr, getattr(self.win, attr)) - self._register_set_prop(self.controller, SAMPLE_RATE_KEY) - self._register_set_prop(self.controller, AVERAGE_KEY) - self._register_set_prop(self.controller, AVG_ALPHA_KEY) + common.register_access_methods(self, self.win) + setattr(self.win, 'set_baseband_freq', getattr(self, 'set_baseband_freq')) #BACKWARDS class waterfall_sink_f(_waterfall_sink_base): _fft_chain = blks2.logpwrfft_f |