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
34 ##################################################
36 ##################################################
38 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
39 DEFAULT_WIN_SIZE = (600, 300)
40 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
41 DIV_LEVELS = (1, 2, 5, 10, 20)
42 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
43 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
46 ##################################################
47 # FFT window control panel
48 ##################################################
49 class control_panel(wx.Panel):
51 A control panel with wx widgits to control the plotter and fft block chain.
54 def __init__(self, parent):
56 Create a new control panel.
57 @param parent the wx parent window
60 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
61 control_box = wx.BoxSizer(wx.VERTICAL)
62 #checkboxes for average and peak hold
63 control_box.AddStretchSpacer()
64 control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
65 peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY)
66 control_box.Add(peak_hold_check_box, 0, wx.EXPAND)
67 average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY)
68 control_box.Add(average_check_box, 0, wx.EXPAND)
69 control_box.AddSpacer(2)
70 avg_alpha_slider = common.LogSliderController(
72 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
73 parent, AVG_ALPHA_KEY,
74 formatter=lambda x: ': %.4f'%x,
76 parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable)
77 control_box.Add(avg_alpha_slider, 0, wx.EXPAND)
78 #radio buttons for div size
79 control_box.AddStretchSpacer()
80 control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
81 radio_box = wx.BoxSizer(wx.VERTICAL)
82 self.radio_buttons = list()
83 for y_per_div in DIV_LEVELS:
84 radio_button = wx.RadioButton(self, label="%d dB/div"%y_per_div)
85 radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div)
86 self.radio_buttons.append(radio_button)
87 radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
88 parent.subscribe(Y_PER_DIV_KEY, self._on_set_y_per_div)
89 control_box.Add(radio_box, 0, wx.EXPAND)
91 control_box.AddStretchSpacer()
92 control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
93 control_box.AddSpacer(2)
94 _ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
95 control_box.Add(_ref_lvl_buttons, 0, wx.ALIGN_CENTER)
97 control_box.AddStretchSpacer()
98 autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
99 autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
100 control_box.Add(autoscale_button, 0, wx.EXPAND)
102 run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
103 control_box.Add(run_button, 0, wx.EXPAND)
105 self.SetSizerAndFit(control_box)
107 def on_mouse_wheel(event):
108 if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
109 else: self._on_decr_ref_level(event)
110 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
112 ##################################################
114 ##################################################
115 def _on_set_y_per_div(self, y_per_div):
117 index = list(DIV_LEVELS).index(y_per_div)
118 self.radio_buttons[index].SetValue(True)
120 def _on_y_per_div(self, event):
121 selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
122 index = self.radio_buttons.index(selected_radio_button)
123 self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index]
124 def _on_incr_ref_level(self, event):
125 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
126 def _on_decr_ref_level(self, event):
127 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
129 ##################################################
130 # FFT window with plotter and control panel
131 ##################################################
132 class fft_window(wx.Panel, pubsub.pubsub):
151 pubsub.pubsub.__init__(self)
153 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
155 self.samples = list()
157 self.fft_size = fft_size
158 self._reset_peak_vals()
160 self.proxy(MSG_KEY, controller, msg_key)
161 self.proxy(AVERAGE_KEY, controller, average_key)
162 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
163 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
165 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
166 self.plotter = plotter.channel_plotter(self)
167 self.plotter.SetSize(wx.Size(*size))
168 self.plotter.set_title(title)
169 self.plotter.enable_legend(True)
170 self.plotter.enable_point_label(True)
171 self.plotter.enable_grid_lines(True)
172 #setup the box with plot and controls
173 self.control_panel = control_panel(self)
174 main_box = wx.BoxSizer(wx.HORIZONTAL)
175 main_box.Add(self.plotter, 1, wx.EXPAND)
176 main_box.Add(self.control_panel, 0, wx.EXPAND)
177 self.SetSizerAndFit(main_box)
179 self[AVERAGE_KEY] = self[AVERAGE_KEY]
180 self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY]
181 self[PEAK_HOLD_KEY] = peak_hold
182 self[Y_PER_DIV_KEY] = y_per_div
183 self[Y_DIVS_KEY] = y_divs
184 self[X_DIVS_KEY] = 8 #approximate
185 self[REF_LEVEL_KEY] = ref_level
186 self[BASEBAND_FREQ_KEY] = baseband_freq
187 self[RUNNING_KEY] = True
189 self.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals())
190 self.subscribe(MSG_KEY, self.handle_msg)
191 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
194 Y_PER_DIV_KEY, X_DIVS_KEY,
195 Y_DIVS_KEY, REF_LEVEL_KEY,
196 ): self.subscribe(key, self.update_grid)
200 def autoscale(self, *args):
202 Autoscale the fft plot to the last frame.
203 Set the dynamic range and reference level.
205 if not len(self.samples): return
206 #get the peak level (max of the samples)
207 peak_level = numpy.max(self.samples)
208 #get the noise floor (averge the smallest samples)
209 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
211 noise_floor -= abs(noise_floor)*.5
212 peak_level += abs(peak_level)*.1
213 #set the reference level to a multiple of y divs
214 self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])
215 #set the range to a clean number of the dynamic range
216 self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])
218 def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
220 def handle_msg(self, msg):
222 Handle the message from the fft sink message queue.
223 If complex, reorder the fft samples so the negative bins come first.
224 If real, keep take only the positive bins.
225 Plot the samples onto the grid as channel 1.
226 If peak hold is enabled, plot peak vals as channel 2.
227 @param msg the fft array as a character array
229 if not self[RUNNING_KEY]: return
230 #convert to floating point numbers
231 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
232 num_samps = len(samples)
234 if self.real: samples = samples[:num_samps/2]
235 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
236 self.samples = samples
237 #peak hold calculation
238 if self[PEAK_HOLD_KEY]:
239 if len(self.peak_vals) != len(samples): self.peak_vals = samples
240 self.peak_vals = numpy.maximum(samples, self.peak_vals)
242 self.plotter.set_waveform(
244 samples=self.peak_vals,
245 color_spec=PEAK_VALS_COLOR_SPEC,
248 self._reset_peak_vals()
249 self.plotter.clear_waveform(channel='Peak')
251 self.plotter.set_waveform(
254 color_spec=FFT_PLOT_COLOR_SPEC,
257 self.plotter.update()
259 def update_grid(self, *args):
261 Update the plotter grid.
262 This update method is dependent on the variables below.
263 Determine the x and y axis grid parameters.
264 The x axis depends on sample rate, baseband freq, and x divs.
265 The y axis depends on y per div, y divs, and ref level.
268 sample_rate = self[SAMPLE_RATE_KEY]
269 baseband_freq = self[BASEBAND_FREQ_KEY]
270 y_per_div = self[Y_PER_DIV_KEY]
271 y_divs = self[Y_DIVS_KEY]
272 x_divs = self[X_DIVS_KEY]
273 ref_level = self[REF_LEVEL_KEY]
274 #determine best fitting x_per_div
275 if self.real: x_width = sample_rate/2.0
276 else: x_width = sample_rate/1.0
277 x_per_div = common.get_clean_num(x_width/x_divs)
280 self.plotter.set_x_grid(
282 baseband_freq + sample_rate/2.0,
286 self.plotter.set_x_grid(
287 baseband_freq - sample_rate/2.0,
288 baseband_freq + sample_rate/2.0,
292 self.plotter.set_x_label('Frequency', 'Hz')
294 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
296 self.plotter.set_y_label('Amplitude', 'dB')
298 self.plotter.update()