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 self._traces[trace] = EMPTY_TRACE
214 self[TRACE_STORE_KEY+trace] = False
215 self[TRACE_SHOW_KEY+trace] = True
216 self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
217 self.subscribe(TRACE_SHOW_KEY+trace, self.update_grid)
219 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
220 self.plotter = plotter.channel_plotter(self)
221 self.plotter.SetSize(wx.Size(*size))
222 self.plotter.set_title(title)
223 self.plotter.enable_legend(True)
224 self.plotter.enable_point_label(True)
225 self.plotter.enable_grid_lines(True)
226 #setup the box with plot and controls
227 self.control_panel = control_panel(self)
228 main_box = wx.BoxSizer(wx.HORIZONTAL)
229 main_box.Add(self.plotter, 1, wx.EXPAND)
230 main_box.Add(self.control_panel, 0, wx.EXPAND)
231 self.SetSizerAndFit(main_box)
233 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
234 self.subscribe(MSG_KEY, self.handle_msg)
235 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
238 Y_PER_DIV_KEY, X_DIVS_KEY,
239 Y_DIVS_KEY, REF_LEVEL_KEY,
240 ): self.subscribe(key, self.update_grid)
244 def autoscale(self, *args):
246 Autoscale the fft plot to the last frame.
247 Set the dynamic range and reference level.
249 if not len(self.samples): return
250 #get the peak level (max of the samples)
251 peak_level = numpy.max(self.samples)
252 #get the noise floor (averge the smallest samples)
253 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
255 noise_floor -= abs(noise_floor)*.5
256 peak_level += abs(peak_level)*.1
257 #set the reference level to a multiple of y divs
258 self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])
259 #set the range to a clean number of the dynamic range
260 self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])
262 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
264 def handle_msg(self, msg):
266 Handle the message from the fft sink message queue.
267 If complex, reorder the fft samples so the negative bins come first.
268 If real, keep take only the positive bins.
269 Plot the samples onto the grid as channel 1.
270 If peak hold is enabled, plot peak vals as channel 2.
271 @param msg the fft array as a character array
273 if not self[RUNNING_KEY]: return
274 #convert to floating point numbers
275 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
276 num_samps = len(samples)
278 if self.real: samples = samples[:(num_samps+1)/2]
279 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
280 self.samples = samples
281 #peak hold calculation
282 if self[PEAK_HOLD_KEY]:
283 if len(self.peak_vals) != len(samples): self.peak_vals = samples
284 self.peak_vals = numpy.maximum(samples, self.peak_vals)
286 self.plotter.set_waveform(
288 samples=self.peak_vals,
289 color_spec=PEAK_VALS_COLOR_SPEC,
292 self._reset_peak_vals()
293 self.plotter.clear_waveform(channel='Peak')
295 self.plotter.set_waveform(
298 color_spec=FFT_PLOT_COLOR_SPEC,
301 self.plotter.update()
303 def update_grid(self, *args):
305 Update the plotter grid.
306 This update method is dependent on the variables below.
307 Determine the x and y axis grid parameters.
308 The x axis depends on sample rate, baseband freq, and x divs.
309 The y axis depends on y per div, y divs, and ref level.
312 channel = '%s'%trace.upper()
313 if self[TRACE_SHOW_KEY+trace]:
314 self.plotter.set_waveform(
316 samples=self._traces[trace],
317 color_spec=TRACES_COLOR_SPEC[trace],
319 else: self.plotter.clear_waveform(channel=channel)
321 sample_rate = self[SAMPLE_RATE_KEY]
322 baseband_freq = self[BASEBAND_FREQ_KEY]
323 y_per_div = self[Y_PER_DIV_KEY]
324 y_divs = self[Y_DIVS_KEY]
325 x_divs = self[X_DIVS_KEY]
326 ref_level = self[REF_LEVEL_KEY]
327 #determine best fitting x_per_div
328 if self.real: x_width = sample_rate/2.0
329 else: x_width = sample_rate/1.0
330 x_per_div = common.get_clean_num(x_width/x_divs)
333 self.plotter.set_x_grid(
335 baseband_freq + sample_rate/2.0,
339 self.plotter.set_x_grid(
340 baseband_freq - sample_rate/2.0,
341 baseband_freq + sample_rate/2.0,
345 self.plotter.set_x_label('Frequency', 'Hz')
347 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
349 self.plotter.set_y_label('Amplitude', 'dB')
351 self.plotter.update()