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
34 ##################################################
36 ##################################################
38 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
39 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'waterfall_rate', 30)
40 DEFAULT_WIN_SIZE = (600, 300)
41 DIV_LEVELS = (1, 2, 5, 10, 20)
42 MIN_DYNAMIC_RANGE, MAX_DYNAMIC_RANGE = 10, 200
50 ##################################################
51 # Waterfall window control panel
52 ##################################################
53 class control_panel(wx.Panel):
55 A control panel with wx widgits to control the plotter and fft block chain.
58 def __init__(self, parent):
60 Create a new control panel.
61 @param parent the wx parent window
64 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
65 control_box = wx.BoxSizer(wx.VERTICAL)
66 control_box.AddStretchSpacer()
67 control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
69 control_box.AddStretchSpacer()
70 color_mode_chooser = common.DropDownController(self, COLOR_MODES, parent, COLOR_MODE_KEY)
71 control_box.Add(common.LabelBox(self, 'Color', color_mode_chooser), 0, wx.EXPAND)
73 control_box.AddStretchSpacer()
74 average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY)
75 control_box.Add(average_check_box, 0, wx.EXPAND)
76 control_box.AddSpacer(2)
77 avg_alpha_slider = common.LogSliderController(
79 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
80 parent, AVG_ALPHA_KEY,
81 formatter=lambda x: ': %.4f'%x,
83 parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable)
84 control_box.Add(avg_alpha_slider, 0, wx.EXPAND)
85 #dyanmic range buttons
86 control_box.AddStretchSpacer()
87 control_box.Add(common.LabelText(self, 'Dynamic Range'), 0, wx.ALIGN_CENTER)
88 control_box.AddSpacer(2)
89 dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range)
90 control_box.Add(dynamic_range_buttons, 0, wx.ALIGN_CENTER)
92 control_box.AddStretchSpacer()
93 control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
94 control_box.AddSpacer(2)
95 ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
96 control_box.Add(ref_lvl_buttons, 0, wx.ALIGN_CENTER)
98 control_box.AddStretchSpacer()
99 control_box.Add(common.LabelText(self, 'Set Time Scale'), 0, wx.ALIGN_CENTER)
100 control_box.AddSpacer(2)
101 time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale)
102 control_box.Add(time_scale_buttons, 0, wx.ALIGN_CENTER)
104 control_box.AddStretchSpacer()
105 autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
106 autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
107 control_box.Add(autoscale_button, 0, wx.EXPAND)
109 clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT)
110 clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button)
111 control_box.Add(clear_button, 0, wx.EXPAND)
113 run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
114 control_box.Add(run_button, 0, wx.EXPAND)
116 self.SetSizerAndFit(control_box)
118 ##################################################
120 ##################################################
121 def _on_clear_button(self, event):
122 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
123 def _on_incr_dynamic_range(self, event):
124 self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)
125 def _on_decr_dynamic_range(self, event):
126 self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)
127 def _on_incr_ref_level(self, event):
128 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1
129 def _on_decr_ref_level(self, event):
130 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1
131 def _on_incr_time_scale(self, event):
132 old_rate = self.parent[FRAME_RATE_KEY]
133 self.parent[FRAME_RATE_KEY] *= 0.75
134 if self.parent[FRAME_RATE_KEY] == old_rate:
135 self.parent[DECIMATION_KEY] += 1
136 def _on_decr_time_scale(self, event):
137 old_rate = self.parent[FRAME_RATE_KEY]
138 self.parent[FRAME_RATE_KEY] *= 1.25
139 if self.parent[FRAME_RATE_KEY] == old_rate:
140 self.parent[DECIMATION_KEY] -= 1
142 ##################################################
143 # Waterfall window with plotter and control panel
144 ##################################################
145 class waterfall_window(wx.Panel, pubsub.pubsub):
165 pubsub.pubsub.__init__(self)
167 self.samples = list()
169 self.fft_size = fft_size
171 self.proxy(MSG_KEY, controller, msg_key)
172 self.proxy(DECIMATION_KEY, controller, decimation_key)
173 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
174 self.proxy(AVERAGE_KEY, controller, average_key)
175 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
176 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
178 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
179 self.plotter = plotter.waterfall_plotter(self)
180 self.plotter.SetSize(wx.Size(*size))
181 self.plotter.set_title(title)
182 self.plotter.enable_point_label(True)
183 self.plotter.enable_grid_lines(False)
184 #setup the box with plot and controls
185 self.control_panel = control_panel(self)
186 main_box = wx.BoxSizer(wx.HORIZONTAL)
187 main_box.Add(self.plotter, 1, wx.EXPAND)
188 main_box.Add(self.control_panel, 0, wx.EXPAND)
189 self.SetSizerAndFit(main_box)
191 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
192 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
194 self[AVERAGE_KEY] = self[AVERAGE_KEY]
195 self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY]
196 self[DYNAMIC_RANGE_KEY] = dynamic_range
197 self[NUM_LINES_KEY] = num_lines
199 self[X_DIVS_KEY] = 8 #approximate
200 self[REF_LEVEL_KEY] = ref_level
201 self[BASEBAND_FREQ_KEY] = baseband_freq
202 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
203 self[RUNNING_KEY] = True
205 self.subscribe(MSG_KEY, self.handle_msg)
207 DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
208 BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
209 ): self.subscribe(key, self.update_grid)
213 def autoscale(self, *args):
215 Autoscale the waterfall plot to the last frame.
216 Set the dynamic range and reference level.
217 Does not affect the current data in the waterfall.
219 if not len(self.samples): return
220 #get the peak level (max of the samples)
221 peak_level = numpy.max(self.samples)
222 #get the noise floor (averge the smallest samples)
223 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
225 noise_floor -= abs(noise_floor)*.5
226 peak_level += abs(peak_level)*.1
227 #set the range and level
228 self[REF_LEVEL_KEY] = peak_level
229 self[DYNAMIC_RANGE_KEY] = peak_level - noise_floor
231 def handle_msg(self, msg):
233 Handle the message from the fft sink message queue.
234 If complex, reorder the fft samples so the negative bins come first.
235 If real, keep take only the positive bins.
236 Send the data to the plotter.
237 @param msg the fft array as a character array
239 if not self[RUNNING_KEY]: return
240 #convert to floating point numbers
241 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
242 num_samps = len(samples)
244 if self.real: samples = samples[:num_samps/2]
245 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
247 self.plotter.set_samples(
249 minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
250 maximum=self[REF_LEVEL_KEY],
253 self.plotter.update()
255 def update_grid(self, *args):
257 Update the plotter grid.
258 This update method is dependent on the variables below.
259 Determine the x and y axis grid parameters.
260 The x axis depends on sample rate, baseband freq, and x divs.
261 The y axis depends on y per div, y divs, and ref level.
264 sample_rate = self[SAMPLE_RATE_KEY]
265 frame_rate = self[FRAME_RATE_KEY]
266 baseband_freq = self[BASEBAND_FREQ_KEY]
267 num_lines = self[NUM_LINES_KEY]
268 y_divs = self[Y_DIVS_KEY]
269 x_divs = self[X_DIVS_KEY]
270 #determine best fitting x_per_div
271 if self.real: x_width = sample_rate/2.0
272 else: x_width = sample_rate/1.0
273 x_per_div = common.get_clean_num(x_width/x_divs)
276 self.plotter.set_x_grid(
278 baseband_freq + sample_rate/2.0,
282 self.plotter.set_x_grid(
283 baseband_freq - sample_rate/2.0,
284 baseband_freq + sample_rate/2.0,
288 self.plotter.set_x_label('Frequency', 'Hz')
290 duration = float(num_lines)/frame_rate
291 y_per_div = common.get_clean_num(duration/y_divs)
292 self.plotter.set_y_grid(0, duration, y_per_div, True)
294 self.plotter.set_y_label('Time', 's')
296 self.plotter.update()