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-1`301, 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_FRAME_RATE = gr.prefs().get_long('wxgui', 'waterfall_rate', 30)
41 DEFAULT_WIN_SIZE = (600, 300)
42 DIV_LEVELS = (1, 2, 5, 10, 20)
43 MIN_DYNAMIC_RANGE, MAX_DYNAMIC_RANGE = 10, 200
51 ##################################################
52 # Waterfall window control panel
53 ##################################################
54 class control_panel(wx.Panel):
56 A control panel with wx widgits to control the plotter and fft block chain.
59 def __init__(self, parent):
61 Create a new control panel.
62 @param parent the wx parent window
65 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
66 control_box = wx.BoxSizer(wx.VERTICAL)
67 control_box.AddStretchSpacer()
68 options_box = forms.static_box_sizer(
69 parent=self, sizer=control_box, label='Options',
70 bold=True, orient=wx.VERTICAL,
74 sizer=options_box, parent=self, label='Average',
75 ps=parent, key=AVERAGE_KEY,
77 avg_alpha_text = forms.static_text(
78 sizer=options_box, parent=self, label='Avg Alpha',
79 converter=forms.float_converter(lambda x: '%.4f'%x),
80 ps=parent, key=AVG_ALPHA_KEY, width=50,
82 avg_alpha_slider = forms.log_slider(
83 sizer=options_box, parent=self,
84 min_exp=AVG_ALPHA_MIN_EXP,
85 max_exp=AVG_ALPHA_MAX_EXP,
86 num_steps=SLIDER_STEPS,
87 ps=parent, key=AVG_ALPHA_KEY,
89 for widget in (avg_alpha_text, avg_alpha_slider):
90 parent.subscribe(AVERAGE_KEY, widget.Enable)
91 widget.Enable(parent[AVERAGE_KEY])
93 control_box.AddStretchSpacer()
94 axes_box = forms.static_box_sizer(
95 parent=self, sizer=control_box, label='Axes Options',
96 bold=True, orient=wx.VERTICAL,
99 forms.incr_decr_buttons(
100 parent=self, sizer=axes_box, label='Time Scale',
101 on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
103 #dyanmic range buttons
104 forms.incr_decr_buttons(
105 parent=self, sizer=axes_box, label='Dyn Range',
106 on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
109 forms.incr_decr_buttons(
110 parent=self, sizer=axes_box, label='Ref Level',
111 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
115 parent=self, sizer=axes_box, width=100,
116 ps=parent, key=COLOR_MODE_KEY, label='Color',
117 choices=map(lambda x: x[1], COLOR_MODES),
118 labels=map(lambda x: x[0], COLOR_MODES),
122 parent=self, sizer=axes_box, label='Autoscale',
123 callback=self.parent.autoscale,
126 control_box.AddStretchSpacer()
128 parent=self, sizer=control_box, label='Clear',
129 callback=self._on_clear_button,
133 sizer=control_box, parent=self,
134 true_label='Stop', false_label='Run',
135 ps=parent, key=RUNNING_KEY,
138 self.SetSizerAndFit(control_box)
140 ##################################################
142 ##################################################
143 def _on_clear_button(self, event):
144 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
145 def _on_incr_dynamic_range(self, event):
146 self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)
147 def _on_decr_dynamic_range(self, event):
148 self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)
149 def _on_incr_ref_level(self, event):
150 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1
151 def _on_decr_ref_level(self, event):
152 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1
153 def _on_incr_time_scale(self, event):
154 old_rate = self.parent[FRAME_RATE_KEY]
155 self.parent[FRAME_RATE_KEY] *= 0.75
156 if self.parent[FRAME_RATE_KEY] == old_rate:
157 self.parent[DECIMATION_KEY] += 1
158 def _on_decr_time_scale(self, event):
159 old_rate = self.parent[FRAME_RATE_KEY]
160 self.parent[FRAME_RATE_KEY] *= 1.25
161 if self.parent[FRAME_RATE_KEY] == old_rate:
162 self.parent[DECIMATION_KEY] -= 1
164 ##################################################
165 # Waterfall window with plotter and control panel
166 ##################################################
167 class waterfall_window(wx.Panel, pubsub.pubsub):
187 pubsub.pubsub.__init__(self)
189 self.samples = list()
191 self.fft_size = fft_size
193 self.proxy(MSG_KEY, controller, msg_key)
194 self.proxy(DECIMATION_KEY, controller, decimation_key)
195 self.proxy(FRAME_RATE_KEY, controller, frame_rate_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 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
201 self.plotter = plotter.waterfall_plotter(self)
202 self.plotter.SetSize(wx.Size(*size))
203 self.plotter.set_title(title)
204 self.plotter.enable_point_label(True)
205 self.plotter.enable_grid_lines(False)
207 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
208 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
210 self[DYNAMIC_RANGE_KEY] = dynamic_range
211 self[NUM_LINES_KEY] = num_lines
213 self[X_DIVS_KEY] = 8 #approximate
214 self[REF_LEVEL_KEY] = ref_level
215 self[BASEBAND_FREQ_KEY] = baseband_freq
216 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
217 self[RUNNING_KEY] = True
218 #setup the box with plot and controls
219 self.control_panel = control_panel(self)
220 main_box = wx.BoxSizer(wx.HORIZONTAL)
221 main_box.Add(self.plotter, 1, wx.EXPAND)
222 main_box.Add(self.control_panel, 0, wx.EXPAND)
223 self.SetSizerAndFit(main_box)
225 self.subscribe(MSG_KEY, self.handle_msg)
227 DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
228 BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
229 ): self.subscribe(key, self.update_grid)
233 def autoscale(self, *args):
235 Autoscale the waterfall plot to the last frame.
236 Set the dynamic range and reference level.
237 Does not affect the current data in the waterfall.
239 if not len(self.samples): return
240 #get the peak level (max of the samples)
241 peak_level = numpy.max(self.samples)
242 #get the noise floor (averge the smallest samples)
243 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
245 noise_floor -= abs(noise_floor)*.5
246 peak_level += abs(peak_level)*.1
247 #set the range and level
248 self[REF_LEVEL_KEY] = peak_level
249 self[DYNAMIC_RANGE_KEY] = peak_level - noise_floor
251 def handle_msg(self, msg):
253 Handle the message from the fft sink message queue.
254 If complex, reorder the fft samples so the negative bins come first.
255 If real, keep take only the positive bins.
256 Send the data to the plotter.
257 @param msg the fft array as a character array
259 if not self[RUNNING_KEY]: return
260 #convert to floating point numbers
261 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
262 num_samps = len(samples)
264 if self.real: samples = samples[:num_samps/2]
265 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
267 self.plotter.set_samples(
269 minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
270 maximum=self[REF_LEVEL_KEY],
273 self.plotter.update()
275 def update_grid(self, *args):
277 Update the plotter grid.
278 This update method is dependent on the variables below.
279 Determine the x and y axis grid parameters.
280 The x axis depends on sample rate, baseband freq, and x divs.
281 The y axis depends on y per div, y divs, and ref level.
284 sample_rate = self[SAMPLE_RATE_KEY]
285 frame_rate = self[FRAME_RATE_KEY]
286 baseband_freq = self[BASEBAND_FREQ_KEY]
287 num_lines = self[NUM_LINES_KEY]
288 y_divs = self[Y_DIVS_KEY]
289 x_divs = self[X_DIVS_KEY]
290 #determine best fitting x_per_div
291 if self.real: x_width = sample_rate/2.0
292 else: x_width = sample_rate/1.0
293 x_per_div = common.get_clean_num(x_width/x_divs)
296 self.plotter.set_x_grid(
298 baseband_freq + sample_rate/2.0,
302 self.plotter.set_x_grid(
303 baseband_freq - sample_rate/2.0,
304 baseband_freq + sample_rate/2.0,
308 self.plotter.set_x_label('Frequency', 'Hz')
310 duration = float(num_lines)/frame_rate
311 y_per_div = common.get_clean_num(duration/y_divs)
312 self.plotter.set_y_grid(0, duration, y_per_div, True)
314 self.plotter.set_y_label('Time', 's')
316 self.plotter.update()