Imported Upstream version 3.2.2
[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 DIV_LEVELS = (1, 2, 5, 10, 20)
43 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
44 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
45 NO_PEAK_VALS = list()
46
47 ##################################################
48 # FFT window control panel
49 ##################################################
50 class control_panel(wx.Panel):
51         """
52         A control panel with wx widgits to control the plotter and fft block chain.
53         """
54
55         def __init__(self, parent):
56                 """
57                 Create a new control panel.
58                 @param parent the wx parent window
59                 """
60                 self.parent = parent
61                 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
62                 control_box = wx.BoxSizer(wx.VERTICAL)
63                 control_box.AddStretchSpacer()
64                 #checkboxes for average and peak hold
65                 options_box = forms.static_box_sizer(
66                         parent=self, sizer=control_box, label='Options',
67                         bold=True, orient=wx.VERTICAL,
68                 )
69                 forms.check_box(
70                         sizer=options_box, parent=self, label='Peak Hold',
71                         ps=parent, key=PEAK_HOLD_KEY,
72                 )
73                 forms.check_box(
74                         sizer=options_box, parent=self, label='Average',
75                         ps=parent, key=AVERAGE_KEY,
76                 )
77                 #static text and slider for averaging
78                 avg_alpha_text = forms.static_text(
79                         sizer=options_box, parent=self, label='Avg Alpha',
80                         converter=forms.float_converter(lambda x: '%.4f'%x),
81                         ps=parent, key=AVG_ALPHA_KEY, width=50,
82                 )
83                 avg_alpha_slider = forms.log_slider(
84                         sizer=options_box, parent=self,
85                         min_exp=AVG_ALPHA_MIN_EXP,
86                         max_exp=AVG_ALPHA_MAX_EXP,
87                         num_steps=SLIDER_STEPS,
88                         ps=parent, key=AVG_ALPHA_KEY,
89                 )
90                 for widget in (avg_alpha_text, avg_alpha_slider):
91                         parent.subscribe(AVERAGE_KEY, widget.Enable)
92                         widget.Enable(parent[AVERAGE_KEY])
93                 #radio buttons for div size
94                 control_box.AddStretchSpacer()
95                 y_ctrl_box = forms.static_box_sizer(
96                         parent=self, sizer=control_box, label='Axis Options',
97                         bold=True, orient=wx.VERTICAL,
98                 )
99                 forms.radio_buttons(
100                         sizer=y_ctrl_box, parent=self,
101                         ps=parent, key=Y_PER_DIV_KEY,
102                         style=wx.RA_VERTICAL|wx.NO_BORDER, choices=DIV_LEVELS,
103                         labels=map(lambda x: '%s dB/div'%x, DIV_LEVELS),
104                 )
105                 #ref lvl buttons
106                 forms.incr_decr_buttons(
107                         parent=self, sizer=y_ctrl_box, label='Ref Level',
108                         on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
109                 )
110                 y_ctrl_box.AddSpacer(2)
111                 #autoscale
112                 forms.single_button(
113                         sizer=y_ctrl_box, parent=self, label='Autoscale',
114                         callback=self.parent.autoscale,
115                 )
116                 #run/stop
117                 control_box.AddStretchSpacer()
118                 forms.toggle_button(
119                         sizer=control_box, parent=self,
120                         true_label='Stop', false_label='Run',
121                         ps=parent, key=RUNNING_KEY,
122                 )
123                 #set sizer
124                 self.SetSizerAndFit(control_box)
125                 #mouse wheel event
126                 def on_mouse_wheel(event):
127                         if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
128                         else: self._on_decr_ref_level(event)
129                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
130
131         ##################################################
132         # Event handlers
133         ##################################################
134         def _on_incr_ref_level(self, event):
135                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
136         def _on_decr_ref_level(self, event):
137                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
138
139 ##################################################
140 # FFT window with plotter and control panel
141 ##################################################
142 class fft_window(wx.Panel, pubsub.pubsub):
143         def __init__(
144                 self,
145                 parent,
146                 controller,
147                 size,
148                 title,
149                 real,
150                 fft_size,
151                 baseband_freq,
152                 sample_rate_key,
153                 y_per_div,
154                 y_divs,
155                 ref_level,
156                 average_key,
157                 avg_alpha_key,
158                 peak_hold,
159                 msg_key,
160         ):
161                 pubsub.pubsub.__init__(self)
162                 #ensure y_per_div
163                 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
164                 #setup
165                 self.samples = list()
166                 self.real = real
167                 self.fft_size = fft_size
168                 self._reset_peak_vals()
169                 #proxy the keys
170                 self.proxy(MSG_KEY, controller, msg_key)
171                 self.proxy(AVERAGE_KEY, controller, average_key)
172                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
173                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
174                 #initialize values
175                 self[PEAK_HOLD_KEY] = peak_hold
176                 self[Y_PER_DIV_KEY] = y_per_div
177                 self[Y_DIVS_KEY] = y_divs
178                 self[X_DIVS_KEY] = 8 #approximate
179                 self[REF_LEVEL_KEY] = ref_level
180                 self[BASEBAND_FREQ_KEY] = baseband_freq
181                 self[RUNNING_KEY] = True
182                 #init panel and plot
183                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
184                 self.plotter = plotter.channel_plotter(self)
185                 self.plotter.SetSize(wx.Size(*size))
186                 self.plotter.set_title(title)
187                 self.plotter.enable_legend(True)
188                 self.plotter.enable_point_label(True)
189                 self.plotter.enable_grid_lines(True)
190                 #setup the box with plot and controls
191                 self.control_panel = control_panel(self)
192                 main_box = wx.BoxSizer(wx.HORIZONTAL)
193                 main_box.Add(self.plotter, 1, wx.EXPAND)
194                 main_box.Add(self.control_panel, 0, wx.EXPAND)
195                 self.SetSizerAndFit(main_box)
196                 #register events
197                 self.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals())
198                 self.subscribe(MSG_KEY, self.handle_msg)
199                 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
200                 for key in (
201                         BASEBAND_FREQ_KEY,
202                         Y_PER_DIV_KEY, X_DIVS_KEY,
203                         Y_DIVS_KEY, REF_LEVEL_KEY,
204                 ): self.subscribe(key, self.update_grid)
205                 #initial update
206                 self.update_grid()
207
208         def autoscale(self, *args):
209                 """
210                 Autoscale the fft plot to the last frame.
211                 Set the dynamic range and reference level.
212                 """
213                 if not len(self.samples): return
214                 #get the peak level (max of the samples)
215                 peak_level = numpy.max(self.samples)
216                 #get the noise floor (averge the smallest samples)
217                 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
218                 #padding
219                 noise_floor -= abs(noise_floor)*.5
220                 peak_level += abs(peak_level)*.1
221                 #set the reference level to a multiple of y divs
222                 self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])
223                 #set the range to a clean number of the dynamic range
224                 self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])
225
226         def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
227
228         def handle_msg(self, msg):
229                 """
230                 Handle the message from the fft sink message queue.
231                 If complex, reorder the fft samples so the negative bins come first.
232                 If real, keep take only the positive bins.
233                 Plot the samples onto the grid as channel 1.
234                 If peak hold is enabled, plot peak vals as channel 2.
235                 @param msg the fft array as a character array
236                 """
237                 if not self[RUNNING_KEY]: return
238                 #convert to floating point numbers
239                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
240                 num_samps = len(samples)
241                 #reorder fft
242                 if self.real: samples = samples[:num_samps/2]
243                 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
244                 self.samples = samples
245                 #peak hold calculation
246                 if self[PEAK_HOLD_KEY]:
247                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
248                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
249                         #plot the peak hold
250                         self.plotter.set_waveform(
251                                 channel='Peak',
252                                 samples=self.peak_vals,
253                                 color_spec=PEAK_VALS_COLOR_SPEC,
254                         )
255                 else:
256                         self._reset_peak_vals()
257                         self.plotter.clear_waveform(channel='Peak')
258                 #plot the fft
259                 self.plotter.set_waveform(
260                         channel='FFT',
261                         samples=samples,
262                         color_spec=FFT_PLOT_COLOR_SPEC,
263                 )
264                 #update the plotter
265                 self.plotter.update()
266
267         def update_grid(self, *args):
268                 """
269                 Update the plotter grid.
270                 This update method is dependent on the variables below.
271                 Determine the x and y axis grid parameters.
272                 The x axis depends on sample rate, baseband freq, and x divs.
273                 The y axis depends on y per div, y divs, and ref level.
274                 """
275                 #grid parameters
276                 sample_rate = self[SAMPLE_RATE_KEY]
277                 baseband_freq = self[BASEBAND_FREQ_KEY]
278                 y_per_div = self[Y_PER_DIV_KEY]
279                 y_divs = self[Y_DIVS_KEY]
280                 x_divs = self[X_DIVS_KEY]
281                 ref_level = self[REF_LEVEL_KEY]
282                 #determine best fitting x_per_div
283                 if self.real: x_width = sample_rate/2.0
284                 else: x_width = sample_rate/1.0
285                 x_per_div = common.get_clean_num(x_width/x_divs)
286                 #update the x grid
287                 if self.real:
288                         self.plotter.set_x_grid(
289                                 baseband_freq,
290                                 baseband_freq + sample_rate/2.0,
291                                 x_per_div, True,
292                         )
293                 else:
294                         self.plotter.set_x_grid(
295                                 baseband_freq - sample_rate/2.0,
296                                 baseband_freq + sample_rate/2.0,
297                                 x_per_div, True,
298                         )
299                 #update x units
300                 self.plotter.set_x_label('Frequency', 'Hz')
301                 #update y grid
302                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
303                 #update y units
304                 self.plotter.set_y_label('Amplitude', 'dB')
305                 #update plotter
306                 self.plotter.update()