Imported Upstream version 3.2.2
[debian/gnuradio] / gr-wxgui / src / python / waterfall_window.py
1 #
2 # Copyright 2008 Free Software Foundation, Inc.
3 #
4 # This file is part of GNU Radio
5 #
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)
9 # any later version.
10 #
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.
15 #
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.
20 #
21
22 ##################################################
23 # Imports
24 ##################################################
25 import plotter
26 import common
27 import wx
28 import numpy
29 import math
30 import pubsub
31 from constants import *
32 from gnuradio import gr #for gr.prefs
33 import forms
34
35 ##################################################
36 # Constants
37 ##################################################
38 SLIDER_STEPS = 100
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 COLOR_MODES = (
45         ('RGB1', 'rgb1'),
46         ('RGB2', 'rgb2'),
47         ('RGB3', 'rgb3'),
48         ('Gray', 'gray'),
49 )
50
51 ##################################################
52 # Waterfall window control panel
53 ##################################################
54 class control_panel(wx.Panel):
55         """
56         A control panel with wx widgits to control the plotter and fft block chain.
57         """
58
59         def __init__(self, parent):
60                 """
61                 Create a new control panel.
62                 @param parent the wx parent window
63                 """
64                 self.parent = parent
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,
71                 )
72                 #average
73                 forms.check_box(
74                         sizer=options_box, parent=self, label='Average',
75                         ps=parent, key=AVERAGE_KEY,
76                 )
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,
81                 )
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,
88                 )
89                 for widget in (avg_alpha_text, avg_alpha_slider):
90                         parent.subscribe(AVERAGE_KEY, widget.Enable)
91                         widget.Enable(parent[AVERAGE_KEY])
92                 #begin axes box
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,
97                 )
98                 #num lines buttons
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,
102                 )
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,
107                 )
108                 #ref lvl buttons
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,
112                 )
113                 #color mode
114                 forms.drop_down(
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),
119                 )
120                 #autoscale
121                 forms.single_button(
122                         parent=self, sizer=axes_box, label='Autoscale',
123                         callback=self.parent.autoscale,
124                 )
125                 #clear
126                 control_box.AddStretchSpacer()
127                 forms.single_button(
128                         parent=self, sizer=control_box, label='Clear',
129                         callback=self._on_clear_button,
130                 )
131                 #run/stop
132                 forms.toggle_button(
133                         sizer=control_box, parent=self,
134                         true_label='Stop', false_label='Run',
135                         ps=parent, key=RUNNING_KEY,
136                 )
137                 #set sizer
138                 self.SetSizerAndFit(control_box)
139
140         ##################################################
141         # Event handlers
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
163
164 ##################################################
165 # Waterfall window with plotter and control panel
166 ##################################################
167 class waterfall_window(wx.Panel, pubsub.pubsub):
168         def __init__(
169                 self,
170                 parent,
171                 controller,
172                 size,
173                 title,
174                 real,
175                 fft_size,
176                 num_lines,
177                 decimation_key,
178                 baseband_freq,
179                 sample_rate_key,
180                 frame_rate_key,
181                 dynamic_range,
182                 ref_level,
183                 average_key,
184                 avg_alpha_key,
185                 msg_key,
186         ):
187                 pubsub.pubsub.__init__(self)
188                 #setup
189                 self.samples = list()
190                 self.real = real
191                 self.fft_size = fft_size
192                 #proxy the keys
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)
199                 #init panel and plot
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)
206                 #plotter listeners
207                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
208                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
209                 #initialize values
210                 self[DYNAMIC_RANGE_KEY] = dynamic_range
211                 self[NUM_LINES_KEY] = num_lines
212                 self[Y_DIVS_KEY] = 8
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)
224                 #register events
225                 self.subscribe(MSG_KEY, self.handle_msg)
226                 for key in (
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)
230                 #initial update
231                 self.update_grid()
232
233         def autoscale(self, *args):
234                 """
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.
238                 """
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])
244                 #padding
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
250
251         def handle_msg(self, msg):
252                 """
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
258                 """
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)
263                 #reorder fft
264                 if self.real: samples = samples[:num_samps/2]
265                 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
266                 #plot the fft
267                 self.plotter.set_samples(
268                         samples=samples,
269                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
270                         maximum=self[REF_LEVEL_KEY],
271                 )
272                 #update the plotter
273                 self.plotter.update()
274
275         def update_grid(self, *args):
276                 """
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.
282                 """
283                 #grid parameters
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)
294                 #update the x grid
295                 if self.real:
296                         self.plotter.set_x_grid(
297                                 baseband_freq,
298                                 baseband_freq + sample_rate/2.0,
299                                 x_per_div, True,
300                         )
301                 else:
302                         self.plotter.set_x_grid(
303                                 baseband_freq - sample_rate/2.0,
304                                 baseband_freq + sample_rate/2.0,
305                                 x_per_div, True,
306                         )
307                 #update x units
308                 self.plotter.set_x_label('Frequency', 'Hz')
309                 #update y grid
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)
313                 #update y units
314                 self.plotter.set_y_label('Time', 's')
315                 #update plotter
316                 self.plotter.update()