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