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
44 DYNAMIC_RANGE_STEP = 10.
52 ##################################################
53 # Waterfall 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 options_box = forms.static_box_sizer(
72 parent=self, sizer=control_box, label='Options',
73 bold=True, orient=wx.VERTICAL,
77 sizer=options_box, parent=self, label='Average',
78 ps=parent, key=AVERAGE_KEY,
80 avg_alpha_text = forms.static_text(
81 sizer=options_box, parent=self, label='Avg Alpha',
82 converter=forms.float_converter(lambda x: '%.4f'%x),
83 ps=parent, key=AVG_ALPHA_KEY, width=50,
85 avg_alpha_slider = forms.log_slider(
86 sizer=options_box, parent=self,
87 min_exp=AVG_ALPHA_MIN_EXP,
88 max_exp=AVG_ALPHA_MAX_EXP,
89 num_steps=SLIDER_STEPS,
90 ps=parent, key=AVG_ALPHA_KEY,
92 for widget in (avg_alpha_text, avg_alpha_slider):
93 parent.subscribe(AVERAGE_KEY, widget.Enable)
94 widget.Enable(parent[AVERAGE_KEY])
96 control_box.AddStretchSpacer()
97 axes_box = forms.static_box_sizer(
98 parent=self, sizer=control_box, label='Axes Options',
99 bold=True, orient=wx.VERTICAL,
102 forms.incr_decr_buttons(
103 parent=self, sizer=axes_box, label='Time Scale',
104 on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
106 #dyanmic range buttons
107 forms.incr_decr_buttons(
108 parent=self, sizer=axes_box, label='Dyn Range',
109 on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
112 forms.incr_decr_buttons(
113 parent=self, sizer=axes_box, label='Ref Level',
114 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
118 parent=self, sizer=axes_box, width=100,
119 ps=parent, key=COLOR_MODE_KEY, label='Color',
120 choices=map(lambda x: x[1], COLOR_MODES),
121 labels=map(lambda x: x[0], COLOR_MODES),
125 parent=self, sizer=axes_box, label='Autoscale',
126 callback=self.parent.autoscale,
129 control_box.AddStretchSpacer()
131 parent=self, sizer=control_box, label='Clear',
132 callback=self._on_clear_button,
136 sizer=control_box, parent=self,
137 true_label='Stop', false_label='Run',
138 ps=parent, key=RUNNING_KEY,
141 self.SetSizerAndFit(control_box)
143 ##################################################
145 ##################################################
146 def _on_clear_button(self, event):
147 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
148 def _on_incr_dynamic_range(self, event):
149 self.parent[DYNAMIC_RANGE_KEY] = min(MAX_DYNAMIC_RANGE, common.get_clean_incr(self.parent[DYNAMIC_RANGE_KEY]))
150 def _on_decr_dynamic_range(self, event):
151 self.parent[DYNAMIC_RANGE_KEY] = max(MIN_DYNAMIC_RANGE, common.get_clean_decr(self.parent[DYNAMIC_RANGE_KEY]))
152 def _on_incr_ref_level(self, event):
153 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
154 def _on_decr_ref_level(self, event):
155 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
156 def _on_incr_time_scale(self, event):
157 old_rate = self.parent[FRAME_RATE_KEY]
158 self.parent[FRAME_RATE_KEY] *= 0.75
159 if self.parent[FRAME_RATE_KEY] == old_rate:
160 self.parent[DECIMATION_KEY] += 1
161 def _on_decr_time_scale(self, event):
162 old_rate = self.parent[FRAME_RATE_KEY]
163 self.parent[FRAME_RATE_KEY] *= 1.25
164 if self.parent[FRAME_RATE_KEY] == old_rate:
165 self.parent[DECIMATION_KEY] -= 1
167 ##################################################
168 # Waterfall window with plotter and control panel
169 ##################################################
170 class waterfall_window(wx.Panel, pubsub.pubsub):
190 pubsub.pubsub.__init__(self)
192 self.samples = list()
194 self.fft_size = fft_size
196 self.proxy(MSG_KEY, controller, msg_key)
197 self.proxy(DECIMATION_KEY, controller, decimation_key)
198 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
199 self.proxy(AVERAGE_KEY, controller, average_key)
200 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
201 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
203 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
204 self.plotter = plotter.waterfall_plotter(self)
205 self.plotter.SetSize(wx.Size(*size))
206 self.plotter.set_title(title)
207 self.plotter.enable_point_label(True)
208 self.plotter.enable_grid_lines(False)
210 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
211 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
213 self[DYNAMIC_RANGE_KEY] = dynamic_range
214 self[NUM_LINES_KEY] = num_lines
216 self[X_DIVS_KEY] = 8 #approximate
217 self[REF_LEVEL_KEY] = ref_level
218 self[BASEBAND_FREQ_KEY] = baseband_freq
219 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
220 self[RUNNING_KEY] = True
221 #setup the box with plot and controls
222 self.control_panel = control_panel(self)
223 main_box = wx.BoxSizer(wx.HORIZONTAL)
224 main_box.Add(self.plotter, 1, wx.EXPAND)
225 main_box.Add(self.control_panel, 0, wx.EXPAND)
226 self.SetSizerAndFit(main_box)
228 self.subscribe(MSG_KEY, self.handle_msg)
230 DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
231 BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
232 ): self.subscribe(key, self.update_grid)
236 def autoscale(self, *args):
238 Autoscale the waterfall plot to the last frame.
239 Set the dynamic range and reference level.
240 Does not affect the current data in the waterfall.
242 if not len(self.samples): return
243 min_level, max_level = common.get_min_max_fft(self.samples)
244 #set the range and level
245 self[DYNAMIC_RANGE_KEY] = common.get_clean_num(max_level - min_level)
246 self[REF_LEVEL_KEY] = DYNAMIC_RANGE_STEP*round(.5+max_level/DYNAMIC_RANGE_STEP)
248 def handle_msg(self, msg):
250 Handle the message from the fft sink message queue.
251 If complex, reorder the fft samples so the negative bins come first.
252 If real, keep take only the positive bins.
253 Send the data to the plotter.
254 @param msg the fft array as a character array
256 if not self[RUNNING_KEY]: return
257 #convert to floating point numbers
258 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
259 num_samps = len(samples)
261 if self.real: samples = samples[:(num_samps+1)/2]
262 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
264 self.plotter.set_samples(
266 minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
267 maximum=self[REF_LEVEL_KEY],
270 self.plotter.update()
272 def update_grid(self, *args):
274 Update the plotter grid.
275 This update method is dependent on the variables below.
276 Determine the x and y axis grid parameters.
277 The x axis depends on sample rate, baseband freq, and x divs.
278 The y axis depends on y per div, y divs, and ref level.
281 sample_rate = self[SAMPLE_RATE_KEY]
282 frame_rate = self[FRAME_RATE_KEY]
283 baseband_freq = self[BASEBAND_FREQ_KEY]
284 num_lines = self[NUM_LINES_KEY]
285 y_divs = self[Y_DIVS_KEY]
286 x_divs = self[X_DIVS_KEY]
287 #determine best fitting x_per_div
288 if self.real: x_width = sample_rate/2.0
289 else: x_width = sample_rate/1.0
290 x_per_div = common.get_clean_num(x_width/x_divs)
293 self.plotter.set_x_grid(
295 baseband_freq + sample_rate/2.0,
299 self.plotter.set_x_grid(
300 baseband_freq - sample_rate/2.0,
301 baseband_freq + sample_rate/2.0,
305 self.plotter.set_x_label('Frequency', 'Hz')
307 duration = float(num_lines)/frame_rate
308 y_per_div = common.get_clean_num(duration/y_divs)
309 self.plotter.set_y_grid(0, duration, y_per_div, True)
311 self.plotter.set_y_label('Time', 's')
313 self.plotter.update()