2 # Copyright 2008 Free Software Foundation, Inc.
4 # This file is part of GNU Radio
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)
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.
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.
22 ##################################################
24 ##################################################
31 from constants import *
32 from gnuradio import gr #for gr.prefs
34 ##################################################
36 ##################################################
38 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
39 DEFAULT_WIN_SIZE = (600, 300)
40 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
41 DIV_LEVELS = (1, 2, 5, 10, 20)
42 FFT_PLOT_COLOR_SPEC = (0, 0, 1)
43 PEAK_VALS_COLOR_SPEC = (0, 1, 0)
46 ##################################################
47 # FFT window control panel
48 ##################################################
49 class control_panel(wx.Panel):
51 A control panel with wx widgits to control the plotter and fft block chain.
54 def __init__(self, parent):
56 Create a new control panel.
57 @param parent the wx parent window
60 wx.Panel.__init__(self, parent, -1, style=wx.SUNKEN_BORDER)
61 control_box = wx.BoxSizer(wx.VERTICAL)
62 #checkboxes for average and peak hold
63 control_box.AddStretchSpacer()
64 control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
65 self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key)
66 control_box.Add(self.average_check_box, 0, wx.EXPAND)
67 self.peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY)
68 control_box.Add(self.peak_hold_check_box, 0, wx.EXPAND)
69 control_box.AddSpacer(2)
70 self.avg_alpha_slider = common.LogSliderController(
72 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
73 parent.ext_controller, parent.avg_alpha_key,
74 formatter=lambda x: ': %.4f'%x,
76 parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable)
77 control_box.Add(self.avg_alpha_slider, 0, wx.EXPAND)
78 #radio buttons for div size
79 control_box.AddStretchSpacer()
80 control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
81 radio_box = wx.BoxSizer(wx.VERTICAL)
82 self.radio_buttons = list()
83 for y_per_div in DIV_LEVELS:
84 radio_button = wx.RadioButton(self, -1, "%d dB/div"%y_per_div)
85 radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div)
86 self.radio_buttons.append(radio_button)
87 radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
88 parent.subscribe(Y_PER_DIV_KEY, self._on_set_y_per_div)
89 control_box.Add(radio_box, 0, wx.EXPAND)
91 control_box.AddStretchSpacer()
92 control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
93 control_box.AddSpacer(2)
94 self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
95 control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER)
97 control_box.AddStretchSpacer()
98 self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
99 self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
100 control_box.Add(self.autoscale_button, 0, wx.EXPAND)
102 self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
103 control_box.Add(self.run_button, 0, wx.EXPAND)
105 self.SetSizerAndFit(control_box)
107 ##################################################
109 ##################################################
110 def _on_set_y_per_div(self, y_per_div):
112 index = list(DIV_LEVELS).index(y_per_div)
113 self.radio_buttons[index].SetValue(True)
115 def _on_y_per_div(self, event):
116 selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
117 index = self.radio_buttons.index(selected_radio_button)
118 self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index]
119 def _on_incr_ref_level(self, event):
120 self.parent.set_ref_level(
121 self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY])
122 def _on_decr_ref_level(self, event):
123 self.parent.set_ref_level(
124 self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY])
126 ##################################################
127 # FFT window with plotter and control panel
128 ##################################################
129 class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter):
148 pubsub.pubsub.__init__(self)
150 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
152 self.ext_controller = controller
154 self.fft_size = fft_size
155 self.sample_rate_key = sample_rate_key
156 self.average_key = average_key
157 self.avg_alpha_key = avg_alpha_key
158 self._reset_peak_vals()
160 wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
161 self.plotter = plotter.channel_plotter(self)
162 self.plotter.SetSize(wx.Size(*size))
163 self.plotter.set_title(title)
164 self.plotter.enable_point_label(True)
165 #setup the box with plot and controls
166 self.control_panel = control_panel(self)
167 main_box = wx.BoxSizer(wx.HORIZONTAL)
168 main_box.Add(self.plotter, 1, wx.EXPAND)
169 main_box.Add(self.control_panel, 0, wx.EXPAND)
170 self.SetSizerAndFit(main_box)
172 self.ext_controller[self.average_key] = self.ext_controller[self.average_key]
173 self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key]
174 self._register_set_prop(self, PEAK_HOLD_KEY, peak_hold)
175 self._register_set_prop(self, Y_PER_DIV_KEY, y_per_div)
176 self._register_set_prop(self, Y_DIVS_KEY, y_divs)
177 self._register_set_prop(self, X_DIVS_KEY, 8) #approximate
178 self._register_set_prop(self, REF_LEVEL_KEY, ref_level)
179 self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq)
180 self._register_set_prop(self, RUNNING_KEY, True)
182 self.subscribe(PEAK_HOLD_KEY, self.plotter.enable_legend)
183 self.ext_controller.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals())
184 self.ext_controller.subscribe(msg_key, self.handle_msg)
185 self.ext_controller.subscribe(self.sample_rate_key, self.update_grid)
188 Y_PER_DIV_KEY, X_DIVS_KEY,
189 Y_DIVS_KEY, REF_LEVEL_KEY,
190 ): self.subscribe(key, self.update_grid)
192 self.plotter.enable_legend(self[PEAK_HOLD_KEY])
195 def autoscale(self, *args):
197 Autoscale the fft plot to the last frame.
198 Set the dynamic range and reference level.
200 #get the peak level (max of the samples)
201 peak_level = numpy.max(self.samples)
202 #get the noise floor (averge the smallest samples)
203 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
205 noise_floor -= abs(noise_floor)*.5
206 peak_level += abs(peak_level)*.1
207 #set the reference level to a multiple of y divs
208 self.set_ref_level(self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY]))
209 #set the range to a clean number of the dynamic range
210 self.set_y_per_div(common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY]))
212 def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
214 def handle_msg(self, msg):
216 Handle the message from the fft sink message queue.
217 If complex, reorder the fft samples so the negative bins come first.
218 If real, keep take only the positive bins.
219 Plot the samples onto the grid as channel 1.
220 If peak hold is enabled, plot peak vals as channel 2.
221 @param msg the fft array as a character array
223 if not self[RUNNING_KEY]: return
224 #convert to floating point numbers
225 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
226 num_samps = len(samples)
228 if self.real: samples = samples[:num_samps/2]
229 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
230 self.samples = samples
231 #peak hold calculation
232 if self[PEAK_HOLD_KEY]:
233 if len(self.peak_vals) != len(samples): self.peak_vals = samples
234 self.peak_vals = numpy.maximum(samples, self.peak_vals)
235 else: self._reset_peak_vals()
237 self.plotter.set_waveform(
240 color_spec=FFT_PLOT_COLOR_SPEC,
243 self.plotter.set_waveform(
245 samples=self.peak_vals,
246 color_spec=PEAK_VALS_COLOR_SPEC,
249 self.plotter.update()
251 def update_grid(self, *args):
253 Update the plotter grid.
254 This update method is dependent on the variables below.
255 Determine the x and y axis grid parameters.
256 The x axis depends on sample rate, baseband freq, and x divs.
257 The y axis depends on y per div, y divs, and ref level.
260 sample_rate = self.ext_controller[self.sample_rate_key]
261 baseband_freq = self[BASEBAND_FREQ_KEY]
262 y_per_div = self[Y_PER_DIV_KEY]
263 y_divs = self[Y_DIVS_KEY]
264 x_divs = self[X_DIVS_KEY]
265 ref_level = self[REF_LEVEL_KEY]
266 #determine best fitting x_per_div
267 if self.real: x_width = sample_rate/2.0
268 else: x_width = sample_rate/1.0
269 x_per_div = common.get_clean_num(x_width/x_divs)
270 coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0))
273 self.plotter.set_x_grid(
275 baseband_freq + sample_rate/2.0,
280 self.plotter.set_x_grid(
281 baseband_freq - sample_rate/2.0,
282 baseband_freq + sample_rate/2.0,
287 self.plotter.set_x_label('Frequency', prefix+'Hz')
289 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
291 self.plotter.set_y_label('Amplitude', 'dB')
293 self.plotter.update()