4ee5520f76167271af35a699266fd20521e9e6c3
[debian/gnuradio] / gr-wxgui / src / python / fft_window.py
1 #
2 # Copyright 2008, 2009 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 import forms
34
35 ##################################################
36 # Constants
37 ##################################################
38 SLIDER_STEPS = 100
39 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
40 DEFAULT_WIN_SIZE = (600, 300)
41 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
42 DB_DIV_MIN, DB_DIV_MAX = 1, 20
43 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
44 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
45 EMPTY_TRACE = list()
46 TRACES = ('A', 'B')
47 TRACES_COLOR_SPEC = {
48         'A': (1.0, 0.0, 0.0),
49         'B': (0.8, 0.0, 0.8),
50 }
51
52 ##################################################
53 # FFT 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                 #checkboxes for average and peak hold
72                 options_box = forms.static_box_sizer(
73                         parent=self, sizer=control_box, label='Trace Options',
74                         bold=True, orient=wx.VERTICAL,
75                 )
76                 forms.check_box(
77                         sizer=options_box, parent=self, label='Peak Hold',
78                         ps=parent, key=PEAK_HOLD_KEY,
79                 )
80                 forms.check_box(
81                         sizer=options_box, parent=self, label='Average',
82                         ps=parent, key=AVERAGE_KEY,
83                 )
84                 #static text and slider for averaging
85                 avg_alpha_text = forms.static_text(
86                         sizer=options_box, parent=self, label='Avg Alpha',
87                         converter=forms.float_converter(lambda x: '%.4f'%x),
88                         ps=parent, key=AVG_ALPHA_KEY, width=50,
89                 )
90                 avg_alpha_slider = forms.log_slider(
91                         sizer=options_box, parent=self,
92                         min_exp=AVG_ALPHA_MIN_EXP,
93                         max_exp=AVG_ALPHA_MAX_EXP,
94                         num_steps=SLIDER_STEPS,
95                         ps=parent, key=AVG_ALPHA_KEY,
96                 )
97                 for widget in (avg_alpha_text, avg_alpha_slider):
98                         parent.subscribe(AVERAGE_KEY, widget.Enable)
99                         widget.Enable(parent[AVERAGE_KEY])
100                 
101                 #trace menu
102                 for trace in TRACES:
103                         trace_box = wx.BoxSizer(wx.HORIZONTAL)
104                         options_box.Add(trace_box, 0, wx.EXPAND)
105                         forms.check_box(
106                                 sizer=trace_box, parent=self,
107                                 ps=parent, key=TRACE_SHOW_KEY+trace,
108                                 label='Trace %s'%trace,
109                         )
110                         trace_box.AddSpacer(10)
111                         forms.single_button(
112                                 sizer=trace_box, parent=self,
113                                 ps=parent, key=TRACE_STORE_KEY+trace,
114                                 label='Store', style=wx.BU_EXACTFIT,
115                         )
116                         trace_box.AddSpacer(10)
117                 #radio buttons for div size
118                 control_box.AddStretchSpacer()
119                 y_ctrl_box = forms.static_box_sizer(
120                         parent=self, sizer=control_box, label='Axis Options',
121                         bold=True, orient=wx.VERTICAL,
122                 )
123                 forms.incr_decr_buttons(
124                         parent=self, sizer=y_ctrl_box, label='dB/Div',
125                         on_incr=self._on_incr_db_div, on_decr=self._on_decr_db_div,
126                 )
127                 #ref lvl buttons
128                 forms.incr_decr_buttons(
129                         parent=self, sizer=y_ctrl_box, label='Ref Level',
130                         on_incr=self._on_incr_ref_level, on_decr=self._on_decr_ref_level,
131                 )
132                 y_ctrl_box.AddSpacer(2)
133                 #autoscale
134                 forms.single_button(
135                         sizer=y_ctrl_box, parent=self, label='Autoscale',
136                         callback=self.parent.autoscale,
137                 )
138                 #run/stop
139                 control_box.AddStretchSpacer()
140                 forms.toggle_button(
141                         sizer=control_box, parent=self,
142                         true_label='Stop', false_label='Run',
143                         ps=parent, key=RUNNING_KEY,
144                 )
145                 #set sizer
146                 self.SetSizerAndFit(control_box)
147                 #mouse wheel event
148                 def on_mouse_wheel(event):
149                         if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
150                         else: self._on_decr_ref_level(event)
151                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
152
153         ##################################################
154         # Event handlers
155         ##################################################
156         def _on_incr_ref_level(self, event):
157                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
158         def _on_decr_ref_level(self, event):
159                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
160         def _on_incr_db_div(self, event):
161                 self.parent[Y_PER_DIV_KEY] = min(DB_DIV_MAX, common.get_clean_incr(self.parent[Y_PER_DIV_KEY]))
162         def _on_decr_db_div(self, event):
163                 self.parent[Y_PER_DIV_KEY] = max(DB_DIV_MIN, common.get_clean_decr(self.parent[Y_PER_DIV_KEY]))
164
165 ##################################################
166 # FFT window with plotter and control panel
167 ##################################################
168 class fft_window(wx.Panel, pubsub.pubsub):
169         def __init__(
170                 self,
171                 parent,
172                 controller,
173                 size,
174                 title,
175                 real,
176                 fft_size,
177                 baseband_freq,
178                 sample_rate_key,
179                 y_per_div,
180                 y_divs,
181                 ref_level,
182                 average_key,
183                 avg_alpha_key,
184                 peak_hold,
185                 msg_key,
186         ):
187                 pubsub.pubsub.__init__(self)
188                 #setup
189                 self.samples = EMPTY_TRACE
190                 self.real = real
191                 self.fft_size = fft_size
192                 self._reset_peak_vals()
193                 self._traces = dict()
194                 #proxy the keys
195                 self.proxy(MSG_KEY, controller, msg_key)
196                 self.proxy(AVERAGE_KEY, controller, average_key)
197                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
198                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
199                 #initialize values
200                 self[PEAK_HOLD_KEY] = peak_hold
201                 self[Y_PER_DIV_KEY] = y_per_div
202                 self[Y_DIVS_KEY] = y_divs
203                 self[X_DIVS_KEY] = 8 #approximate
204                 self[REF_LEVEL_KEY] = ref_level
205                 self[BASEBAND_FREQ_KEY] = baseband_freq
206                 self[RUNNING_KEY] = True
207                 for trace in TRACES:
208                         #a function that returns a function
209                         #so the function wont use local trace
210                         def new_store_trace(my_trace):
211                                 def store_trace(*args):
212                                         self._traces[my_trace] = self.samples
213                                         self.update_grid()
214                                 return store_trace
215                         def new_toggle_trace(my_trace):
216                                 def toggle_trace(toggle):
217                                         #do an automatic store if toggled on and empty trace
218                                         if toggle and not len(self._traces[my_trace]):
219                                                 self._traces[my_trace] = self.samples
220                                         self.update_grid()
221                                 return toggle_trace
222                         self._traces[trace] = EMPTY_TRACE
223                         self[TRACE_STORE_KEY+trace] = False
224                         self[TRACE_SHOW_KEY+trace] = False
225                         self.subscribe(TRACE_STORE_KEY+trace, new_store_trace(trace))
226                         self.subscribe(TRACE_SHOW_KEY+trace, new_toggle_trace(trace))
227                 #init panel and plot
228                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
229                 self.plotter = plotter.channel_plotter(self)
230                 self.plotter.SetSize(wx.Size(*size))
231                 self.plotter.set_title(title)
232                 self.plotter.enable_legend(True)
233                 self.plotter.enable_point_label(True)
234                 self.plotter.enable_grid_lines(True)
235                 #setup the box with plot and controls
236                 self.control_panel = control_panel(self)
237                 main_box = wx.BoxSizer(wx.HORIZONTAL)
238                 main_box.Add(self.plotter, 1, wx.EXPAND)
239                 main_box.Add(self.control_panel, 0, wx.EXPAND)
240                 self.SetSizerAndFit(main_box)
241                 #register events
242                 self.subscribe(AVERAGE_KEY, self._reset_peak_vals)
243                 self.subscribe(MSG_KEY, self.handle_msg)
244                 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
245                 for key in (
246                         BASEBAND_FREQ_KEY,
247                         Y_PER_DIV_KEY, X_DIVS_KEY,
248                         Y_DIVS_KEY, REF_LEVEL_KEY,
249                 ): self.subscribe(key, self.update_grid)
250                 #initial update
251                 self.update_grid()
252
253         def autoscale(self, *args):
254                 """
255                 Autoscale the fft plot to the last frame.
256                 Set the dynamic range and reference level.
257                 """
258                 if not len(self.samples): return
259                 min_level, max_level = common.get_min_max_fft(self.samples)
260                 #set the range to a clean number of the dynamic range
261                 self[Y_PER_DIV_KEY] = common.get_clean_num(1+(max_level - min_level)/self[Y_DIVS_KEY])
262                 #set the reference level to a multiple of y per div
263                 self[REF_LEVEL_KEY] = self[Y_PER_DIV_KEY]*round(.5+max_level/self[Y_PER_DIV_KEY])
264
265         def _reset_peak_vals(self, *args): self.peak_vals = EMPTY_TRACE
266
267         def handle_msg(self, msg):
268                 """
269                 Handle the message from the fft sink message queue.
270                 If complex, reorder the fft samples so the negative bins come first.
271                 If real, keep take only the positive bins.
272                 Plot the samples onto the grid as channel 1.
273                 If peak hold is enabled, plot peak vals as channel 2.
274                 @param msg the fft array as a character array
275                 """
276                 if not self[RUNNING_KEY]: return
277                 #convert to floating point numbers
278                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
279                 num_samps = len(samples)
280                 #reorder fft
281                 if self.real: samples = samples[:(num_samps+1)/2]
282                 else: samples = numpy.concatenate((samples[num_samps/2+1:], samples[:(num_samps+1)/2]))
283                 self.samples = samples
284                 #peak hold calculation
285                 if self[PEAK_HOLD_KEY]:
286                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
287                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
288                         #plot the peak hold
289                         self.plotter.set_waveform(
290                                 channel='Peak',
291                                 samples=self.peak_vals,
292                                 color_spec=PEAK_VALS_COLOR_SPEC,
293                         )
294                 else:
295                         self._reset_peak_vals()
296                         self.plotter.clear_waveform(channel='Peak')
297                 #plot the fft
298                 self.plotter.set_waveform(
299                         channel='FFT',
300                         samples=samples,
301                         color_spec=FFT_PLOT_COLOR_SPEC,
302                 )
303                 #update the plotter
304                 self.plotter.update()
305
306         def update_grid(self, *args):
307                 """
308                 Update the plotter grid.
309                 This update method is dependent on the variables below.
310                 Determine the x and y axis grid parameters.
311                 The x axis depends on sample rate, baseband freq, and x divs.
312                 The y axis depends on y per div, y divs, and ref level.
313                 """
314                 for trace in TRACES:
315                         channel = '%s'%trace.upper()
316                         if self[TRACE_SHOW_KEY+trace]:
317                                 self.plotter.set_waveform(
318                                         channel=channel,
319                                         samples=self._traces[trace],
320                                         color_spec=TRACES_COLOR_SPEC[trace],
321                                 )
322                         else: self.plotter.clear_waveform(channel=channel)
323                 #grid parameters
324                 sample_rate = self[SAMPLE_RATE_KEY]
325                 baseband_freq = self[BASEBAND_FREQ_KEY]
326                 y_per_div = self[Y_PER_DIV_KEY]
327                 y_divs = self[Y_DIVS_KEY]
328                 x_divs = self[X_DIVS_KEY]
329                 ref_level = self[REF_LEVEL_KEY]
330                 #determine best fitting x_per_div
331                 if self.real: x_width = sample_rate/2.0
332                 else: x_width = sample_rate/1.0
333                 x_per_div = common.get_clean_num(x_width/x_divs)
334                 #update the x grid
335                 if self.real:
336                         self.plotter.set_x_grid(
337                                 baseband_freq,
338                                 baseband_freq + sample_rate/2.0,
339                                 x_per_div, True,
340                         )
341                 else:
342                         self.plotter.set_x_grid(
343                                 baseband_freq - sample_rate/2.0,
344                                 baseband_freq + sample_rate/2.0,
345                                 x_per_div, True,
346                         )
347                 #update x units
348                 self.plotter.set_x_label('Frequency', 'Hz')
349                 #update y grid
350                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
351                 #update y units
352                 self.plotter.set_y_label('Amplitude', 'dB')
353                 #update plotter
354                 self.plotter.update()