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 DB_DIV_MIN, DB_DIV_MAX = 1, 20
43 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
44 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
52 ##################################################
53 # FFT window control panel
54 ##################################################
55 class control_panel(wx.Panel):
57 A control panel with wx widgits to control the plotter and fft block chain.
60 def __init__(self, parent):
62 Create a new control panel.
63 @param parent the wx parent window
66 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
67 control_box = wx.BoxSizer(wx.VERTICAL)
68 control_box.AddStretchSpacer()
69 #checkboxes for average and peak hold
70 options_box = forms.static_box_sizer(
71 parent=self, sizer=control_box, label='Trace Options',
72 bold=True, orient=wx.VERTICAL,
75 sizer=options_box, parent=self, label='Peak Hold',
76 ps=parent, key=PEAK_HOLD_KEY,
79 sizer=options_box, parent=self, label='Average',
80 ps=parent, key=AVERAGE_KEY,
82 #static text and slider for averaging
83 avg_alpha_text = forms.static_text(
84 sizer=options_box, parent=self, label='Avg Alpha',
85 converter=forms.float_converter(lambda x: '%.4f'%x),
86 ps=parent, key=AVG_ALPHA_KEY, width=50,
88 avg_alpha_slider = forms.log_slider(
89 sizer=options_box, parent=self,
90 min_exp=AVG_ALPHA_MIN_EXP,
91 max_exp=AVG_ALPHA_MAX_EXP,
92 num_steps=SLIDER_STEPS,
93 ps=parent, key=AVG_ALPHA_KEY,
95 for widget in (avg_alpha_text, avg_alpha_slider):
96 parent.subscribe(AVERAGE_KEY, widget.Enable)
97 widget.Enable(parent[AVERAGE_KEY])
101 trace_box = wx.BoxSizer(wx.HORIZONTAL)
102 options_box.Add(trace_box, 0, wx.EXPAND)
104 sizer=trace_box, parent=self,
105 ps=parent, key=TRACE_SHOW_KEY+trace,
106 label='Trace %s'%trace,
108 trace_box.AddSpacer(10)
110 sizer=trace_box, parent=self,
111 ps=parent, key=TRACE_STORE_KEY+trace,
112 label='Store', style=wx.BU_EXACTFIT,
114 trace_box.AddSpacer(10)
115 #radio buttons for div size
116 control_box.AddStretchSpacer()
117 y_ctrl_box = forms.static_box_sizer(
118 parent=self, sizer=control_box, label='Axis Options',
119 bold=True, orient=wx.VERTICAL,
121 forms.incr_decr_buttons(
122 parent=self, sizer=y_ctrl_box, label='dB/Div',
123 on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
126 forms.incr_decr_buttons(
127 parent=self, sizer=y_ctrl_box, label='Ref Level',
128 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
130 y_ctrl_box.AddSpacer(2)
133 sizer=y_ctrl_box, parent=self, label='Autoscale',
134 callback=self.parent.autoscale,
137 control_box.AddStretchSpacer()
139 sizer=control_box, parent=self,
140 true_label='Stop', false_label='Run',
141 ps=parent, key=RUNNING_KEY,
144 self.SetSizerAndFit(control_box)
146 def on_mouse_wheel(event):
147 if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
148 else: self._on_decr_ref_level(event)
149 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
151 ##################################################
153 ##################################################
154 def _on_incr_ref_level(self, event):
155 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
156 def _on_decr_ref_level(self, event):
157 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
158 def _on_incr_db_div(self, event):
159 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, self.parent[Y_PER_DIV_KEY]*2)
160 def _on_decr_db_div(self, event):
161 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, self.parent[Y_PER_DIV_KEY]/2)
163 ##################################################
164 # FFT window with plotter and control panel
165 ##################################################
166 class fft_window(wx.Panel, pubsub.pubsub):
185 pubsub.pubsub.__init__(self)
187 self.samples = EMPTY_TRACE
189 self.fft_size = fft_size
190 self._reset_peak_vals()
191 self._traces = dict()
193 self.proxy(MSG_KEY, controller, msg_key)
194 self.proxy(AVERAGE_KEY, controller, average_key)
195 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
196 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
198 self[PEAK_HOLD_KEY] = peak_hold
199 self[Y_PER_DIV_KEY] = y_per_div
200 self[Y_DIVS_KEY] = y_divs
201 self[X_DIVS_KEY] = 8 #approximate
202 self[REF_LEVEL_KEY] = ref_level
203 self[BASEBAND_FREQ_KEY] = baseband_freq
204 self[RUNNING_KEY] = True
206 #a function that returns a function
207 #so the function wont use local trace
208 def new_store_trace(my_trace):
209 def store_trace(*args):
210 self._traces[my_trace] = self.samples
213 def new_toggle_trace(my_trace):
214 def toggle_trace(toggle):
215 #do an automatic store if toggled on and empty trace
216 if toggle and not len(self._traces[my_trace]):
217 self._traces[my_trace] = self.samples
220 self._traces[trace] = EMPTY_TRACE
221 self[TRACE_STORE_KEY+trace] = False
222 self[TRACE_SHOW_KEY+trace] = False
223 self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
224 self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
226 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
227 self.plotter = plotter.channel_plotter(self)
228 self.plotter.SetSize(wx.Size(*size))
229 self.plotter.set_title(title)
230 self.plotter.enable_legend(True)
231 self.plotter.enable_point_label(True)
232 self.plotter.enable_grid_lines(True)
233 #setup the box with plot and controls
234 self.control_panel = control_panel(self)
235 main_box = wx.BoxSizer(wx.HORIZONTAL)
236 main_box.Add(self.plotter, 1, wx.EXPAND)
237 main_box.Add(self.control_panel, 0, wx.EXPAND)
238 self.SetSizerAndFit(main_box)
240 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
241 self.subscribe(MSG_KEY, self.handle_msg)
242 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
245 Y_PER_DIV_KEY, X_DIVS_KEY,
246 Y_DIVS_KEY, REF_LEVEL_KEY,
247 ): self.subscribe(key, self.update_grid)
251 def autoscale(self, *args):
253 Autoscale the fft plot to the last frame.
254 Set the dynamic range and reference level.
256 if not len(self.samples): return
257 #get the peak level (max of the samples)
258 peak_level = numpy.max(self.samples)
259 #separate noise samples
260 noise_samps = numpy.sort(self.samples)[:len(self.samples)/2]
262 noise_floor = numpy.average(noise_samps)
263 #get the noise deviation
264 noise_dev = numpy.std(noise_samps)
265 #determine the maximum and minimum levels
266 max_level = peak_level
267 min_level = noise_floor - abs(2*noise_dev)
268 #set the range to a clean number of the dynamic range
269 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
270 #set the reference level to a multiple of y per div
271 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
273 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
275 def handle_msg(self, msg):
277 Handle the message from the fft sink message queue.
278 If complex, reorder the fft samples so the negative bins come first.
279 If real, keep take only the positive bins.
280 Plot the samples onto the grid as channel 1.
281 If peak hold is enabled, plot peak vals as channel 2.
282 @param msg the fft array as a character array
284 if not self[RUNNING_KEY]: return
285 #convert to floating point numbers
286 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
287 num_samps = len(samples)
289 if self.real: samples = samples[:(num_samps+1)/2]
290 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
291 self.samples = samples
292 #peak hold calculation
293 if self[PEAK_HOLD_KEY]:
294 if len(self.peak_vals) != len(samples): self.peak_vals = samples
295 self.peak_vals = numpy.maximum(samples, self.peak_vals)
297 self.plotter.set_waveform(
299 samples=self.peak_vals,
300 color_spec=PEAK_VALS_COLOR_SPEC,
303 self._reset_peak_vals()
304 self.plotter.clear_waveform(channel='Peak')
306 self.plotter.set_waveform(
309 color_spec=FFT_PLOT_COLOR_SPEC,
312 self.plotter.update()
314 def update_grid(self, *args):
316 Update the plotter grid.
317 This update method is dependent on the variables below.
318 Determine the x and y axis grid parameters.
319 The x axis depends on sample rate, baseband freq, and x divs.
320 The y axis depends on y per div, y divs, and ref level.
323 channel = '%s'%trace.upper()
324 if self[TRACE_SHOW_KEY+trace]:
325 self.plotter.set_waveform(
327 samples=self._traces[trace],
328 color_spec=TRACES_COLOR_SPEC[trace],
330 else: self.plotter.clear_waveform(channel=channel)
332 sample_rate = self[SAMPLE_RATE_KEY]
333 baseband_freq = self[BASEBAND_FREQ_KEY]
334 y_per_div = self[Y_PER_DIV_KEY]
335 y_divs = self[Y_DIVS_KEY]
336 x_divs = self[X_DIVS_KEY]
337 ref_level = self[REF_LEVEL_KEY]
338 #determine best fitting x_per_div
339 if self.real: x_width = sample_rate/2.0
340 else: x_width = sample_rate/1.0
341 x_per_div = common.get_clean_num(x_width/x_divs)
344 self.plotter.set_x_grid(
346 baseband_freq + sample_rate/2.0,
350 self.plotter.set_x_grid(
351 baseband_freq - sample_rate/2.0,
352 baseband_freq + sample_rate/2.0,
356 self.plotter.set_x_label('Frequency', 'Hz')
358 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
360 self.plotter.set_y_label('Amplitude', 'dB')
362 self.plotter.update()