Imported Upstream version 3.0.4
[debian/gnuradio] / gr-wxgui / src / python / waterfallsink.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2003,2004,2005 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 3, 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 stdgui
25 import wx
26 import gnuradio.wxgui.plot as plot
27 import Numeric
28 import os
29 import threading
30 import math    
31
32 default_fftsink_size = (640,240)
33 default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
34
35 class waterfall_sink_base(object):
36     def __init__(self, input_is_real=False, baseband_freq=0,
37                  sample_rate=1, fft_size=512,
38                  fft_rate=default_fft_rate,
39                  average=False, avg_alpha=None, title=''):
40
41         # initialize common attributes
42         self.baseband_freq = baseband_freq
43         self.sample_rate = sample_rate
44         self.fft_size = fft_size
45         self.fft_rate = fft_rate
46         self.average = average
47         if avg_alpha is None:
48             self.avg_alpha = 2.0 / fft_rate
49         else:
50             self.avg_alpha = avg_alpha
51         self.title = title
52         self.input_is_real = input_is_real
53         self.msgq = gr.msg_queue(2)         # queue up to 2 messages
54
55     def set_average(self, average):
56         self.average = average
57         if average:
58             self.avg.set_taps(self.avg_alpha)
59         else:
60             self.avg.set_taps(1.0)
61
62     def set_avg_alpha(self, avg_alpha):
63         self.avg_alpha = avg_alpha
64
65     def set_baseband_freq(self, baseband_freq):
66         self.baseband_freq = baseband_freq
67
68     def set_sample_rate(self, sample_rate):
69         self.sample_rate = sample_rate
70         self._set_n()
71
72     def _set_n(self):
73         self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
74         
75 class waterfall_sink_f(gr.hier_block, waterfall_sink_base):
76     def __init__(self, fg, parent, baseband_freq=0,
77                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
78                  fft_rate=default_fft_rate, average=False, avg_alpha=None,
79                  title='', size=default_fftsink_size):
80
81         waterfall_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
82                                sample_rate=sample_rate, fft_size=fft_size,
83                                fft_rate=fft_rate,
84                                average=average, avg_alpha=avg_alpha, title=title)
85                                
86         s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
87         self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
88                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
89         mywindow = window.blackmanharris(self.fft_size)
90         fft = gr.fft_vfc(self.fft_size, True, mywindow)
91         c2mag = gr.complex_to_mag(self.fft_size)
92         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
93         log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
94         sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
95
96         fg.connect (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
97         gr.hier_block.__init__(self, fg, s2p, sink)
98
99         self.win = waterfall_window(self, parent, size=size)
100         self.set_average(self.average)
101
102
103 class waterfall_sink_c(gr.hier_block, waterfall_sink_base):
104     def __init__(self, fg, parent, baseband_freq=0,
105                  y_per_div=10, ref_level=50, sample_rate=1, fft_size=512,
106                  fft_rate=default_fft_rate, average=False, avg_alpha=None, 
107                  title='', size=default_fftsink_size):
108
109         waterfall_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
110                                      sample_rate=sample_rate, fft_size=fft_size,
111                                      fft_rate=fft_rate,
112                                      average=average, avg_alpha=avg_alpha, title=title)
113
114         s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
115         self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
116                                          max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
117
118         mywindow = window.blackmanharris(self.fft_size)
119         fft = gr.fft_vcc(self.fft_size, True, mywindow)
120         c2mag = gr.complex_to_mag(self.fft_size)
121         self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
122         log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
123         sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
124
125         fg.connect(s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
126         gr.hier_block.__init__(self, fg, s2p, sink)
127
128         self.win = waterfall_window(self, parent, size=size)
129         self.set_average(self.average)
130
131
132 # ------------------------------------------------------------------------
133
134 myDATA_EVENT = wx.NewEventType()
135 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
136
137
138 class DataEvent(wx.PyEvent):
139     def __init__(self, data):
140         wx.PyEvent.__init__(self)
141         self.SetEventType (myDATA_EVENT)
142         self.data = data
143
144     def Clone (self): 
145         self.__class__ (self.GetId())
146
147
148 class input_watcher (threading.Thread):
149     def __init__ (self, msgq, fft_size, event_receiver, **kwds):
150         threading.Thread.__init__ (self, **kwds)
151         self.setDaemon (1)
152         self.msgq = msgq
153         self.fft_size = fft_size
154         self.event_receiver = event_receiver
155         self.keep_running = True
156         self.start ()
157
158     def run (self):
159         while (self.keep_running):
160             msg = self.msgq.delete_head()  # blocking read of message queue
161             itemsize = int(msg.arg1())
162             nitems = int(msg.arg2())
163
164             s = msg.to_string()            # get the body of the msg as a string
165
166             # There may be more than one FFT frame in the message.
167             # If so, we take only the last one
168             if nitems > 1:
169                 start = itemsize * (nitems - 1)
170                 s = s[start:start+itemsize]
171
172             complex_data = Numeric.fromstring (s, Numeric.Float32)
173             de = DataEvent (complex_data)
174             wx.PostEvent (self.event_receiver, de)
175             del de
176     
177
178 class waterfall_window (wx.Panel):
179     def __init__ (self, fftsink, parent, id = -1,
180                   pos = wx.DefaultPosition, size = wx.DefaultSize,
181                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
182         wx.Panel.__init__(self, parent, id, pos, size, style, name)
183
184         self.fftsink = fftsink
185         self.bm = wx.EmptyBitmap(self.fftsink.fft_size, 300, -1)
186
187         self.scale_factor = 5.0           # FIXME should autoscale, or set this
188         
189         dc1 = wx.MemoryDC()
190         dc1.SelectObject(self.bm)
191         dc1.Clear()
192
193         self.pens = self.make_pens()
194
195         wx.EVT_PAINT( self, self.OnPaint )
196         wx.EVT_CLOSE (self, self.on_close_window)
197         EVT_DATA_EVENT (self, self.set_data)
198         
199         self.build_popup_menu()
200         
201         wx.EVT_CLOSE (self, self.on_close_window)
202         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
203
204         self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
205
206
207     def on_close_window (self, event):
208         print "waterfall_window: on_close_window"
209         self.keep_running = False
210
211     def const_list(self,const,len):
212         return [const] * len
213
214     def make_colormap(self):
215         r = []
216         r.extend(self.const_list(0,96))
217         r.extend(range(0,255,4))
218         r.extend(self.const_list(255,64))
219         r.extend(range(255,128,-4))
220         
221         g = []
222         g.extend(self.const_list(0,32))
223         g.extend(range(0,255,4))
224         g.extend(self.const_list(255,64))
225         g.extend(range(255,0,-4))
226         g.extend(self.const_list(0,32))
227         
228         b = range(128,255,4)
229         b.extend(self.const_list(255,64))
230         b.extend(range(255,0,-4))
231         b.extend(self.const_list(0,96))
232         return (r,g,b)
233
234     def make_pens(self):
235         (r,g,b) = self.make_colormap()
236         pens = []
237         for i in range(0,256):
238             colour = wx.Colour(r[i], g[i], b[i])
239             pens.append( wx.Pen(colour, 2, wx.SOLID))
240         return pens
241         
242     def OnPaint(self, event):
243         dc = wx.PaintDC(self)
244         self.DoDrawing(dc)
245
246     def DoDrawing(self, dc=None):
247         if dc is None:
248             dc = wx.ClientDC(self)
249         dc.DrawBitmap(self.bm, 0, 0, False )
250     
251
252     def const_list(self,const,len):
253         a = [const]
254         for i in range(1,len):
255             a.append(const)
256         return a
257
258     def make_colormap(self):
259         r = []
260         r.extend(self.const_list(0,96))
261         r.extend(range(0,255,4))
262         r.extend(self.const_list(255,64))
263         r.extend(range(255,128,-4))
264         
265         g = []
266         g.extend(self.const_list(0,32))
267         g.extend(range(0,255,4))
268         g.extend(self.const_list(255,64))
269         g.extend(range(255,0,-4))
270         g.extend(self.const_list(0,32))
271         
272         b = range(128,255,4)
273         b.extend(self.const_list(255,64))
274         b.extend(range(255,0,-4))
275         b.extend(self.const_list(0,96))
276         return (r,g,b)
277
278     def set_data (self, evt):
279         dB = evt.data
280         L = len (dB)
281
282         dc1 = wx.MemoryDC()
283         dc1.SelectObject(self.bm)
284         dc1.Blit(0,1,self.fftsink.fft_size,300,dc1,0,0,wx.COPY,False,-1,-1)
285
286         x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
287         if x >= 1e9:
288             sf = 1e-9
289             units = "GHz"
290         elif x >= 1e6:
291             sf = 1e-6
292             units = "MHz"
293         else:
294             sf = 1e-3
295             units = "kHz"
296
297
298         if self.fftsink.input_is_real:     # only plot 1/2 the points
299             d_max = L/2
300             p_width = 2
301         else:
302             d_max = L/2
303             p_width = 1
304
305         scale_factor = self.scale_factor
306         if self.fftsink.input_is_real:     # real fft
307            for x_pos in range(0, d_max):
308                value = int(dB[x_pos] * scale_factor)
309                value = min(255, max(0, value))
310                dc1.SetPen(self.pens[value])
311                dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1) 
312         else:                               # complex fft
313            for x_pos in range(0, d_max):    # positive freqs
314                value = int(dB[x_pos] * scale_factor)
315                value = min(255, max(0, value))
316                dc1.SetPen(self.pens[value])
317                dc1.DrawRectangle(x_pos*p_width + d_max, 0, p_width, 1) 
318            for x_pos in range(0 , d_max):   # negative freqs
319                value = int(dB[x_pos+d_max] * scale_factor)
320                value = min(255, max(0, value))
321                dc1.SetPen(self.pens[value])
322                dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1) 
323
324         self.DoDrawing (None)
325
326     def on_average(self, evt):
327         # print "on_average"
328         self.fftsink.set_average(evt.IsChecked())
329
330     def on_right_click(self, event):
331         menu = self.popup_menu
332         for id, pred in self.checkmarks.items():
333             item = menu.FindItemById(id)
334             item.Check(pred())
335         self.PopupMenu(menu, event.GetPosition())
336
337
338     def build_popup_menu(self):
339         self.id_incr_ref_level = wx.NewId()
340         self.id_decr_ref_level = wx.NewId()
341         self.id_incr_y_per_div = wx.NewId()
342         self.id_decr_y_per_div = wx.NewId()
343         self.id_y_per_div_1 = wx.NewId()
344         self.id_y_per_div_2 = wx.NewId()
345         self.id_y_per_div_5 = wx.NewId()
346         self.id_y_per_div_10 = wx.NewId()
347         self.id_y_per_div_20 = wx.NewId()
348         self.id_average = wx.NewId()
349
350         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
351         #self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
352         #self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
353         #self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
354         #self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
355         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
356         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
357         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
358         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
359         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
360
361
362         # make a menu
363         menu = wx.Menu()
364         self.popup_menu = menu
365         menu.AppendCheckItem(self.id_average, "Average")
366         # menu.Append(self.id_incr_ref_level, "Incr Ref Level")
367         # menu.Append(self.id_decr_ref_level, "Decr Ref Level")
368         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
369         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
370         # menu.AppendSeparator()
371         # we'd use RadioItems for these, but they're not supported on Mac
372         #menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
373         #menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
374         #menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
375         #menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
376         #menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
377
378         self.checkmarks = {
379             self.id_average : lambda : self.fftsink.average
380             #self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
381             #self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
382             #self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
383             #self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
384             #self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
385             }
386
387
388 def next_up(v, seq):
389     """
390     Return the first item in seq that is > v.
391     """
392     for s in seq:
393         if s > v:
394             return s
395     return v
396
397 def next_down(v, seq):
398     """
399     Return the last item in seq that is < v.
400     """
401     rseq = list(seq[:])
402     rseq.reverse()
403
404     for s in rseq:
405         if s < v:
406             return s
407     return v
408
409
410 # ----------------------------------------------------------------
411 #                     Deprecated interfaces
412 # ----------------------------------------------------------------
413
414 # returns (block, win).
415 #   block requires a single input stream of float
416 #   win is a subclass of wxWindow
417
418 def make_waterfall_sink_f(fg, parent, title, fft_size, input_rate):
419     
420     block = waterfall_sink_f(fg, parent, title=title, fft_size=fft_size,
421                              sample_rate=input_rate)
422     return (block, block.win)
423
424 # returns (block, win).
425 #   block requires a single input stream of gr_complex
426 #   win is a subclass of wxWindow
427
428 def make_waterfall_sink_c(fg, parent, title, fft_size, input_rate):
429     block = waterfall_sink_c(fg, parent, title=title, fft_size=fft_size,
430                              sample_rate=input_rate)
431     return (block, block.win)
432
433
434 # ----------------------------------------------------------------
435 # Standalone test app
436 # ----------------------------------------------------------------
437
438 class test_app_flow_graph (stdgui.gui_flow_graph):
439     def __init__(self, frame, panel, vbox, argv):
440         stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
441
442         fft_size = 512
443
444         # build our flow graph
445         input_rate = 20.000e3
446
447         # Generate a complex sinusoid
448         src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
449         #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
450
451         # We add these throttle blocks so that this demo doesn't
452         # suck down all the CPU available.  Normally you wouldn't use these.
453         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
454
455         sink1 = waterfall_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
456                                   sample_rate=input_rate, baseband_freq=100e3)
457         vbox.Add (sink1.win, 1, wx.EXPAND)
458         self.connect (src1, thr1, sink1)
459
460         # generate a real sinusoid
461         src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
462         #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
463         thr2 = gr.throttle(gr.sizeof_float, input_rate)
464         sink2 = waterfall_sink_f (self, panel, title="Real Data", fft_size=fft_size,
465                                   sample_rate=input_rate, baseband_freq=100e3)
466         vbox.Add (sink2.win, 1, wx.EXPAND)
467         self.connect (src2, thr2, sink2)
468
469 def main ():
470     app = stdgui.stdapp (test_app_flow_graph,
471                          "Waterfall Sink Test App")
472     app.MainLoop ()
473
474 if __name__ == '__main__':
475     main ()