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 #get the noise floor (averge the smallest samples)
260 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
262 noise_floor -= abs(noise_floor)*.5
263 peak_level += abs(peak_level)*.1
264 #set the reference level to a multiple of y divs
265 self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])
266 #set the range to a clean number of the dynamic range
267 self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])
269 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
271 def handle_msg(self, msg):
273 Handle the message from the fft sink message queue.
274 If complex, reorder the fft samples so the negative bins come first.
275 If real, keep take only the positive bins.
276 Plot the samples onto the grid as channel 1.
277 If peak hold is enabled, plot peak vals as channel 2.
278 @param msg the fft array as a character array
280 if not self[RUNNING_KEY]: return
281 #convert to floating point numbers
282 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
283 num_samps = len(samples)
285 if self.real: samples = samples[:(num_samps+1)/2]
286 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
287 self.samples = samples
288 #peak hold calculation
289 if self[PEAK_HOLD_KEY]:
290 if len(self.peak_vals) != len(samples): self.peak_vals = samples
291 self.peak_vals = numpy.maximum(samples, self.peak_vals)
293 self.plotter.set_waveform(
295 samples=self.peak_vals,
296 color_spec=PEAK_VALS_COLOR_SPEC,
299 self._reset_peak_vals()
300 self.plotter.clear_waveform(channel='Peak')
302 self.plotter.set_waveform(
305 color_spec=FFT_PLOT_COLOR_SPEC,
308 self.plotter.update()
310 def update_grid(self, *args):
312 Update the plotter grid.
313 This update method is dependent on the variables below.
314 Determine the x and y axis grid parameters.
315 The x axis depends on sample rate, baseband freq, and x divs.
316 The y axis depends on y per div, y divs, and ref level.
319 channel = '%s'%trace.upper()
320 if self[TRACE_SHOW_KEY+trace]:
321 self.plotter.set_waveform(
323 samples=self._traces[trace],
324 color_spec=TRACES_COLOR_SPEC[trace],
326 else: self.plotter.clear_waveform(channel=channel)
328 sample_rate = self[SAMPLE_RATE_KEY]
329 baseband_freq = self[BASEBAND_FREQ_KEY]
330 y_per_div = self[Y_PER_DIV_KEY]
331 y_divs = self[Y_DIVS_KEY]
332 x_divs = self[X_DIVS_KEY]
333 ref_level = self[REF_LEVEL_KEY]
334 #determine best fitting x_per_div
335 if self.real: x_width = sample_rate/2.0
336 else: x_width = sample_rate/1.0
337 x_per_div = common.get_clean_num(x_width/x_divs)
340 self.plotter.set_x_grid(
342 baseband_freq + sample_rate/2.0,
346 self.plotter.set_x_grid(
347 baseband_freq - sample_rate/2.0,
348 baseband_freq + sample_rate/2.0,
352 self.plotter.set_x_label('Frequency', 'Hz')
354 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
356 self.plotter.set_y_label('Amplitude', 'dB')
358 self.plotter.update()