2 # Copyright 2008, 2009, 2010 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 PERSIST_ALPHA_MIN_EXP, PERSIST_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 parent[SHOW_CONTROL_PANEL_KEY] = True
69 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
70 control_box = wx.BoxSizer(wx.VERTICAL)
71 control_box.AddStretchSpacer()
72 #checkboxes for average and peak hold
73 options_box = forms.static_box_sizer(
74 parent=self, sizer=control_box, label='Trace Options',
75 bold=True, orient=wx.VERTICAL,
78 sizer=options_box, parent=self, label='Peak Hold',
79 ps=parent, key=PEAK_HOLD_KEY,
82 sizer=options_box, parent=self, label='Average',
83 ps=parent, key=AVERAGE_KEY,
85 #static text and slider for averaging
86 avg_alpha_text = forms.static_text(
87 sizer=options_box, parent=self, label='Avg Alpha',
88 converter=forms.float_converter(lambda x: '%.4f'%x),
89 ps=parent, key=AVG_ALPHA_KEY, width=50,
91 avg_alpha_slider = forms.log_slider(
92 sizer=options_box, parent=self,
93 min_exp=AVG_ALPHA_MIN_EXP,
94 max_exp=AVG_ALPHA_MAX_EXP,
95 num_steps=SLIDER_STEPS,
96 ps=parent, key=AVG_ALPHA_KEY,
98 for widget in (avg_alpha_text, avg_alpha_slider):
99 parent.subscribe(AVERAGE_KEY, widget.Enable)
100 widget.Enable(parent[AVERAGE_KEY])
101 parent.subscribe(AVERAGE_KEY, widget.ShowItems)
102 #allways show initially, so room is reserved for them
103 widget.ShowItems(True) # (parent[AVERAGE_KEY])
105 parent.subscribe(AVERAGE_KEY, self._update_layout)
108 sizer=options_box, parent=self, label='Persistence',
109 ps=parent, key=USE_PERSISTENCE_KEY,
111 #static text and slider for persist alpha
112 persist_alpha_text = forms.static_text(
113 sizer=options_box, parent=self, label='Persist Alpha',
114 converter=forms.float_converter(lambda x: '%.4f'%x),
115 ps=parent, key=PERSIST_ALPHA_KEY, width=50,
117 persist_alpha_slider = forms.log_slider(
118 sizer=options_box, parent=self,
119 min_exp=PERSIST_ALPHA_MIN_EXP,
120 max_exp=PERSIST_ALPHA_MAX_EXP,
121 num_steps=SLIDER_STEPS,
122 ps=parent, key=PERSIST_ALPHA_KEY,
124 for widget in (persist_alpha_text, persist_alpha_slider):
125 parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable)
126 widget.Enable(parent[USE_PERSISTENCE_KEY])
127 parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems)
128 #allways show initially, so room is reserved for them
129 widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY])
131 parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout)
135 trace_box = wx.BoxSizer(wx.HORIZONTAL)
136 options_box.Add(trace_box, 0, wx.EXPAND)
138 sizer=trace_box, parent=self,
139 ps=parent, key=TRACE_SHOW_KEY+trace,
140 label='Trace %s'%trace,
142 trace_box.AddSpacer(10)
144 sizer=trace_box, parent=self,
145 ps=parent, key=TRACE_STORE_KEY+trace,
146 label='Store', style=wx.BU_EXACTFIT,
148 trace_box.AddSpacer(10)
149 #radio buttons for div size
150 control_box.AddStretchSpacer()
151 y_ctrl_box = forms.static_box_sizer(
152 parent=self, sizer=control_box, label='Axis Options',
153 bold=True, orient=wx.VERTICAL,
155 forms.incr_decr_buttons(
156 parent=self, sizer=y_ctrl_box, label='dB/Div',
157 on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
160 forms.incr_decr_buttons(
161 parent=self, sizer=y_ctrl_box, label='Ref Level',
162 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
164 y_ctrl_box.AddSpacer(2)
167 sizer=y_ctrl_box, parent=self, label='Autoscale',
168 callback=self.parent.autoscale,
171 control_box.AddStretchSpacer()
173 sizer=control_box, parent=self,
174 true_label='Stop', false_label='Run',
175 ps=parent, key=RUNNING_KEY,
178 self.SetSizerAndFit(control_box)
181 def on_mouse_wheel(event):
182 if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
183 else: self._on_decr_ref_level(event)
184 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
186 ##################################################
188 ##################################################
189 def _on_incr_ref_level(self, event):
190 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
191 def _on_decr_ref_level(self, event):
192 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
193 def _on_incr_db_div(self, event):
194 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
195 def _on_decr_db_div(self, event):
196 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
197 ##################################################
198 # subscriber handlers
199 ##################################################
200 def _update_layout(self,key):
201 # Just ignore the key value we get
202 # we only need to now that the visability or size of something has changed
206 ##################################################
207 # FFT window with plotter and control panel
208 ##################################################
209 class fft_window(wx.Panel, pubsub.pubsub):
231 pubsub.pubsub.__init__(self)
233 self.samples = EMPTY_TRACE
235 self.fft_size = fft_size
236 self._reset_peak_vals()
237 self._traces = dict()
239 self.proxy(MSG_KEY, controller, msg_key)
240 self.proxy(AVERAGE_KEY, controller, average_key)
241 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
242 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
244 self[PEAK_HOLD_KEY] = peak_hold
245 self[Y_PER_DIV_KEY] = y_per_div
246 self[Y_DIVS_KEY] = y_divs
247 self[X_DIVS_KEY] = 8 #approximate
248 self[REF_LEVEL_KEY] = ref_level
249 self[BASEBAND_FREQ_KEY] = baseband_freq
250 self[RUNNING_KEY] = True
251 self[USE_PERSISTENCE_KEY] = use_persistence
252 self[PERSIST_ALPHA_KEY] = persist_alpha
254 #a function that returns a function
255 #so the function wont use local trace
256 def new_store_trace(my_trace):
257 def store_trace(*args):
258 self._traces[my_trace] = self.samples
261 def new_toggle_trace(my_trace):
262 def toggle_trace(toggle):
263 #do an automatic store if toggled on and empty trace
264 if toggle and not len(self._traces[my_trace]):
265 self._traces[my_trace] = self.samples
268 self._traces[trace] = EMPTY_TRACE
269 self[TRACE_STORE_KEY+trace] = False
270 self[TRACE_SHOW_KEY+trace] = False
271 self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
272 self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
274 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
275 self.plotter = plotter.channel_plotter(self)
276 self.plotter.SetSize(wx.Size(*size))
277 self.plotter.set_title(title)
278 self.plotter.enable_legend(True)
279 self.plotter.enable_point_label(True)
280 self.plotter.enable_grid_lines(True)
281 self.plotter.set_use_persistence(use_persistence)
282 self.plotter.set_persist_alpha(persist_alpha)
283 #setup the box with plot and controls
284 self.control_panel = control_panel(self)
285 main_box = wx.BoxSizer(wx.HORIZONTAL)
286 main_box.Add(self.plotter, 1, wx.EXPAND)
287 main_box.Add(self.control_panel, 0, wx.EXPAND)
288 self.SetSizerAndFit(main_box)
290 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
291 self.subscribe(MSG_KEY, self.handle_msg)
292 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
295 Y_PER_DIV_KEY, X_DIVS_KEY,
296 Y_DIVS_KEY, REF_LEVEL_KEY,
297 ): self.subscribe(key, self.update_grid)
298 self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
299 self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
304 def autoscale(self, *args):
306 Autoscale the fft plot to the last frame.
307 Set the dynamic range and reference level.
309 if not len(self.samples): return
310 min_level, max_level = common.get_min_max_fft(self.samples)
311 #set the range to a clean number of the dynamic range
312 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
313 #set the reference level to a multiple of y per div
314 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
316 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
318 def handle_msg(self, msg):
320 Handle the message from the fft sink message queue.
321 If complex, reorder the fft samples so the negative bins come first.
322 If real, keep take only the positive bins.
323 Plot the samples onto the grid as channel 1.
324 If peak hold is enabled, plot peak vals as channel 2.
325 @param msg the fft array as a character array
327 if not self[RUNNING_KEY]: return
328 #convert to floating point numbers
329 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
330 num_samps = len(samples)
332 if self.real: samples = samples[:(num_samps+1)/2]
333 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
334 self.samples = samples
335 #peak hold calculation
336 if self[PEAK_HOLD_KEY]:
337 if len(self.peak_vals) != len(samples): self.peak_vals = samples
338 self.peak_vals = numpy.maximum(samples, self.peak_vals)
340 self.plotter.set_waveform(
342 samples=self.peak_vals,
343 color_spec=PEAK_VALS_COLOR_SPEC,
346 self._reset_peak_vals()
347 self.plotter.clear_waveform(channel='Peak')
349 self.plotter.set_waveform(
352 color_spec=FFT_PLOT_COLOR_SPEC,
355 self.plotter.update()
357 def update_grid(self, *args):
359 Update the plotter grid.
360 This update method is dependent on the variables below.
361 Determine the x and y axis grid parameters.
362 The x axis depends on sample rate, baseband freq, and x divs.
363 The y axis depends on y per div, y divs, and ref level.
366 channel = '%s'%trace.upper()
367 if self[TRACE_SHOW_KEY+trace]:
368 self.plotter.set_waveform(
370 samples=self._traces[trace],
371 color_spec=TRACES_COLOR_SPEC[trace],
373 else: self.plotter.clear_waveform(channel=channel)
375 sample_rate = self[SAMPLE_RATE_KEY]
376 baseband_freq = self[BASEBAND_FREQ_KEY]
377 y_per_div = self[Y_PER_DIV_KEY]
378 y_divs = self[Y_DIVS_KEY]
379 x_divs = self[X_DIVS_KEY]
380 ref_level = self[REF_LEVEL_KEY]
381 #determine best fitting x_per_div
382 if self.real: x_width = sample_rate/2.0
383 else: x_width = sample_rate/1.0
384 x_per_div = common.get_clean_num(x_width/x_divs)
387 self.plotter.set_x_grid(
389 baseband_freq + sample_rate/2.0,
393 self.plotter.set_x_grid(
394 baseband_freq - sample_rate/2.0,
395 baseband_freq + sample_rate/2.0,
399 self.plotter.set_x_label('Frequency', 'Hz')
401 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
403 self.plotter.set_y_label('Amplitude', 'dB')
405 self.plotter.update()