Houston, we have a trunk.
[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 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., 59 Temple Place - Suite 330,
20 # Boston, MA 02111-1307, 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.r_fd, self.w_fd) = os.pipe()
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.file_descriptor_sink(gr.sizeof_float * self.fft_size, self.w_fd)
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.file_descriptor_sink(gr.sizeof_float * self.fft_size, self.w_fd)
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, file_descriptor, fft_size, event_receiver, **kwds):
150         threading.Thread.__init__ (self, **kwds)
151         self.setDaemon (1)
152         self.file_descriptor = file_descriptor
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             s = gru.os_read_exactly (self.file_descriptor,
161                                      gr.sizeof_float * self.fft_size)
162             if not s:
163                 self.keep_running = False
164                 break
165
166             complex_data = Numeric.fromstring (s, Numeric.Float32)
167             de = DataEvent (complex_data)
168             wx.PostEvent (self.event_receiver, de)
169             del de
170     
171
172 class waterfall_window (wx.Panel):
173     def __init__ (self, fftsink, parent, id = -1,
174                   pos = wx.DefaultPosition, size = wx.DefaultSize,
175                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
176         wx.Panel.__init__(self, parent, id, pos, size, style, name)
177
178         self.fftsink = fftsink
179         self.bm = wx.EmptyBitmap(self.fftsink.fft_size, 300, -1)
180
181         self.scale_factor = 5.0           # FIXME should autoscale, or set this
182         
183         dc1 = wx.MemoryDC()
184         dc1.SelectObject(self.bm)
185         dc1.Clear()
186
187         self.pens = self.make_pens()
188
189         wx.EVT_PAINT( self, self.OnPaint )
190         wx.EVT_CLOSE (self, self.on_close_window)
191         EVT_DATA_EVENT (self, self.set_data)
192         
193         self.build_popup_menu()
194         
195         wx.EVT_CLOSE (self, self.on_close_window)
196         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
197
198         self.input_watcher = input_watcher(fftsink.r_fd, fftsink.fft_size, self)
199
200
201     def on_close_window (self, event):
202         print "waterfall_window: on_close_window"
203         self.keep_running = False
204
205     def const_list(self,const,len):
206         return [const] * len
207
208     def make_colormap(self):
209         r = []
210         r.extend(self.const_list(0,96))
211         r.extend(range(0,255,4))
212         r.extend(self.const_list(255,64))
213         r.extend(range(255,128,-4))
214         
215         g = []
216         g.extend(self.const_list(0,32))
217         g.extend(range(0,255,4))
218         g.extend(self.const_list(255,64))
219         g.extend(range(255,0,-4))
220         g.extend(self.const_list(0,32))
221         
222         b = range(128,255,4)
223         b.extend(self.const_list(255,64))
224         b.extend(range(255,0,-4))
225         b.extend(self.const_list(0,96))
226         return (r,g,b)
227
228     def make_pens(self):
229         (r,g,b) = self.make_colormap()
230         pens = []
231         for i in range(0,256):
232             colour = wx.Colour(r[i], g[i], b[i])
233             pens.append( wx.Pen(colour, 2, wx.SOLID))
234         return pens
235         
236     def OnPaint(self, event):
237         dc = wx.PaintDC(self)
238         self.DoDrawing(dc)
239
240     def DoDrawing(self, dc=None):
241         if dc is None:
242             dc = wx.ClientDC(self)
243         dc.DrawBitmap(self.bm, 0, 0, False )
244     
245
246     def const_list(self,const,len):
247         a = [const]
248         for i in range(1,len):
249             a.append(const)
250         return a
251
252     def make_colormap(self):
253         r = []
254         r.extend(self.const_list(0,96))
255         r.extend(range(0,255,4))
256         r.extend(self.const_list(255,64))
257         r.extend(range(255,128,-4))
258         
259         g = []
260         g.extend(self.const_list(0,32))
261         g.extend(range(0,255,4))
262         g.extend(self.const_list(255,64))
263         g.extend(range(255,0,-4))
264         g.extend(self.const_list(0,32))
265         
266         b = range(128,255,4)
267         b.extend(self.const_list(255,64))
268         b.extend(range(255,0,-4))
269         b.extend(self.const_list(0,96))
270         return (r,g,b)
271
272     def set_data (self, evt):
273         dB = evt.data
274         L = len (dB)
275
276         dc1 = wx.MemoryDC()
277         dc1.SelectObject(self.bm)
278         dc1.Blit(0,1,self.fftsink.fft_size,300,dc1,0,0,wx.COPY,False,-1,-1)
279
280         x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
281         if x >= 1e9:
282             sf = 1e-9
283             units = "GHz"
284         elif x >= 1e6:
285             sf = 1e-6
286             units = "MHz"
287         else:
288             sf = 1e-3
289             units = "kHz"
290
291
292         if self.fftsink.input_is_real:     # only plot 1/2 the points
293             d_max = L/2
294             p_width = 2
295         else:
296             d_max = L/2
297             p_width = 1
298
299         scale_factor = self.scale_factor
300         if self.fftsink.input_is_real:     # real fft
301            for x_pos in range(0, d_max):
302                value = int(dB[x_pos] * scale_factor)
303                value = min(255, max(0, value))
304                dc1.SetPen(self.pens[value])
305                dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1) 
306         else:                               # complex fft
307            for x_pos in range(0, d_max):    # positive freqs
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 + d_max, 0, p_width, 1) 
312            for x_pos in range(0 , d_max):   # negative freqs
313                value = int(dB[x_pos+d_max] * scale_factor)
314                value = min(255, max(0, value))
315                dc1.SetPen(self.pens[value])
316                dc1.DrawRectangle(x_pos*p_width, 0, p_width, 1) 
317
318         self.DoDrawing (None)
319
320     def on_average(self, evt):
321         # print "on_average"
322         self.fftsink.set_average(evt.IsChecked())
323
324     def on_right_click(self, event):
325         menu = self.popup_menu
326         for id, pred in self.checkmarks.items():
327             item = menu.FindItemById(id)
328             item.Check(pred())
329         self.PopupMenu(menu, event.GetPosition())
330
331
332     def build_popup_menu(self):
333         self.id_incr_ref_level = wx.NewId()
334         self.id_decr_ref_level = wx.NewId()
335         self.id_incr_y_per_div = wx.NewId()
336         self.id_decr_y_per_div = wx.NewId()
337         self.id_y_per_div_1 = wx.NewId()
338         self.id_y_per_div_2 = wx.NewId()
339         self.id_y_per_div_5 = wx.NewId()
340         self.id_y_per_div_10 = wx.NewId()
341         self.id_y_per_div_20 = wx.NewId()
342         self.id_average = wx.NewId()
343
344         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
345         #self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
346         #self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
347         #self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
348         #self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
349         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
350         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
351         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
352         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
353         #self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
354
355
356         # make a menu
357         menu = wx.Menu()
358         self.popup_menu = menu
359         menu.AppendCheckItem(self.id_average, "Average")
360         # menu.Append(self.id_incr_ref_level, "Incr Ref Level")
361         # menu.Append(self.id_decr_ref_level, "Decr Ref Level")
362         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
363         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
364         # menu.AppendSeparator()
365         # we'd use RadioItems for these, but they're not supported on Mac
366         #menu.AppendCheckItem(self.id_y_per_div_1, "1 dB/div")
367         #menu.AppendCheckItem(self.id_y_per_div_2, "2 dB/div")
368         #menu.AppendCheckItem(self.id_y_per_div_5, "5 dB/div")
369         #menu.AppendCheckItem(self.id_y_per_div_10, "10 dB/div")
370         #menu.AppendCheckItem(self.id_y_per_div_20, "20 dB/div")
371
372         self.checkmarks = {
373             self.id_average : lambda : self.fftsink.average
374             #self.id_y_per_div_1 : lambda : self.fftsink.y_per_div == 1,
375             #self.id_y_per_div_2 : lambda : self.fftsink.y_per_div == 2,
376             #self.id_y_per_div_5 : lambda : self.fftsink.y_per_div == 5,
377             #self.id_y_per_div_10 : lambda : self.fftsink.y_per_div == 10,
378             #self.id_y_per_div_20 : lambda : self.fftsink.y_per_div == 20,
379             }
380
381
382 def next_up(v, seq):
383     """
384     Return the first item in seq that is > v.
385     """
386     for s in seq:
387         if s > v:
388             return s
389     return v
390
391 def next_down(v, seq):
392     """
393     Return the last item in seq that is < v.
394     """
395     rseq = list(seq[:])
396     rseq.reverse()
397
398     for s in rseq:
399         if s < v:
400             return s
401     return v
402
403
404 # ----------------------------------------------------------------
405 #                     Deprecated interfaces
406 # ----------------------------------------------------------------
407
408 # returns (block, win).
409 #   block requires a single input stream of float
410 #   win is a subclass of wxWindow
411
412 def make_waterfall_sink_f(fg, parent, title, fft_size, input_rate):
413     
414     block = waterfall_sink_f(fg, parent, title=title, fft_size=fft_size,
415                              sample_rate=input_rate)
416     return (block, block.win)
417
418 # returns (block, win).
419 #   block requires a single input stream of gr_complex
420 #   win is a subclass of wxWindow
421
422 def make_waterfall_sink_c(fg, parent, title, fft_size, input_rate):
423     block = waterfall_sink_c(fg, parent, title=title, fft_size=fft_size,
424                              sample_rate=input_rate)
425     return (block, block.win)
426
427
428 # ----------------------------------------------------------------
429 # Standalone test app
430 # ----------------------------------------------------------------
431
432 class test_app_flow_graph (stdgui.gui_flow_graph):
433     def __init__(self, frame, panel, vbox, argv):
434         stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
435
436         fft_size = 512
437
438         # build our flow graph
439         input_rate = 20.000e3
440
441         # Generate a complex sinusoid
442         src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
443         #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
444
445         # We add these throttle blocks so that this demo doesn't
446         # suck down all the CPU available.  Normally you wouldn't use these.
447         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
448
449         sink1 = waterfall_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
450                                   sample_rate=input_rate, baseband_freq=100e3)
451         vbox.Add (sink1.win, 1, wx.EXPAND)
452         self.connect (src1, thr1, sink1)
453
454         # generate a real sinusoid
455         src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
456         #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
457         thr2 = gr.throttle(gr.sizeof_float, input_rate)
458         sink2 = waterfall_sink_f (self, panel, title="Real Data", fft_size=fft_size,
459                                   sample_rate=input_rate, baseband_freq=100e3)
460         vbox.Add (sink2.win, 1, wx.EXPAND)
461         self.connect (src2, thr2, sink2)
462
463 def main ():
464     app = stdgui.stdapp (test_app_flow_graph,
465                          "Waterfall Sink Test App")
466     app.MainLoop ()
467
468 if __name__ == '__main__':
469     main ()