Merge branch 'maint'
[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_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.
46 COLOR_MODES = (
47         ('RGB1', 'rgb1'),
48         ('RGB2', 'rgb2'),
49         ('RGB3', 'rgb3'),
50         ('Gray', 'gray'),
51 )
52
53 ##################################################
54 # Waterfall window control panel
55 ##################################################
56 class control_panel(wx.Panel):
57         """
58         A control panel with wx widgits to control the plotter and fft block chain.
59         """
60
61         def __init__(self, parent):
62                 """
63                 Create a new control panel.
64                 @param parent the wx parent window
65                 """
66                 self.parent = parent
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,
75                 )
76                 #average
77                 forms.check_box(
78                         sizer=options_box, parent=self, label='Average',
79                         ps=parent, key=AVERAGE_KEY,
80                 )
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,
85                 )
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,
92                 )
93                 for widget in (avg_alpha_text, avg_alpha_slider):
94                         parent.subscribe(AVERAGE_KEY, widget.Enable)
95                         widget.Enable(parent[AVERAGE_KEY])
96                 #begin axes box
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,
101                 )
102                 #num lines buttons
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,
106                 )
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,
111                 )
112                 #ref lvl buttons
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,
116                 )
117                 #color mode
118                 forms.drop_down(
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),
123                 )
124                 #autoscale
125                 forms.single_button(
126                         parent=self, sizer=axes_box, label='Autoscale',
127                         callback=self.parent.autoscale,
128                 )
129                 #clear
130                 control_box.AddStretchSpacer()
131                 forms.single_button(
132                         parent=self, sizer=control_box, label='Clear',
133                         callback=self._on_clear_button,
134                 )
135                 #run/stop
136                 forms.toggle_button(
137                         sizer=control_box, parent=self,
138                         true_label='Stop', false_label='Run',
139                         ps=parent, key=RUNNING_KEY,
140                 )
141                 #set sizer
142                 self.SetSizerAndFit(control_box)
143
144         ##################################################
145         # Event handlers
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
162                 
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
170
171 ##################################################
172 # Waterfall window with plotter and control panel
173 ##################################################
174 class waterfall_window(wx.Panel, pubsub.pubsub):
175         def __init__(
176                 self,
177                 parent,
178                 controller,
179                 size,
180                 title,
181                 real,
182                 fft_size,
183                 num_lines,
184                 decimation_key,
185                 baseband_freq,
186                 sample_rate_key,
187                 frame_rate_key,
188                 dynamic_range,
189                 ref_level,
190                 average_key,
191                 avg_alpha_key,
192                 msg_key,
193         ):
194                 pubsub.pubsub.__init__(self)
195                 #setup
196                 self.samples = list()
197                 self.real = real
198                 self.fft_size = fft_size
199                 #proxy the keys
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)
206                 #init panel and plot
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)
213                 #plotter listeners
214                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
215                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
216                 #initialize values
217                 self[DYNAMIC_RANGE_KEY] = dynamic_range
218                 self[NUM_LINES_KEY] = num_lines
219                 self[Y_DIVS_KEY] = 8
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)
232                 #register events
233                 self.subscribe(MSG_KEY, self.handle_msg)
234                 for key in (
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)
238                 #initial update
239                 self.update_grid()
240
241         def autoscale(self, *args):
242                 """
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.
246                 """
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)
252
253         def handle_msg(self, msg):
254                 """
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
260                 """
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)
265                 #reorder fft
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]))
268                 #plot the fft
269                 self.plotter.set_samples(
270                         samples=samples,
271                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
272                         maximum=self[REF_LEVEL_KEY],
273                 )
274                 #update the plotter
275                 self.plotter.update()
276
277         def update_grid(self, *args):
278                 """
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.
284                 """
285                 #grid parameters
286                 sample_rate = self[SAMPLE_RATE_KEY]
287                 frame_rate = self[FRAME_RATE_KEY]
288                 if frame_rate < 1.0 :
289                         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)
298                 #update the x grid
299                 if self.real:
300                         self.plotter.set_x_grid(
301                                 baseband_freq,
302                                 baseband_freq + sample_rate/2.0,
303                                 x_per_div, True,
304                         )
305                 else:
306                         self.plotter.set_x_grid(
307                                 baseband_freq - sample_rate/2.0,
308                                 baseband_freq + sample_rate/2.0,
309                                 x_per_div, True,
310                         )
311                 #update x units
312                 self.plotter.set_x_label('Frequency', 'Hz')
313                 #update y grid
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)
317                 #update y units
318                 self.plotter.set_y_label('Time', 's')
319                 #update plotter
320                 self.plotter.update()