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