Adds a new parameter "y_axis_label" to scopesink2 and the GRC .xml file that contains...
[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                 y_axis_label,
439         ):
440                 pubsub.pubsub.__init__(self)
441                 #check num inputs
442                 assert num_inputs <= len(CHANNEL_COLOR_SPECS)
443                 #setup
444                 self.sampleses = None
445                 self.num_inputs = num_inputs
446                 autorange = not v_scale
447                 self.autorange_ts = 0
448                 v_scale = v_scale or 1
449                 self.frame_rate_ts = 0
450                 #proxy the keys
451                 self.proxy(MSG_KEY, controller, msg_key)
452                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
453                 self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key)
454                 self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key)
455                 self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key)
456                 self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key)
457                 self.proxy(DECIMATION_KEY, controller, decimation_key)
458                 #initialize values
459                 self[RUNNING_KEY] = True
460                 self[XY_MARKER_KEY] = 2.0
461                 self[CHANNEL_OPTIONS_KEY] = 0
462                 self[XY_MODE_KEY] = xy_mode
463                 self[X_CHANNEL_KEY] = 0
464                 self[Y_CHANNEL_KEY] = self.num_inputs-1
465                 self[AUTORANGE_KEY] = autorange
466                 self[T_PER_DIV_KEY] = t_scale
467                 self[X_PER_DIV_KEY] = v_scale
468                 self[Y_PER_DIV_KEY] = v_scale
469                 self[T_OFF_KEY] = 0
470                 self[X_OFF_KEY] = v_offset
471                 self[Y_OFF_KEY] = v_offset
472                 self[T_DIVS_KEY] = 8
473                 self[X_DIVS_KEY] = 8
474                 self[Y_DIVS_KEY] = 8
475                 self[Y_AXIS_LABEL] = y_axis_label
476                 self[FRAME_RATE_KEY] = frame_rate
477                 self[TRIGGER_LEVEL_KEY] = 0
478                 self[TRIGGER_CHANNEL_KEY] = 0
479                 self[TRIGGER_MODE_KEY] = trig_mode
480                 
481                 self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS
482                 self[T_FRAC_OFF_KEY] = 0.5
483                 self[USE_PERSISTENCE_KEY] = use_persistence
484                 self[PERSIST_ALPHA_KEY] = persist_alpha
485                 
486                 if self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_STRIPCHART:
487                         self[T_FRAC_OFF_KEY] = 0.0
488
489                 for i in range(num_inputs):
490                         self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i))
491                 #init panel and plot
492                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
493                 self.plotter = plotter.channel_plotter(self)
494                 self.plotter.SetSize(wx.Size(*size))
495                 self.plotter.set_title(title)
496                 self.plotter.enable_legend(True)
497                 self.plotter.enable_point_label(True)
498                 self.plotter.enable_grid_lines(True)
499                 self.plotter.set_use_persistence(use_persistence)
500                 self.plotter.set_persist_alpha(persist_alpha)
501                 #setup the box with plot and controls
502                 self.control_panel = control_panel(self)
503                 main_box = wx.BoxSizer(wx.HORIZONTAL)
504                 main_box.Add(self.plotter, 1, wx.EXPAND)
505                 main_box.Add(self.control_panel, 0, wx.EXPAND)
506                 self.SetSizerAndFit(main_box)
507                 #register events for message
508                 self.subscribe(MSG_KEY, self.handle_msg)
509                 #register events for grid
510                 for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [
511                         TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY,
512                         T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY,
513                         T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY,
514                         T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY,
515                         XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY,
516                         TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY,
517                 ]: self.subscribe(key, self.update_grid)
518                 #register events for plotter settings
519                 self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
520                 self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
521                 #initial update
522                 self.update_grid()
523
524         def handle_msg(self, msg):
525                 """
526                 Handle the message from the scope sink message queue.
527                 Plot the list of arrays of samples onto the grid.
528                 Each samples array gets its own channel.
529                 @param msg the time domain data as a character array
530                 """
531                 if not self[RUNNING_KEY]: return
532                 #check time elapsed
533                 if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return
534                 #convert to floating point numbers
535                 samples = numpy.fromstring(msg, numpy.float32)
536                 #extract the trigger offset
537                 self.trigger_offset = samples[-1]
538                 samples = samples[:-1]
539                 samps_per_ch = len(samples)/self.num_inputs
540                 self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)]
541                 #handle samples
542                 self.handle_samples()
543                 self.frame_rate_ts = time.time()
544
545         def set_auto_trigger_level(self, *args):
546                 """
547                 Use the current trigger channel and samples to calculate the 50% level.
548                 """
549                 if not self.sampleses: return
550                 samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]]
551                 self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2
552
553         def handle_samples(self):
554                 """
555                 Handle the cached samples from the scope input.
556                 Perform ac coupling, triggering, and auto ranging.
557                 """
558                 if not self.sampleses: return
559                 sampleses = self.sampleses
560                 if self[XY_MODE_KEY]:
561                         self[DECIMATION_KEY] = 1
562                         x_samples = sampleses[self[X_CHANNEL_KEY]]
563                         y_samples = sampleses[self[Y_CHANNEL_KEY]]
564                         #autorange
565                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
566                                 x_min, x_max = common.get_min_max(x_samples)
567                                 y_min, y_max = common.get_min_max(y_samples)
568                                 #adjust the x per div
569                                 x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY])
570                                 if x_per_div != self[X_PER_DIV_KEY]: self[X_PER_DIV_KEY] = x_per_div; return
571                                 #adjust the x offset
572                                 x_off = x_per_div*round((x_max+x_min)/2/x_per_div)
573                                 if x_off != self[X_OFF_KEY]: self[X_OFF_KEY] = x_off; return
574                                 #adjust the y per div
575                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
576                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
577                                 #adjust the y offset
578                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
579                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
580                                 self.autorange_ts = time.time()
581                         #plot xy channel
582                         self.plotter.set_waveform(
583                                 channel='XY',
584                                 samples=(x_samples, y_samples),
585                                 color_spec=CHANNEL_COLOR_SPECS[0],
586                                 marker=self[XY_MARKER_KEY],
587                         )
588                         #turn off each waveform
589                         for i, samples in enumerate(sampleses):
590                                 self.plotter.clear_waveform(channel='Ch%d'%(i+1))
591                 else:
592                         #autorange
593                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
594                                 bounds = [common.get_min_max(samples) for samples in sampleses]
595                                 y_min = numpy.min([bound[0] for bound in bounds])
596                                 y_max = numpy.max([bound[1] for bound in bounds])
597                                 #adjust the y per div
598                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
599                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
600                                 #adjust the y offset
601                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
602                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
603                                 self.autorange_ts = time.time()
604                         #number of samples to scale to the screen
605                         actual_rate = self.get_actual_rate()
606                         time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY]
607                         num_samps = int(round(time_span*actual_rate))
608                         #handle the time offset
609                         t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span)
610                         if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return
611                         samps_off = int(round(actual_rate*self[T_OFF_KEY]))
612                         #adjust the decim so that we use about half the samps
613                         self[DECIMATION_KEY] = int(round(
614                                         time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0]))
615                                 )
616                         )
617                         #num samps too small, auto increment the time
618                         if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY])
619                         #num samps in bounds, plot each waveform
620                         elif num_samps <= len(sampleses[0]):
621                                 for i, samples in enumerate(sampleses):
622                                         #plot samples
623                                         self.plotter.set_waveform(
624                                                 channel='Ch%d'%(i+1),
625                                                 samples=samples[samps_off:num_samps+samps_off],
626                                                 color_spec=CHANNEL_COLOR_SPECS[i],
627                                                 marker=self[common.index_key(MARKER_KEY, i)],
628                                                 trig_off=self.trigger_offset,
629                                         )
630                         #turn XY channel off
631                         self.plotter.clear_waveform(channel='XY')
632                 #keep trigger level within range
633                 if self[TRIGGER_LEVEL_KEY] > self.get_y_max():
634                         self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return
635                 if self[TRIGGER_LEVEL_KEY] < self.get_y_min():
636                         self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return
637                 #disable the trigger channel
638                 if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE:
639                         self.plotter.clear_waveform(channel='Trig')
640                 else: #show trigger channel
641                         trigger_level = self[TRIGGER_LEVEL_KEY]
642                         trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0
643                         self.plotter.set_waveform(
644                                 channel='Trig',
645                                 samples=(
646                                         [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()],
647                                         [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level]
648                                 ),
649                                 color_spec=TRIGGER_COLOR_SPEC,
650                         )
651                 #update the plotter
652                 self.plotter.update()
653
654         def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY]
655         def get_t_min(self): return self[T_OFF_KEY]
656         def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY]
657         def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
658         def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
659         def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
660         def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
661
662         def update_grid(self, *args):
663                 """
664                 Update the grid to reflect the current settings:
665                 xy divisions, xy offset, xy mode setting
666                 """
667                 if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return
668                 if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return
669                 if self[XY_MODE_KEY]:
670                         #update the x axis
671                         self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1))
672                         self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY])
673                         #update the y axis
674                         self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1))
675                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
676                 else:
677                         #update the t axis
678                         self.plotter.set_x_label('Time', 's')
679                         self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True)
680                         #update the y axis
681                         self.plotter.set_y_label(self[Y_AXIS_LABEL])
682                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
683                 #redraw current sample
684                 self.handle_samples()
685