X-Git-Url: https://git.gag.com/?a=blobdiff_plain;f=gr-wxgui%2Fsrc%2Fpython%2Ffft_window.py;h=a460fe995de617a2e412a031d0989d84c82bf914;hb=3a730f46faf1942c713350b312a1dfeffb587550;hp=5f48e8324e3538d3ac1c7c31196e42d8d1f16282;hpb=36649d4e472172fe840444ac0268c7b6b4da94b4;p=debian%2Fgnuradio diff --git a/gr-wxgui/src/python/fft_window.py b/gr-wxgui/src/python/fft_window.py index 5f48e832..a460fe99 100644 --- a/gr-wxgui/src/python/fft_window.py +++ b/gr-wxgui/src/python/fft_window.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 # @@ -29,103 +29,184 @@ import numpy import math import pubsub from constants import * +from gnuradio import gr #for gr.prefs +import forms ################################################## # Constants ################################################## SLIDER_STEPS = 100 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0 +PERSIST_ALPHA_MIN_EXP, PERSIST_ALPHA_MAX_EXP = -2, 0 DEFAULT_WIN_SIZE = (600, 300) -DEFAULT_FRAME_RATE = 30 -DIV_LEVELS = (1, 2, 5, 10, 20) -FFT_PLOT_COLOR_SPEC = (0, 0, 1) -PEAK_VALS_COLOR_SPEC = (0, 1, 0) -NO_PEAK_VALS = list() +DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30) +DB_DIV_MIN, DB_DIV_MAX = 1, 20 +FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0) +PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0) +EMPTY_TRACE = list() +TRACES = ('A', 'B') +TRACES_COLOR_SPEC = { + 'A': (1.0, 0.0, 0.0), + 'B': (0.8, 0.0, 0.8), +} ################################################## # FFT window control panel ################################################## class control_panel(wx.Panel): - """! + """ A control panel with wx widgits to control the plotter and fft block chain. """ def __init__(self, parent): - """! + """ Create a new control panel. @param parent the wx parent window """ self.parent = parent - wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER) + wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) + parent[SHOW_CONTROL_PANEL_KEY] = True + parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show) control_box = wx.BoxSizer(wx.VERTICAL) - #checkboxes for average and peak hold control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER) - self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key) - control_box.Add(self.average_check_box, 0, wx.EXPAND) - self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY) - control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND) - control_box.AddSpacer(2) - self.avg_alpha_slider = common.LogSliderController( - self, 'Avg Alpha', - AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS, - parent.ext_controller, parent.avg_alpha_key, - formatter=lambda x: ': %.4f'%x, + #checkboxes for average and peak hold + options_box = forms.static_box_sizer( + parent=self, sizer=control_box, label='Trace Options', + bold=True, orient=wx.VERTICAL, + ) + forms.check_box( + sizer=options_box, parent=self, label='Peak Hold', + ps=parent, key=PEAK_HOLD_KEY, + ) + forms.check_box( + sizer=options_box, parent=self, label='Average', + ps=parent, key=AVERAGE_KEY, + ) + #static text and slider for averaging + avg_alpha_text = forms.static_text( + sizer=options_box, parent=self, label='Avg Alpha', + converter=forms.float_converter(lambda x: '%.4f'%x), + ps=parent, key=AVG_ALPHA_KEY, width=50, + ) + avg_alpha_slider = forms.log_slider( + sizer=options_box, parent=self, + min_exp=AVG_ALPHA_MIN_EXP, + max_exp=AVG_ALPHA_MAX_EXP, + num_steps=SLIDER_STEPS, + ps=parent, key=AVG_ALPHA_KEY, + ) + for widget in (avg_alpha_text, avg_alpha_slider): + parent.subscribe(AVERAGE_KEY, widget.Enable) + widget.Enable(parent[AVERAGE_KEY]) + parent.subscribe(AVERAGE_KEY, widget.ShowItems) + #allways show initially, so room is reserved for them + widget.ShowItems(True) # (parent[AVERAGE_KEY]) + + parent.subscribe(AVERAGE_KEY, self._update_layout) + + forms.check_box( + sizer=options_box, parent=self, label='Persistence', + ps=parent, key=USE_PERSISTENCE_KEY, + ) + #static text and slider for persist alpha + persist_alpha_text = forms.static_text( + sizer=options_box, parent=self, label='Persist Alpha', + converter=forms.float_converter(lambda x: '%.4f'%x), + ps=parent, key=PERSIST_ALPHA_KEY, width=50, ) - parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable) - control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND) + persist_alpha_slider = forms.log_slider( + sizer=options_box, parent=self, + min_exp=PERSIST_ALPHA_MIN_EXP, + max_exp=PERSIST_ALPHA_MAX_EXP, + num_steps=SLIDER_STEPS, + ps=parent, key=PERSIST_ALPHA_KEY, + ) + for widget in (persist_alpha_text, persist_alpha_slider): + parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable) + widget.Enable(parent[USE_PERSISTENCE_KEY]) + parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems) + #allways show initially, so room is reserved for them + widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY]) + + parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout) + + #trace menu + for trace in TRACES: + trace_box = wx.BoxSizer(wx.HORIZONTAL) + options_box.Add(trace_box, 0, wx.EXPAND) + forms.check_box( + sizer=trace_box, parent=self, + ps=parent, key=TRACE_SHOW_KEY+trace, + label='Trace %s'%trace, + ) + trace_box.AddSpacer(10) + forms.single_button( + sizer=trace_box, parent=self, + ps=parent, key=TRACE_STORE_KEY+trace, + label='Store', style=wx.BU_EXACTFIT, + ) + trace_box.AddSpacer(10) #radio buttons for div size control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER) - radio_box = wx.BoxSizer(wx.VERTICAL) - self.radio_buttons = list() - for y_per_div in DIV_LEVELS: - radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div) - radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div) - self.radio_buttons.append(radio_button) - radio_box.Add(radio_button, 0, wx.ALIGN_LEFT) - parent.subscribe(Y_PER_DIV_KEY, self._on_set_y_per_div) - control_box.Add(radio_box, 0, wx.EXPAND) + y_ctrl_box = forms.static_box_sizer( + parent=self, sizer=control_box, label='Axis Options', + bold=True, orient=wx.VERTICAL, + ) + forms.incr_decr_buttons( + parent=self, sizer=y_ctrl_box, label='dB/Div', + on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div, + ) #ref lvl buttons - 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) + forms.incr_decr_buttons( + parent=self, sizer=y_ctrl_box, label='Ref Level', + on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level, + ) + y_ctrl_box.AddSpacer(2) #autoscale - 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) + forms.single_button( + sizer=y_ctrl_box, parent=self, label='Autoscale', + callback=self.parent.autoscale, + ) #run/stop - self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') - control_box.Add(self.run_button, 0, wx.EXPAND) + control_box.AddStretchSpacer() + forms.toggle_button( + sizer=control_box, parent=self, + true_label='Stop', false_label='Run', + ps=parent, key=RUNNING_KEY, + ) #set sizer self.SetSizerAndFit(control_box) + #mouse wheel event + def on_mouse_wheel(event): + if event.GetWheelRotation() < 0: self._on_incr_ref_level(event) + else: self._on_decr_ref_level(event) + parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel) + ################################################## # Event handlers ################################################## - def _on_set_y_per_div(self, y_per_div): - try: - index = list(DIV_LEVELS).index(y_per_div) - self.radio_buttons[index].SetValue(True) - except: pass - def _on_y_per_div(self, event): - selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0] - index = self.radio_buttons.index(selected_radio_button) - self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index] def _on_incr_ref_level(self, event): - self.parent.set_ref_level( - self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]) + 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] + def _on_incr_db_div(self, event): + self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY])) + def _on_decr_db_div(self, event): + self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY])) + ################################################## + # subscriber handlers + ################################################## + def _update_layout(self,key): + # Just ignore the key value we get + # we only need to now that the visability or size of something has changed + self.parent.Layout() + #self.parent.Fit() ################################################## # FFT window with plotter and control panel ################################################## -class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class fft_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -143,75 +224,99 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): avg_alpha_key, peak_hold, msg_key, + use_persistence, + persist_alpha, ): + pubsub.pubsub.__init__(self) - #ensure y_per_div - if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0] #setup - self.ext_controller = controller + self.samples = EMPTY_TRACE 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() + self._traces = dict() + #proxy the keys + self.proxy(MSG_KEY, controller, msg_key) + self.proxy(AVERAGE_KEY, controller, average_key) + self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key) + self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key) + #initialize values + self[PEAK_HOLD_KEY] = peak_hold + self[Y_PER_DIV_KEY] = y_per_div + self[Y_DIVS_KEY] = y_divs + self[X_DIVS_KEY] = 8 #approximate + self[REF_LEVEL_KEY] = ref_level + self[BASEBAND_FREQ_KEY] = baseband_freq + self[RUNNING_KEY] = True + self[USE_PERSISTENCE_KEY] = use_persistence + self[PERSIST_ALPHA_KEY] = persist_alpha + for trace in TRACES: + #a function that returns a function + #so the function wont use local trace + def new_store_trace(my_trace): + def store_trace(*args): + self._traces[my_trace] = self.samples + self.update_grid() + return store_trace + def new_toggle_trace(my_trace): + def toggle_trace(toggle): + #do an automatic store if toggled on and empty trace + if toggle and not len(self._traces[my_trace]): + self._traces[my_trace] = self.samples + self.update_grid() + return toggle_trace + self._traces[trace] = EMPTY_TRACE + self[TRACE_STORE_KEY+trace] = False + self[TRACE_SHOW_KEY+trace] = False + self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace)) + self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace)) #init panel and plot - wx.Panel.__init__(self, parent, -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) + self.plotter.set_use_persistence(use_persistence) + self.plotter.set_persist_alpha(persist_alpha) #setup the box with plot and controls self.control_panel = control_panel(self) main_box = wx.BoxSizer(wx.HORIZONTAL) main_box.Add(self.plotter, 1, wx.EXPAND) main_box.Add(self.control_panel, 0, wx.EXPAND) self.SetSizerAndFit(main_box) - #initial setup - self.ext_controller[self.average_key] = self.ext_controller[self.average_key] - self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key] - self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold) - self._register_set_prop(self, Y_PER_DIV_KEY, y_per_div) - self._register_set_prop(self, Y_DIVS_KEY, y_divs) - self._register_set_prop(self, X_DIVS_KEY, 8) #approximate - self._register_set_prop(self, REF_LEVEL_KEY, ref_level) - self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq) - self._register_set_prop(self, RUNNING_KEY, True) #register events - self.subscribe(PEAK_HOLD_KEY, self.plotter.enable_legend) - self.ext_controller.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals()) - self.ext_controller.subscribe(msg_key, self.handle_msg) - self.ext_controller.subscribe(self.sample_rate_key, self.update_grid) + self.subscribe(AVERAGE_KEY, self._reset_peak_vals) + self.subscribe(MSG_KEY, self.handle_msg) + self.subscribe(SAMPLE_RATE_KEY, self.update_grid) for key in ( BASEBAND_FREQ_KEY, Y_PER_DIV_KEY, X_DIVS_KEY, Y_DIVS_KEY, REF_LEVEL_KEY, ): self.subscribe(key, self.update_grid) + self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence) + self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha) #initial update - self.plotter.enable_legend(self[PEAK_HOLD_KEY]) self.update_grid() + def autoscale(self, *args): - """! + """ Autoscale the fft plot to the last frame. Set the dynamic range and reference level. """ - #get the peak level (max of the samples) - peak_level = numpy.max(self.samples) - #get the noise floor (averge the smallest samples) - noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4]) - #padding - noise_floor -= abs(noise_floor)*.5 - peak_level += abs(peak_level)*.1 - #set the reference level to a multiple of y divs - self.set_ref_level(self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])) + if not len(self.samples): return + min_level, max_level = common.get_min_max_fft(self.samples) #set the range to a clean number of the dynamic range - self.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(1+(max_level - min_level)/self[Y_DIVS_KEY]) + #set the reference level to a multiple of y per div + self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY]) - def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS + def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE def handle_msg(self, msg): - """! + """ Handle the message from the fft sink message queue. If complex, reorder the fft samples so the negative bins come first. If real, keep take only the positive bins. @@ -224,39 +329,50 @@ class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter): samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame num_samps = len(samples) #reorder fft - if self.real: samples = samples[:num_samps/2] - else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2])) + if self.real: samples = samples[:(num_samps+1)/2] + else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2])) self.samples = samples #peak hold calculation if self[PEAK_HOLD_KEY]: if len(self.peak_vals) != len(samples): self.peak_vals = samples self.peak_vals = numpy.maximum(samples, self.peak_vals) - 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() def update_grid(self, *args): - """! + """ Update the plotter grid. This update method is dependent on the variables below. Determine the x and y axis grid parameters. The x axis depends on sample rate, baseband freq, and x divs. The y axis depends on y per div, y divs, and ref level. """ + for trace in TRACES: + channel = '%s'%trace.upper() + if self[TRACE_SHOW_KEY+trace]: + self.plotter.set_waveform( + channel=channel, + samples=self._traces[trace], + color_spec=TRACES_COLOR_SPEC[trace], + ) + else: self.plotter.clear_waveform(channel=channel) #grid parameters - sample_rate = self.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] @@ -266,24 +382,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