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