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 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 parent[SHOW_CONTROL_PANEL_KEY] = True
68 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
69 control_box = wx.BoxSizer(wx.VERTICAL)
70 control_box.AddStretchSpacer()
71 #checkboxes for average and peak hold
72 options_box = forms.static_box_sizer(
73 parent=self, sizer=control_box, label='Trace Options',
74 bold=True, orient=wx.VERTICAL,
77 sizer=options_box, parent=self, label='Peak Hold',
78 ps=parent, key=PEAK_HOLD_KEY,
81 sizer=options_box, parent=self, label='Average',
82 ps=parent, key=AVERAGE_KEY,
84 #static text and slider for averaging
85 avg_alpha_text = forms.static_text(
86 sizer=options_box, parent=self, label='Avg Alpha',
87 converter=forms.float_converter(lambda x: '%.4f'%x),
88 ps=parent, key=AVG_ALPHA_KEY, width=50,
90 avg_alpha_slider = forms.log_slider(
91 sizer=options_box, parent=self,
92 min_exp=AVG_ALPHA_MIN_EXP,
93 max_exp=AVG_ALPHA_MAX_EXP,
94 num_steps=SLIDER_STEPS,
95 ps=parent, key=AVG_ALPHA_KEY,
97 for widget in (avg_alpha_text, avg_alpha_slider):
98 parent.subscribe(AVERAGE_KEY, widget.Enable)
99 widget.Enable(parent[AVERAGE_KEY])
103 trace_box = wx.BoxSizer(wx.HORIZONTAL)
104 options_box.Add(trace_box, 0, wx.EXPAND)
106 sizer=trace_box, parent=self,
107 ps=parent, key=TRACE_SHOW_KEY+trace,
108 label='Trace %s'%trace,
110 trace_box.AddSpacer(10)
112 sizer=trace_box, parent=self,
113 ps=parent, key=TRACE_STORE_KEY+trace,
114 label='Store', style=wx.BU_EXACTFIT,
116 trace_box.AddSpacer(10)
117 #radio buttons for div size
118 control_box.AddStretchSpacer()
119 y_ctrl_box = forms.static_box_sizer(
120 parent=self, sizer=control_box, label='Axis Options',
121 bold=True, orient=wx.VERTICAL,
123 forms.incr_decr_buttons(
124 parent=self, sizer=y_ctrl_box, label='dB/Div',
125 on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
128 forms.incr_decr_buttons(
129 parent=self, sizer=y_ctrl_box, label='Ref Level',
130 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
132 y_ctrl_box.AddSpacer(2)
135 sizer=y_ctrl_box, parent=self, label='Autoscale',
136 callback=self.parent.autoscale,
139 control_box.AddStretchSpacer()
141 sizer=control_box, parent=self,
142 true_label='Stop', false_label='Run',
143 ps=parent, key=RUNNING_KEY,
146 self.SetSizerAndFit(control_box)
148 def on_mouse_wheel(event):
149 if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
150 else: self._on_decr_ref_level(event)
151 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
153 ##################################################
155 ##################################################
156 def _on_incr_ref_level(self, event):
157 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
158 def _on_decr_ref_level(self, event):
159 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
160 def _on_incr_db_div(self, event):
161 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
162 def _on_decr_db_div(self, event):
163 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
165 ##################################################
166 # FFT window with plotter and control panel
167 ##################################################
168 class fft_window(wx.Panel, pubsub.pubsub):
187 pubsub.pubsub.__init__(self)
189 self.samples = EMPTY_TRACE
191 self.fft_size = fft_size
192 self._reset_peak_vals()
193 self._traces = dict()
195 self.proxy(MSG_KEY, controller, msg_key)
196 self.proxy(AVERAGE_KEY, controller, average_key)
197 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
198 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
200 self[PEAK_HOLD_KEY] = peak_hold
201 self[Y_PER_DIV_KEY] = y_per_div
202 self[Y_DIVS_KEY] = y_divs
203 self[X_DIVS_KEY] = 8 #approximate
204 self[REF_LEVEL_KEY] = ref_level
205 self[BASEBAND_FREQ_KEY] = baseband_freq
206 self[RUNNING_KEY] = True
208 #a function that returns a function
209 #so the function wont use local trace
210 def new_store_trace(my_trace):
211 def store_trace(*args):
212 self._traces[my_trace] = self.samples
215 def new_toggle_trace(my_trace):
216 def toggle_trace(toggle):
217 #do an automatic store if toggled on and empty trace
218 if toggle and not len(self._traces[my_trace]):
219 self._traces[my_trace] = self.samples
222 self._traces[trace] = EMPTY_TRACE
223 self[TRACE_STORE_KEY+trace] = False
224 self[TRACE_SHOW_KEY+trace] = False
225 self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
226 self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
228 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
229 self.plotter = plotter.channel_plotter(self)
230 self.plotter.SetSize(wx.Size(*size))
231 self.plotter.set_title(title)
232 self.plotter.enable_legend(True)
233 self.plotter.enable_point_label(True)
234 self.plotter.enable_grid_lines(True)
235 #setup the box with plot and controls
236 self.control_panel = control_panel(self)
237 main_box = wx.BoxSizer(wx.HORIZONTAL)
238 main_box.Add(self.plotter, 1, wx.EXPAND)
239 main_box.Add(self.control_panel, 0, wx.EXPAND)
240 self.SetSizerAndFit(main_box)
242 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
243 self.subscribe(MSG_KEY, self.handle_msg)
244 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
247 Y_PER_DIV_KEY, X_DIVS_KEY,
248 Y_DIVS_KEY, REF_LEVEL_KEY,
249 ): self.subscribe(key, self.update_grid)
253 def autoscale(self, *args):
255 Autoscale the fft plot to the last frame.
256 Set the dynamic range and reference level.
258 if not len(self.samples): return
259 min_level, max_level = common.get_min_max_fft(self.samples)
260 #set the range to a clean number of the dynamic range
261 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
262 #set the reference level to a multiple of y per div
263 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
265 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
267 def handle_msg(self, msg):
269 Handle the message from the fft sink message queue.
270 If complex, reorder the fft samples so the negative bins come first.
271 If real, keep take only the positive bins.
272 Plot the samples onto the grid as channel 1.
273 If peak hold is enabled, plot peak vals as channel 2.
274 @param msg the fft array as a character array
276 if not self[RUNNING_KEY]: return
277 #convert to floating point numbers
278 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
279 num_samps = len(samples)
281 if self.real: samples = samples[:(num_samps+1)/2]
282 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
283 self.samples = samples
284 #peak hold calculation
285 if self[PEAK_HOLD_KEY]:
286 if len(self.peak_vals) != len(samples): self.peak_vals = samples
287 self.peak_vals = numpy.maximum(samples, self.peak_vals)
289 self.plotter.set_waveform(
291 samples=self.peak_vals,
292 color_spec=PEAK_VALS_COLOR_SPEC,
295 self._reset_peak_vals()
296 self.plotter.clear_waveform(channel='Peak')
298 self.plotter.set_waveform(
301 color_spec=FFT_PLOT_COLOR_SPEC,
304 self.plotter.update()
306 def update_grid(self, *args):
308 Update the plotter grid.
309 This update method is dependent on the variables below.
310 Determine the x and y axis grid parameters.
311 The x axis depends on sample rate, baseband freq, and x divs.
312 The y axis depends on y per div, y divs, and ref level.
315 channel = '%s'%trace.upper()
316 if self[TRACE_SHOW_KEY+trace]:
317 self.plotter.set_waveform(
319 samples=self._traces[trace],
320 color_spec=TRACES_COLOR_SPEC[trace],
322 else: self.plotter.clear_waveform(channel=channel)
324 sample_rate = self[SAMPLE_RATE_KEY]
325 baseband_freq = self[BASEBAND_FREQ_KEY]
326 y_per_div = self[Y_PER_DIV_KEY]
327 y_divs = self[Y_DIVS_KEY]
328 x_divs = self[X_DIVS_KEY]
329 ref_level = self[REF_LEVEL_KEY]
330 #determine best fitting x_per_div
331 if self.real: x_width = sample_rate/2.0
332 else: x_width = sample_rate/1.0
333 x_per_div = common.get_clean_num(x_width/x_divs)
336 self.plotter.set_x_grid(
338 baseband_freq + sample_rate/2.0,
342 self.plotter.set_x_grid(
343 baseband_freq - sample_rate/2.0,
344 baseband_freq + sample_rate/2.0,
348 self.plotter.set_x_label('Frequency', 'Hz')
350 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
352 self.plotter.set_y_label('Amplitude', 'dB')
354 self.plotter.update()