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