a9917782f4f736b86f7cea5eaaa8cd327b638338
[debian/gnuradio] / gr-wxgui / src / python / scope_window.py
1 #
2 # Copyright 2008,2010 Free Software Foundation, Inc.
3 #
4 # This file is part of GNU Radio
5 #
6 # GNU Radio is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3, or (at your option)
9 # any later version.
10 #
11 # GNU Radio is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with GNU Radio; see the file COPYING.  If not, write to
18 # the Free Software Foundation, Inc., 51 Franklin Street,
19 # Boston, MA 02110-1301, USA.
20 #
21
22 ##################################################
23 # Imports
24 ##################################################
25 import plotter
26 import common
27 import wx
28 import numpy
29 import time
30 import pubsub
31 from constants import *
32 from gnuradio import gr #for gr.prefs, trigger modes
33 import forms
34
35 ##################################################
36 # Constants
37 ##################################################
38 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'scope_rate', 30)
39 PERSIST_ALPHA_MIN_EXP, PERSIST_ALPHA_MAX_EXP = -2, 0
40 SLIDER_STEPS = 100
41 DEFAULT_TRIG_MODE = gr.prefs().get_long('wxgui', 'trig_mode', gr.gr_TRIG_MODE_AUTO)
42 DEFAULT_WIN_SIZE = (600, 300)
43 COUPLING_MODES = (
44         ('DC', False),
45         ('AC', True),
46 )
47 TRIGGER_MODES = (
48         ('Freerun', gr.gr_TRIG_MODE_FREE),
49         ('Auto', gr.gr_TRIG_MODE_AUTO),
50         ('Normal', gr.gr_TRIG_MODE_NORM),
51         ('Stripchart', gr.gr_TRIG_MODE_STRIPCHART),
52 )
53 TRIGGER_SLOPES = (
54         ('Pos +', gr.gr_TRIG_SLOPE_POS),
55         ('Neg -', gr.gr_TRIG_SLOPE_NEG),
56 )
57 CHANNEL_COLOR_SPECS = (
58         (0.3, 0.3, 1.0),
59         (0.0, 0.8, 0.0),
60         (1.0, 0.0, 0.0),
61         (0.8, 0.0, 0.8),
62         (0.7, 0.7, 0.0),
63         (0.15, 0.90, 0.98),
64
65 )
66 TRIGGER_COLOR_SPEC = (1.0, 0.4, 0.0)
67 AUTORANGE_UPDATE_RATE = 0.5 #sec
68 MARKER_TYPES = (
69         ('Line Link', None),
70         ('Dot Large', 3.0),
71         ('Dot Med', 2.0),
72         ('Dot Small', 1.0),
73         ('None', 0.0),
74 )
75 DEFAULT_MARKER_TYPE = None
76
77 ##################################################
78 # Scope window control panel
79 ##################################################
80 class control_panel(wx.Panel):
81         """
82         A control panel with wx widgits to control the plotter and scope block.
83         """
84         def __init__(self, parent):
85                 """
86                 Create a new control panel.
87                 @param parent the wx parent window
88                 """
89                 WIDTH = 90
90                 self.parent = parent
91                 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
92                 parent[SHOW_CONTROL_PANEL_KEY] = True
93                 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
94                 control_box = wx.BoxSizer(wx.VERTICAL)
95
96                 ##################################################
97                 # Persistence
98                 ##################################################
99
100                 forms.check_box(
101                         sizer=control_box, parent=self, label='Persistence',
102                         ps=parent, key=USE_PERSISTENCE_KEY,
103                 )
104                 #static text and slider for analog alpha
105                 persist_alpha_text = forms.static_text(
106                         sizer=control_box, parent=self, label='Analog Alpha',
107                         converter=forms.float_converter(lambda x: '%.4f'%x),
108                         ps=parent, key=PERSIST_ALPHA_KEY, width=50,
109                 )
110                 persist_alpha_slider = forms.log_slider(
111                         sizer=control_box, parent=self,
112                         min_exp=PERSIST_ALPHA_MIN_EXP,
113                         max_exp=PERSIST_ALPHA_MAX_EXP,
114                         num_steps=SLIDER_STEPS,
115                         ps=parent, key=PERSIST_ALPHA_KEY,
116                 )
117                 for widget in (persist_alpha_text, persist_alpha_slider):
118                         parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable)
119                         widget.Enable(parent[USE_PERSISTENCE_KEY])
120                         parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems)
121                         #allways show initially, so room is reserved for them
122                         widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY])
123                 
124                 parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout)
125
126                 ##################################################
127                 # Axes Options
128                 ##################################################
129                 control_box.AddStretchSpacer()
130                 axes_options_box = forms.static_box_sizer(
131                         parent=self, sizer=control_box, label='Axes Options',
132                         bold=True, orient=wx.VERTICAL,
133                 )
134                 ##################################################
135                 # Scope Mode Box
136                 ##################################################
137                 scope_mode_box = wx.BoxSizer(wx.VERTICAL)
138                 axes_options_box.Add(scope_mode_box, 0, wx.EXPAND)
139                 #x axis divs
140                 forms.incr_decr_buttons(
141                         parent=self, sizer=scope_mode_box, label='Secs/Div',
142                         on_incr=self._on_incr_t_divs, on_decr=self._on_decr_t_divs,
143                 )
144                 #y axis divs
145                 y_buttons_scope = forms.incr_decr_buttons(
146                         parent=self, sizer=scope_mode_box, label='Counts/Div',
147                         on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
148                 )
149                 #y axis ref lvl
150                 y_off_buttons_scope = forms.incr_decr_buttons(
151                         parent=self, sizer=scope_mode_box, label='Y Offset',
152                         on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
153                 )
154                 #t axis ref lvl
155                 scope_mode_box.AddSpacer(5)
156                 forms.slider(
157                         parent=self, sizer=scope_mode_box,
158                         ps=parent, key=T_FRAC_OFF_KEY, label='T Offset',
159                         minimum=0, maximum=1, num_steps=1000,
160                 )
161                 scope_mode_box.AddSpacer(5)
162                 ##################################################
163                 # XY Mode Box
164                 ##################################################
165                 xy_mode_box = wx.BoxSizer(wx.VERTICAL)
166                 axes_options_box.Add(xy_mode_box, 0, wx.EXPAND)
167                 #x div controls
168                 x_buttons = forms.incr_decr_buttons(
169                         parent=self, sizer=xy_mode_box, label='X/Div',
170                         on_incr=self._on_incr_x_divs, on_decr=self._on_decr_x_divs,
171                 )
172                 #y div controls
173                 y_buttons = forms.incr_decr_buttons(
174                         parent=self, sizer=xy_mode_box, label='Y/Div',
175                         on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
176                 )
177                 #x offset controls
178                 x_off_buttons = forms.incr_decr_buttons(
179                         parent=self, sizer=xy_mode_box, label='X Off',
180                         on_incr=self._on_incr_x_off, on_decr=self._on_decr_x_off,
181                 )
182                 #y offset controls
183                 y_off_buttons = forms.incr_decr_buttons(
184                         parent=self, sizer=xy_mode_box, label='Y Off',
185                         on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
186                 )
187                 for widget in (y_buttons_scope, y_off_buttons_scope, x_buttons, y_buttons, x_off_buttons, y_off_buttons):
188                         parent.subscribe(AUTORANGE_KEY, widget.Disable)
189                         widget.Disable(parent[AUTORANGE_KEY])
190                 xy_mode_box.ShowItems(False)
191                 #autorange check box
192                 forms.check_box(
193                         parent=self, sizer=axes_options_box, label='Autorange',
194                         ps=parent, key=AUTORANGE_KEY,
195                 )
196                 ##################################################
197                 # Channel Options
198                 ##################################################
199                 TRIGGER_PAGE_INDEX = parent.num_inputs
200                 XY_PAGE_INDEX = parent.num_inputs+1
201                 control_box.AddStretchSpacer()
202                 chan_options_box = forms.static_box_sizer(
203                         parent=self, sizer=control_box, label='Channel Options',
204                         bold=True, orient=wx.VERTICAL,
205                 )
206                 options_notebook = wx.Notebook(self)
207                 options_notebook_args = list()
208                 CHANNELS = [('Ch %d'%(i+1), i) for i in range(parent.num_inputs)]
209                 ##################################################
210                 # Channel Menu Boxes
211                 ##################################################
212                 for i in range(parent.num_inputs):
213                         channel_menu_panel = wx.Panel(options_notebook)
214                         options_notebook_args.append((channel_menu_panel, i, 'Ch%d'%(i+1)))
215                         channel_menu_box = wx.BoxSizer(wx.VERTICAL)
216                         channel_menu_panel.SetSizer(channel_menu_box)
217                         #ac couple check box
218                         channel_menu_box.AddStretchSpacer()
219                         forms.drop_down(
220                                 parent=channel_menu_panel, sizer=channel_menu_box,
221                                 ps=parent, key=common.index_key(AC_COUPLE_KEY, i),
222                                 choices=map(lambda x: x[1], COUPLING_MODES),
223                                 labels=map(lambda x: x[0], COUPLING_MODES),
224                                 label='Coupling', width=WIDTH,
225                         )
226                         #marker
227                         channel_menu_box.AddStretchSpacer()
228                         forms.drop_down(
229                                 parent=channel_menu_panel, sizer=channel_menu_box,
230                                 ps=parent, key=common.index_key(MARKER_KEY, i),
231                                 choices=map(lambda x: x[1], MARKER_TYPES),
232                                 labels=map(lambda x: x[0], MARKER_TYPES),
233                                 label='Marker', width=WIDTH,
234                         )
235                         channel_menu_box.AddStretchSpacer()
236                 ##################################################
237                 # Trigger Menu Box
238                 ##################################################
239                 trigger_menu_panel = wx.Panel(options_notebook)
240                 options_notebook_args.append((trigger_menu_panel, TRIGGER_PAGE_INDEX, 'Trig'))
241                 trigger_menu_box = wx.BoxSizer(wx.VERTICAL)
242                 trigger_menu_panel.SetSizer(trigger_menu_box)
243                 #trigger mode
244                 forms.drop_down(
245                         parent=trigger_menu_panel, sizer=trigger_menu_box,
246                         ps=parent, key=TRIGGER_MODE_KEY,
247                         choices=map(lambda x: x[1], TRIGGER_MODES),
248                         labels=map(lambda x: x[0], TRIGGER_MODES),
249                         label='Mode', width=WIDTH,
250                 )
251                 #trigger slope
252                 trigger_slope_chooser = forms.drop_down(
253                         parent=trigger_menu_panel, sizer=trigger_menu_box,
254                         ps=parent, key=TRIGGER_SLOPE_KEY,
255                         choices=map(lambda x: x[1], TRIGGER_SLOPES),
256                         labels=map(lambda x: x[0], TRIGGER_SLOPES),
257                         label='Slope', width=WIDTH,
258                 )
259                 #trigger channel
260                 trigger_channel_chooser = forms.drop_down(
261                         parent=trigger_menu_panel, sizer=trigger_menu_box,
262                         ps=parent, key=TRIGGER_CHANNEL_KEY,
263                         choices=map(lambda x: x[1], CHANNELS),
264                         labels=map(lambda x: x[0], CHANNELS),
265                         label='Channel', width=WIDTH,
266                 )
267                 #trigger level
268                 hbox = wx.BoxSizer(wx.HORIZONTAL)
269                 trigger_menu_box.Add(hbox, 0, wx.EXPAND)
270                 hbox.Add(wx.StaticText(trigger_menu_panel, label='Level:'), 1, wx.ALIGN_CENTER_VERTICAL)
271                 trigger_level_button = forms.single_button(
272                         parent=trigger_menu_panel, sizer=hbox, label='50%',
273                         callback=parent.set_auto_trigger_level, style=wx.BU_EXACTFIT,
274                 )
275                 hbox.AddSpacer(WIDTH-60)
276                 trigger_level_buttons = forms.incr_decr_buttons(
277                         parent=trigger_menu_panel, sizer=hbox,
278                         on_incr=self._on_incr_trigger_level, on_decr=self._on_decr_trigger_level,
279                 )
280                 def disable_all(trigger_mode):
281                         for widget in (trigger_slope_chooser, trigger_channel_chooser, trigger_level_buttons, trigger_level_button):
282                                 widget.Disable(trigger_mode == gr.gr_TRIG_MODE_FREE)
283                 parent.subscribe(TRIGGER_MODE_KEY, disable_all)
284                 disable_all(parent[TRIGGER_MODE_KEY])
285                 ##################################################
286                 # XY Menu Box
287                 ##################################################
288                 if parent.num_inputs > 1:
289                         xy_menu_panel = wx.Panel(options_notebook)
290                         options_notebook_args.append((xy_menu_panel, XY_PAGE_INDEX, 'XY'))
291                         xy_menu_box = wx.BoxSizer(wx.VERTICAL)
292                         xy_menu_panel.SetSizer(xy_menu_box)
293                         #x and y channel choosers
294                         xy_menu_box.AddStretchSpacer()
295                         forms.drop_down(
296                                 parent=xy_menu_panel, sizer=xy_menu_box,
297                                 ps=parent, key=X_CHANNEL_KEY,
298                                 choices=map(lambda x: x[1], CHANNELS),
299                                 labels=map(lambda x: x[0], CHANNELS),
300                                 label='Channel X', width=WIDTH,
301                         )
302                         xy_menu_box.AddStretchSpacer()
303                         forms.drop_down(
304                                 parent=xy_menu_panel, sizer=xy_menu_box,
305                                 ps=parent, key=Y_CHANNEL_KEY,
306                                 choices=map(lambda x: x[1], CHANNELS),
307                                 labels=map(lambda x: x[0], CHANNELS),
308                                 label='Channel Y', width=WIDTH,
309                         )
310                         #marker
311                         xy_menu_box.AddStretchSpacer()
312                         forms.drop_down(
313                                 parent=xy_menu_panel, sizer=xy_menu_box,
314                                 ps=parent, key=XY_MARKER_KEY,
315                                 choices=map(lambda x: x[1], MARKER_TYPES),
316                                 labels=map(lambda x: x[0], MARKER_TYPES),
317                                 label='Marker', width=WIDTH,
318                         )
319                         xy_menu_box.AddStretchSpacer()
320                 ##################################################
321                 # Setup Options Notebook
322                 ##################################################
323                 forms.notebook(
324                         parent=self, sizer=chan_options_box,
325                         notebook=options_notebook,
326                         ps=parent, key=CHANNEL_OPTIONS_KEY,
327                         pages=map(lambda x: x[0], options_notebook_args),
328                         choices=map(lambda x: x[1], options_notebook_args),
329                         labels=map(lambda x: x[2], options_notebook_args),
330                 )
331                 #gui handling for channel options changing
332                 def options_notebook_changed(chan_opt):
333                         try:
334                                 parent[TRIGGER_SHOW_KEY] = chan_opt == TRIGGER_PAGE_INDEX
335                                 parent[XY_MODE_KEY] = chan_opt == XY_PAGE_INDEX
336                         except wx.PyDeadObjectError: pass
337                 parent.subscribe(CHANNEL_OPTIONS_KEY, options_notebook_changed)
338                 #gui handling for xy mode changing
339                 def xy_mode_changed(mode):
340                         #ensure xy tab is selected
341                         if mode and parent[CHANNEL_OPTIONS_KEY] != XY_PAGE_INDEX:
342                                 parent[CHANNEL_OPTIONS_KEY] = XY_PAGE_INDEX
343                         #ensure xy tab is not selected
344                         elif not mode and parent[CHANNEL_OPTIONS_KEY] == XY_PAGE_INDEX:
345                                 parent[CHANNEL_OPTIONS_KEY] = 0
346                         #show/hide control buttons
347                         scope_mode_box.ShowItems(not mode)
348                         xy_mode_box.ShowItems(mode)
349                         control_box.Layout()
350                 parent.subscribe(XY_MODE_KEY, xy_mode_changed)
351                 xy_mode_changed(parent[XY_MODE_KEY])
352                 ##################################################
353                 # Run/Stop Button
354                 ##################################################
355                 #run/stop
356                 control_box.AddStretchSpacer()
357                 forms.toggle_button(
358                         sizer=control_box, parent=self,
359                         true_label='Stop', false_label='Run',
360                         ps=parent, key=RUNNING_KEY,
361                 )
362                 #set sizer
363                 self.SetSizerAndFit(control_box)
364                 #mouse wheel event
365                 def on_mouse_wheel(event):
366                         if not parent[XY_MODE_KEY]:
367                                 if event.GetWheelRotation() < 0: self._on_incr_t_divs(event)
368                                 else: self._on_decr_t_divs(event)
369                 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
370
371         ##################################################
372         # Event handlers
373         ##################################################
374         #trigger level
375         def _on_incr_trigger_level(self, event):
376                 self.parent[TRIGGER_LEVEL_KEY] += self.parent[Y_PER_DIV_KEY]/3.
377         def _on_decr_trigger_level(self, event):
378                 self.parent[TRIGGER_LEVEL_KEY] -= self.parent[Y_PER_DIV_KEY]/3.
379         #incr/decr divs
380         def _on_incr_t_divs(self, event):
381                 self.parent[T_PER_DIV_KEY] = common.get_clean_incr(self.parent[T_PER_DIV_KEY])
382         def _on_decr_t_divs(self, event):
383                 self.parent[T_PER_DIV_KEY] = common.get_clean_decr(self.parent[T_PER_DIV_KEY])
384         def _on_incr_x_divs(self, event):
385                 self.parent[X_PER_DIV_KEY] = common.get_clean_incr(self.parent[X_PER_DIV_KEY])
386         def _on_decr_x_divs(self, event):
387                 self.parent[X_PER_DIV_KEY] = common.get_clean_decr(self.parent[X_PER_DIV_KEY])
388         def _on_incr_y_divs(self, event):
389                 self.parent[Y_PER_DIV_KEY] = common.get_clean_incr(self.parent[Y_PER_DIV_KEY])
390         def _on_decr_y_divs(self, event):
391                 self.parent[Y_PER_DIV_KEY] = common.get_clean_decr(self.parent[Y_PER_DIV_KEY])
392         #incr/decr offset
393         def _on_incr_x_off(self, event):
394                 self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY]
395         def _on_decr_x_off(self, event):
396                 self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY]
397         def _on_incr_y_off(self, event):
398                 self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY]
399         def _on_decr_y_off(self, event):
400                 self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]
401
402         ##################################################
403         # subscriber handlers
404         ##################################################
405         def _update_layout(self,key):
406           # Just ignore the key value we get
407           # we only need to now that the visability or size of something has changed
408           self.parent.Layout()
409           #self.parent.Fit()  
410
411 ##################################################
412 # Scope window with plotter and control panel
413 ##################################################
414 class scope_window(wx.Panel, pubsub.pubsub):
415         def __init__(
416                 self,
417                 parent,
418                 controller,
419                 size,
420                 title,
421                 frame_rate,
422                 num_inputs,
423                 sample_rate_key,
424                 t_scale,
425                 v_scale,
426                 v_offset,
427                 xy_mode,
428                 ac_couple_key,
429                 trigger_level_key,
430                 trigger_mode_key,
431                 trigger_slope_key,
432                 trigger_channel_key,
433                 decimation_key,
434                 msg_key,
435                 use_persistence,
436                 persist_alpha,
437                 trig_mode,
438         ):
439                 pubsub.pubsub.__init__(self)
440                 #check num inputs
441                 assert num_inputs <= len(CHANNEL_COLOR_SPECS)
442                 #setup
443                 self.sampleses = None
444                 self.num_inputs = num_inputs
445                 autorange = not v_scale
446                 self.autorange_ts = 0
447                 v_scale = v_scale or 1
448                 self.frame_rate_ts = 0
449                 #proxy the keys
450                 self.proxy(MSG_KEY, controller, msg_key)
451                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
452                 self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key)
453                 self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key)
454                 self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key)
455                 self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key)
456                 self.proxy(DECIMATION_KEY, controller, decimation_key)
457                 #initialize values
458                 self[RUNNING_KEY] = True
459                 self[XY_MARKER_KEY] = 2.0
460                 self[CHANNEL_OPTIONS_KEY] = 0
461                 self[XY_MODE_KEY] = xy_mode
462                 self[X_CHANNEL_KEY] = 0
463                 self[Y_CHANNEL_KEY] = self.num_inputs-1
464                 self[AUTORANGE_KEY] = autorange
465                 self[T_PER_DIV_KEY] = t_scale
466                 self[X_PER_DIV_KEY] = v_scale
467                 self[Y_PER_DIV_KEY] = v_scale
468                 self[T_OFF_KEY] = 0
469                 self[X_OFF_KEY] = v_offset
470                 self[Y_OFF_KEY] = v_offset
471                 self[T_DIVS_KEY] = 8
472                 self[X_DIVS_KEY] = 8
473                 self[Y_DIVS_KEY] = 8
474                 self[FRAME_RATE_KEY] = frame_rate
475                 self[TRIGGER_LEVEL_KEY] = 0
476                 self[TRIGGER_CHANNEL_KEY] = 0
477                 self[TRIGGER_MODE_KEY] = trig_mode
478                 
479                 self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS
480                 self[T_FRAC_OFF_KEY] = 0.5
481                 self[USE_PERSISTENCE_KEY] = use_persistence
482                 self[PERSIST_ALPHA_KEY] = persist_alpha
483                 
484                 if self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_STRIPCHART:
485                         self[T_FRAC_OFF_KEY] = 0.0
486
487                 for i in range(num_inputs):
488                         self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i))
489                 #init panel and plot
490                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
491                 self.plotter = plotter.channel_plotter(self)
492                 self.plotter.SetSize(wx.Size(*size))
493                 self.plotter.set_title(title)
494                 self.plotter.enable_legend(True)
495                 self.plotter.enable_point_label(True)
496                 self.plotter.enable_grid_lines(True)
497                 self.plotter.set_use_persistence(use_persistence)
498                 self.plotter.set_persist_alpha(persist_alpha)
499                 #setup the box with plot and controls
500                 self.control_panel = control_panel(self)
501                 main_box = wx.BoxSizer(wx.HORIZONTAL)
502                 main_box.Add(self.plotter, 1, wx.EXPAND)
503                 main_box.Add(self.control_panel, 0, wx.EXPAND)
504                 self.SetSizerAndFit(main_box)
505                 #register events for message
506                 self.subscribe(MSG_KEY, self.handle_msg)
507                 #register events for grid
508                 for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [
509                         TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY,
510                         T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY,
511                         T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY,
512                         T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY,
513                         XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY,
514                         TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY,
515                 ]: self.subscribe(key, self.update_grid)
516                 #register events for plotter settings
517                 self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
518                 self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
519                 #initial update
520                 self.update_grid()
521
522         def handle_msg(self, msg):
523                 """
524                 Handle the message from the scope sink message queue.
525                 Plot the list of arrays of samples onto the grid.
526                 Each samples array gets its own channel.
527                 @param msg the time domain data as a character array
528                 """
529                 if not self[RUNNING_KEY]: return
530                 #check time elapsed
531                 if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return
532                 #convert to floating point numbers
533                 samples = numpy.fromstring(msg, numpy.float32)
534                 #extract the trigger offset
535                 self.trigger_offset = samples[-1]
536                 samples = samples[:-1]
537                 samps_per_ch = len(samples)/self.num_inputs
538                 self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)]
539                 #handle samples
540                 self.handle_samples()
541                 self.frame_rate_ts = time.time()
542
543         def set_auto_trigger_level(self, *args):
544                 """
545                 Use the current trigger channel and samples to calculate the 50% level.
546                 """
547                 if not self.sampleses: return
548                 samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]]
549                 self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2
550
551         def handle_samples(self):
552                 """
553                 Handle the cached samples from the scope input.
554                 Perform ac coupling, triggering, and auto ranging.
555                 """
556                 if not self.sampleses: return
557                 sampleses = self.sampleses
558                 if self[XY_MODE_KEY]:
559                         self[DECIMATION_KEY] = 1
560                         x_samples = sampleses[self[X_CHANNEL_KEY]]
561                         y_samples = sampleses[self[Y_CHANNEL_KEY]]
562                         #autorange
563                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
564                                 x_min, x_max = common.get_min_max(x_samples)
565                                 y_min, y_max = common.get_min_max(y_samples)
566                                 #adjust the x per div
567                                 x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY])
568                                 if x_per_div != self[X_PER_DIV_KEY]: self[X_PER_DIV_KEY] = x_per_div; return
569                                 #adjust the x offset
570                                 x_off = x_per_div*round((x_max+x_min)/2/x_per_div)
571                                 if x_off != self[X_OFF_KEY]: self[X_OFF_KEY] = x_off; return
572                                 #adjust the y per div
573                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
574                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
575                                 #adjust the y offset
576                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
577                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
578                                 self.autorange_ts = time.time()
579                         #plot xy channel
580                         self.plotter.set_waveform(
581                                 channel='XY',
582                                 samples=(x_samples, y_samples),
583                                 color_spec=CHANNEL_COLOR_SPECS[0],
584                                 marker=self[XY_MARKER_KEY],
585                         )
586                         #turn off each waveform
587                         for i, samples in enumerate(sampleses):
588                                 self.plotter.clear_waveform(channel='Ch%d'%(i+1))
589                 else:
590                         #autorange
591                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
592                                 bounds = [common.get_min_max(samples) for samples in sampleses]
593                                 y_min = numpy.min([bound[0] for bound in bounds])
594                                 y_max = numpy.max([bound[1] for bound in bounds])
595                                 #adjust the y per div
596                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
597                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
598                                 #adjust the y offset
599                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
600                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
601                                 self.autorange_ts = time.time()
602                         #number of samples to scale to the screen
603                         actual_rate = self.get_actual_rate()
604                         time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY]
605                         num_samps = int(round(time_span*actual_rate))
606                         #handle the time offset
607                         t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span)
608                         if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return
609                         samps_off = int(round(actual_rate*self[T_OFF_KEY]))
610                         #adjust the decim so that we use about half the samps
611                         self[DECIMATION_KEY] = int(round(
612                                         time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0]))
613                                 )
614                         )
615                         #num samps too small, auto increment the time
616                         if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY])
617                         #num samps in bounds, plot each waveform
618                         elif num_samps <= len(sampleses[0]):
619                                 for i, samples in enumerate(sampleses):
620                                         #plot samples
621                                         self.plotter.set_waveform(
622                                                 channel='Ch%d'%(i+1),
623                                                 samples=samples[samps_off:num_samps+samps_off],
624                                                 color_spec=CHANNEL_COLOR_SPECS[i],
625                                                 marker=self[common.index_key(MARKER_KEY, i)],
626                                                 trig_off=self.trigger_offset,
627                                         )
628                         #turn XY channel off
629                         self.plotter.clear_waveform(channel='XY')
630                 #keep trigger level within range
631                 if self[TRIGGER_LEVEL_KEY] > self.get_y_max():
632                         self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return
633                 if self[TRIGGER_LEVEL_KEY] < self.get_y_min():
634                         self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return
635                 #disable the trigger channel
636                 if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE:
637                         self.plotter.clear_waveform(channel='Trig')
638                 else: #show trigger channel
639                         trigger_level = self[TRIGGER_LEVEL_KEY]
640                         trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0
641                         self.plotter.set_waveform(
642                                 channel='Trig',
643                                 samples=(
644                                         [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()],
645                                         [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level]
646                                 ),
647                                 color_spec=TRIGGER_COLOR_SPEC,
648                         )
649                 #update the plotter
650                 self.plotter.update()
651
652         def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY]
653         def get_t_min(self): return self[T_OFF_KEY]
654         def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY]
655         def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
656         def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
657         def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
658         def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
659
660         def update_grid(self, *args):
661                 """
662                 Update the grid to reflect the current settings:
663                 xy divisions, xy offset, xy mode setting
664                 """
665                 if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return
666                 if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return
667                 if self[XY_MODE_KEY]:
668                         #update the x axis
669                         self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1))
670                         self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY])
671                         #update the y axis
672                         self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1))
673                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
674                 else:
675                         #update the t axis
676                         self.plotter.set_x_label('Time', 's')
677                         self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True)
678                         #update the y axis
679                         self.plotter.set_y_label('Counts')
680                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
681                 #redraw current sample
682                 self.handle_samples()
683