3 # Copyright 2003,2004,2005,2007 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 stdgui2
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 set_average(self, average):
90 self.average = average
92 self.avg.set_taps(self.avg_alpha)
94 self.avg.set_taps(1.0)
96 def set_avg_alpha(self, avg_alpha):
97 self.avg_alpha = avg_alpha
99 def set_baseband_freq(self, baseband_freq):
100 self.baseband_freq = baseband_freq
102 def set_sample_rate(self, sample_rate):
103 self.sample_rate = sample_rate
107 self.one_in_n.set_n(max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
109 class waterfall_sink_f(gr.hier_block2, waterfall_sink_base):
110 def __init__(self, parent, baseband_freq=0,
111 ref_level=0, sample_rate=1, fft_size=512,
112 fft_rate=default_fft_rate, average=False, avg_alpha=None,
113 title='', size=default_fftsink_size, report=None, span=40, ofunc=None, xydfunc=None):
115 gr.hier_block2.__init__(self, "waterfall_sink_f",
116 gr.io_signature(1, 1, gr.sizeof_float),
117 gr.io_signature(0, 0, 0))
119 waterfall_sink_base.__init__(self, input_is_real=True,
120 baseband_freq=baseband_freq,
121 sample_rate=sample_rate,
122 fft_size=fft_size, fft_rate=fft_rate,
123 average=average, avg_alpha=avg_alpha,
126 s2p = gr.serial_to_parallel(gr.sizeof_float, self.fft_size)
127 self.one_in_n = gr.keep_one_in_n(gr.sizeof_float * self.fft_size,
128 max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
129 mywindow = window.blackmanharris(self.fft_size)
130 fft = gr.fft_vfc(self.fft_size, True, mywindow)
131 c2mag = gr.complex_to_mag(self.fft_size)
132 self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
133 log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
134 sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
136 self.connect(self, s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
137 self.win = waterfall_window(self, parent, size=size, report=report,
138 ref_level=ref_level, span=span, ofunc=ofunc, xydfunc=xydfunc)
139 self.set_average(self.average)
142 class waterfall_sink_c(gr.hier_block2, waterfall_sink_base):
143 def __init__(self, parent, baseband_freq=0,
144 ref_level=0, sample_rate=1, fft_size=512,
145 fft_rate=default_fft_rate, average=False, avg_alpha=None,
146 title='', size=default_fftsink_size, report=None, span=40, ofunc=None, xydfunc=None):
148 gr.hier_block2.__init__(self, "waterfall_sink_c",
149 gr.io_signature(1, 1, gr.sizeof_gr_complex),
150 gr.io_signature(0, 0, 0))
152 waterfall_sink_base.__init__(self, input_is_real=False,
153 baseband_freq=baseband_freq,
154 sample_rate=sample_rate,
157 average=average, avg_alpha=avg_alpha,
160 s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, self.fft_size)
161 self.one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * self.fft_size,
162 max(1, int(self.sample_rate/self.fft_size/self.fft_rate)))
164 mywindow = window.blackmanharris(self.fft_size)
165 fft = gr.fft_vcc(self.fft_size, True, mywindow)
166 c2mag = gr.complex_to_mag(self.fft_size)
167 self.avg = gr.single_pole_iir_filter_ff(1.0, self.fft_size)
168 log = gr.nlog10_ff(20, self.fft_size, -20*math.log10(self.fft_size))
169 sink = gr.message_sink(gr.sizeof_float * self.fft_size, self.msgq, True)
171 self.connect(self, s2p, self.one_in_n, fft, c2mag, self.avg, log, sink)
172 self.win = waterfall_window(self, parent, size=size, report=report,
173 ref_level=ref_level, span=span, ofunc=ofunc, xydfunc=xydfunc)
174 self.set_average(self.average)
177 # ------------------------------------------------------------------------
179 myDATA_EVENT = wx.NewEventType()
180 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
183 class DataEvent(wx.PyEvent):
184 def __init__(self, data):
185 wx.PyEvent.__init__(self)
186 self.SetEventType (myDATA_EVENT)
190 self.__class__ (self.GetId())
193 class input_watcher (threading.Thread):
194 def __init__ (self, msgq, fft_size, event_receiver, **kwds):
195 threading.Thread.__init__ (self, **kwds)
198 self.fft_size = fft_size
199 self.event_receiver = event_receiver
200 self.keep_running = True
204 while (self.keep_running):
205 msg = self.msgq.delete_head() # blocking read of message queue
206 itemsize = int(msg.arg1())
207 nitems = int(msg.arg2())
209 s = msg.to_string() # get the body of the msg as a string
211 # There may be more than one FFT frame in the message.
212 # If so, we take only the last one
214 start = itemsize * (nitems - 1)
215 s = s[start:start+itemsize]
217 complex_data = numpy.fromstring (s, numpy.float32)
218 de = DataEvent (complex_data)
219 wx.PostEvent (self.event_receiver, de)
223 class waterfall_window (wx.ScrolledWindow):
224 def __init__ (self, fftsink, parent, id = -1,
225 pos = wx.DefaultPosition, size = wx.DefaultSize,
226 style = wx.DEFAULT_FRAME_STYLE, name = "", report=None,
227 ref_level = 0, span = 50, ofunc=None, xydfunc=None):
228 wx.ScrolledWindow.__init__(self, parent, id, pos, size,
229 style|wx.HSCROLL, name)
231 self.SetCursor(wx.StockCursor(wx.CURSOR_IBEAM))
232 self.ref_level = ref_level
233 self.scale_factor = 256./span
235 self.ppsh = 128 # pixels per scroll, horizontal
236 self.SetScrollbars( self.ppsh, 0, fftsink.fft_size/self.ppsh, 0 )
238 self.fftsink = fftsink
242 self.xydfunc = xydfunc
245 dc1.SetFont( wx.SMALL_FONT )
246 self.h_scale = dc1.GetCharHeight() + 3
247 #self.bm_size = ( self.fftsink.fft_size, self.size[1] - self.h_scale )
248 self.im_size = ( self.fftsink.fft_size, self.size[1] - self.h_scale )
249 #self.bm = wx.EmptyBitmap( self.bm_size[0], self.bm_size[1], -1)
250 self.im = wx.EmptyImage( self.im_size[0], self.im_size[1], True )
253 self.baseband_freq = None
257 wx.EVT_PAINT( self, self.OnPaint )
258 wx.EVT_CLOSE (self, self.on_close_window)
259 #wx.EVT_LEFT_UP(self, self.on_left_up)
260 #wx.EVT_LEFT_DOWN(self, self.on_left_down)
261 EVT_DATA_EVENT (self, self.set_data)
263 self.build_popup_menu()
265 wx.EVT_CLOSE (self, self.on_close_window)
266 self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
267 self.Bind(wx.EVT_MOTION, self.on_motion)
271 self.input_watcher = input_watcher(fftsink.msgq, fftsink.fft_size, self)
273 def on_close_window (self, event):
274 self.keep_running = False
276 def on_left_down( self, evt ):
277 self.down_pos = evt.GetPosition()
278 self.down_time = evt.GetTimestamp()
280 def on_left_up( self, evt ):
282 dt = ( evt.GetTimestamp() - self.down_time )/1000.
283 pph = self.fftsink.fft_size/float(self.fftsink.sample_rate)
284 dx = evt.GetPosition()[0] - self.down_pos[0]
289 t = 'Down time: %f Delta f: %f Period: %f' % ( dt, dx/pph, rt )
294 def on_motion(self, event):
296 pos = event.GetPosition()
300 def const_list(self,const,len):
303 def make_colormap(self):
305 r.extend(self.const_list(0,96))
306 r.extend(range(0,255,4))
307 r.extend(self.const_list(255,64))
308 r.extend(range(255,128,-4))
311 g.extend(self.const_list(0,32))
312 g.extend(range(0,255,4))
313 g.extend(self.const_list(255,64))
314 g.extend(range(255,0,-4))
315 g.extend(self.const_list(0,32))
318 b.extend(self.const_list(255,64))
319 b.extend(range(255,0,-4))
320 b.extend(self.const_list(0,96))
324 (r,g,b) = self.make_colormap()
325 self.rgb = numpy.transpose( numpy.array( (r,g,b) ).astype(numpy.int8) )
327 def OnPaint(self, event):
328 dc = wx.BufferedPaintDC(self)
331 def DoDrawing(self,dc):
332 w, h = self.GetClientSizeTuple()
333 w = min( w, self.fftsink.fft_size )
338 dc = wx.BufferedDC( wx.ClientDC(self), (w,h) )
340 dc.SetBackground( wx.Brush( self.GetBackgroundColour(), wx.SOLID ) )
343 x, y = self.GetViewStart()
346 ih = min( h - self.h_scale, self.im_size[1] - self.im_cur )
347 r = wx.Rect( x, self.im_cur, w, ih )
348 bm = wx.BitmapFromImage( self.im.GetSubImage(r) )
349 dc.DrawBitmap( bm, 0, self.h_scale )
350 rem = min( self.im_size[1] - ih, h - ih - self.h_scale )
352 r = wx.Rect( x, 0, w, rem )
353 bm = wx.BitmapFromImage( self.im.GetSubImage(r) )
354 dc.DrawBitmap( bm, 0, ih + self.h_scale )
357 if self.baseband_freq != self.fftsink.baseband_freq:
358 self.baseband_freq = self.fftsink.baseband_freq
359 t = self.fftsink.sample_rate*w/float(self.fftsink.fft_size)
360 self.ax_spec = axis_design( self.baseband_freq - t/2,
361 self.baseband_freq + t/2, 7 )
362 dc.SetFont( wx.SMALL_FONT )
363 fo = self.baseband_freq
364 po = self.fftsink.fft_size/2
365 pph = self.fftsink.fft_size/float(self.fftsink.sample_rate)
366 f = math.floor((fo-po/pph)/self.ax_spec[1])*self.ax_spec[1]
368 t = po + ( f - fo )*pph
369 s = str( f*self.ax_spec[3] )
370 e = dc.GetTextExtent( s )
371 if t - e[1]/2 >= x + w:
373 dc.DrawText( s, t - x - e[0]/2, 0 )
374 dc.DrawLine( t - x, e[1] - 1, t - x, self.h_scale )
375 dt = self.ax_spec[1]/self.ax_spec[2]*pph
376 for i in range(self.ax_spec[2]-1):
380 dc.DrawLine( t - x, e[1] + 1, t - x, self.h_scale )
383 def const_list(self,const,len):
385 for i in range(1,len):
389 def make_colormap(self):
391 r.extend(self.const_list(0,96))
392 r.extend(range(0,255,4))
393 r.extend(self.const_list(255,64))
394 r.extend(range(255,128,-4))
397 g.extend(self.const_list(0,32))
398 g.extend(range(0,255,4))
399 g.extend(self.const_list(255,64))
400 g.extend(range(255,0,-4))
401 g.extend(self.const_list(0,32))
404 b.extend(self.const_list(255,64))
405 b.extend(range(255,0,-4))
406 b.extend(self.const_list(0,96))
409 def set_data (self, evt):
413 if self.ofunc != None:
414 self.ofunc(evt.data, L)
416 #dc1.SelectObject(self.bm)
418 # Scroll existing bitmap
420 #dc1.Blit(0,1,self.bm_size[0],self.bm_size[1]-1,dc1,0,0,
421 # wx.COPY,False,-1,-1)
424 for i in range( self.bm_size[1]-1, 0, -1 ):
425 dc1.Blit( 0, i, self.bm_size[0], 1, dc1, 0, i-1 )
427 x = max(abs(self.fftsink.sample_rate), abs(self.fftsink.baseband_freq))
439 if self.fftsink.input_is_real: # only plot 1/2 the points
446 scale_factor = self.scale_factor
449 dB = dB.astype(numpy.int_).clip( min=0, max=255 )
450 if self.fftsink.input_is_real: # real fft
451 dB = numpy.array( ( dB[0:d_max][::-1], dB[0:d_max] ) )
453 dB = numpy.concatenate( ( dB[d_max:L], dB[0:d_max] ) )
456 img = wx.ImageFromData( L, 1, dB.ravel().tostring() )
457 #bm = wx.BitmapFromImage( img )
458 #dc1.DrawBitmap( bm, 0, 0 )
459 ibuf = self.im.GetDataBuffer()
462 self.im_cur = self.im_size[1] - 1
463 start = 3*self.im_cur*self.im_size[0]
464 ibuf[start:start+3*self.im_size[0]] = img.GetData()
469 def on_average(self, evt):
471 self.fftsink.set_average(evt.IsChecked())
473 def on_right_click(self, event):
474 menu = self.popup_menu
475 self.PopupMenu(menu, event.GetPosition())
478 def build_popup_menu(self):
479 id_ref_gain = wx.NewId()
480 self.Bind( wx.EVT_MENU, self.on_ref_gain, id=id_ref_gain )
484 self.popup_menu = menu
485 menu.Append( id_ref_gain, "Ref Level and Gain" )
486 self.rg_dialog = None
489 #self.id_average : lambda : self.fftsink.average
492 def on_ref_gain( self, evt ):
493 if self.rg_dialog == None:
494 self.rg_dialog = rg_dialog( self.parent, self.set_ref_gain,
496 span=256./self.scale_factor )
497 self.rg_dialog.Show( True )
499 def set_ref_gain( self, ref, span ):
501 self.scale_factor = 256/span
503 class rg_dialog( wx.Dialog ):
504 def __init__( self, parent, set_function, ref=0, span=256./5. ):
505 wx.Dialog.__init__( self, parent, -1, "Waterfall Settings" )
506 self.set_function = set_function
507 #status_bar = wx.StatusBar( self, -1 )
509 d_sizer = wx.BoxSizer( wx.VERTICAL ) # dialog sizer
510 f_sizer = wx.BoxSizer( wx.VERTICAL ) # form sizer
513 #f_sizer.Add( fn_sizer, 0, flag=wx.TOP, border=10 )
515 h_sizer = wx.BoxSizer( wx.HORIZONTAL )
516 self.ref = tab_item( self, "Ref Level:", 4, "dB" )
517 self.ref.ctrl.SetValue( "%d" % ref )
519 h_sizer.Add( self.ref, 0 )
521 self.span = tab_item( self, "Range:", 4, "dB" )
522 self.span.ctrl.SetValue( "%d" % span )
523 h_sizer.Add( self.span, 0 )
525 f_sizer.Add( h_sizer, 0, flag=wx.TOP|wx.EXPAND, border=vs )
528 d_sizer.Add( f_sizer, 0, flag=wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND )
532 button_sizer = wx.BoxSizer( wx.HORIZONTAL )
533 apply_button = wx.Button( self, -1, "Apply" )
534 apply_button.Bind( wx.EVT_BUTTON, self.apply_evt )
535 cancel_button = wx.Button( self, -1, "Cancel" )
536 cancel_button.Bind( wx.EVT_BUTTON, self.cancel_evt )
537 ok_button = wx.Button( self, -1, "OK" )
538 ok_button.Bind( wx.EVT_BUTTON, self.ok_evt )
539 button_sizer.Add((0,0),1)
540 button_sizer.Add( apply_button, 0,
541 flag=wx.ALIGN_CENTER_HORIZONTAL )
542 button_sizer.Add((0,0),1)
543 button_sizer.Add( cancel_button, 0,
544 flag=wx.ALIGN_CENTER_HORIZONTAL )
545 button_sizer.Add((0,0),1)
546 button_sizer.Add( ok_button, 0,
547 flag=wx.ALIGN_CENTER_HORIZONTAL )
548 button_sizer.Add((0,0),1)
549 d_sizer.Add( button_sizer, 0,
550 flag=wx.EXPAND|wx.ALIGN_CENTER|wx.BOTTOM, border=30 )
551 self.SetSizer( d_sizer )
553 def apply_evt( self, evt ):
556 def cancel_evt( self, evt ):
559 def ok_evt( self, evt ):
563 def do_apply( self ):
564 r = float( self.ref.ctrl.GetValue() )
565 g = float( self.span.ctrl.GetValue() )
566 self.set_function( r, g )
570 Return the first item in seq that is > v.
577 def next_down(v, seq):
579 Return the last item in seq that is < v.
589 # One of many copies that should be consolidated . . .
590 def tab_item( parent, label, chars, units, style=wx.TE_RIGHT, value="" ):
591 s = wx.BoxSizer( wx.HORIZONTAL )
592 s.Add( wx.StaticText( parent, -1, label ), 0,
593 flag=wx.ALIGN_CENTER_VERTICAL )
594 s.ctrl = wx.TextCtrl( parent, -1, style=style, value=value )
595 s.ctrl.SetMinSize( ( (1.00+chars)*s.ctrl.GetCharWidth(),
596 1.25*s.ctrl.GetCharHeight() ) )
597 s.Add( s.ctrl, -1, flag=wx.LEFT, border=3 )
598 s.Add( wx.StaticText( parent, -1, units ), 0,
599 flag=wx.ALIGN_CENTER_VERTICAL|wx.LEFT, border=1 )
603 # ----------------------------------------------------------------
604 # Standalone test app
605 # ----------------------------------------------------------------
607 class test_app_flow_graph (stdgui2.std_top_block):
608 def __init__(self, frame, panel, vbox, argv):
609 stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
613 # build our flow graph
614 input_rate = 20.000e3
616 # Generate a complex sinusoid
617 src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
618 #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
620 # We add these throttle blocks so that this demo doesn't
621 # suck down all the CPU available. Normally you wouldn't use these.
622 thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
624 sink1 = waterfall_sink_c (panel, title="Complex Data",
626 sample_rate=input_rate, baseband_freq=0,
628 vbox.Add (sink1.win, 1, wx.EXPAND)
629 self.connect (src1, thr1, sink1)
631 # generate a real sinusoid
632 src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
633 #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
634 thr2 = gr.throttle(gr.sizeof_float, input_rate)
635 sink2 = waterfall_sink_f (panel, title="Real Data", fft_size=fft_size,
636 sample_rate=input_rate, baseband_freq=0)
637 vbox.Add (sink2.win, 1, wx.EXPAND)
638 self.connect (src2, thr2, sink2)
641 app = stdgui2.stdapp (test_app_flow_graph,
642 "Waterfall Sink Test App")
645 if __name__ == '__main__':