]> git.gag.com Git - debian/gnuradio/blob - gr-wxgui/src/python/fftsink2.py
Merged r4671:4680 from jcorgan/est into trunk. Pulls in gr-wxgui updates for new...
[debian/gnuradio] / gr-wxgui / src / python / fftsink2.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2003,2004,2005,2006 Free Software Foundation, Inc.
4
5 # This file is part of GNU Radio
6
7 # GNU Radio is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2, or (at your option)
10 # any later version.
11
12 # GNU Radio is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with GNU Radio; see the file COPYING.  If not, write to
19 # the Free Software Foundation, Inc., 51 Franklin Street,
20 # Boston, MA 02110-1301, USA.
21
22
23 from gnuradio import gr, gru, window
24 from gnuradio.wxgui import stdgui2
25 import wx
26 import gnuradio.wxgui.plot as plot
27 import Numeric
28 import threading
29 import math    
30
31 default_fftsink_size = (640,240)
32 default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
33
34 class fft_sink_base(object):
35     def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10, ref_level=50,
36                  sample_rate=1, fft_size=512,
37                  fft_rate=default_fft_rate,
38                  average=False, avg_alpha=None, title='', peak_hold=False):
39
40         # initialize common attributes
41         self.baseband_freq = baseband_freq
42         self.y_divs = 8
43         self.y_per_div=y_per_div
44         self.ref_level = ref_level
45         self.sample_rate = sample_rate
46         self.fft_size = fft_size
47         self.fft_rate = fft_rate
48         self.average = average
49         if avg_alpha is None:
50             self.avg_alpha = 2.0 / fft_rate
51         else:
52             self.avg_alpha = avg_alpha
53         self.title = title
54         self.peak_hold = peak_hold
55         self.input_is_real = input_is_real
56         self.msgq = gr.msg_queue(2)         # queue that holds a maximum of 2 messages
57
58     def set_y_per_div(self, y_per_div):
59         self.y_per_div = y_per_div
60
61     def set_ref_level(self, ref_level):
62         self.ref_level = ref_level
63
64     def set_average(self, average):
65         self.average = average
66         if average:
67             self.avg.set_taps(self.avg_alpha)
68             self.set_peak_hold(False)
69         else:
70             self.avg.set_taps(1.0)
71
72     def set_peak_hold(self, enable):
73         self.peak_hold = enable
74         if enable:
75             self.set_average(False)
76         self.win.set_peak_hold(enable)
77
78     def set_avg_alpha(self, avg_alpha):
79         self.avg_alpha = avg_alpha
80
81     def set_baseband_freq(self, baseband_freq):
82         self.baseband_freq = baseband_freq
83
84     def set_sample_rate(self, sample_rate):
85         self.sample_rate = sample_rate
86         self._set_n()
87
88     def _set_n(self):
89         self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
90         
91
92 class fft_sink_f(gr.hier_block2, fft_sink_base):
93     def __init__(self, parent, baseband_freq=0,
94                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
95                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
96                  title='', size=default_fftsink_size, peak_hold=False):
97
98         gr.hier_block2.__init__(self, "fft_sink_f",
99                                 gr.io_signature(1, 1, gr.sizeof_float),
100                                 gr.io_signature(0,0,0))
101
102         fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
103                                y_per_div=y_per_div, ref_level=ref_level,
104                                sample_rate=sample_rate, fft_size=fft_size,
105                                fft_rate=fft_rate,
106                                average=average, avg_alpha=avg_alpha, title=title,
107                                peak_hold=peak_hold)
108                                
109         self.define_component("s2p", gr.stream_to_vector(gr.sizeof_float, self.fft_size))
110         self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
111                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
112         self.define_component("one_in_n", self.one_in_n)
113         
114         mywindow = window.blackmanharris(self.fft_size)
115         self.define_component("fft", gr.fft_vfc(self.fft_size, True, mywindow))
116         power = 0
117         for tap in mywindow:
118             power += tap*tap
119             
120         self.define_component("c2mag", gr.complex_to_mag(self.fft_size))
121         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
122         self.define_component("avg", self.avg)
123
124         # FIXME  We need to add 3dB to all bins but the DC bin
125         self.define_component("log", gr.nlog10_ff(20, self.fft_size,
126                                      -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size)))
127         self.define_component("sink", gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True))
128
129
130         # Ultimately this will be
131         # self.connect("self s2p one_in_n fft c2mag avg log sink")
132         self.connect("self", 0, "s2p", 0)
133         self.connect("s2p", 0, "one_in_n", 0)
134         self.connect("one_in_n", 0, "fft", 0)
135         self.connect("fft", 0, "c2mag", 0)
136         self.connect("c2mag", 0, "avg", 0)
137         self.connect("avg", 0, "log", 0)
138         self.connect("log", 0, "sink", 0)
139                               
140         self.win = fft_window(self, parent, size=size)
141         self.set_average(self.average)
142
143
144 class fft_sink_c(gr.hier_block2, fft_sink_base):
145     def __init__(self, parent, baseband_freq=0,
146                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
147                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
148                  title='', size=default_fftsink_size, peak_hold=False):
149
150         gr.hier_block2.__init__(self, "fft_sink_c",
151                                 gr.io_signature(1, 1, gr.sizeof_gr_complex),
152                                 gr.io_signature(0,0,0))
153
154         fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
155                                y_per_div=y_per_div, ref_level=ref_level,
156                                sample_rate=sample_rate, fft_size=fft_size,
157                                fft_rate=fft_rate,
158                                average=average, avg_alpha=avg_alpha, title=title,
159                                peak_hold=peak_hold)
160
161         self.define_component("s2p", gr.stream_to_vector(gr.sizeof_gr_complex, self.fft_size))
162         self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
163                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
164         self.define_component("one_in_n", self.one_in_n)
165         
166         mywindow = window.blackmanharris(self.fft_size)
167         self.define_component("fft", gr.fft_vcc(self.fft_size, True, mywindow))
168         power = 0
169         for tap in mywindow:
170             power += tap*tap
171             
172         self.define_component("c2mag", gr.complex_to_mag(self.fft_size))
173         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
174         self.define_component("avg", self.avg)
175
176         # FIXME  We need to add 3dB to all bins but the DC bin
177         self.define_component("log", gr.nlog10_ff(20, self.fft_size,
178                                      -20*math.log10(self.fft_size)-10*math.log10(power/self.fft_size)))
179         self.define_component("sink", gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True))
180
181         # Ultimately this will be
182         # self.connect("self s2p one_in_n fft c2mag avg log sink")
183         self.connect("self", 0, "s2p", 0)
184         self.connect("s2p", 0, "one_in_n", 0)
185         self.connect("one_in_n", 0, "fft", 0)
186         self.connect("fft", 0, "c2mag", 0)
187         self.connect("c2mag", 0, "avg", 0)
188         self.connect("avg", 0, "log", 0)
189         self.connect("log", 0, "sink", 0)
190
191         self.win = fft_window(self, parent, size=size)
192         self.set_average(self.average)
193
194
195 # ------------------------------------------------------------------------
196
197 myDATA_EVENT = wx.NewEventType()
198 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
199
200
201 class DataEvent(wx.PyEvent):
202     def __init__(self, data):
203         wx.PyEvent.__init__(self)
204         self.SetEventType (myDATA_EVENT)
205         self.data = data
206
207     def Clone (self): 
208         self.__class__ (self.GetId())
209
210
211 class input_watcher (threading.Thread):
212     def __init__ (self, msgq, fft_size, event_receiver, **kwds):
213         threading.Thread.__init__ (self, **kwds)
214         self.setDaemon (1)
215         self.msgq = msgq
216         self.fft_size = fft_size
217         self.event_receiver = event_receiver
218         self.keep_running = True
219         self.start ()
220
221     def run (self):
222         while (self.keep_running):
223             msg = self.msgq.delete_head()  # blocking read of message queue
224             itemsize = int(msg.arg1())
225             nitems = int(msg.arg2())
226
227             s = msg.to_string()            # get the body of the msg as a string
228
229             # There may be more than one FFT frame in the message.
230             # If so, we take only the last one
231             if nitems > 1:
232                 start = itemsize * (nitems - 1)
233                 s = s[start:start+itemsize]
234
235             complex_data = Numeric.fromstring (s, Numeric.Float32)
236             de = DataEvent (complex_data)
237             wx.PostEvent (self.event_receiver, de)
238             del de
239     
240
241 class fft_window (plot.PlotCanvas):
242     def __init__ (self, fftsink, parent, id = -1,
243                   pos = wx.DefaultPosition, size = wx.DefaultSize,
244                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
245         plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
246
247         self.y_range = None
248         self.fftsink = fftsink
249         self.peak_hold = False
250         self.peak_vals = None
251
252         self.SetEnableGrid (True)
253         # self.SetEnableZoom (True)
254         # self.SetBackgroundColour ('black')
255         
256         self.build_popup_menu()
257         
258         EVT_DATA_EVENT (self, self.set_data)
259         wx.EVT_CLOSE (self, self.on_close_window)
260         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
261
262         self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
263
264
265     def on_close_window (self, event):
266         print "fft_window:on_close_window"
267         self.keep_running = False
268
269
270     def set_data (self, evt):
271         dB = evt.data
272         L = len (dB)
273
274         if self.peak_hold:
275             if self.peak_vals is None:
276                 self.peak_vals = dB
277             else:
278                 self.peak_vals = Numeric.maximum(dB, self.peak_vals)
279                 dB = self.peak_vals
280
281         x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
282         if x >= 1e9:
283             sf = 1e-9
284             units = "GHz"
285         elif x >= 1e6:
286             sf = 1e-6
287             units = "MHz"
288         else:
289             sf = 1e-3
290             units = "kHz"
291
292         if self.fftsink.input_is_real:     # only plot 1/2 the points
293             x_vals = ((Numeric.arrayrange (L/2)
294                        * (self.fftsink.sample_rate * sf / L))
295                       + self.fftsink.baseband_freq * sf)
296             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
297             points[:,0] = x_vals
298             points[:,1] = dB[0:L/2]
299         else:
300             # the "negative freqs" are in the second half of the array
301             x_vals = ((Numeric.arrayrange (-L/2, L/2)
302                        * (self.fftsink.sample_rate * sf / L))
303                       + self.fftsink.baseband_freq * sf)
304             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
305             points[:,0] = x_vals
306             points[:,1] = Numeric.concatenate ((dB[L/2:], dB[0:L/2]))
307
308
309         lines = plot.PolyLine (points, colour='BLUE')
310
311         graphics = plot.PlotGraphics ([lines],
312                                       title=self.fftsink.title,
313                                       xLabel = units, yLabel = "dB")
314
315         self.Draw (graphics, xAxis=None, yAxis=self.y_range)
316         self.update_y_range ()
317
318     def set_peak_hold(self, enable):
319         self.peak_hold = enable
320         self.peak_vals = None
321
322     def update_y_range (self):
323         ymax = self.fftsink.ref_level
324         ymin = self.fftsink.ref_level - self.fftsink.y_per_div * self.fftsink.y_divs
325         self.y_range = self._axisInterval ('min', ymin, ymax)
326
327     def on_average(self, evt):
328         # print "on_average"
329         self.fftsink.set_average(evt.IsChecked())
330
331     def on_peak_hold(self, evt):
332         # print "on_peak_hold"
333         self.fftsink.set_peak_hold(evt.IsChecked())
334
335     def on_incr_ref_level(self, evt):
336         # print "on_incr_ref_level"
337         self.fftsink.set_ref_level(self.fftsink.ref_level
338                                    + self.fftsink.y_per_div)
339
340     def on_decr_ref_level(self, evt):
341         # print "on_decr_ref_level"
342         self.fftsink.set_ref_level(self.fftsink.ref_level
343                                    - self.fftsink.y_per_div)
344
345     def on_incr_y_per_div(self, evt):
346         # print "on_incr_y_per_div"
347         self.fftsink.set_y_per_div(next_up(self.fftsink.y_per_div, (1,2,5,10,20)))
348
349     def on_decr_y_per_div(self, evt):
350         # print "on_decr_y_per_div"
351         self.fftsink.set_y_per_div(next_down(self.fftsink.y_per_div, (1,2,5,10,20)))
352
353     def on_y_per_div(self, evt):
354         # print "on_y_per_div"
355         Id = evt.GetId()
356         if Id == self.id_y_per_div_1:
357             self.fftsink.set_y_per_div(1)
358         elif Id == self.id_y_per_div_2:
359             self.fftsink.set_y_per_div(2)
360         elif Id == self.id_y_per_div_5:
361             self.fftsink.set_y_per_div(5)
362         elif Id == self.id_y_per_div_10:
363             self.fftsink.set_y_per_div(10)
364         elif Id == self.id_y_per_div_20:
365             self.fftsink.set_y_per_div(20)
366
367         
368     def on_right_click(self, event):
369         menu = self.popup_menu
370         for id, pred in self.checkmarks.items():
371             item = menu.FindItemById(id)
372             item.Check(pred())
373         self.PopupMenu(menu, event.GetPosition())
374
375
376     def build_popup_menu(self):
377         self.id_incr_ref_level = wx.NewId()
378         self.id_decr_ref_level = wx.NewId()
379         self.id_incr_y_per_div = wx.NewId()
380         self.id_decr_y_per_div = wx.NewId()
381         self.id_y_per_div_1 = wx.NewId()
382         self.id_y_per_div_2 = wx.NewId()
383         self.id_y_per_div_5 = wx.NewId()
384         self.id_y_per_div_10 = wx.NewId()
385         self.id_y_per_div_20 = wx.NewId()
386         self.id_average = wx.NewId()
387         self.id_peak_hold = wx.NewId()
388
389         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
390         self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
391         self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
392         self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
393         self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
394         self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
395         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
396         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
397         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
398         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
399         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
400
401
402         # make a menu
403         menu = wx.Menu()
404         self.popup_menu = menu
405         menu.AppendCheckItem(self.id_average, "Average")
406         menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
407         menu.Append(self.id_incr_ref_level, "Incr Ref Level")
408         menu.Append(self.id_decr_ref_level, "Decr Ref Level")
409         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
410         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
411         menu.AppendSeparator()
412         # we'd use RadioItems for these, but they're not supported on Mac
413         menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
414         menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
415         menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
416         menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
417         menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
418
419         self.checkmarks = {
420             self.id_average : lambda : self.fftsink.average,
421             self.id_peak_hold : lambda : self.fftsink.peak_hold,
422             self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
423             self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
424             self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
425             self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
426             self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
427             }
428
429
430 def next_up(v, seq):
431     """
432     Return the first item in seq that is > v.
433     """
434     for s in seq:
435         if s > v:
436             return s
437     return v
438
439 def next_down(v, seq):
440     """
441     Return the last item in seq that is < v.
442     """
443     rseq = list(seq[:])
444     rseq.reverse()
445
446     for s in rseq:
447         if s < v:
448             return s
449     return v
450
451
452 # ----------------------------------------------------------------
453 # Standalone test app
454 # ----------------------------------------------------------------
455
456 class test_app_block (stdgui2.std_top_block):
457     def __init__(self, frame, panel, vbox, argv):
458         stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
459
460         fft_size = 256
461
462         # build our flow graph
463         input_rate = 20.48e3
464
465         # Generate a complex sinusoid
466         #src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
467         src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
468
469         # We add these throttle blocks so that this demo doesn't
470         # suck down all the CPU available.  Normally you wouldn't use these.
471         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
472
473         sink1 = fft_sink_c (panel, title="Complex Data", fft_size=fft_size,
474                             sample_rate=input_rate, baseband_freq=100e3,
475                             ref_level=0, y_per_div=20)
476         vbox.Add (sink1.win, 1, wx.EXPAND)
477
478         self.define_component("src1", src1)
479         self.define_component("thr1", thr1)
480         self.define_component("sink1", sink1)
481
482         self.connect ("src1", 0, "thr1", 0)
483         self.connect ("thr1", 0, "sink1", 0)
484
485         #src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 2e3, 1)
486         src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1)
487         thr2 = gr.throttle(gr.sizeof_float, input_rate)
488         sink2 = fft_sink_f (panel, title="Real Data", fft_size=fft_size*2,
489                             sample_rate=input_rate, baseband_freq=100e3,
490                             ref_level=0, y_per_div=20)
491         vbox.Add (sink2.win, 1, wx.EXPAND)
492
493         self.define_component("src2", src2)
494         self.define_component("thr2", thr2)
495         self.define_component("sink2", sink2)
496         
497         self.connect ("src2", 0, "thr2", 0)
498         self.connect ("thr2", 0, "sink2", 0)
499
500 def main ():
501     app = stdgui2.stdapp (test_app_block,
502                          "FFT Sink Test App")
503     app.MainLoop ()
504
505 if __name__ == '__main__':
506     main ()