use clean numbers for waterfall ref and range
[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 DYNAMIC_RANGE_STEP = 10.
45 COLOR_MODES = (
46         ('RGB1', 'rgb1'),
47         ('RGB2', 'rgb2'),
48         ('RGB3', 'rgb3'),
49         ('Gray', 'gray'),
50 )
51
52 ##################################################
53 # Waterfall window control panel
54 ##################################################
55 class control_panel(wx.Panel):
56         """
57         A control panel with wx widgits to control the plotter and fft block chain.
58         """
59
60         def __init__(self, parent):
61                 """
62                 Create a new control panel.
63                 @param parent the wx parent window
64                 """
65                 self.parent = parent
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,
74                 )
75                 #average
76                 forms.check_box(
77                         sizer=options_box, parent=self, label='Average',
78                         ps=parent, key=AVERAGE_KEY,
79                 )
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,
84                 )
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,
91                 )
92                 for widget in (avg_alpha_text, avg_alpha_slider):
93                         parent.subscribe(AVERAGE_KEY, widget.Enable)
94                         widget.Enable(parent[AVERAGE_KEY])
95                 #begin axes box
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,
100                 )
101                 #num lines buttons
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,
105                 )
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,
110                 )
111                 #ref lvl buttons
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,
115                 )
116                 #color mode
117                 forms.drop_down(
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),
122                 )
123                 #autoscale
124                 forms.single_button(
125                         parent=self, sizer=axes_box, label='Autoscale',
126                         callback=self.parent.autoscale,
127                 )
128                 #clear
129                 control_box.AddStretchSpacer()
130                 forms.single_button(
131                         parent=self, sizer=control_box, label='Clear',
132                         callback=self._on_clear_button,
133                 )
134                 #run/stop
135                 forms.toggle_button(
136                         sizer=control_box, parent=self,
137                         true_label='Stop', false_label='Run',
138                         ps=parent, key=RUNNING_KEY,
139                 )
140                 #set sizer
141                 self.SetSizerAndFit(control_box)
142
143         ##################################################
144         # Event handlers
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
166
167 ##################################################
168 # Waterfall window with plotter and control panel
169 ##################################################
170 class waterfall_window(wx.Panel, pubsub.pubsub):
171         def __init__(
172                 self,
173                 parent,
174                 controller,
175                 size,
176                 title,
177                 real,
178                 fft_size,
179                 num_lines,
180                 decimation_key,
181                 baseband_freq,
182                 sample_rate_key,
183                 frame_rate_key,
184                 dynamic_range,
185                 ref_level,
186                 average_key,
187                 avg_alpha_key,
188                 msg_key,
189         ):
190                 pubsub.pubsub.__init__(self)
191                 #setup
192                 self.samples = list()
193                 self.real = real
194                 self.fft_size = fft_size
195                 #proxy the keys
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)
202                 #init panel and plot
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)
209                 #plotter listeners
210                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
211                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
212                 #initialize values
213                 self[DYNAMIC_RANGE_KEY] = dynamic_range
214                 self[NUM_LINES_KEY] = num_lines
215                 self[Y_DIVS_KEY] = 8
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)
227                 #register events
228                 self.subscribe(MSG_KEY, self.handle_msg)
229                 for key in (
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)
233                 #initial update
234                 self.update_grid()
235
236         def autoscale(self, *args):
237                 """
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.
241                 """
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)
247
248         def handle_msg(self, msg):
249                 """
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
255                 """
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)
260                 #reorder fft
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]))
263                 #plot the fft
264                 self.plotter.set_samples(
265                         samples=samples,
266                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
267                         maximum=self[REF_LEVEL_KEY],
268                 )
269                 #update the plotter
270                 self.plotter.update()
271
272         def update_grid(self, *args):
273                 """
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.
279                 """
280                 #grid parameters
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)
291                 #update the x grid
292                 if self.real:
293                         self.plotter.set_x_grid(
294                                 baseband_freq,
295                                 baseband_freq + sample_rate/2.0,
296                                 x_per_div, True,
297                         )
298                 else:
299                         self.plotter.set_x_grid(
300                                 baseband_freq - sample_rate/2.0,
301                                 baseband_freq + sample_rate/2.0,
302                                 x_per_div, True,
303                         )
304                 #update x units
305                 self.plotter.set_x_label('Frequency', 'Hz')
306                 #update y grid
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)
310                 #update y units
311                 self.plotter.set_y_label('Time', 's')
312                 #update plotter
313                 self.plotter.update()