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