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