Merged r10463:10658 from jblum/gui_guts into trunk. Trunk passes distcheck.
[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
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, 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                 color_mode_chooser = common.DropDownController(self, COLOR_MODES, parent, COLOR_MODE_KEY)
71                 control_box.Add(common.LabelBox(self, 'Color', color_mode_chooser), 0, wx.EXPAND)
72                 #average
73                 control_box.AddStretchSpacer()
74                 average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY)
75                 control_box.Add(average_check_box, 0, wx.EXPAND)
76                 control_box.AddSpacer(2)
77                 avg_alpha_slider = common.LogSliderController(
78                         self, 'Avg Alpha',
79                         AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
80                         parent, AVG_ALPHA_KEY,
81                         formatter=lambda x: ': %.4f'%x,
82                 )
83                 parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable)
84                 control_box.Add(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                 dynamic_range_buttons = common.IncrDecrButtons(self, self._on_incr_dynamic_range, self._on_decr_dynamic_range)
90                 control_box.Add(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                 ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
96                 control_box.Add(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                 time_scale_buttons = common.IncrDecrButtons(self, self._on_incr_time_scale, self._on_decr_time_scale)
102                 control_box.Add(time_scale_buttons, 0, wx.ALIGN_CENTER)
103                 #autoscale
104                 control_box.AddStretchSpacer()
105                 autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
106                 autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
107                 control_box.Add(autoscale_button, 0, wx.EXPAND)
108                 #clear
109                 clear_button = wx.Button(self, label='Clear', style=wx.BU_EXACTFIT)
110                 clear_button.Bind(wx.EVT_BUTTON, self._on_clear_button)
111                 control_box.Add(clear_button, 0, wx.EXPAND)
112                 #run/stop
113                 run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
114                 control_box.Add(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[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
123         def _on_incr_dynamic_range(self, event):
124                 self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)
125         def _on_decr_dynamic_range(self, event):
126                 self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)
127         def _on_incr_ref_level(self, event):
128                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1
129         def _on_decr_ref_level(self, event):
130                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1
131         def _on_incr_time_scale(self, event):
132                 old_rate = self.parent[FRAME_RATE_KEY]
133                 self.parent[FRAME_RATE_KEY] *= 0.75
134                 if self.parent[FRAME_RATE_KEY] == old_rate:
135                         self.parent[DECIMATION_KEY] += 1
136         def _on_decr_time_scale(self, event):
137                 old_rate = self.parent[FRAME_RATE_KEY]
138                 self.parent[FRAME_RATE_KEY] *= 1.25
139                 if self.parent[FRAME_RATE_KEY] == old_rate:
140                         self.parent[DECIMATION_KEY] -= 1
141
142 ##################################################
143 # Waterfall window with plotter and control panel
144 ##################################################
145 class waterfall_window(wx.Panel, pubsub.pubsub):
146         def __init__(
147                 self,
148                 parent,
149                 controller,
150                 size,
151                 title,
152                 real,
153                 fft_size,
154                 num_lines,
155                 decimation_key,
156                 baseband_freq,
157                 sample_rate_key,
158                 frame_rate_key,
159                 dynamic_range,
160                 ref_level,
161                 average_key,
162                 avg_alpha_key,
163                 msg_key,
164         ):
165                 pubsub.pubsub.__init__(self)
166                 #setup
167                 self.samples = list()
168                 self.real = real
169                 self.fft_size = fft_size
170                 #proxy the keys
171                 self.proxy(MSG_KEY, controller, msg_key)
172                 self.proxy(DECIMATION_KEY, controller, decimation_key)
173                 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
174                 self.proxy(AVERAGE_KEY, controller, average_key)
175                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
176                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
177                 #init panel and plot
178                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
179                 self.plotter = plotter.waterfall_plotter(self)
180                 self.plotter.SetSize(wx.Size(*size))
181                 self.plotter.set_title(title)
182                 self.plotter.enable_point_label(True)
183                 self.plotter.enable_grid_lines(False)
184                 #setup the box with plot and controls
185                 self.control_panel = control_panel(self)
186                 main_box = wx.BoxSizer(wx.HORIZONTAL)
187                 main_box.Add(self.plotter, 1, wx.EXPAND)
188                 main_box.Add(self.control_panel, 0, wx.EXPAND)
189                 self.SetSizerAndFit(main_box)
190                 #plotter listeners
191                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
192                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
193                 #initialize values
194                 self[AVERAGE_KEY] = self[AVERAGE_KEY]
195                 self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY]
196                 self[DYNAMIC_RANGE_KEY] = dynamic_range
197                 self[NUM_LINES_KEY] = num_lines
198                 self[Y_DIVS_KEY] = 8
199                 self[X_DIVS_KEY] = 8 #approximate
200                 self[REF_LEVEL_KEY] = ref_level
201                 self[BASEBAND_FREQ_KEY] = baseband_freq
202                 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
203                 self[RUNNING_KEY] = True
204                 #register events
205                 self.subscribe(MSG_KEY, self.handle_msg)
206                 for key in (
207                         DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
208                         BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
209                 ): self.subscribe(key, self.update_grid)
210                 #initial update
211                 self.update_grid()
212
213         def autoscale(self, *args):
214                 """
215                 Autoscale the waterfall plot to the last frame.
216                 Set the dynamic range and reference level.
217                 Does not affect the current data in the waterfall.
218                 """
219                 if not len(self.samples): return
220                 #get the peak level (max of the samples)
221                 peak_level = numpy.max(self.samples)
222                 #get the noise floor (averge the smallest samples)
223                 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
224                 #padding
225                 noise_floor -= abs(noise_floor)*.5
226                 peak_level += abs(peak_level)*.1
227                 #set the range and level
228                 self[REF_LEVEL_KEY] = peak_level
229                 self[DYNAMIC_RANGE_KEY] = peak_level - noise_floor
230
231         def handle_msg(self, msg):
232                 """
233                 Handle the message from the fft sink message queue.
234                 If complex, reorder the fft samples so the negative bins come first.
235                 If real, keep take only the positive bins.
236                 Send the data to the plotter.
237                 @param msg the fft array as a character array
238                 """
239                 if not self[RUNNING_KEY]: return
240                 #convert to floating point numbers
241                 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
242                 num_samps = len(samples)
243                 #reorder fft
244                 if self.real: samples = samples[:num_samps/2]
245                 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
246                 #plot the fft
247                 self.plotter.set_samples(
248                         samples=samples,
249                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
250                         maximum=self[REF_LEVEL_KEY],
251                 )
252                 #update the plotter
253                 self.plotter.update()
254
255         def update_grid(self, *args):
256                 """
257                 Update the plotter grid.
258                 This update method is dependent on the variables below.
259                 Determine the x and y axis grid parameters.
260                 The x axis depends on sample rate, baseband freq, and x divs.
261                 The y axis depends on y per div, y divs, and ref level.
262                 """
263                 #grid parameters
264                 sample_rate = self[SAMPLE_RATE_KEY]
265                 frame_rate = self[FRAME_RATE_KEY]
266                 baseband_freq = self[BASEBAND_FREQ_KEY]
267                 num_lines = self[NUM_LINES_KEY]
268                 y_divs = self[Y_DIVS_KEY]
269                 x_divs = self[X_DIVS_KEY]
270                 #determine best fitting x_per_div
271                 if self.real: x_width = sample_rate/2.0
272                 else: x_width = sample_rate/1.0
273                 x_per_div = common.get_clean_num(x_width/x_divs)
274                 #update the x grid
275                 if self.real:
276                         self.plotter.set_x_grid(
277                                 baseband_freq,
278                                 baseband_freq + sample_rate/2.0,
279                                 x_per_div, True,
280                         )
281                 else:
282                         self.plotter.set_x_grid(
283                                 baseband_freq - sample_rate/2.0,
284                                 baseband_freq + sample_rate/2.0,
285                                 x_per_div, True,
286                         )
287                 #update x units
288                 self.plotter.set_x_label('Frequency', 'Hz')
289                 #update y grid
290                 duration = float(num_lines)/frame_rate
291                 y_per_div = common.get_clean_num(duration/y_divs)
292                 self.plotter.set_y_grid(0, duration, y_per_div, True)
293                 #update y units
294                 self.plotter.set_y_label('Time', 's')
295                 #update plotter
296                 self.plotter.update()