check for samples before autoscale, avoids potential error condition
[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-1301, 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
34 ##################################################
35 # Constants
36 ##################################################
37 SLIDER_STEPS = 100
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
43 COLOR_MODES = (
44         ('RGB1', 'rgb1'),
45         ('RGB2', 'rgb2'),
46         ('RGB3', 'rgb3'),
47         ('Gray', 'gray'),
48 )
49
50 ##################################################
51 # Waterfall window control panel
52 ##################################################
53 class control_panel(wx.Panel):
54         """
55         A control panel with wx widgits to control the plotter and fft block chain.
56         """
57
58         def __init__(self, parent):
59                 """
60                 Create a new control panel.
61                 @param parent the wx parent window
62                 """
63                 self.parent = parent
64                 wx.Panel.__init__(self, parent, -1, 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)
68                 #color mode
69                 control_box.AddStretchSpacer()
70                 self.color_mode_chooser = common.DropDownController(self, 'Color', COLOR_MODES, parent, COLOR_MODE_KEY)
71                 control_box.Add(self.color_mode_chooser, 0, wx.EXPAND)
72                 #average
73                 control_box.AddStretchSpacer()
74                 self.average_check_box = common.CheckBoxController(self, 'Average', parent.ext_controller, parent.average_key)
75                 control_box.Add(self.average_check_box, 0, wx.EXPAND)
76                 control_box.AddSpacer(2)
77                 self.avg_alpha_slider = common.LogSliderController(
78                         self, 'Avg Alpha',
79                         AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
80                         parent.ext_controller, parent.avg_alpha_key,
81                         formatter=lambda x: ': %.4f'%x,
82                 )
83                 parent.ext_controller.subscribe(parent.average_key, self.avg_alpha_slider.Enable)
84                 control_box.Add(self.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                 self._dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range)
90                 control_box.Add(self._dynamic_range_buttons, 0, wx.ALIGN_CENTER)
91                 #ref lvl buttons
92                 control_box.AddStretchSpacer()
93                 control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
94                 control_box.AddSpacer(2)
95                 self._ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
96                 control_box.Add(self._ref_lvl_buttons, 0, wx.ALIGN_CENTER)
97                 #num lines buttons
98                 control_box.AddStretchSpacer()
99                 control_box.Add(common.LabelText(self, 'Set Time Scale'), 0, wx.ALIGN_CENTER)
100                 control_box.AddSpacer(2)
101                 self._time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale)
102                 control_box.Add(self._time_scale_buttons, 0, wx.ALIGN_CENTER)
103                 #autoscale
104                 control_box.AddStretchSpacer()
105                 self.autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
106                 self.autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
107                 control_box.Add(self.autoscale_button, 0, wx.EXPAND)
108                 #clear
109                 self.clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT)
110                 self.clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button)
111                 control_box.Add(self.clear_button, 0, wx.EXPAND)
112                 #run/stop
113                 self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
114                 control_box.Add(self.run_button, 0, wx.EXPAND)
115                 #set sizer
116                 self.SetSizerAndFit(control_box)
117
118         ##################################################
119         # Event handlers
120         ##################################################
121         def _on_clear_button(self, event):
122                 self.parent.set_num_lines(self.parent[NUM_LINES_KEY])
123         def _on_incr_dynamic_range(self, event):
124                 self.parent.set_dynamic_range(
125                         min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE))
126         def _on_decr_dynamic_range(self, event):
127                 self.parent.set_dynamic_range(
128                         max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE))
129         def _on_incr_ref_level(self, event):
130                 self.parent.set_ref_level(
131                         self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1)
132         def _on_decr_ref_level(self, event):
133                 self.parent.set_ref_level(
134                         self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1)
135         def _on_incr_time_scale(self, event):
136                 old_rate = self.parent.ext_controller[self.parent.frame_rate_key]
137                 self.parent.ext_controller[self.parent.frame_rate_key] *= 0.75
138                 if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate:
139                         self.parent.ext_controller[self.parent.decimation_key] += 1
140         def _on_decr_time_scale(self, event):
141                 old_rate = self.parent.ext_controller[self.parent.frame_rate_key]
142                 self.parent.ext_controller[self.parent.frame_rate_key] *= 1.25
143                 if self.parent.ext_controller[self.parent.frame_rate_key] == old_rate:
144                         self.parent.ext_controller[self.parent.decimation_key] -= 1
145
146 ##################################################
147 # Waterfall window with plotter and control panel
148 ##################################################
149 class waterfall_window(wx.Panel, pubsub.pubsub, common.prop_setter):
150         def __init__(
151                 self,
152                 parent,
153                 controller,
154                 size,
155                 title,
156                 real,
157                 fft_size,
158                 num_lines,
159                 decimation_key,
160                 baseband_freq,
161                 sample_rate_key,
162                 frame_rate_key,
163                 dynamic_range,
164                 ref_level,
165                 average_key,
166                 avg_alpha_key,
167                 msg_key,
168         ):
169                 pubsub.pubsub.__init__(self)
170                 #setup
171                 self.samples = list()
172                 self.ext_controller = controller
173                 self.real = real
174                 self.fft_size = fft_size
175                 self.decimation_key = decimation_key
176                 self.sample_rate_key = sample_rate_key
177                 self.frame_rate_key = frame_rate_key
178                 self.average_key = average_key
179                 self.avg_alpha_key = avg_alpha_key
180                 #init panel and plot
181                 wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER)
182                 self.plotter = plotter.waterfall_plotter(self)
183                 self.plotter.SetSize(wx.Size(*size))
184                 self.plotter.set_title(title)
185                 self.plotter.enable_point_label(True)
186                 #setup the box with plot and controls
187                 self.control_panel = control_panel(self)
188                 main_box = wx.BoxSizer(wx.HORIZONTAL)
189                 main_box.Add(self.plotter, 1, wx.EXPAND)
190                 main_box.Add(self.control_panel, 0, wx.EXPAND)
191                 self.SetSizerAndFit(main_box)
192                 #plotter listeners
193                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
194                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
195                 #initial setup
196                 self.ext_controller[self.average_key] = self.ext_controller[self.average_key]
197                 self.ext_controller[self.avg_alpha_key] = self.ext_controller[self.avg_alpha_key]
198                 self._register_set_prop(self, DYNAMIC_RANGE_KEY, dynamic_range)
199                 self._register_set_prop(self, NUM_LINES_KEY, num_lines)
200                 self._register_set_prop(self, Y_DIVS_KEY, 8)
201                 self._register_set_prop(self, X_DIVS_KEY, 8) #approximate
202                 self._register_set_prop(self, REF_LEVEL_KEY, ref_level)
203                 self._register_set_prop(self, BASEBAND_FREQ_KEY, baseband_freq)
204                 self._register_set_prop(self, COLOR_MODE_KEY, COLOR_MODES[0][1])
205                 self._register_set_prop(self, RUNNING_KEY, True)
206                 #register events
207                 self.ext_controller.subscribe(msg_key, self.handle_msg)
208                 self.ext_controller.subscribe(self.decimation_key, self.update_grid)
209                 self.ext_controller.subscribe(self.sample_rate_key, self.update_grid)
210                 self.ext_controller.subscribe(self.frame_rate_key, self.update_grid)
211                 self.subscribe(BASEBAND_FREQ_KEY, self.update_grid)
212                 self.subscribe(NUM_LINES_KEY, self.update_grid)
213                 self.subscribe(Y_DIVS_KEY, self.update_grid)
214                 self.subscribe(X_DIVS_KEY, self.update_grid)
215                 #initial update
216                 self.update_grid()
217
218         def autoscale(self, *args):
219                 """
220                 Autoscale the waterfall plot to the last frame.
221                 Set the dynamic range and reference level.
222                 Does not affect the current data in the waterfall.
223                 """
224                 if not len(self.samples): return
225                 #get the peak level (max of the samples)
226                 peak_level = numpy.max(self.samples)
227                 #get the noise floor (averge the smallest samples)
228                 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
229                 #padding
230                 noise_floor -= abs(noise_floor)*.5
231                 peak_level += abs(peak_level)*.1
232                 #set the range and level
233                 self.set_ref_level(peak_level)
234                 self.set_dynamic_range(peak_level - noise_floor)
235
236         def handle_msg(self, msg):
237                 """
238                 Handle the message from the fft sink message queue.
239                 If complex, reorder the fft samples so the negative bins come first.
240                 If real, keep take only the positive bins.
241                 Send the data to the plotter.
242                 @param msg the fft array as a character array
243                 """
244                 if not self[RUNNING_KEY]: return
245                 #convert to floating point numbers
246                 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
247                 num_samps = len(samples)
248                 #reorder fft
249                 if self.real: samples = samples[:num_samps/2]
250                 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
251                 #plot the fft
252                 self.plotter.set_samples(
253                         samples=samples,
254                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
255                         maximum=self[REF_LEVEL_KEY],
256                 )
257                 #update the plotter
258                 self.plotter.update()
259
260         def update_grid(self, *args):
261                 """
262                 Update the plotter grid.
263                 This update method is dependent on the variables below.
264                 Determine the x and y axis grid parameters.
265                 The x axis depends on sample rate, baseband freq, and x divs.
266                 The y axis depends on y per div, y divs, and ref level.
267                 """
268                 #grid parameters
269                 sample_rate = self.ext_controller[self.sample_rate_key]
270                 frame_rate = self.ext_controller[self.frame_rate_key]
271                 baseband_freq = self[BASEBAND_FREQ_KEY]
272                 num_lines = self[NUM_LINES_KEY]
273                 y_divs = self[Y_DIVS_KEY]
274                 x_divs = self[X_DIVS_KEY]
275                 #determine best fitting x_per_div
276                 if self.real: x_width = sample_rate/2.0
277                 else: x_width = sample_rate/1.0
278                 x_per_div = common.get_clean_num(x_width/x_divs)
279                 coeff, exp, prefix = common.get_si_components(abs(baseband_freq) + abs(sample_rate/2.0))
280                 #update the x grid
281                 if self.real:
282                         self.plotter.set_x_grid(
283                                 baseband_freq,
284                                 baseband_freq + sample_rate/2.0,
285                                 x_per_div,
286                                 10**(-exp),
287                         )
288                 else:
289                         self.plotter.set_x_grid(
290                                 baseband_freq - sample_rate/2.0,
291                                 baseband_freq + sample_rate/2.0,
292                                 x_per_div,
293                                 10**(-exp),
294                         )
295                 #update x units
296                 self.plotter.set_x_label('Frequency', prefix+'Hz')
297                 #update y grid
298                 duration = float(num_lines)/frame_rate
299                 y_per_div = common.get_clean_num(duration/y_divs)
300                 self.plotter.set_y_grid(0, duration, y_per_div)
301                 #update y units
302                 self.plotter.set_y_label('Time', 's')
303                 #update plotter
304                 self.plotter.update()