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