2 # Copyright 2008, 2009 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 ANALOG_ALPHA_MIN_EXP, ANALOG_ALPHA_MAX_EXP = -2, 0
41 DEFAULT_WIN_SIZE = (600, 300)
42 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
43 DB_DIV_MIN, DB_DIV_MAX = 1, 20
44 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
45 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
53 ##################################################
54 # FFT window control panel
55 ##################################################
56 class control_panel(wx.Panel):
58 A control panel with wx widgits to control the plotter and fft block chain.
61 def __init__(self, parent):
63 Create a new control panel.
64 @param parent the wx parent window
67 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
68 control_box = wx.BoxSizer(wx.VERTICAL)
69 control_box.AddStretchSpacer()
70 #checkboxes for average and peak hold
71 options_box = forms.static_box_sizer(
72 parent=self, sizer=control_box, label='Trace Options',
73 bold=True, orient=wx.VERTICAL,
76 sizer=options_box, parent=self, label='Peak Hold',
77 ps=parent, key=PEAK_HOLD_KEY,
80 sizer=options_box, parent=self, label='Average',
81 ps=parent, key=AVERAGE_KEY,
83 #static text and slider for averaging
84 avg_alpha_text = forms.static_text(
85 sizer=options_box, parent=self, label='Avg Alpha',
86 converter=forms.float_converter(lambda x: '%.4f'%x),
87 ps=parent, key=AVG_ALPHA_KEY, width=50,
89 avg_alpha_slider = forms.log_slider(
90 sizer=options_box, parent=self,
91 min_exp=AVG_ALPHA_MIN_EXP,
92 max_exp=AVG_ALPHA_MAX_EXP,
93 num_steps=SLIDER_STEPS,
94 ps=parent, key=AVG_ALPHA_KEY,
96 for widget in (avg_alpha_text, avg_alpha_slider):
97 parent.subscribe(AVERAGE_KEY, widget.Enable)
98 widget.Enable(parent[AVERAGE_KEY])
99 parent.subscribe(AVERAGE_KEY, widget.ShowItems)
100 #allways show initially, so room is reserved for them
101 widget.ShowItems(True) # (parent[AVERAGE_KEY])
103 parent.subscribe(AVERAGE_KEY, self._update_layout)
106 sizer=options_box, parent=self, label='Emulate Analog',
107 ps=parent, key=EMULATE_ANALOG_KEY,
109 #static text and slider for analog alpha
110 analog_alpha_text = forms.static_text(
111 sizer=options_box, parent=self, label='Analog Alpha',
112 converter=forms.float_converter(lambda x: '%.4f'%x),
113 ps=parent, key=ANALOG_ALPHA_KEY, width=50,
115 analog_alpha_slider = forms.log_slider(
116 sizer=options_box, parent=self,
117 min_exp=ANALOG_ALPHA_MIN_EXP,
118 max_exp=ANALOG_ALPHA_MAX_EXP,
119 num_steps=SLIDER_STEPS,
120 ps=parent, key=ANALOG_ALPHA_KEY,
122 for widget in (analog_alpha_text, analog_alpha_slider):
123 parent.subscribe(EMULATE_ANALOG_KEY, widget.Enable)
124 widget.Enable(parent[EMULATE_ANALOG_KEY])
125 parent.subscribe(EMULATE_ANALOG_KEY, widget.ShowItems)
126 #allways show initially, so room is reserved for them
127 widget.ShowItems(True) # (parent[EMULATE_ANALOG_KEY])
129 parent.subscribe(EMULATE_ANALOG_KEY, self._update_layout)
133 trace_box = wx.BoxSizer(wx.HORIZONTAL)
134 options_box.Add(trace_box, 0, wx.EXPAND)
136 sizer=trace_box, parent=self,
137 ps=parent, key=TRACE_SHOW_KEY+trace,
138 label='Trace %s'%trace,
140 trace_box.AddSpacer(10)
142 sizer=trace_box, parent=self,
143 ps=parent, key=TRACE_STORE_KEY+trace,
144 label='Store', style=wx.BU_EXACTFIT,
146 trace_box.AddSpacer(10)
147 #radio buttons for div size
148 control_box.AddStretchSpacer()
149 y_ctrl_box = forms.static_box_sizer(
150 parent=self, sizer=control_box, label='Axis Options',
151 bold=True, orient=wx.VERTICAL,
153 forms.incr_decr_buttons(
154 parent=self, sizer=y_ctrl_box, label='dB/Div',
155 on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
158 forms.incr_decr_buttons(
159 parent=self, sizer=y_ctrl_box, label='Ref Level',
160 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
162 y_ctrl_box.AddSpacer(2)
165 sizer=y_ctrl_box, parent=self, label='Autoscale',
166 callback=self.parent.autoscale,
169 control_box.AddStretchSpacer()
171 sizer=control_box, parent=self,
172 true_label='Stop', false_label='Run',
173 ps=parent, key=RUNNING_KEY,
176 self.SetSizerAndFit(control_box)
179 def on_mouse_wheel(event):
180 if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
181 else: self._on_decr_ref_level(event)
182 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
184 ##################################################
186 ##################################################
187 def _on_incr_ref_level(self, event):
188 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
189 def _on_decr_ref_level(self, event):
190 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
191 def _on_incr_db_div(self, event):
192 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
193 def _on_decr_db_div(self, event):
194 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
195 ##################################################
196 # subscriber handlers
197 ##################################################
198 def _update_layout(self,key):
199 # Just ignore the key value we get
200 # we only need to now that the visability or size of something has changed
204 ##################################################
205 # FFT window with plotter and control panel
206 ##################################################
207 class fft_window(wx.Panel, pubsub.pubsub):
229 pubsub.pubsub.__init__(self)
231 self.samples = EMPTY_TRACE
233 self.fft_size = fft_size
234 self._reset_peak_vals()
235 self._traces = dict()
237 self.proxy(MSG_KEY, controller, msg_key)
238 self.proxy(AVERAGE_KEY, controller, average_key)
239 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
240 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
242 self[PEAK_HOLD_KEY] = peak_hold
243 self[Y_PER_DIV_KEY] = y_per_div
244 self[Y_DIVS_KEY] = y_divs
245 self[X_DIVS_KEY] = 8 #approximate
246 self[REF_LEVEL_KEY] = ref_level
247 self[BASEBAND_FREQ_KEY] = baseband_freq
248 self[RUNNING_KEY] = True
249 self[EMULATE_ANALOG_KEY] = emulate_analog
250 self[ANALOG_ALPHA_KEY] = analog_alpha
252 #a function that returns a function
253 #so the function wont use local trace
254 def new_store_trace(my_trace):
255 def store_trace(*args):
256 self._traces[my_trace] = self.samples
259 def new_toggle_trace(my_trace):
260 def toggle_trace(toggle):
261 #do an automatic store if toggled on and empty trace
262 if toggle and not len(self._traces[my_trace]):
263 self._traces[my_trace] = self.samples
266 self._traces[trace] = EMPTY_TRACE
267 self[TRACE_STORE_KEY+trace] = False
268 self[TRACE_SHOW_KEY+trace] = False
269 self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
270 self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
272 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
273 self.plotter = plotter.channel_plotter(self)
274 self.plotter.SetSize(wx.Size(*size))
275 self.plotter.set_title(title)
276 self.plotter.enable_legend(True)
277 self.plotter.enable_point_label(True)
278 self.plotter.enable_grid_lines(True)
279 self.plotter.set_emulate_analog(emulate_analog)
280 self.plotter.set_analog_alpha(analog_alpha)
281 #setup the box with plot and controls
282 self.control_panel = control_panel(self)
283 main_box = wx.BoxSizer(wx.HORIZONTAL)
284 main_box.Add(self.plotter, 1, wx.EXPAND)
285 main_box.Add(self.control_panel, 0, wx.EXPAND)
286 self.SetSizerAndFit(main_box)
288 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
289 self.subscribe(MSG_KEY, self.handle_msg)
290 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
293 Y_PER_DIV_KEY, X_DIVS_KEY,
294 Y_DIVS_KEY, REF_LEVEL_KEY,
295 ): self.subscribe(key, self.update_grid)
296 self.subscribe(EMULATE_ANALOG_KEY, self.plotter.set_emulate_analog)
297 self.subscribe(ANALOG_ALPHA_KEY, self.plotter.set_analog_alpha)
302 def autoscale(self, *args):
304 Autoscale the fft plot to the last frame.
305 Set the dynamic range and reference level.
307 if not len(self.samples): return
308 min_level, max_level = common.get_min_max_fft(self.samples)
309 #set the range to a clean number of the dynamic range
310 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
311 #set the reference level to a multiple of y per div
312 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
314 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
316 def handle_msg(self, msg):
318 Handle the message from the fft sink message queue.
319 If complex, reorder the fft samples so the negative bins come first.
320 If real, keep take only the positive bins.
321 Plot the samples onto the grid as channel 1.
322 If peak hold is enabled, plot peak vals as channel 2.
323 @param msg the fft array as a character array
325 if not self[RUNNING_KEY]: return
326 #convert to floating point numbers
327 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
328 num_samps = len(samples)
330 if self.real: samples = samples[:(num_samps+1)/2]
331 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
332 self.samples = samples
333 #peak hold calculation
334 if self[PEAK_HOLD_KEY]:
335 if len(self.peak_vals) != len(samples): self.peak_vals = samples
336 self.peak_vals = numpy.maximum(samples, self.peak_vals)
338 self.plotter.set_waveform(
340 samples=self.peak_vals,
341 color_spec=PEAK_VALS_COLOR_SPEC,
344 self._reset_peak_vals()
345 self.plotter.clear_waveform(channel='Peak')
347 self.plotter.set_waveform(
350 color_spec=FFT_PLOT_COLOR_SPEC,
353 self.plotter.update()
355 def update_grid(self, *args):
357 Update the plotter grid.
358 This update method is dependent on the variables below.
359 Determine the x and y axis grid parameters.
360 The x axis depends on sample rate, baseband freq, and x divs.
361 The y axis depends on y per div, y divs, and ref level.
364 channel = '%s'%trace.upper()
365 if self[TRACE_SHOW_KEY+trace]:
366 self.plotter.set_waveform(
368 samples=self._traces[trace],
369 color_spec=TRACES_COLOR_SPEC[trace],
371 else: self.plotter.clear_waveform(channel=channel)
373 sample_rate = self[SAMPLE_RATE_KEY]
374 baseband_freq = self[BASEBAND_FREQ_KEY]
375 y_per_div = self[Y_PER_DIV_KEY]
376 y_divs = self[Y_DIVS_KEY]
377 x_divs = self[X_DIVS_KEY]
378 ref_level = self[REF_LEVEL_KEY]
379 #determine best fitting x_per_div
380 if self.real: x_width = sample_rate/2.0
381 else: x_width = sample_rate/1.0
382 x_per_div = common.get_clean_num(x_width/x_divs)
385 self.plotter.set_x_grid(
387 baseband_freq + sample_rate/2.0,
391 self.plotter.set_x_grid(
392 baseband_freq - sample_rate/2.0,
393 baseband_freq + sample_rate/2.0,
397 self.plotter.set_x_label('Frequency', 'Hz')
399 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
401 self.plotter.set_y_label('Amplitude', 'dB')
403 self.plotter.update()