3 # Copyright 2003,2004,2006,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 3, 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, eng_notation
24 from gnuradio.wxgui import stdgui2
26 import gnuradio.wxgui.plot as plot
31 default_scopesink_size = (640, 240)
32 default_v_scale = 1000
33 default_frame_decim = gr.prefs().get_long('wxgui', 'frame_decim', 1)
35 class scope_sink_f(gr.hier_block2):
36 def __init__(self, parent, title='', sample_rate=1,
37 size=default_scopesink_size, frame_decim=default_frame_decim,
38 v_scale=default_v_scale, t_scale=None, num_inputs=1):
40 gr.hier_block2.__init__(self, "scope_sink_f",
41 gr.io_signature(num_inputs, num_inputs, gr.sizeof_float),
42 gr.io_signature(0,0,0))
44 msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
45 self.guts = gr.oscope_sink_f(sample_rate, msgq)
46 for i in range(num_inputs):
47 self.connect((self, i), (self.guts, i))
49 self.win = scope_window(win_info (msgq, sample_rate, frame_decim,
50 v_scale, t_scale, self.guts, title), parent)
52 def set_sample_rate(self, sample_rate):
53 self.guts.set_sample_rate(sample_rate)
54 self.win.info.set_sample_rate(sample_rate)
56 class scope_sink_c(gr.hier_block2):
57 def __init__(self, parent, title='', sample_rate=1,
58 size=default_scopesink_size, frame_decim=default_frame_decim,
59 v_scale=default_v_scale, t_scale=None, num_inputs=1):
61 gr.hier_block2.__init__(self, "scope_sink_c",
62 gr.io_signature(num_inputs, num_inputs, gr.sizeof_gr_complex),
63 gr.io_signature(0,0,0))
65 msgq = gr.msg_queue(2) # message queue that holds at most 2 messages
66 self.guts = gr.oscope_sink_f(sample_rate, msgq)
67 for i in range(num_inputs):
68 c2f = gr.complex_to_float()
69 self.connect((self, i), c2f)
70 self.connect((c2f, 0), (self.guts, 2*i+0))
71 self.connect((c2f, 1), (self.guts, 2*i+1))
73 self.win = scope_window(win_info(msgq, sample_rate, frame_decim,
74 v_scale, t_scale, self.guts, title), parent)
76 def set_sample_rate(self, sample_rate):
77 self.guts.set_sample_rate(sample_rate)
78 self.win.info.set_sample_rate(sample_rate)
80 class constellation_sink(scope_sink_c):
81 def __init__(self, parent, title='Constellation', sample_rate=1,
82 size=default_scopesink_size, frame_decim=default_frame_decim):
83 scope_sink_c.__init__(self, parent=parent, title=title, sample_rate=sample_rate,
84 size=size, frame_decim=frame_decim)
85 self.win.info.xy = True #constellation mode
87 # ========================================================================
90 time_base_list = [ # time / division
100 1.0e-4, # 100us / div
111 v_scale_list = [ # counts / div, LARGER gains are SMALLER /div, appear EARLIER
112 2.0e-3, # 2m / div, don't call it V/div it's actually counts/div
132 1.0e+4 # 10000 /div, USRP full scale is -/+ 32767
136 wxDATA_EVENT = wx.NewEventType()
138 def EVT_DATA_EVENT(win, func):
139 win.Connect(-1, -1, wxDATA_EVENT, func)
141 class DataEvent(wx.PyEvent):
142 def __init__(self, data):
143 wx.PyEvent.__init__(self)
144 self.SetEventType (wxDATA_EVENT)
148 self.__class__ (self.GetId())
151 class win_info (object):
152 __slots__ = ['msgq', 'sample_rate', 'frame_decim', 'v_scale',
153 'scopesink', 'title',
154 'time_scale_cursor', 'v_scale_cursor', 'marker', 'xy',
155 'autorange', 'running']
157 def __init__ (self, msgq, sample_rate, frame_decim, v_scale, t_scale,
158 scopesink, title = "Oscilloscope", xy=False):
160 self.sample_rate = sample_rate
161 self.frame_decim = frame_decim
162 self.scopesink = scopesink
165 self.time_scale_cursor = gru.seq_with_cursor(time_base_list, initial_value = t_scale)
166 self.v_scale_cursor = gru.seq_with_cursor(v_scale_list, initial_value = v_scale)
170 if v_scale == None: # 0 and None are both False, but 0 != None
171 self.autorange = True
173 self.autorange = False # 0 is a valid v_scale
176 def get_time_per_div (self):
177 return self.time_scale_cursor.current ()
179 def get_volts_per_div (self):
180 return self.v_scale_cursor.current ()
182 def set_sample_rate(self, sample_rate):
183 self.sample_rate = sample_rate
185 def get_sample_rate (self):
186 return self.sample_rate
188 def get_decimation_rate (self):
191 def set_marker (self, s):
194 def get_marker (self):
198 class input_watcher (threading.Thread):
199 def __init__ (self, msgq, event_receiver, frame_decim, **kwds):
200 threading.Thread.__init__ (self, **kwds)
203 self.event_receiver = event_receiver
204 self.frame_decim = frame_decim
206 self.keep_running = True
210 # print "input_watcher: pid = ", os.getpid ()
211 while (self.keep_running):
212 msg = self.msgq.delete_head() # blocking read of message queue
213 if self.iscan == 0: # only display at frame_decim
214 self.iscan = self.frame_decim
216 nchan = int(msg.arg1()) # number of channels of data in msg
217 nsamples = int(msg.arg2()) # number of samples in each channel
219 s = msg.to_string() # get the body of the msg as a string
221 bytes_per_chan = nsamples * gr.sizeof_float
224 for ch in range (nchan):
226 start = ch * bytes_per_chan
227 chan_data = s[start:start+bytes_per_chan]
228 rec = numpy.fromstring (chan_data, numpy.float32)
231 # print "nrecords = %d, reclen = %d" % (len (records),nsamples)
233 de = DataEvent (records)
234 wx.PostEvent (self.event_receiver, de)
242 class scope_window (wx.Panel):
244 def __init__ (self, info, parent, id = -1,
245 pos = wx.DefaultPosition, size = wx.DefaultSize, name = ""):
246 wx.Panel.__init__ (self, parent, -1)
249 vbox = wx.BoxSizer (wx.VERTICAL)
251 self.graph = graph_window (info, self, -1)
253 vbox.Add (self.graph, 1, wx.EXPAND)
254 vbox.Add (self.make_control_box(), 0, wx.EXPAND)
255 vbox.Add (self.make_control2_box(), 0, wx.EXPAND)
258 self.SetSizer (self.sizer)
259 self.SetAutoLayout (True)
260 self.sizer.Fit (self)
261 self.set_autorange(self.info.autorange)
264 # second row of control buttons etc. appears BELOW control_box
265 def make_control2_box (self):
266 ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
268 self.inc_v_button = wx.Button (self, 1101, " < ", style=wx.BU_EXACTFIT)
269 self.inc_v_button.SetToolTipString ("Increase vertical range")
270 wx.EVT_BUTTON (self, 1101, self.incr_v_scale) # ID matches button ID above
272 self.dec_v_button = wx.Button (self, 1100, " > ", style=wx.BU_EXACTFIT)
273 self.dec_v_button.SetToolTipString ("Decrease vertical range")
274 wx.EVT_BUTTON (self, 1100, self.decr_v_scale)
276 self.v_scale_label = wx.StaticText (self, 1002, "None") # vertical /div
277 self.update_v_scale_label ()
279 self.autorange_checkbox = wx.CheckBox (self, 1102, "Autorange")
280 self.autorange_checkbox.SetToolTipString ("Select autorange on/off")
281 wx.EVT_CHECKBOX(self, 1102, self.autorange_checkbox_event)
283 ctrlbox.Add ((5,0) ,0) # left margin space
284 ctrlbox.Add (self.inc_v_button, 0, wx.EXPAND)
285 ctrlbox.Add (self.dec_v_button, 0, wx.EXPAND)
286 ctrlbox.Add (self.v_scale_label, 0, wx.ALIGN_CENTER)
287 ctrlbox.Add ((20,0) ,0) # spacer
288 ctrlbox.Add (self.autorange_checkbox, 0, wx.ALIGN_CENTER)
292 def make_control_box (self):
293 ctrlbox = wx.BoxSizer (wx.HORIZONTAL)
295 tb_left = wx.Button (self, 1001, " < ", style=wx.BU_EXACTFIT)
296 tb_left.SetToolTipString ("Increase time base")
297 wx.EVT_BUTTON (self, 1001, self.incr_timebase)
300 tb_right = wx.Button (self, 1000, " > ", style=wx.BU_EXACTFIT)
301 tb_right.SetToolTipString ("Decrease time base")
302 wx.EVT_BUTTON (self, 1000, self.decr_timebase)
304 self.time_base_label = wx.StaticText (self, 1002, "")
305 self.update_timebase_label ()
307 ctrlbox.Add ((5,0) ,0)
308 # ctrlbox.Add (wx.StaticText (self, -1, "Horiz Scale: "), 0, wx.ALIGN_CENTER)
309 ctrlbox.Add (tb_left, 0, wx.EXPAND)
310 ctrlbox.Add (tb_right, 0, wx.EXPAND)
311 ctrlbox.Add (self.time_base_label, 0, wx.ALIGN_CENTER)
313 ctrlbox.Add ((10,0) ,1) # stretchy space
315 ctrlbox.Add (wx.StaticText (self, -1, "Trig: "), 0, wx.ALIGN_CENTER)
316 self.trig_chan_choice = wx.Choice (self, 1004,
317 choices = ['Ch1', 'Ch2', 'Ch3', 'Ch4'])
318 self.trig_chan_choice.SetToolTipString ("Select channel for trigger")
319 wx.EVT_CHOICE (self, 1004, self.trig_chan_choice_event)
320 ctrlbox.Add (self.trig_chan_choice, 0, wx.ALIGN_CENTER)
322 self.trig_mode_choice = wx.Choice (self, 1005,
323 choices = ['Auto', 'Pos', 'Neg'])
324 self.trig_mode_choice.SetToolTipString ("Select trigger slope or Auto (untriggered roll)")
325 wx.EVT_CHOICE (self, 1005, self.trig_mode_choice_event)
326 ctrlbox.Add (self.trig_mode_choice, 0, wx.ALIGN_CENTER)
328 trig_level50 = wx.Button (self, 1006, "50%")
329 trig_level50.SetToolTipString ("Set trigger level to 50%")
330 wx.EVT_BUTTON (self, 1006, self.set_trig_level50)
331 ctrlbox.Add (trig_level50, 0, wx.EXPAND)
333 run_stop = wx.Button (self, 1007, "Run/Stop")
334 run_stop.SetToolTipString ("Toggle Run/Stop mode")
335 wx.EVT_BUTTON (self, 1007, self.run_stop)
336 ctrlbox.Add (run_stop, 0, wx.EXPAND)
338 ctrlbox.Add ((10, 0) ,1) # stretchy space
340 ctrlbox.Add (wx.StaticText (self, -1, "Fmt: "), 0, wx.ALIGN_CENTER)
341 self.marker_choice = wx.Choice (self, 1002, choices = self._marker_choices)
342 self.marker_choice.SetToolTipString ("Select plotting with lines, pluses or dots")
343 wx.EVT_CHOICE (self, 1002, self.marker_choice_event)
344 ctrlbox.Add (self.marker_choice, 0, wx.ALIGN_CENTER)
346 self.xy_choice = wx.Choice (self, 1003, choices = ['X:t', 'X:Y'])
347 self.xy_choice.SetToolTipString ("Select X vs time or X vs Y display")
348 wx.EVT_CHOICE (self, 1003, self.xy_choice_event)
349 ctrlbox.Add (self.xy_choice, 0, wx.ALIGN_CENTER)
353 _marker_choices = ['line', 'plus', 'dot']
355 def update_timebase_label (self):
356 time_per_div = self.info.get_time_per_div ()
357 s = ' ' + eng_notation.num_to_str (time_per_div) + 's/div'
358 self.time_base_label.SetLabel (s)
360 def decr_timebase (self, evt):
361 self.info.time_scale_cursor.prev ()
362 self.update_timebase_label ()
364 def incr_timebase (self, evt):
365 self.info.time_scale_cursor.next ()
366 self.update_timebase_label ()
368 def update_v_scale_label (self):
369 volts_per_div = self.info.get_volts_per_div ()
370 s = ' ' + eng_notation.num_to_str (volts_per_div) + '/div' # Not V/div
371 self.v_scale_label.SetLabel (s)
373 def decr_v_scale (self, evt):
374 self.info.v_scale_cursor.prev ()
375 self.update_v_scale_label ()
377 def incr_v_scale (self, evt):
378 self.info.v_scale_cursor.next ()
379 self.update_v_scale_label ()
381 def marker_choice_event (self, evt):
385 def set_autorange(self, on):
387 self.v_scale_label.SetLabel(" (auto)")
388 self.info.autorange = True
389 self.autorange_checkbox.SetValue(True)
390 self.inc_v_button.Enable(False)
391 self.dec_v_button.Enable(False)
393 if self.graph.y_range:
394 (l,u) = self.graph.y_range # found by autorange
395 self.info.v_scale_cursor.set_index_by_value((u-l)/8.0)
396 self.update_v_scale_label()
397 self.info.autorange = False
398 self.autorange_checkbox.SetValue(False)
399 self.inc_v_button.Enable(True)
400 self.dec_v_button.Enable(True)
402 def autorange_checkbox_event(self, evt):
404 self.set_autorange(True)
406 self.set_autorange(False)
408 def set_marker (self, s):
409 self.info.set_marker (s) # set info for drawing routines
410 i = self.marker_choice.FindString (s)
411 assert i >= 0, "Hmmm, set_marker problem"
412 self.marker_choice.SetSelection (i)
414 def set_format_line (self):
415 self.set_marker ('line')
417 def set_format_dot (self):
418 self.set_marker ('dot')
420 def set_format_plus (self):
421 self.set_marker ('plus')
423 def xy_choice_event (self, evt):
425 self.info.xy = s == 'X:Y'
427 def trig_chan_choice_event (self, evt):
430 self.info.scopesink.set_trigger_channel (ch)
432 def trig_mode_choice_event (self, evt):
433 sink = self.info.scopesink
436 sink.set_trigger_mode (gr.gr_TRIG_POS_SLOPE)
438 sink.set_trigger_mode (gr.gr_TRIG_NEG_SLOPE)
440 sink.set_trigger_mode (gr.gr_TRIG_AUTO)
442 assert 0, "Bad trig_mode_choice string"
444 def set_trig_level50 (self, evt):
445 self.info.scopesink.set_trigger_level_auto ()
447 def run_stop (self, evt):
448 self.info.running = not self.info.running
451 class graph_window (plot.PlotCanvas):
453 channel_colors = ['BLUE', 'RED',
454 'CYAN', 'MAGENTA', 'GREEN', 'YELLOW']
456 def __init__ (self, info, parent, id = -1,
457 pos = wx.DefaultPosition, size = (640, 240),
458 style = wx.DEFAULT_FRAME_STYLE, name = ""):
459 plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
461 self.SetXUseScopeTicks (True)
462 self.SetEnableGrid (True)
463 self.SetEnableZoom (True)
464 self.SetEnableLegend(True)
465 # self.SetBackgroundColour ('black')
470 self.avg_y_min = None
471 self.avg_y_max = None
472 self.avg_x_min = None
473 self.avg_x_max = None
475 EVT_DATA_EVENT (self, self.format_data)
477 self.input_watcher = input_watcher (info.msgq, self, info.frame_decim)
479 def channel_color (self, ch):
480 return self.channel_colors[ch % len(self.channel_colors)]
482 def format_data (self, evt):
483 if not self.info.running:
487 self.format_xy_data (evt)
492 nchannels = len (records)
493 npoints = len (records[0])
497 Ts = 1.0 / (info.get_sample_rate () / info.get_decimation_rate ())
498 x_vals = Ts * numpy.arange (-npoints/2, npoints/2)
500 # preliminary clipping based on time axis here, instead of in graphics code
501 time_per_window = self.info.get_time_per_div () * 10
502 n = int (time_per_window / Ts + 0.5)
503 n = n & ~0x1 # make even
504 n = max (2, min (n, npoints))
506 self.SetXUseScopeTicks (True) # use 10 divisions, no labels
508 for ch in range(nchannels):
511 # plot middle n points of record
515 # points = zip (x_vals[lb:ub], r[lb:ub])
516 points = numpy.zeros ((ub-lb, 2), numpy.float64)
517 points[:,0] = x_vals[lb:ub]
518 points[:,1] = r[lb:ub]
520 m = info.get_marker ()
522 objects.append (plot.PolyLine (points,
523 colour=self.channel_color (ch),
524 legend=('Ch%d' % (ch+1,))))
526 objects.append (plot.PolyMarker (points,
528 colour=self.channel_color (ch),
529 legend=('Ch%d' % (ch+1,))))
531 graphics = plot.PlotGraphics (objects,
532 title=self.info.title,
533 xLabel = '', yLabel = '')
535 time_per_div = info.get_time_per_div ()
536 x_range = (-5.0 * time_per_div, 5.0 * time_per_div) # ranges are tuples!
537 volts_per_div = info.get_volts_per_div ()
538 if not self.info.autorange:
539 self.y_range = (-4.0 * volts_per_div, 4.0 * volts_per_div)
540 self.Draw (graphics, xAxis=x_range, yAxis=self.y_range)
541 self.update_y_range () # autorange to self.y_range
544 def format_xy_data (self, evt):
547 nchannels = len (records)
548 npoints = len (records[0])
554 # points = zip (records[0], records[1])
555 points = numpy.zeros ((len(records[0]), 2), numpy.float32)
556 points[:,0] = records[0]
557 points[:,1] = records[1]
559 self.SetXUseScopeTicks (False)
561 m = info.get_marker ()
563 objects.append (plot.PolyLine (points,
564 colour=self.channel_color (0)))
566 objects.append (plot.PolyMarker (points,
568 colour=self.channel_color (0)))
570 graphics = plot.PlotGraphics (objects,
571 title=self.info.title,
572 xLabel = 'I', yLabel = 'Q')
574 self.Draw (graphics, xAxis=self.x_range, yAxis=self.y_range)
575 self.update_y_range ()
576 self.update_x_range ()
579 def update_y_range (self):
581 graphics = self.last_draw[0]
582 p1, p2 = graphics.boundingBox () # min, max points of graphics
584 if self.avg_y_min: # prevent vertical scale from jumping abruptly --?
585 self.avg_y_min = p1[1] * alpha + self.avg_y_min * (1 - alpha)
586 self.avg_y_max = p2[1] * alpha + self.avg_y_max * (1 - alpha)
587 else: # initial guess
588 self.avg_y_min = p1[1] # -500.0 workaround, sometimes p1 is ~ 10^35
589 self.avg_y_max = p2[1] # 500.0
591 self.y_range = self._axisInterval ('auto', self.avg_y_min, self.avg_y_max)
592 # print "p1 %s p2 %s y_min %s y_max %s y_range %s" \
593 # % (p1, p2, self.avg_y_min, self.avg_y_max, self.y_range)
596 def update_x_range (self):
598 graphics = self.last_draw[0]
599 p1, p2 = graphics.boundingBox () # min, max points of graphics
602 self.avg_x_min = p1[0] * alpha + self.avg_x_min * (1 - alpha)
603 self.avg_x_max = p2[0] * alpha + self.avg_x_max * (1 - alpha)
605 self.avg_x_min = p1[0]
606 self.avg_x_max = p2[0]
608 self.x_range = self._axisInterval ('auto', self.avg_x_min, self.avg_x_max)
611 # ----------------------------------------------------------------
612 # Stand-alone test application
613 # ----------------------------------------------------------------
615 class test_top_block (stdgui2.std_top_block):
616 def __init__(self, frame, panel, vbox, argv):
617 stdgui2.std_top_block.__init__ (self, frame, panel, vbox, argv)
620 frame_decim = int(argv[1])
625 v_scale = float(argv[2]) # start up at this v_scale value
627 v_scale = None # start up in autorange mode, default
630 t_scale = float(argv[3]) # start up at this t_scale value
632 t_scale = None # old behavior
634 print "frame decim %s v_scale %s t_scale %s" % (frame_decim,v_scale,t_scale)
638 # Generate a complex sinusoid
639 self.src0 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 25.1e3, 1e3)
641 # We add this throttle block so that this demo doesn't suck down
642 # all the CPU available. You normally wouldn't use it...
643 self.thr = gr.throttle(gr.sizeof_gr_complex, input_rate)
645 scope = scope_sink_c (panel,"Secret Data",sample_rate=input_rate,
646 frame_decim=frame_decim,
647 v_scale=v_scale, t_scale=t_scale)
648 vbox.Add (scope.win, 1, wx.EXPAND)
650 # Ultimately this will be
651 # self.connect("src0 throttle scope")
652 self.connect(self.src0, self.thr, scope)
655 app = stdgui2.stdapp (test_top_block, "O'Scope Test App")
658 if __name__ == '__main__':
661 # ----------------------------------------------------------------