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
35 ##################################################
37 ##################################################
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)
47 ##################################################
48 # FFT window control panel
49 ##################################################
50 class control_panel(wx.Panel):
52 A control panel with wx widgits to control the plotter and fft block chain.
55 def __init__(self, parent):
57 Create a new control panel.
58 @param parent the wx parent window
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,
70 sizer=options_box, parent=self, label='Peak Hold',
71 ps=parent, key=PEAK_HOLD_KEY,
74 sizer=options_box, parent=self, label='Average',
75 ps=parent, key=AVERAGE_KEY,
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,
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,
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,
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),
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,
110 y_ctrl_box.AddSpacer(2)
113 sizer=y_ctrl_box, parent=self, label='Autoscale',
114 callback=self.parent.autoscale,
117 control_box.AddStretchSpacer()
119 sizer=control_box, parent=self,
120 true_label='Stop', false_label='Run',
121 ps=parent, key=RUNNING_KEY,
124 self.SetSizerAndFit(control_box)
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)
131 ##################################################
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]
139 ##################################################
140 # FFT window with plotter and control panel
141 ##################################################
142 class fft_window(wx.Panel, pubsub.pubsub):
161 pubsub.pubsub.__init__(self)
163 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
165 self.samples = list()
167 self.fft_size = fft_size
168 self._reset_peak_vals()
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)
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
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)
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)
202 Y_PER_DIV_KEY, X_DIVS_KEY,
203 Y_DIVS_KEY, REF_LEVEL_KEY,
204 ): self.subscribe(key, self.update_grid)
208 def autoscale(self, *args):
210 Autoscale the fft plot to the last frame.
211 Set the dynamic range and reference level.
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])
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])
226 def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
228 def handle_msg(self, msg):
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
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)
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)
250 self.plotter.set_waveform(
252 samples=self.peak_vals,
253 color_spec=PEAK_VALS_COLOR_SPEC,
256 self._reset_peak_vals()
257 self.plotter.clear_waveform(channel='Peak')
259 self.plotter.set_waveform(
262 color_spec=FFT_PLOT_COLOR_SPEC,
265 self.plotter.update()
267 def update_grid(self, *args):
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.
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)
288 self.plotter.set_x_grid(
290 baseband_freq + sample_rate/2.0,
294 self.plotter.set_x_grid(
295 baseband_freq - sample_rate/2.0,
296 baseband_freq + sample_rate/2.0,
300 self.plotter.set_x_label('Frequency', 'Hz')
302 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
304 self.plotter.set_y_label('Amplitude', 'dB')
306 self.plotter.update()