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 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, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
160 def _on_decr_db_div(self, event):
161 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
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 min_level, max_level = common.get_min_max_fft(self.samples)
258 #set the range to a clean number of the dynamic range
259 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
260 #set the reference level to a multiple of y per div
261 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
263 def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
265 def handle_msg(self, msg):
267 Handle the message from the fft sink message queue.
268 If complex, reorder the fft samples so the negative bins come first.
269 If real, keep take only the positive bins.
270 Plot the samples onto the grid as channel 1.
271 If peak hold is enabled, plot peak vals as channel 2.
272 @param msg the fft array as a character array
274 if not self[RUNNING_KEY]: return
275 #convert to floating point numbers
276 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
277 num_samps = len(samples)
279 if self.real: samples = samples[:(num_samps+1)/2]
280 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
281 self.samples = samples
282 #peak hold calculation
283 if self[PEAK_HOLD_KEY]:
284 if len(self.peak_vals) != len(samples): self.peak_vals = samples
285 self.peak_vals = numpy.maximum(samples, self.peak_vals)
287 self.plotter.set_waveform(
289 samples=self.peak_vals,
290 color_spec=PEAK_VALS_COLOR_SPEC,
293 self._reset_peak_vals()
294 self.plotter.clear_waveform(channel='Peak')
296 self.plotter.set_waveform(
299 color_spec=FFT_PLOT_COLOR_SPEC,
302 self.plotter.update()
304 def update_grid(self, *args):
306 Update the plotter grid.
307 This update method is dependent on the variables below.
308 Determine the x and y axis grid parameters.
309 The x axis depends on sample rate, baseband freq, and x divs.
310 The y axis depends on y per div, y divs, and ref level.
313 channel = '%s'%trace.upper()
314 if self[TRACE_SHOW_KEY+trace]:
315 self.plotter.set_waveform(
317 samples=self._traces[trace],
318 color_spec=TRACES_COLOR_SPEC[trace],
320 else: self.plotter.clear_waveform(channel=channel)
322 sample_rate = self[SAMPLE_RATE_KEY]
323 baseband_freq = self[BASEBAND_FREQ_KEY]
324 y_per_div = self[Y_PER_DIV_KEY]
325 y_divs = self[Y_DIVS_KEY]
326 x_divs = self[X_DIVS_KEY]
327 ref_level = self[REF_LEVEL_KEY]
328 #determine best fitting x_per_div
329 if self.real: x_width = sample_rate/2.0
330 else: x_width = sample_rate/1.0
331 x_per_div = common.get_clean_num(x_width/x_divs)
334 self.plotter.set_x_grid(
336 baseband_freq + sample_rate/2.0,
340 self.plotter.set_x_grid(
341 baseband_freq - sample_rate/2.0,
342 baseband_freq + sample_rate/2.0,
346 self.plotter.set_x_label('Frequency', 'Hz')
348 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
350 self.plotter.set_y_label('Amplitude', 'dB')
352 self.plotter.update()