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 *
33 ##################################################
35 ##################################################
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)
45 ##################################################
46 # FFT window control panel
47 ##################################################
48 class control_panel(wx.Panel):
50 A control panel with wx widgits to control the plotter and fft block chain.
53 def __init__(self, parent):
55 Create a new control panel.
56 @param parent the wx parent window
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(
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,
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)
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)
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)
101 self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
102 control_box.Add(self.run_button, 0, wx.EXPAND)
104 self.SetSizerAndFit(control_box)
106 ##################################################
108 ##################################################
109 def _on_set_y_per_div(self, y_per_div):
111 index = list(DIV_LEVELS).index(y_per_div)
112 self.radio_buttons[index].SetValue(True)
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])
125 ##################################################
126 # FFT window with plotter and control panel
127 ##################################################
128 class fft_window(wx.Panel, pubsub.pubsub, common.prop_setter):
147 pubsub.pubsub.__init__(self)
149 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
151 self.ext_controller = controller
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()
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)
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)
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)
187 Y_PER_DIV_KEY, X_DIVS_KEY,
188 Y_DIVS_KEY, REF_LEVEL_KEY,
189 ): self.subscribe(key, self.update_grid)
191 self.plotter.enable_legend(self[PEAK_HOLD_KEY])
194 def autoscale(self, *args):
196 Autoscale the fft plot to the last frame.
197 Set the dynamic range and reference level.
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])
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]))
211 def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
213 def handle_msg(self, msg):
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
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)
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()
236 self.plotter.set_waveform(
239 color_spec=FFT_PLOT_COLOR_SPEC,
242 self.plotter.set_waveform(
244 samples=self.peak_vals,
245 color_spec=PEAK_VALS_COLOR_SPEC,
248 self.plotter.update()
250 def update_grid(self, *args):
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.
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))
272 self.plotter.set_x_grid(
274 baseband_freq + sample_rate/2.0,
279 self.plotter.set_x_grid(
280 baseband_freq - sample_rate/2.0,
281 baseband_freq + sample_rate/2.0,
286 self.plotter.set_x_label('Frequency', prefix+'Hz')
288 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
290 self.plotter.set_y_label('Amplitude', 'dB')
292 self.plotter.update()