Merge branch 'grc' from http://gnuradio.org/git/jblum.git into master
[debian/gnuradio] / gr-wxgui / src / python / fft_window.py
1 #
2 # Copyright 2008, 2009 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, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
160         def _on_decr_db_div(self, event):
161                 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
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                 min_level, max_level = common.get_min_max_fft(self.samples)
258                 #set the range to a clean number of the dynamic range
259                 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
260                 #set the reference level to a multiple of y per div
261                 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
262
263         def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
264
265         def handle_msg(self, msg):
266                 """
267                 Handle the message from the fft sink message queue.
268                 If complex, reorder the fft samples so the negative bins come first.
269                 If real, keep take only the positive bins.
270                 Plot the samples onto the grid as channel 1.
271                 If peak hold is enabled, plot peak vals as channel 2.
272                 @param msg the fft array as a character array
273                 """
274                 if not self[RUNNING_KEY]: return
275                 #convert to floating point numbers
276                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
277                 num_samps = len(samples)
278                 #reorder fft
279                 if self.real: samples = samples[:(num_samps+1)/2]
280                 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
281                 self.samples = samples
282                 #peak hold calculation
283                 if self[PEAK_HOLD_KEY]:
284                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
285                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
286                         #plot the peak hold
287                         self.plotter.set_waveform(
288                                 channel='Peak',
289                                 samples=self.peak_vals,
290                                 color_spec=PEAK_VALS_COLOR_SPEC,
291                         )
292                 else:
293                         self._reset_peak_vals()
294                         self.plotter.clear_waveform(channel='Peak')
295                 #plot the fft
296                 self.plotter.set_waveform(
297                         channel='FFT',
298                         samples=samples,
299                         color_spec=FFT_PLOT_COLOR_SPEC,
300                 )
301                 #update the plotter
302                 self.plotter.update()
303
304         def update_grid(self, *args):
305                 """
306                 Update the plotter grid.
307                 This update method is dependent on the variables below.
308                 Determine the x and y axis grid parameters.
309                 The x axis depends on sample rate, baseband freq, and x divs.
310                 The y axis depends on y per div, y divs, and ref level.
311                 """
312                 for trace in TRACES:
313                         channel = '%s'%trace.upper()
314                         if self[TRACE_SHOW_KEY+trace]:
315                                 self.plotter.set_waveform(
316                                         channel=channel,
317                                         samples=self._traces[trace],
318                                         color_spec=TRACES_COLOR_SPEC[trace],
319                                 )
320                         else: self.plotter.clear_waveform(channel=channel)
321                 #grid parameters
322                 sample_rate = self[SAMPLE_RATE_KEY]
323                 baseband_freq = self[BASEBAND_FREQ_KEY]
324                 y_per_div = self[Y_PER_DIV_KEY]
325                 y_divs = self[Y_DIVS_KEY]
326                 x_divs = self[X_DIVS_KEY]
327                 ref_level = self[REF_LEVEL_KEY]
328                 #determine best fitting x_per_div
329                 if self.real: x_width = sample_rate/2.0
330                 else: x_width = sample_rate/1.0
331                 x_per_div = common.get_clean_num(x_width/x_divs)
332                 #update the x grid
333                 if self.real:
334                         self.plotter.set_x_grid(
335                                 baseband_freq,
336                                 baseband_freq + sample_rate/2.0,
337                                 x_per_div, True,
338                         )
339                 else:
340                         self.plotter.set_x_grid(
341                                 baseband_freq - sample_rate/2.0,
342                                 baseband_freq + sample_rate/2.0,
343                                 x_per_div, True,
344                         )
345                 #update x units
346                 self.plotter.set_x_label('Frequency', 'Hz')
347                 #update y grid
348                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
349                 #update y units
350                 self.plotter.set_y_label('Amplitude', 'dB')
351                 #update plotter
352                 self.plotter.update()