Merged r10463:10658 from jblum/gui_guts into trunk. Trunk passes distcheck.
[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
34 ##################################################
35 # Constants
36 ##################################################
37 SLIDER_STEPS = 100
38 AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP = -3, 0
39 DEFAULT_WIN_SIZE = (600, 300)
40 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'fft_rate', 30)
41 DIV_LEVELS = (1, 2, 5, 10, 20)
42 FFT_PLOT_COLOR_SPEC = (0.3, 0.3, 1.0)
43 PEAK_VALS_COLOR_SPEC = (0.0, 0.8, 0.0)
44 NO_PEAK_VALS = list()
45
46 ##################################################
47 # FFT window control panel
48 ##################################################
49 class control_panel(wx.Panel):
50         """
51         A control panel with wx widgits to control the plotter and fft block chain.
52         """
53
54         def __init__(self, parent):
55                 """
56                 Create a new control panel.
57                 @param parent the wx parent window
58                 """
59                 self.parent = parent
60                 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
61                 control_box = wx.BoxSizer(wx.VERTICAL)
62                 #checkboxes for average and peak hold
63                 control_box.AddStretchSpacer()
64                 control_box.Add(common.LabelText(self, 'Options'), 0, wx.ALIGN_CENTER)
65                 peak_hold_check_box = common.CheckBoxController(self, 'Peak Hold', parent, PEAK_HOLD_KEY)
66                 control_box.Add(peak_hold_check_box, 0, wx.EXPAND)
67                 average_check_box = common.CheckBoxController(self, 'Average', parent, AVERAGE_KEY)
68                 control_box.Add(average_check_box, 0, wx.EXPAND)
69                 control_box.AddSpacer(2)
70                 avg_alpha_slider = common.LogSliderController(
71                         self, 'Avg Alpha',
72                         AVG_ALPHA_MIN_EXP, AVG_ALPHA_MAX_EXP, SLIDER_STEPS,
73                         parent, AVG_ALPHA_KEY,
74                         formatter=lambda x: ': %.4f'%x,
75                 )
76                 parent.subscribe(AVERAGE_KEY, avg_alpha_slider.Enable)
77                 control_box.Add(avg_alpha_slider, 0, wx.EXPAND)
78                 #radio buttons for div size
79                 control_box.AddStretchSpacer()
80                 control_box.Add(common.LabelText(self, 'Set dB/div'), 0, wx.ALIGN_CENTER)
81                 radio_box = wx.BoxSizer(wx.VERTICAL)
82                 self.radio_buttons = list()
83                 for y_per_div in DIV_LEVELS:
84                         radio_button = wx.RadioButton(self, label="%d dB/div"%y_per_div)
85                         radio_button.Bind(wx.EVT_RADIOBUTTON, self._on_y_per_div)
86                         self.radio_buttons.append(radio_button)
87                         radio_box.Add(radio_button, 0, wx.ALIGN_LEFT)
88                 parent.subscribe(Y_PER_DIV_KEY, self._on_set_y_per_div)
89                 control_box.Add(radio_box, 0, wx.EXPAND)
90                 #ref lvl buttons
91                 control_box.AddStretchSpacer()
92                 control_box.Add(common.LabelText(self, 'Set Ref Level'), 0, wx.ALIGN_CENTER)
93                 control_box.AddSpacer(2)
94                 _ref_lvl_buttons = common.IncrDecrButtons(self, self._on_incr_ref_level, self._on_decr_ref_level)
95                 control_box.Add(_ref_lvl_buttons, 0, wx.ALIGN_CENTER)
96                 #autoscale
97                 control_box.AddStretchSpacer()
98                 autoscale_button = wx.Button(self, label='Autoscale', style=wx.BU_EXACTFIT)
99                 autoscale_button.Bind(wx.EVT_BUTTON, self.parent.autoscale)
100                 control_box.Add(autoscale_button, 0, wx.EXPAND)
101                 #run/stop
102                 run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run')
103                 control_box.Add(run_button, 0, wx.EXPAND)
104                 #set sizer
105                 self.SetSizerAndFit(control_box)
106                 #mouse wheel event
107                 def on_mouse_wheel(event):
108                         if event.GetWheelRotation() < 0: self._on_incr_ref_level(event)
109                         else: self._on_decr_ref_level(event)
110                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
111
112         ##################################################
113         # Event handlers
114         ##################################################
115         def _on_set_y_per_div(self, y_per_div):
116                 try:
117                         index = list(DIV_LEVELS).index(y_per_div)
118                         self.radio_buttons[index].SetValue(True)
119                 except: pass
120         def _on_y_per_div(self, event):
121                 selected_radio_button = filter(lambda rb: rb.GetValue(), self.radio_buttons)[0]
122                 index = self.radio_buttons.index(selected_radio_button)
123                 self.parent[Y_PER_DIV_KEY] = DIV_LEVELS[index]
124         def _on_incr_ref_level(self, event):
125                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] + self.parent[Y_PER_DIV_KEY]
126         def _on_decr_ref_level(self, event):
127                 self.parent[REF_LEVEL_KEY] = self.parent[REF_LEVEL_KEY] - self.parent[Y_PER_DIV_KEY]
128
129 ##################################################
130 # FFT window with plotter and control panel
131 ##################################################
132 class fft_window(wx.Panel, pubsub.pubsub):
133         def __init__(
134                 self,
135                 parent,
136                 controller,
137                 size,
138                 title,
139                 real,
140                 fft_size,
141                 baseband_freq,
142                 sample_rate_key,
143                 y_per_div,
144                 y_divs,
145                 ref_level,
146                 average_key,
147                 avg_alpha_key,
148                 peak_hold,
149                 msg_key,
150         ):
151                 pubsub.pubsub.__init__(self)
152                 #ensure y_per_div
153                 if y_per_div not in DIV_LEVELS: y_per_div = DIV_LEVELS[0]
154                 #setup
155                 self.samples = list()
156                 self.real = real
157                 self.fft_size = fft_size
158                 self._reset_peak_vals()
159                 #proxy the keys
160                 self.proxy(MSG_KEY, controller, msg_key)
161                 self.proxy(AVERAGE_KEY, controller, average_key)
162                 self.proxy(AVG_ALPHA_KEY, controller, avg_alpha_key)
163                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
164                 #init panel and plot
165                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
166                 self.plotter = plotter.channel_plotter(self)
167                 self.plotter.SetSize(wx.Size(*size))
168                 self.plotter.set_title(title)
169                 self.plotter.enable_legend(True)
170                 self.plotter.enable_point_label(True)
171                 self.plotter.enable_grid_lines(True)
172                 #setup the box with plot and controls
173                 self.control_panel = control_panel(self)
174                 main_box = wx.BoxSizer(wx.HORIZONTAL)
175                 main_box.Add(self.plotter, 1, wx.EXPAND)
176                 main_box.Add(self.control_panel, 0, wx.EXPAND)
177                 self.SetSizerAndFit(main_box)
178                 #initialize values
179                 self[AVERAGE_KEY] = self[AVERAGE_KEY]
180                 self[AVG_ALPHA_KEY] = self[AVG_ALPHA_KEY]
181                 self[PEAK_HOLD_KEY] = peak_hold
182                 self[Y_PER_DIV_KEY] = y_per_div
183                 self[Y_DIVS_KEY] = y_divs
184                 self[X_DIVS_KEY] = 8 #approximate
185                 self[REF_LEVEL_KEY] = ref_level
186                 self[BASEBAND_FREQ_KEY] = baseband_freq
187                 self[RUNNING_KEY] = True
188                 #register events
189                 self.subscribe(AVERAGE_KEY, lambda x: self._reset_peak_vals())
190                 self.subscribe(MSG_KEY, self.handle_msg)
191                 self.subscribe(SAMPLE_RATE_KEY, self.update_grid)
192                 for key in (
193                         BASEBAND_FREQ_KEY,
194                         Y_PER_DIV_KEY, X_DIVS_KEY,
195                         Y_DIVS_KEY, REF_LEVEL_KEY,
196                 ): self.subscribe(key, self.update_grid)
197                 #initial update
198                 self.update_grid()
199
200         def autoscale(self, *args):
201                 """
202                 Autoscale the fft plot to the last frame.
203                 Set the dynamic range and reference level.
204                 """
205                 if not len(self.samples): return
206                 #get the peak level (max of the samples)
207                 peak_level = numpy.max(self.samples)
208                 #get the noise floor (averge the smallest samples)
209                 noise_floor = numpy.average(numpy.sort(self.samples)[:len(self.samples)/4])
210                 #padding
211                 noise_floor -= abs(noise_floor)*.5
212                 peak_level += abs(peak_level)*.1
213                 #set the reference level to a multiple of y divs
214                 self[REF_LEVEL_KEY] = self[Y_DIVS_KEY]*math.ceil(peak_level/self[Y_DIVS_KEY])
215                 #set the range to a clean number of the dynamic range
216                 self[Y_PER_DIV_KEY] = common.get_clean_num((peak_level - noise_floor)/self[Y_DIVS_KEY])
217
218         def _reset_peak_vals(self): self.peak_vals = NO_PEAK_VALS
219
220         def handle_msg(self, msg):
221                 """
222                 Handle the message from the fft sink message queue.
223                 If complex, reorder the fft samples so the negative bins come first.
224                 If real, keep take only the positive bins.
225                 Plot the samples onto the grid as channel 1.
226                 If peak hold is enabled, plot peak vals as channel 2.
227                 @param msg the fft array as a character array
228                 """
229                 if not self[RUNNING_KEY]: return
230                 #convert to floating point numbers
231                 samples = numpy.fromstring(msg, numpy.float32)[:self.fft_size] #only take first frame
232                 num_samps = len(samples)
233                 #reorder fft
234                 if self.real: samples = samples[:num_samps/2]
235                 else: samples = numpy.concatenate((samples[num_samps/2:], samples[:num_samps/2]))
236                 self.samples = samples
237                 #peak hold calculation
238                 if self[PEAK_HOLD_KEY]:
239                         if len(self.peak_vals) != len(samples): self.peak_vals = samples
240                         self.peak_vals = numpy.maximum(samples, self.peak_vals)
241                         #plot the peak hold
242                         self.plotter.set_waveform(
243                                 channel='Peak',
244                                 samples=self.peak_vals,
245                                 color_spec=PEAK_VALS_COLOR_SPEC,
246                         )
247                 else:
248                         self._reset_peak_vals()
249                         self.plotter.clear_waveform(channel='Peak')
250                 #plot the fft
251                 self.plotter.set_waveform(
252                         channel='FFT',
253                         samples=samples,
254                         color_spec=FFT_PLOT_COLOR_SPEC,
255                 )
256                 #update the plotter
257                 self.plotter.update()
258
259         def update_grid(self, *args):
260                 """
261                 Update the plotter grid.
262                 This update method is dependent on the variables below.
263                 Determine the x and y axis grid parameters.
264                 The x axis depends on sample rate, baseband freq, and x divs.
265                 The y axis depends on y per div, y divs, and ref level.
266                 """
267                 #grid parameters
268                 sample_rate = self[SAMPLE_RATE_KEY]
269                 baseband_freq = self[BASEBAND_FREQ_KEY]
270                 y_per_div = self[Y_PER_DIV_KEY]
271                 y_divs = self[Y_DIVS_KEY]
272                 x_divs = self[X_DIVS_KEY]
273                 ref_level = self[REF_LEVEL_KEY]
274                 #determine best fitting x_per_div
275                 if self.real: x_width = sample_rate/2.0
276                 else: x_width = sample_rate/1.0
277                 x_per_div = common.get_clean_num(x_width/x_divs)
278                 #update the x grid
279                 if self.real:
280                         self.plotter.set_x_grid(
281                                 baseband_freq,
282                                 baseband_freq + sample_rate/2.0,
283                                 x_per_div, True,
284                         )
285                 else:
286                         self.plotter.set_x_grid(
287                                 baseband_freq - sample_rate/2.0,
288                                 baseband_freq + sample_rate/2.0,
289                                 x_per_div, True,
290                         )
291                 #update x units
292                 self.plotter.set_x_label('Frequency', 'Hz')
293                 #update y grid
294                 self.plotter.set_y_grid(ref_level-y_per_div*y_divs, ref_level, y_per_div)
295                 #update y units
296                 self.plotter.set_y_label('Amplitude', 'dB')
297                 #update plotter
298                 self.plotter.update()