Merge branch 'wxgui' from http://gnuradio.org/git/jblum.git into master
[debian/gnuradio] / gr-wxgui / src / python / fft_window.py
1 #
2 # Copyright 2008 Free Software Foundation, Inc.
3 #
4 # This file is part of GNU Radio
5 #
6 # GNU Radio is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3, or (at your option)
9 # any later version.
10 #
11 # GNU Radio is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with GNU Radio; see the file COPYING.  If not, write to
18 # the Free Software Foundation, Inc., 51 Franklin Street,
19 # Boston, MA 02110-1301, USA.
20 #
21
22 ##################################################
23 # Imports
24 ##################################################
25 import plotter
26 import common
27 import wx
28 import numpy
29 import math
30 import pubsub
31 from constants import *
32 from gnuradio import gr #for gr.prefs
33 import forms
34
35 ##################################################
36 # Constants
37 ##################################################
38 SLIDER_STEPS = 100
39 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
40 DEFAULT_WIN_SIZE = (600, 300)
41 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
42 DB_DIV_MIN, DB_DIV_MAX = 1, 20
43 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
44 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
45 EMPTY_TRACE = list()
46 TRACES = ('A', 'B')
47 TRACES_COLOR_SPEC = {
48         'A': (1.0, 0.0, 0.0),
49         'B': (0.8, 0.0, 0.8),
50 }
51
52 ##################################################
53 # FFT window control panel
54 ##################################################
55 class control_panel(wx.Panel):
56         """
57         A control panel with wx widgits to control the plotter and fft block chain.
58         """
59
60         def __init__(self, parent):
61                 """
62                 Create a new control panel.
63                 @param parent the wx parent window
64                 """
65                 self.parent = parent
66                 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
67                 control_box = wx.BoxSizer(wx.VERTICAL)
68                 control_box.AddStretchSpacer()
69                 #checkboxes for average and peak hold
70                 options_box = forms.static_box_sizer(
71                         parent=self, sizer=control_box, label='Trace Options',
72                         bold=True, orient=wx.VERTICAL,
73                 )
74                 forms.check_box(
75                         sizer=options_box, parent=self, label='Peak Hold',
76                         ps=parent, key=PEAK_HOLD_KEY,
77                 )
78                 forms.check_box(
79                         sizer=options_box, parent=self, label='Average',
80                         ps=parent, key=AVERAGE_KEY,
81                 )
82                 #static text and slider for averaging
83                 avg_alpha_text = forms.static_text(
84                         sizer=options_box, parent=self, label='Avg Alpha',
85                         converter=forms.float_converter(lambda x: '%.4f'%x),
86                         ps=parent, key=AVG_ALPHA_KEY, width=50,
87                 )
88                 avg_alpha_slider = forms.log_slider(
89                         sizer=options_box, parent=self,
90                         min_exp=AVG_ALPHA_MIN_EXP,
91                         max_exp=AVG_ALPHA_MAX_EXP,
92                         num_steps=SLIDER_STEPS,
93                         ps=parent, key=AVG_ALPHA_KEY,
94                 )
95                 for widget in (avg_alpha_text, avg_alpha_slider):
96                         parent.subscribe(AVERAGE_KEY, widget.Enable)
97                         widget.Enable(parent[AVERAGE_KEY])
98                 
99                 #trace menu
100                 for trace in TRACES:
101                         trace_box = wx.BoxSizer(wx.HORIZONTAL)
102                         options_box.Add(trace_box, 0, wx.EXPAND)
103                         forms.check_box(
104                                 sizer=trace_box, parent=self,
105                                 ps=parent, key=TRACE_SHOW_KEY+trace,
106                                 label='Trace %s'%trace,
107                         )
108                         trace_box.AddSpacer(10)
109                         forms.single_button(
110                                 sizer=trace_box, parent=self,
111                                 ps=parent, key=TRACE_STORE_KEY+trace,
112                                 label='Store', style=wx.BU_EXACTFIT,
113                         )
114                         trace_box.AddSpacer(10)
115                 #radio buttons for div size
116                 control_box.AddStretchSpacer()
117                 y_ctrl_box = forms.static_box_sizer(
118                         parent=self, sizer=control_box, label='Axis Options',
119                         bold=True, orient=wx.VERTICAL,
120                 )
121                 forms.incr_decr_buttons(
122                         parent=self, sizer=y_ctrl_box, label='dB/Div',
123                         on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
124                 )
125                 #ref lvl buttons
126                 forms.incr_decr_buttons(
127                         parent=self, sizer=y_ctrl_box, label='Ref Level',
128                         on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
129                 )
130                 y_ctrl_box.AddSpacer(2)
131                 #autoscale
132                 forms.single_button(
133                         sizer=y_ctrl_box, parent=self, label='Autoscale',
134                         callback=self.parent.autoscale,
135                 )
136                 #run/stop
137                 control_box.AddStretchSpacer()
138                 forms.toggle_button(
139                         sizer=control_box, parent=self,
140                         true_label='Stop', false_label='Run',
141                         ps=parent, key=RUNNING_KEY,
142                 )
143                 #set sizer
144                 self.SetSizerAndFit(control_box)
145                 #mouse wheel event
146                 def on_mouse_wheel(event):
147                         if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
148                         else: self._on_decr_ref_level(event)
149                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
150
151         ##################################################
152         # Event handlers
153         ##################################################
154         def _on_incr_ref_level(self, event):
155                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
156         def _on_decr_ref_level(self, event):
157                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
158         def _on_incr_db_div(self, event):
159                 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, self.parent[Y_PER_DIV_KEY]*2)
160         def _on_decr_db_div(self, event):
161                 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, self.parent[Y_PER_DIV_KEY]/2)
162
163 ##################################################
164 # FFT window with plotter and control panel
165 ##################################################
166 class fft_window(wx.Panel, pubsub.pubsub):
167         def __init__(
168                 self,
169                 parent,
170                 controller,
171                 size,
172                 title,
173                 real,
174                 fft_size,
175                 baseband_freq,
176                 sample_rate_key,
177                 y_per_div,
178                 y_divs,
179                 ref_level,
180                 average_key,
181                 avg_alpha_key,
182                 peak_hold,
183                 msg_key,
184         ):
185                 pubsub.pubsub.__init__(self)
186                 #setup
187                 self.samples = EMPTY_TRACE
188                 self.real = real
189                 self.fft_size = fft_size
190                 self._reset_peak_vals()
191                 self._traces = dict()
192                 #proxy the keys
193                 self.proxy(MSG_KEY, controller, msg_key)
194                 self.proxy(AVERAGE_KEY, controller, average_key)
195                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
196                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
197                 #initialize values
198                 self[PEAK_HOLD_KEY] = peak_hold
199                 self[Y_PER_DIV_KEY] = y_per_div
200                 self[Y_DIVS_KEY] = y_divs
201                 self[X_DIVS_KEY] = 8 #approximate
202                 self[REF_LEVEL_KEY] = ref_level
203                 self[BASEBAND_FREQ_KEY] = baseband_freq
204                 self[RUNNING_KEY] = True
205                 for trace in TRACES:
206                         #a function that returns a function
207                         #so the function wont use local trace
208                         def new_store_trace(my_trace):
209                                 def store_trace(*args):
210                                         self._traces[my_trace] = self.samples
211                                         self.update_grid()
212                                 return store_trace
213                         def new_toggle_trace(my_trace):
214                                 def toggle_trace(toggle):
215                                         #do an automatic store if toggled on and empty trace
216                                         if toggle and not len(self._traces[my_trace]):
217                                                 self._traces[my_trace] = self.samples
218                                         self.update_grid()
219                                 return toggle_trace
220                         self._traces[trace] = EMPTY_TRACE
221                         self[TRACE_STORE_KEY+trace] = False
222                         self[TRACE_SHOW_KEY+trace] = False
223                         self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
224                         self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
225                 #init panel and plot
226                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
227                 self.plotter = plotter.channel_plotter(self)
228                 self.plotter.SetSize(wx.Size(*size))
229                 self.plotter.set_title(title)
230                 self.plotter.enable_legend(True)
231                 self.plotter.enable_point_label(True)
232                 self.plotter.enable_grid_lines(True)
233                 #setup the box with plot and controls
234                 self.control_panel = control_panel(self)
235                 main_box = wx.BoxSizer(wx.HORIZONTAL)
236                 main_box.Add(self.plotter, 1, wx.EXPAND)
237                 main_box.Add(self.control_panel, 0, wx.EXPAND)
238                 self.SetSizerAndFit(main_box)
239                 #register events
240                 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
241                 self.subscribe(MSG_KEY, self.handle_msg)
242                 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
243                 for key in (
244                         BASEBAND_FREQ_KEY,
245                         Y_PER_DIV_KEY, X_DIVS_KEY,
246                         Y_DIVS_KEY, REF_LEVEL_KEY,
247                 ): self.subscribe(key, self.update_grid)
248                 #initial update
249                 self.update_grid()
250
251         def autoscale(self, *args):
252                 """
253                 Autoscale the fft plot to the last frame.
254                 Set the dynamic range and reference level.
255                 """
256                 if not len(self.samples): return
257                 #get the peak level (max of the samples)
258                 peak_level = numpy.max(self.samples)
259                 #separate noise samples
260                 noise_samps = numpy.sort(self.samples)[:len(self.samples)/2]
261                 #get the noise floor
262                 noise_floor = numpy.average(noise_samps)
263                 #get the noise deviation
264                 noise_dev = numpy.std(noise_samps)
265                 #determine the maximum and minimum levels
266                 max_level = peak_level
267                 min_level = noise_floor - abs(2*noise_dev)
268                 #set the range to a clean number of the dynamic range
269                 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
270                 #set the reference level to a multiple of y per div
271                 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
272
273         def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
274
275         def handle_msg(self, msg):
276                 """
277                 Handle the message from the fft sink message queue.
278                 If complex, reorder the fft samples so the negative bins come first.
279                 If real, keep take only the positive bins.
280                 Plot the samples onto the grid as channel 1.
281                 If peak hold is enabled, plot peak vals as channel 2.
282                 @param msg the fft array as a character array
283                 """
284                 if not self[RUNNING_KEY]: return
285                 #convert to floating point numbers
286                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
287                 num_samps = len(samples)
288                 #reorder fft
289                 if self.real: samples = samples[:(num_samps+1)/2]
290                 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
291                 self.samples = samples
292                 #peak hold calculation
293                 if self[PEAK_HOLD_KEY]:
294                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
295                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
296                         #plot the peak hold
297                         self.plotter.set_waveform(
298                                 channel='Peak',
299                                 samples=self.peak_vals,
300                                 color_spec=PEAK_VALS_COLOR_SPEC,
301                         )
302                 else:
303                         self._reset_peak_vals()
304                         self.plotter.clear_waveform(channel='Peak')
305                 #plot the fft
306                 self.plotter.set_waveform(
307                         channel='FFT',
308                         samples=samples,
309                         color_spec=FFT_PLOT_COLOR_SPEC,
310                 )
311                 #update the plotter
312                 self.plotter.update()
313
314         def update_grid(self, *args):
315                 """
316                 Update the plotter grid.
317                 This update method is dependent on the variables below.
318                 Determine the x and y axis grid parameters.
319                 The x axis depends on sample rate, baseband freq, and x divs.
320                 The y axis depends on y per div, y divs, and ref level.
321                 """
322                 for trace in TRACES:
323                         channel = '%s'%trace.upper()
324                         if self[TRACE_SHOW_KEY+trace]:
325                                 self.plotter.set_waveform(
326                                         channel=channel,
327                                         samples=self._traces[trace],
328                                         color_spec=TRACES_COLOR_SPEC[trace],
329                                 )
330                         else: self.plotter.clear_waveform(channel=channel)
331                 #grid parameters
332                 sample_rate = self[SAMPLE_RATE_KEY]
333                 baseband_freq = self[BASEBAND_FREQ_KEY]
334                 y_per_div = self[Y_PER_DIV_KEY]
335                 y_divs = self[Y_DIVS_KEY]
336                 x_divs = self[X_DIVS_KEY]
337                 ref_level = self[REF_LEVEL_KEY]
338                 #determine best fitting x_per_div
339                 if self.real: x_width = sample_rate/2.0
340                 else: x_width = sample_rate/1.0
341                 x_per_div = common.get_clean_num(x_width/x_divs)
342                 #update the x grid
343                 if self.real:
344                         self.plotter.set_x_grid(
345                                 baseband_freq,
346                                 baseband_freq + sample_rate/2.0,
347                                 x_per_div, True,
348                         )
349                 else:
350                         self.plotter.set_x_grid(
351                                 baseband_freq - sample_rate/2.0,
352                                 baseband_freq + sample_rate/2.0,
353                                 x_per_div, True,
354                         )
355                 #update x units
356                 self.plotter.set_x_label('Frequency', 'Hz')
357                 #update y grid
358                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
359                 #update y units
360                 self.plotter.set_y_label('Amplitude', 'dB')
361                 #update plotter
362                 self.plotter.update()