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_COLOR_MODE = gr.prefs().get_string('wxgui', 'waterfall_color', 'rgb1')
42 DEFAULT_WIN_SIZE = (600, 300)
43 DIV_LEVELS = (1, 2, 5, 10, 20)
44 MIN_DYNAMIC_RANGE, MAX_DYNAMIC_RANGE = 10, 200
45 DYNAMIC_RANGE_STEP = 10.
53 ##################################################
54 # Waterfall window control panel
55 ##################################################
56 class control_panel(wx.Panel):
58 A control panel with wx widgits to control the plotter and fft block chain.
61 def __init__(self, parent):
63 Create a new control panel.
64 @param parent the wx parent window
67 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
68 parent[SHOW_CONTROL_PANEL_KEY] = True
69 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
70 control_box = wx.BoxSizer(wx.VERTICAL)
71 control_box.AddStretchSpacer()
72 options_box = forms.static_box_sizer(
73 parent=self, sizer=control_box, label='Options',
74 bold=True, orient=wx.VERTICAL,
78 sizer=options_box, parent=self, label='Average',
79 ps=parent, key=AVERAGE_KEY,
81 avg_alpha_text = forms.static_text(
82 sizer=options_box, parent=self, label='Avg Alpha',
83 converter=forms.float_converter(lambda x: '%.4f'%x),
84 ps=parent, key=AVG_ALPHA_KEY, width=50,
86 avg_alpha_slider = forms.log_slider(
87 sizer=options_box, parent=self,
88 min_exp=AVG_ALPHA_MIN_EXP,
89 max_exp=AVG_ALPHA_MAX_EXP,
90 num_steps=SLIDER_STEPS,
91 ps=parent, key=AVG_ALPHA_KEY,
93 for widget in (avg_alpha_text, avg_alpha_slider):
94 parent.subscribe(AVERAGE_KEY, widget.Enable)
95 widget.Enable(parent[AVERAGE_KEY])
97 control_box.AddStretchSpacer()
98 axes_box = forms.static_box_sizer(
99 parent=self, sizer=control_box, label='Axes Options',
100 bold=True, orient=wx.VERTICAL,
103 forms.incr_decr_buttons(
104 parent=self, sizer=axes_box, label='Time Scale',
105 on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
107 #dyanmic range buttons
108 forms.incr_decr_buttons(
109 parent=self, sizer=axes_box, label='Dyn Range',
110 on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
113 forms.incr_decr_buttons(
114 parent=self, sizer=axes_box, label='Ref Level',
115 on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
119 parent=self, sizer=axes_box, width=100,
120 ps=parent, key=COLOR_MODE_KEY, label='Color',
121 choices=map(lambda x: x[1], COLOR_MODES),
122 labels=map(lambda x: x[0], COLOR_MODES),
126 parent=self, sizer=axes_box, label='Autoscale',
127 callback=self.parent.autoscale,
130 control_box.AddStretchSpacer()
132 parent=self, sizer=control_box, label='Clear',
133 callback=self._on_clear_button,
137 sizer=control_box, parent=self,
138 true_label='Stop', false_label='Run',
139 ps=parent, key=RUNNING_KEY,
142 self.SetSizerAndFit(control_box)
144 ##################################################
146 ##################################################
147 def _on_clear_button(self, event):
148 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
149 def _on_incr_dynamic_range(self, event):
150 self.parent[DYNAMIC_RANGE_KEY] = min(MAX_DYNAMIC_RANGE, common.get_clean_incr(self.parent[DYNAMIC_RANGE_KEY]))
151 def _on_decr_dynamic_range(self, event):
152 self.parent[DYNAMIC_RANGE_KEY] = max(MIN_DYNAMIC_RANGE, common.get_clean_decr(self.parent[DYNAMIC_RANGE_KEY]))
153 def _on_incr_ref_level(self, event):
154 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
155 def _on_decr_ref_level(self, event):
156 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]/DYNAMIC_RANGE_STEP
157 def _on_incr_time_scale(self, event):
158 old_rate = self.parent[FRAME_RATE_KEY]
159 self.parent[FRAME_RATE_KEY] *= 0.75
160 if self.parent[FRAME_RATE_KEY] < 1.0:
161 self.parent[FRAME_RATE_KEY] = 1.0
163 if self.parent[FRAME_RATE_KEY] == old_rate:
164 self.parent[DECIMATION_KEY] += 1
165 def _on_decr_time_scale(self, event):
166 old_rate = self.parent[FRAME_RATE_KEY]
167 self.parent[FRAME_RATE_KEY] *= 1.25
168 if self.parent[FRAME_RATE_KEY] == old_rate:
169 self.parent[DECIMATION_KEY] -= 1
171 ##################################################
172 # Waterfall window with plotter and control panel
173 ##################################################
174 class waterfall_window(wx.Panel, pubsub.pubsub):
194 pubsub.pubsub.__init__(self)
196 self.samples = list()
198 self.fft_size = fft_size
200 self.proxy(MSG_KEY, controller, msg_key)
201 self.proxy(DECIMATION_KEY, controller, decimation_key)
202 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
203 self.proxy(AVERAGE_KEY, controller, average_key)
204 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
205 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
207 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
208 self.plotter = plotter.waterfall_plotter(self)
209 self.plotter.SetSize(wx.Size(*size))
210 self.plotter.set_title(title)
211 self.plotter.enable_point_label(True)
212 self.plotter.enable_grid_lines(False)
214 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
215 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
217 self[DYNAMIC_RANGE_KEY] = dynamic_range
218 self[NUM_LINES_KEY] = num_lines
220 self[X_DIVS_KEY] = 8 #approximate
221 self[REF_LEVEL_KEY] = ref_level
222 self[BASEBAND_FREQ_KEY] = baseband_freq
223 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
224 self[COLOR_MODE_KEY] = DEFAULT_COLOR_MODE
225 self[RUNNING_KEY] = True
226 #setup the box with plot and controls
227 self.control_panel = control_panel(self)
228 main_box = wx.BoxSizer(wx.HORIZONTAL)
229 main_box.Add(self.plotter, 1, wx.EXPAND)
230 main_box.Add(self.control_panel, 0, wx.EXPAND)
231 self.SetSizerAndFit(main_box)
233 self.subscribe(MSG_KEY, self.handle_msg)
235 DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
236 BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
237 ): self.subscribe(key, self.update_grid)
241 def autoscale(self, *args):
243 Autoscale the waterfall plot to the last frame.
244 Set the dynamic range and reference level.
245 Does not affect the current data in the waterfall.
247 if not len(self.samples): return
248 min_level, max_level = common.get_min_max_fft(self.samples)
249 #set the range and level
250 self[DYNAMIC_RANGE_KEY] = common.get_clean_num(max_level - min_level)
251 self[REF_LEVEL_KEY] = DYNAMIC_RANGE_STEP*round(.5+max_level/DYNAMIC_RANGE_STEP)
253 def handle_msg(self, msg):
255 Handle the message from the fft sink message queue.
256 If complex, reorder the fft samples so the negative bins come first.
257 If real, keep take only the positive bins.
258 Send the data to the plotter.
259 @param msg the fft array as a character array
261 if not self[RUNNING_KEY]: return
262 #convert to floating point numbers
263 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
264 num_samps = len(samples)
266 if self.real: samples = samples[:(num_samps+1)/2]
267 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
269 self.plotter.set_samples(
271 minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY],
272 maximum=self[REF_LEVEL_KEY],
275 self.plotter.update()
277 def update_grid(self, *args):
279 Update the plotter grid.
280 This update method is dependent on the variables below.
281 Determine the x and y axis grid parameters.
282 The x axis depends on sample rate, baseband freq, and x divs.
283 The y axis depends on y per div, y divs, and ref level.
286 sample_rate = self[SAMPLE_RATE_KEY]
287 frame_rate = self[FRAME_RATE_KEY]
288 if frame_rate < 1.0 :
290 baseband_freq = self[BASEBAND_FREQ_KEY]
291 num_lines = self[NUM_LINES_KEY]
292 y_divs = self[Y_DIVS_KEY]
293 x_divs = self[X_DIVS_KEY]
294 #determine best fitting x_per_div
295 if self.real: x_width = sample_rate/2.0
296 else: x_width = sample_rate/1.0
297 x_per_div = common.get_clean_num(x_width/x_divs)
300 self.plotter.set_x_grid(
302 baseband_freq + sample_rate/2.0,
306 self.plotter.set_x_grid(
307 baseband_freq - sample_rate/2.0,
308 baseband_freq + sample_rate/2.0,
312 self.plotter.set_x_label('Frequency', 'Hz')
314 duration = float(num_lines)/frame_rate
315 y_per_div = common.get_clean_num(duration/y_divs)
316 self.plotter.set_y_grid(0, duration, y_per_div, True)
318 self.plotter.set_y_label('Time', 's')
320 self.plotter.update()