3 # Copyright 2003,2004,2005 Free Software Foundation, Inc.
5 # This file is part of GNU Radio
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)
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.
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.
23 from gnuradio import gr, gru, window
24 from gnuradio.wxgui import stdgui
26 import gnuradio.wxgui.plot as plot
32 default_fftsink_size = (640,240)
33 default_fft_rate = gr.prefs().get_long('wxgui', 'fft_rate', 15)
35 def axis_design( x1, x2, nx ):
36 # Given start, end, and number of labels, return value of first label,
37 # increment between labels, number of unlabeled division between labels,
40 dx = abs( x2 - x1 )/float(nx+1) # allow for space at each end
59 inc = c*pow( 10., le )
60 first = math.ceil( x1/inc )*inc
62 while ( abs(x1*scale) >= 1e5 ) or ( abs(x2*scale) >= 1e5 ):
64 return ( first, inc, dt, scale )
67 class waterfall_sink_base(object):
68 def __init__(self, input_is_real=False, baseband_freq=0,
69 sample_rate=1, fft_size=512,
70 fft_rate=default_fft_rate,
71 average=False, avg_alpha=None, title='', ofunc=None, xydfunc=None):
73 # initialize common attributes
74 self.baseband_freq = baseband_freq
75 self.sample_rate = sample_rate
76 self.fft_size = fft_size
77 self.fft_rate = fft_rate
78 self.average = average
80 self.xydfunc = xydfunc
82 self.avg_alpha = 2.0 / fft_rate
84 self.avg_alpha = avg_alpha
86 self.input_is_real = input_is_real
87 self.msgq = gr.msg_queue(2) # queue up to 2 messages
89 def reconnect( self, fg ):
90 fg.connect( *self.block_list )
92 def set_average(self, average):
93 self.average = average
95 self.avg.set_taps(self.avg_alpha)
97 self.avg.set_taps(1.0)
99 def set_avg_alpha(self, avg_alpha):
100 self.avg_alpha = avg_alpha
102 def set_baseband_freq(self, baseband_freq):
103 self.baseband_freq = baseband_freq
105 def set_sample_rate(self, sample_rate):
106 self.sample_rate = sample_rate
110 self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
112 class waterfall_sink_f(gr.hier_block, waterfall_sink_base):
113 def __init__(self, fg, parent, baseband_freq=0,
114 ref_level=0, sample_rate=1, fft_size=512,
115 fft_rate=default_fft_rate, average=False, avg_alpha=None,
116 title='', size=default_fftsink_size, report=None, span=40, ofunc=None, xydfunc=None):
118 waterfall_sink_base.__init__(self, input_is_real=True,
119 baseband_freq=baseband_freq,
120 sample_rate=sample_rate,
121 fft_size=fft_size, fft_rate=fft_rate,
122 average=average, avg_alpha=avg_alpha,
125 s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
126 self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
127 max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
128 mywindow = window.blackmanharris(self.fft_size)
129 fft = gr.fft_vfc(self.fft_size, True, mywindow)
130 c2mag = gr.complex_to_mag(self.fft_size)
131 self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
132 log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
133 sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
135 self.block_list = (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
137 gr.hier_block.__init__(self, fg, s2p, sink)
139 self.win = waterfall_window(self, parent, size=size, report=report,
140 ref_level=ref_level, span=span, ofunc=ofunc, xydfunc=xydfunc)
141 self.set_average(self.average)
144 class waterfall_sink_c(gr.hier_block, waterfall_sink_base):
145 def __init__(self, fg, parent, baseband_freq=0,
146 ref_level=0, sample_rate=1, fft_size=512,
147 fft_rate=default_fft_rate, average=False, avg_alpha=None,
148 title='', size=default_fftsink_size, report=None, span=40, ofunc=None, xydfunc=None):
150 waterfall_sink_base.__init__(self, input_is_real=False,
151 baseband_freq=baseband_freq,
152 sample_rate=sample_rate,
155 average=average, avg_alpha=avg_alpha,
158 s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
159 self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
160 max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
162 mywindow = window.blackmanharris(self.fft_size)
163 fft = gr.fft_vcc(self.fft_size, True, mywindow)
164 c2mag = gr.complex_to_mag(self.fft_size)
165 self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
166 log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
167 sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
169 self.block_list = (s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
171 gr.hier_block.__init__(self, fg, s2p, sink)
173 self.win = waterfall_window(self, parent, size=size, report=report,
174 ref_level=ref_level, span=span, ofunc=ofunc, xydfunc=xydfunc)
175 self.set_average(self.average)
178 # ------------------------------------------------------------------------
180 myDATA_EVENT = wx.NewEventType()
181 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
184 class DataEvent(wx.PyEvent):
185 def __init__(self, data):
186 wx.PyEvent.__init__(self)
187 self.SetEventType (myDATA_EVENT)
191 self.__class__ (self.GetId())
194 class input_watcher (threading.Thread):
195 def __init__ (self, msgq, fft_size, event_receiver, **kwds):
196 threading.Thread.__init__ (self, **kwds)
199 self.fft_size = fft_size
200 self.event_receiver = event_receiver
201 self.keep_running = True
205 while (self.keep_running):
206 msg = self.msgq.delete_head() # blocking read of message queue
207 itemsize = int(msg.arg1())
208 nitems = int(msg.arg2())
210 s = msg.to_string() # get the body of the msg as a string
212 # There may be more than one FFT frame in the message.
213 # If so, we take only the last one
215 start = itemsize * (nitems - 1)
216 s = s[start:start+itemsize]
218 complex_data = numpy.fromstring (s, numpy.float32)
219 de = DataEvent (complex_data)
220 wx.PostEvent (self.event_receiver, de)
224 class waterfall_window (wx.ScrolledWindow):
225 def __init__ (self, fftsink, parent, id = -1,
226 pos = wx.DefaultPosition, size = wx.DefaultSize,
227 style = wx.DEFAULT_FRAME_STYLE, name = "", report=None,
228 ref_level = 0, span = 50, ofunc=None, xydfunc=None):
229 wx.ScrolledWindow.__init__(self, parent, id, pos, size,
230 style|wx.HSCROLL, name)
232 self.SetCursor(wx.StockCursor(wx.CURSOR_IBEAM))
233 self.ref_level = ref_level
234 self.scale_factor = 256./span
236 self.ppsh = 128 # pixels per scroll, horizontal
237 self.SetScrollbars( self.ppsh, 0, fftsink.fft_size/self.ppsh, 0 )
239 self.fftsink = fftsink
243 self.xydfunc = xydfunc
246 dc1.SetFont( wx.SMALL_FONT )
247 self.h_scale = dc1.GetCharHeight() + 3
248 #self.bm_size = ( self.fftsink.fft_size, self.size[1] - self.h_scale )
249 self.im_size = ( self.fftsink.fft_size, self.size[1] - self.h_scale )
250 #self.bm = wx.EmptyBitmap( self.bm_size[0], self.bm_size[1], -1)
251 self.im = wx.EmptyImage( self.im_size[0], self.im_size[1], True )
254 self.baseband_freq = None
258 wx.EVT_PAINT( self, self.OnPaint )
259 wx.EVT_CLOSE (self, self.on_close_window)
260 #wx.EVT_LEFT_UP(self, self.on_left_up)
261 #wx.EVT_LEFT_DOWN(self, self.on_left_down)
262 EVT_DATA_EVENT (self, self.set_data)
264 self.build_popup_menu()
266 wx.EVT_CLOSE (self, self.on_close_window)
267 self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
268 self.Bind(wx.EVT_MOTION, self.on_motion)
272 self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
274 def on_close_window (self, event):
275 self.keep_running = False
277 def on_left_down( self, evt ):
278 self.down_pos = evt.GetPosition()
279 self.down_time = evt.GetTimestamp()
281 def on_left_up( self, evt ):
283 dt = ( evt.GetTimestamp() - self.down_time )/1000.
284 pph = self.fftsink.fft_size/float(self.fftsink.sample_rate)
285 dx = evt.GetPosition()[0] - self.down_pos[0]
290 t = 'Down time: %f Delta f: %f Period: %f' % ( dt, dx/pph, rt )
295 def on_motion(self, event):
297 pos = event.GetPosition()
301 def const_list(self,const,len):
304 def make_colormap(self):
306 r.extend(self.const_list(0,96))
307 r.extend(range(0,255,4))
308 r.extend(self.const_list(255,64))
309 r.extend(range(255,128,-4))
312 g.extend(self.const_list(0,32))
313 g.extend(range(0,255,4))
314 g.extend(self.const_list(255,64))
315 g.extend(range(255,0,-4))
316 g.extend(self.const_list(0,32))
319 b.extend(self.const_list(255,64))
320 b.extend(range(255,0,-4))
321 b.extend(self.const_list(0,96))
325 (r,g,b) = self.make_colormap()
326 self.rgb = numpy.transpose( numpy.array( (r,g,b) ).astype(numpy.int8) )
328 def OnPaint(self, event):
329 dc = wx.BufferedPaintDC(self)
332 def DoDrawing(self,dc):
333 w, h = self.GetClientSizeTuple()
334 w = min( w, self.fftsink.fft_size )
339 dc = wx.BufferedDC( wx.ClientDC(self), (w,h) )
341 dc.SetBackground( wx.Brush( self.GetBackgroundColour(), wx.SOLID ) )
344 x, y = self.GetViewStart()
347 ih = min( h - self.h_scale, self.im_size[1] - self.im_cur )
348 r = wx.Rect( x, self.im_cur, w, ih )
349 bm = wx.BitmapFromImage( self.im.GetSubImage(r) )
350 dc.DrawBitmap( bm, 0, self.h_scale )
351 rem = min( self.im_size[1] - ih, h - ih - self.h_scale )
353 r = wx.Rect( x, 0, w, rem )
354 bm = wx.BitmapFromImage( self.im.GetSubImage(r) )
355 dc.DrawBitmap( bm, 0, ih + self.h_scale )
358 if self.baseband_freq != self.fftsink.baseband_freq:
359 self.baseband_freq = self.fftsink.baseband_freq
360 t = self.fftsink.sample_rate*w/float(self.fftsink.fft_size)
361 self.ax_spec = axis_design( self.baseband_freq - t/2,
362 self.baseband_freq + t/2, 7 )
363 dc.SetFont( wx.SMALL_FONT )
364 fo = self.baseband_freq
365 po = self.fftsink.fft_size/2
366 pph = self.fftsink.fft_size/float(self.fftsink.sample_rate)
367 f = math.floor((fo-po/pph)/self.ax_spec[1])*self.ax_spec[1]
369 t = po + ( f - fo )*pph
370 s = str( f*self.ax_spec[3] )
371 e = dc.GetTextExtent( s )
372 if t - e[1]/2 >= x + w:
374 dc.DrawText( s, t - x - e[0]/2, 0 )
375 dc.DrawLine( t - x, e[1] - 1, t - x, self.h_scale )
376 dt = self.ax_spec[1]/self.ax_spec[2]*pph
377 for i in range(self.ax_spec[2]-1):
381 dc.DrawLine( t - x, e[1] + 1, t - x, self.h_scale )
384 def const_list(self,const,len):
386 for i in range(1,len):
390 def make_colormap(self):
392 r.extend(self.const_list(0,96))
393 r.extend(range(0,255,4))
394 r.extend(self.const_list(255,64))
395 r.extend(range(255,128,-4))
398 g.extend(self.const_list(0,32))
399 g.extend(range(0,255,4))
400 g.extend(self.const_list(255,64))
401 g.extend(range(255,0,-4))
402 g.extend(self.const_list(0,32))
405 b.extend(self.const_list(255,64))
406 b.extend(range(255,0,-4))
407 b.extend(self.const_list(0,96))
410 def set_data (self, evt):
414 if self.ofunc != None:
415 self.ofunc(evt.data, L)
417 #dc1.SelectObject(self.bm)
419 # Scroll existing bitmap
421 #dc1.Blit(0,1,self.bm_size[0],self.bm_size[1]-1,dc1,0,0,
422 # wx.COPY,False,-1,-1)
425 for i in range( self.bm_size[1]-1, 0, -1 ):
426 dc1.Blit( 0, i, self.bm_size[0], 1, dc1, 0, i-1 )
428 x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
440 if self.fftsink.input_is_real: # only plot 1/2 the points
447 scale_factor = self.scale_factor
450 dB = dB.astype(numpy.int_).clip( min=0, max=255 )
451 if self.fftsink.input_is_real: # real fft
452 dB = numpy.array( ( dB[0:d_max][::-1], dB[0:d_max] ) )
454 dB = numpy.concatenate( ( dB[d_max:L], dB[0:d_max] ) )
457 img = wx.ImageFromData( L, 1, dB.ravel().tostring() )
458 #bm = wx.BitmapFromImage( img )
459 #dc1.DrawBitmap( bm, 0, 0 )
460 ibuf = self.im.GetDataBuffer()
463 self.im_cur = self.im_size[1] - 1
464 start = 3*self.im_cur*self.im_size[0]
465 ibuf[start:start+3*self.im_size[0]] = img.GetData()
470 def on_average(self, evt):
472 self.fftsink.set_average(evt.IsChecked())
474 def on_right_click(self, event):
475 menu = self.popup_menu
476 self.PopupMenu(menu, event.GetPosition())
479 def build_popup_menu(self):
480 id_ref_gain = wx.NewId()
481 self.Bind( wx.EVT_MENU, self.on_ref_gain, id=id_ref_gain )
485 self.popup_menu = menu
486 menu.Append( id_ref_gain, "Ref Level and Gain" )
487 self.rg_dialog = None
490 #self.id_average : lambda : self.fftsink.average
493 def on_ref_gain( self, evt ):
494 if self.rg_dialog == None:
495 self.rg_dialog = rg_dialog( self.parent, self.set_ref_gain,
497 span=256./self.scale_factor )
498 self.rg_dialog.Show( True )
500 def set_ref_gain( self, ref, span ):
502 self.scale_factor = 256/span
504 class rg_dialog( wx.Dialog ):
505 def __init__( self, parent, set_function, ref=0, span=256./5. ):
506 wx.Dialog.__init__( self, parent, -1, "Waterfall Settings" )
507 self.set_function = set_function
508 #status_bar = wx.StatusBar( self, -1 )
510 d_sizer = wx.BoxSizer( wx.VERTICAL ) # dialog sizer
511 f_sizer = wx.BoxSizer( wx.VERTICAL ) # form sizer
514 #f_sizer.Add( fn_sizer, 0, flag=wx.TOP, border=10 )
516 h_sizer = wx.BoxSizer( wx.HORIZONTAL )
517 self.ref = tab_item( self, "Ref Level:", 4, "dB" )
518 self.ref.ctrl.SetValue( "%d" % ref )
520 h_sizer.Add( self.ref, 0 )
522 self.span = tab_item( self, "Range:", 4, "dB" )
523 self.span.ctrl.SetValue( "%d" % span )
524 h_sizer.Add( self.span, 0 )
526 f_sizer.Add( h_sizer, 0, flag=wx.TOP|wx.EXPAND, border=vs )
529 d_sizer.Add( f_sizer, 0, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND )
533 button_sizer = wx.BoxSizer( wx.HORIZONTAL )
534 apply_button = wx.Button( self, -1, "Apply" )
535 apply_button.Bind( wx.EVT_BUTTON, self.apply_evt )
536 cancel_button = wx.Button( self, -1, "Cancel" )
537 cancel_button.Bind( wx.EVT_BUTTON, self.cancel_evt )
538 ok_button = wx.Button( self, -1, "OK" )
539 ok_button.Bind( wx.EVT_BUTTON, self.ok_evt )
540 button_sizer.Add((0,0),1)
541 button_sizer.Add( apply_button, 0,
542 flag=wx.ALIGN_CENTER_HORIZONTAL )
543 button_sizer.Add((0,0),1)
544 button_sizer.Add( cancel_button, 0,
545 flag=wx.ALIGN_CENTER_HORIZONTAL )
546 button_sizer.Add((0,0),1)
547 button_sizer.Add( ok_button, 0,
548 flag=wx.ALIGN_CENTER_HORIZONTAL )
549 button_sizer.Add((0,0),1)
550 d_sizer.Add( button_sizer, 0,
551 flag=wx.EXPAND|wx.ALIGN_CENTER|wx.BOTTOM, border=30 )
552 self.SetSizer( d_sizer )
554 def apply_evt( self, evt ):
557 def cancel_evt( self, evt ):
560 def ok_evt( self, evt ):
564 def do_apply( self ):
565 r = float( self.ref.ctrl.GetValue() )
566 g = float( self.span.ctrl.GetValue() )
567 self.set_function( r, g )
571 Return the first item in seq that is > v.
578 def next_down(v, seq):
580 Return the last item in seq that is < v.
590 # One of many copies that should be consolidated . . .
591 def tab_item( parent, label, chars, units, style=wx.TE_RIGHT, value="" ):
592 s = wx.BoxSizer( wx.HORIZONTAL )
593 s.Add( wx.StaticText( parent, -1, label ), 0,
594 flag=wx.ALIGN_CENTER_VERTICAL )
595 s.ctrl = wx.TextCtrl( parent, -1, style=style, value=value )
596 s.ctrl.SetMinSize( ( (1.00+chars)*s.ctrl.GetCharWidth(),
597 1.25*s.ctrl.GetCharHeight() ) )
598 s.Add( s.ctrl, -1, flag=wx.LEFT, border=3 )
599 s.Add( wx.StaticText( parent, -1, units ), 0,
600 flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1 )
604 # ----------------------------------------------------------------
605 # Deprecated interfaces
606 # ----------------------------------------------------------------
608 # returns (block, win).
609 # block requires a single input stream of float
610 # win is a subclass of wxWindow
612 def make_waterfall_sink_f(fg, parent, title, fft_size, input_rate):
614 block = waterfall_sink_f(fg, parent, title=title, fft_size=fft_size,
615 sample_rate=input_rate)
616 return (block, block.win)
618 # returns (block, win).
619 # block requires a single input stream of gr_complex
620 # win is a subclass of wxWindow
622 def make_waterfall_sink_c(fg, parent, title, fft_size, input_rate):
623 block = waterfall_sink_c(fg, parent, title=title, fft_size=fft_size,
624 sample_rate=input_rate)
625 return (block, block.win)
628 # ----------------------------------------------------------------
629 # Standalone test app
630 # ----------------------------------------------------------------
632 class test_app_flow_graph (stdgui.gui_flow_graph):
633 def __init__(self, frame, panel, vbox, argv):
634 stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
638 # build our flow graph
639 input_rate = 20.000e3
641 # Generate a complex sinusoid
642 src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
643 #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
645 # We add these throttle blocks so that this demo doesn't
646 # suck down all the CPU available. Normally you wouldn't use these.
647 thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
649 sink1 = waterfall_sink_c (self, panel, title="Complex Data",
651 sample_rate=input_rate, baseband_freq=0,
653 vbox.Add (sink1.win, 1, wx.EXPAND)
654 self.connect (src1, thr1, sink1)
656 # generate a real sinusoid
657 src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
658 #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
659 thr2 = gr.throttle(gr.sizeof_float, input_rate)
660 sink2 = waterfall_sink_f (self, panel, title="Real Data", fft_size=fft_size,
661 sample_rate=input_rate, baseband_freq=0)
662 vbox.Add (sink2.win, 1, wx.EXPAND)
663 self.connect (src2, thr2, sink2)
666 app = stdgui.stdapp (test_app_flow_graph,
667 "Waterfall Sink Test App")
670 if __name__ == '__main__':