gr-wxgui: Renamed "emulate analog" feature to "use persistence"
[debian/gnuradio] / gr-wxgui / src / python / fft_window.py
index 5f48e8324e3538d3ac1c7c31196e42d8d1f16282..a460fe995de617a2e412a031d0989d84c82bf914 100644 (file)
@@ -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