registered key to hide/show control panel in wxgui windows
[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                 parent[SHOW_CONTROL_PANEL_KEY] = True
67                 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
68                 control_box = wx.BoxSizer(wx.VERTICAL)
69                 control_box.AddStretchSpacer()
70                 options_box = forms.static_box_sizer(
71                         parent=self, sizer=control_box, label='Options',
72                         bold=True, orient=wx.VERTICAL,
73                 )
74                 #average
75                 forms.check_box(
76                         sizer=options_box, parent=self, label='Average',
77                         ps=parent, key=AVERAGE_KEY,
78                 )
79                 avg_alpha_text = forms.static_text(
80                         sizer=options_box, parent=self, label='Avg Alpha',
81                         converter=forms.float_converter(lambda x: '%.4f'%x),
82                         ps=parent, key=AVG_ALPHA_KEY, width=50,
83                 )
84                 avg_alpha_slider = forms.log_slider(
85                         sizer=options_box, parent=self,
86                         min_exp=AVG_ALPHA_MIN_EXP,
87                         max_exp=AVG_ALPHA_MAX_EXP,
88                         num_steps=SLIDER_STEPS,
89                         ps=parent, key=AVG_ALPHA_KEY,
90                 )
91                 for widget in (avg_alpha_text, avg_alpha_slider):
92                         parent.subscribe(AVERAGE_KEY, widget.Enable)
93                         widget.Enable(parent[AVERAGE_KEY])
94                 #begin axes box
95                 control_box.AddStretchSpacer()
96                 axes_box = forms.static_box_sizer(
97                         parent=self, sizer=control_box, label='Axes Options',
98                         bold=True, orient=wx.VERTICAL,
99                 )
100                 #num lines buttons
101                 forms.incr_decr_buttons(
102                         parent=self, sizer=axes_box, label='Time Scale',
103                         on_incr=self._on_incr_time_scale, on_decr=self._on_decr_time_scale,
104                 )
105                 #dyanmic range buttons
106                 forms.incr_decr_buttons(
107                         parent=self, sizer=axes_box, label='Dyn Range',
108                         on_incr=self._on_incr_dynamic_range, on_decr=self._on_decr_dynamic_range,
109                 )
110                 #ref lvl buttons
111                 forms.incr_decr_buttons(
112                         parent=self, sizer=axes_box, label='Ref Level',
113                         on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
114                 )
115                 #color mode
116                 forms.drop_down(
117                         parent=self, sizer=axes_box, width=100,
118                         ps=parent, key=COLOR_MODE_KEY, label='Color',
119                         choices=map(lambda x: x[1], COLOR_MODES),
120                         labels=map(lambda x: x[0], COLOR_MODES),
121                 )
122                 #autoscale
123                 forms.single_button(
124                         parent=self, sizer=axes_box, label='Autoscale',
125                         callback=self.parent.autoscale,
126                 )
127                 #clear
128                 control_box.AddStretchSpacer()
129                 forms.single_button(
130                         parent=self, sizer=control_box, label='Clear',
131                         callback=self._on_clear_button,
132                 )
133                 #run/stop
134                 forms.toggle_button(
135                         sizer=control_box, parent=self,
136                         true_label='Stop', false_label='Run',
137                         ps=parent, key=RUNNING_KEY,
138                 )
139                 #set sizer
140                 self.SetSizerAndFit(control_box)
141
142         ##################################################
143         # Event handlers
144         ##################################################
145         def _on_clear_button(self, event):
146                 self.parent[NUM_LINES_KEY] = self.parent[NUM_LINES_KEY]
147         def _on_incr_dynamic_range(self, event):
148                 self.parent[DYNAMIC_RANGE_KEY] = min(self.parent[DYNAMIC_RANGE_KEY] + 10, MAX_DYNAMIC_RANGE)
149         def _on_decr_dynamic_range(self, event):
150                 self.parent[DYNAMIC_RANGE_KEY] = max(self.parent[DYNAMIC_RANGE_KEY] - 10, MIN_DYNAMIC_RANGE)
151         def _on_incr_ref_level(self, event):
152                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[DYNAMIC_RANGE_KEY]*.1
153         def _on_decr_ref_level(self, event):
154                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[DYNAMIC_RANGE_KEY]*.1
155         def _on_incr_time_scale(self, event):
156                 old_rate = self.parent[FRAME_RATE_KEY]
157                 self.parent[FRAME_RATE_KEY] *= 0.75
158                 if self.parent[FRAME_RATE_KEY] == old_rate:
159                         self.parent[DECIMATION_KEY] += 1
160         def _on_decr_time_scale(self, event):
161                 old_rate = self.parent[FRAME_RATE_KEY]
162                 self.parent[FRAME_RATE_KEY] *= 1.25
163                 if self.parent[FRAME_RATE_KEY] == old_rate:
164                         self.parent[DECIMATION_KEY] -= 1
165
166 ##################################################
167 # Waterfall window with plotter and control panel
168 ##################################################
169 class waterfall_window(wx.Panel, pubsub.pubsub):
170         def __init__(
171                 self,
172                 parent,
173                 controller,
174                 size,
175                 title,
176                 real,
177                 fft_size,
178                 num_lines,
179                 decimation_key,
180                 baseband_freq,
181                 sample_rate_key,
182                 frame_rate_key,
183                 dynamic_range,
184                 ref_level,
185                 average_key,
186                 avg_alpha_key,
187                 msg_key,
188         ):
189                 pubsub.pubsub.__init__(self)
190                 #setup
191                 self.samples = list()
192                 self.real = real
193                 self.fft_size = fft_size
194                 #proxy the keys
195                 self.proxy(MSG_KEY, controller, msg_key)
196                 self.proxy(DECIMATION_KEY, controller, decimation_key)
197                 self.proxy(FRAME_RATE_KEY, controller, frame_rate_key)
198                 self.proxy(AVERAGE_KEY, controller, average_key)
199                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
200                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
201                 #init panel and plot
202                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
203                 self.plotter = plotter.waterfall_plotter(self)
204                 self.plotter.SetSize(wx.Size(*size))
205                 self.plotter.set_title(title)
206                 self.plotter.enable_point_label(True)
207                 self.plotter.enable_grid_lines(False)
208                 #plotter listeners
209                 self.subscribe(COLOR_MODE_KEY, self.plotter.set_color_mode)
210                 self.subscribe(NUM_LINES_KEY, self.plotter.set_num_lines)
211                 #initialize values
212                 self[DYNAMIC_RANGE_KEY] = dynamic_range
213                 self[NUM_LINES_KEY] = num_lines
214                 self[Y_DIVS_KEY] = 8
215                 self[X_DIVS_KEY] = 8 #approximate
216                 self[REF_LEVEL_KEY] = ref_level
217                 self[BASEBAND_FREQ_KEY] = baseband_freq
218                 self[COLOR_MODE_KEY] = COLOR_MODES[0][1]
219                 self[RUNNING_KEY] = True
220                 #setup the box with plot and controls
221                 self.control_panel = control_panel(self)
222                 main_box = wx.BoxSizer(wx.HORIZONTAL)
223                 main_box.Add(self.plotter, 1, wx.EXPAND)
224                 main_box.Add(self.control_panel, 0, wx.EXPAND)
225                 self.SetSizerAndFit(main_box)
226                 #register events
227                 self.subscribe(MSG_KEY, self.handle_msg)
228                 for key in (
229                         DECIMATION_KEY, SAMPLE_RATE_KEY, FRAME_RATE_KEY,
230                         BASEBAND_FREQ_KEY, X_DIVS_KEY, Y_DIVS_KEY, NUM_LINES_KEY,
231                 ): self.subscribe(key, self.update_grid)
232                 #initial update
233                 self.update_grid()
234
235         def autoscale(self, *args):
236                 """
237                 Autoscale the waterfall plot to the last frame.
238                 Set the dynamic range and reference level.
239                 Does not affect the current data in the waterfall.
240                 """
241                 if not len(self.samples): return
242                 min_level, max_level = common.get_min_max_fft(self.samples)
243                 #set the range and level
244                 self[REF_LEVEL_KEY] = max_level
245                 self[DYNAMIC_RANGE_KEY] = max_level - min_level
246
247         def handle_msg(self, msg):
248                 """
249                 Handle the message from the fft sink message queue.
250                 If complex, reorder the fft samples so the negative bins come first.
251                 If real, keep take only the positive bins.
252                 Send the data to the plotter.
253                 @param msg the fft array as a character array
254                 """
255                 if not self[RUNNING_KEY]: return
256                 #convert to floating point numbers
257                 self.samples = samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
258                 num_samps = len(samples)
259                 #reorder fft
260                 if self.real: samples = samples[:(num_samps+1)/2]
261                 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
262                 #plot the fft
263                 self.plotter.set_samples(
264                         samples=samples,
265                         minimum=self[REF_LEVEL_KEY] - self[DYNAMIC_RANGE_KEY], 
266                         maximum=self[REF_LEVEL_KEY],
267                 )
268                 #update the plotter
269                 self.plotter.update()
270
271         def update_grid(self, *args):
272                 """
273                 Update the plotter grid.
274                 This update method is dependent on the variables below.
275                 Determine the x and y axis grid parameters.
276                 The x axis depends on sample rate, baseband freq, and x divs.
277                 The y axis depends on y per div, y divs, and ref level.
278                 """
279                 #grid parameters
280                 sample_rate = self[SAMPLE_RATE_KEY]
281                 frame_rate = self[FRAME_RATE_KEY]
282                 baseband_freq = self[BASEBAND_FREQ_KEY]
283                 num_lines = self[NUM_LINES_KEY]
284                 y_divs = self[Y_DIVS_KEY]
285                 x_divs = self[X_DIVS_KEY]
286                 #determine best fitting x_per_div
287                 if self.real: x_width = sample_rate/2.0
288                 else: x_width = sample_rate/1.0
289                 x_per_div = common.get_clean_num(x_width/x_divs)
290                 #update the x grid
291                 if self.real:
292                         self.plotter.set_x_grid(
293                                 baseband_freq,
294                                 baseband_freq + sample_rate/2.0,
295                                 x_per_div, True,
296                         )
297                 else:
298                         self.plotter.set_x_grid(
299                                 baseband_freq - sample_rate/2.0,
300                                 baseband_freq + sample_rate/2.0,
301                                 x_per_div, True,
302                         )
303                 #update x units
304                 self.plotter.set_x_label('Frequency', 'Hz')
305                 #update y grid
306                 duration = float(num_lines)/frame_rate
307                 y_per_div = common.get_clean_num(duration/y_divs)
308                 self.plotter.set_y_grid(0, duration, y_per_div, True)
309                 #update y units
310                 self.plotter.set_y_label('Time', 's')
311                 #update plotter
312                 self.plotter.update()