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 parent[SHOW_CONTROL_PANEL_KEY] = True
67 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
68 control_box = wx.BoxSizer(wx.VERTICAL)
69 control_box.AddStretchSpacer()
70 options_box = forms.static_box_sizer(
71 parent=self, sizer=control_box, label='Options',
72 bold=True, orient=wx.VERTICAL,
76 sizer=options_box, parent=self, label='Average',
77 ps=parent, key=AVERAGE_KEY,
79 avg_alpha_text = forms.static_text(
80 sizer=options_box, parent=self, label='Avg Alpha',
81 converter=forms.float_converter(lambda x: '%.4f'%x),
82 ps=parent, key=AVG_ALPHA_KEY, width=50,
84 avg_alpha_slider = forms.log_slider(
85 sizer=options_box, parent=self,
86 min_exp=AVG_ALPHA_MIN_EXP,
87 max_exp=AVG_ALPHA_MAX_EXP,
88 num_steps=SLIDER_STEPS,
89 ps=parent, key=AVG_ALPHA_KEY,
91 for widget in (avg_alpha_text, avg_alpha_slider):
92 parent.subscribe(AVERAGE_KEY, widget.Enable)
93 widget.Enable(parent[AVERAGE_KEY])
95 control_box.AddStretchSpacer()
96 axes_box = forms.static_box_sizer(
97 parent=self, sizer=control_box, label='Axes Options',
98 bold=True, orient=wx.VERTICAL,
101 forms.incr_decr_buttons(
102 parent=self, sizer=axes_box, label='Time Scale',
103 on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
105 #dyanmic range buttons
106 forms.incr_decr_buttons(
107 parent=self, sizer=axes_box, label='Dyn Range',
108 on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
111 forms.incr_decr_buttons(
112 parent=self, sizer=axes_box, label='Ref Level',
113 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
117 parent=self, sizer=axes_box, width=100,
118 ps=parent, key=COLOR_MODE_KEY, label='Color',
119 choices=map(lambda x: x[1], COLOR_MODES),
120 labels=map(lambda x: x[0], COLOR_MODES),
124 parent=self, sizer=axes_box, label='Autoscale',
125 callback=self.parent.autoscale,
128 control_box.AddStretchSpacer()
130 parent=self, sizer=control_box, label='Clear',
131 callback=self._on_clear_button,
135 sizer=control_box, parent=self,
136 true_label='Stop', false_label='Run',
137 ps=parent, key=RUNNING_KEY,
140 self.SetSizerAndFit(control_box)
142 ##################################################
144 ##################################################
145 def _on_clear_button(self, event):
146 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
147 def _on_incr_dynamic_range(self, event):
148 self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)
149 def _on_decr_dynamic_range(self, event):
150 self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)
151 def _on_incr_ref_level(self, event):
152 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1
153 def _on_decr_ref_level(self, event):
154 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1
155 def _on_incr_time_scale(self, event):
156 old_rate = self.parent[FRAME_RATE_KEY]
157 self.parent[FRAME_RATE_KEY] *= 0.75
158 if self.parent[FRAME_RATE_KEY] == old_rate:
159 self.parent[DECIMATION_KEY] += 1
160 def _on_decr_time_scale(self, event):
161 old_rate = self.parent[FRAME_RATE_KEY]
162 self.parent[FRAME_RATE_KEY] *= 1.25
163 if self.parent[FRAME_RATE_KEY] == old_rate:
164 self.parent[DECIMATION_KEY] -= 1
166 ##################################################
167 # Waterfall window with plotter and control panel
168 ##################################################
169 class waterfall_window(wx.Panel, pubsub.pubsub):
189 pubsub.pubsub.__init__(self)
191 self.samples = list()
193 self.fft_size = fft_size
195 self.proxy(MSG_KEY, controller, msg_key)
196 self.proxy(DECIMATION_KEY, controller, decimation_key)
197 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
198 self.proxy(AVERAGE_KEY, controller, average_key)
199 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
200 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
202 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
203 self.plotter = plotter.waterfall_plotter(self)
204 self.plotter.SetSize(wx.Size(*size))
205 self.plotter.set_title(title)
206 self.plotter.enable_point_label(True)
207 self.plotter.enable_grid_lines(False)
209 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
210 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
212 self[DYNAMIC_RANGE_KEY] = dynamic_range
213 self[NUM_LINES_KEY] = num_lines
215 self[X_DIVS_KEY] = 8 #approximate
216 self[REF_LEVEL_KEY] = ref_level
217 self[BASEBAND_FREQ_KEY] = baseband_freq
218 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
219 self[RUNNING_KEY] = True
220 #setup the box with plot and controls
221 self.control_panel = control_panel(self)
222 main_box = wx.BoxSizer(wx.HORIZONTAL)
223 main_box.Add(self.plotter, 1, wx.EXPAND)
224 main_box.Add(self.control_panel, 0, wx.EXPAND)
225 self.SetSizerAndFit(main_box)
227 self.subscribe(MSG_KEY, self.handle_msg)
229 DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
230 BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
231 ): self.subscribe(key, self.update_grid)
235 def autoscale(self, *args):
237 Autoscale the waterfall plot to the last frame.
238 Set the dynamic range and reference level.
239 Does not affect the current data in the waterfall.
241 if not len(self.samples): return
242 min_level, max_level = common.get_min_max_fft(self.samples)
243 #set the range and level
244 self[REF_LEVEL_KEY] = max_level
245 self[DYNAMIC_RANGE_KEY] = max_level - min_level
247 def handle_msg(self, msg):
249 Handle the message from the fft sink message queue.
250 If complex, reorder the fft samples so the negative bins come first.
251 If real, keep take only the positive bins.
252 Send the data to the plotter.
253 @param msg the fft array as a character array
255 if not self[RUNNING_KEY]: return
256 #convert to floating point numbers
257 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
258 num_samps = len(samples)
260 if self.real: samples = samples[:(num_samps+1)/2]
261 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
263 self.plotter.set_samples(
265 minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
266 maximum=self[REF_LEVEL_KEY],
269 self.plotter.update()
271 def update_grid(self, *args):
273 Update the plotter grid.
274 This update method is dependent on the variables below.
275 Determine the x and y axis grid parameters.
276 The x axis depends on sample rate, baseband freq, and x divs.
277 The y axis depends on y per div, y divs, and ref level.
280 sample_rate = self[SAMPLE_RATE_KEY]
281 frame_rate = self[FRAME_RATE_KEY]
282 baseband_freq = self[BASEBAND_FREQ_KEY]
283 num_lines = self[NUM_LINES_KEY]
284 y_divs = self[Y_DIVS_KEY]
285 x_divs = self[X_DIVS_KEY]
286 #determine best fitting x_per_div
287 if self.real: x_width = sample_rate/2.0
288 else: x_width = sample_rate/1.0
289 x_per_div = common.get_clean_num(x_width/x_divs)
292 self.plotter.set_x_grid(
294 baseband_freq + sample_rate/2.0,
298 self.plotter.set_x_grid(
299 baseband_freq - sample_rate/2.0,
300 baseband_freq + sample_rate/2.0,
304 self.plotter.set_x_label('Frequency', 'Hz')
306 duration = float(num_lines)/frame_rate
307 y_per_div = common.get_clean_num(duration/y_divs)
308 self.plotter.set_y_grid(0, duration, y_per_div, True)
310 self.plotter.set_y_label('Time', 's')
312 self.plotter.update()