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