Change default bandwidth to 25 MHz to match maximum USRP2 bandwidth
[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                 v_offset,
380                 xy_mode,
381                 ac_couple_key,
382                 trigger_level_key,
383                 trigger_mode_key,
384                 trigger_slope_key,
385                 trigger_channel_key,
386                 decimation_key,
387                 msg_key,
388         ):
389                 pubsub.pubsub.__init__(self)
390                 #check num inputs
391                 assert num_inputs <= len(CHANNEL_COLOR_SPECS)
392                 #setup
393                 self.sampleses = None
394                 self.num_inputs = num_inputs
395                 autorange = not v_scale
396                 self.autorange_ts = 0
397                 v_scale = v_scale or 1
398                 self.frame_rate_ts = 0
399                 #proxy the keys
400                 self.proxy(MSG_KEY, controller, msg_key)
401                 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
402                 self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key)
403                 self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key)
404                 self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key)
405                 self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key)
406                 self.proxy(DECIMATION_KEY, controller, decimation_key)
407                 #initialize values
408                 self[RUNNING_KEY] = True
409                 self[XY_MARKER_KEY] = 2.0
410                 self[CHANNEL_OPTIONS_KEY] = 0
411                 self[XY_MODE_KEY] = xy_mode
412                 self[X_CHANNEL_KEY] = 0
413                 self[Y_CHANNEL_KEY] = self.num_inputs-1
414                 self[AUTORANGE_KEY] = autorange
415                 self[T_PER_DIV_KEY] = t_scale
416                 self[X_PER_DIV_KEY] = v_scale
417                 self[Y_PER_DIV_KEY] = v_scale
418                 self[T_OFF_KEY] = 0
419                 self[X_OFF_KEY] = v_offset
420                 self[Y_OFF_KEY] = v_offset
421                 self[T_DIVS_KEY] = 8
422                 self[X_DIVS_KEY] = 8
423                 self[Y_DIVS_KEY] = 8
424                 self[FRAME_RATE_KEY] = frame_rate
425                 self[TRIGGER_LEVEL_KEY] = 0
426                 self[TRIGGER_CHANNEL_KEY] = 0
427                 self[TRIGGER_MODE_KEY] = gr.gr_TRIG_MODE_AUTO
428                 self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS
429                 self[T_FRAC_OFF_KEY] = 0.5
430                 for i in range(num_inputs):
431                         self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i))
432                 #init panel and plot
433                 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
434                 self.plotter = plotter.channel_plotter(self)
435                 self.plotter.SetSize(wx.Size(*size))
436                 self.plotter.set_title(title)
437                 self.plotter.enable_legend(True)
438                 self.plotter.enable_point_label(True)
439                 self.plotter.enable_grid_lines(True)
440                 #setup the box with plot and controls
441                 self.control_panel = control_panel(self)
442                 main_box = wx.BoxSizer(wx.HORIZONTAL)
443                 main_box.Add(self.plotter, 1, wx.EXPAND)
444                 main_box.Add(self.control_panel, 0, wx.EXPAND)
445                 self.SetSizerAndFit(main_box)
446                 #register events for message
447                 self.subscribe(MSG_KEY, self.handle_msg)
448                 #register events for grid
449                 for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [
450                         TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY,
451                         T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY,
452                         T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY,
453                         T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY,
454                         XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY,
455                         TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY,
456                 ]: self.subscribe(key, self.update_grid)
457                 #initial update
458                 self.update_grid()
459
460         def handle_msg(self, msg):
461                 """
462                 Handle the message from the scope sink message queue.
463                 Plot the list of arrays of samples onto the grid.
464                 Each samples array gets its own channel.
465                 @param msg the time domain data as a character array
466                 """
467                 if not self[RUNNING_KEY]: return
468                 #check time elapsed
469                 if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return
470                 #convert to floating point numbers
471                 samples = numpy.fromstring(msg, numpy.float32)
472                 #extract the trigger offset
473                 self.trigger_offset = samples[-1]
474                 samples = samples[:-1]
475                 samps_per_ch = len(samples)/self.num_inputs
476                 self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)]
477                 #handle samples
478                 self.handle_samples()
479                 self.frame_rate_ts = time.time()
480
481         def set_auto_trigger_level(self, *args):
482                 """
483                 Use the current trigger channel and samples to calculate the 50% level.
484                 """
485                 if not self.sampleses: return
486                 samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]]
487                 self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2
488
489         def handle_samples(self):
490                 """
491                 Handle the cached samples from the scope input.
492                 Perform ac coupling, triggering, and auto ranging.
493                 """
494                 if not self.sampleses: return
495                 sampleses = self.sampleses
496                 if self[XY_MODE_KEY]:
497                         self[DECIMATION_KEY] = 1
498                         x_samples = sampleses[self[X_CHANNEL_KEY]]
499                         y_samples = sampleses[self[Y_CHANNEL_KEY]]
500                         #autorange
501                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
502                                 x_min, x_max = common.get_min_max(x_samples)
503                                 y_min, y_max = common.get_min_max(y_samples)
504                                 #adjust the x per div
505                                 x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY])
506                                 if x_per_div != self[X_PER_DIV_KEY]: self[X_PER_DIV_KEY] = x_per_div; return
507                                 #adjust the x offset
508                                 x_off = x_per_div*round((x_max+x_min)/2/x_per_div)
509                                 if x_off != self[X_OFF_KEY]: self[X_OFF_KEY] = x_off; return
510                                 #adjust the y per div
511                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
512                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
513                                 #adjust the y offset
514                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
515                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
516                                 self.autorange_ts = time.time()
517                         #plot xy channel
518                         self.plotter.set_waveform(
519                                 channel='XY',
520                                 samples=(x_samples, y_samples),
521                                 color_spec=CHANNEL_COLOR_SPECS[0],
522                                 marker=self[XY_MARKER_KEY],
523                         )
524                         #turn off each waveform
525                         for i, samples in enumerate(sampleses):
526                                 self.plotter.clear_waveform(channel='Ch%d'%(i+1))
527                 else:
528                         #autorange
529                         if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
530                                 bounds = [common.get_min_max(samples) for samples in sampleses]
531                                 y_min = numpy.min([bound[0] for bound in bounds])
532                                 y_max = numpy.max([bound[1] for bound in bounds])
533                                 #adjust the y per div
534                                 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
535                                 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
536                                 #adjust the y offset
537                                 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
538                                 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
539                                 self.autorange_ts = time.time()
540                         #number of samples to scale to the screen
541                         actual_rate = self.get_actual_rate()
542                         time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY]
543                         num_samps = int(round(time_span*actual_rate))
544                         #handle the time offset
545                         t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span)
546                         if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return
547                         samps_off = int(round(actual_rate*self[T_OFF_KEY]))
548                         #adjust the decim so that we use about half the samps
549                         self[DECIMATION_KEY] = int(round(
550                                         time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0]))
551                                 )
552                         )
553                         #num samps too small, auto increment the time
554                         if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY])
555                         #num samps in bounds, plot each waveform
556                         elif num_samps <= len(sampleses[0]):
557                                 for i, samples in enumerate(sampleses):
558                                         #plot samples
559                                         self.plotter.set_waveform(
560                                                 channel='Ch%d'%(i+1),
561                                                 samples=samples[samps_off:num_samps+samps_off],
562                                                 color_spec=CHANNEL_COLOR_SPECS[i],
563                                                 marker=self[common.index_key(MARKER_KEY, i)],
564                                                 trig_off=self.trigger_offset,
565                                         )
566                         #turn XY channel off
567                         self.plotter.clear_waveform(channel='XY')
568                 #keep trigger level within range
569                 if self[TRIGGER_LEVEL_KEY] > self.get_y_max():
570                         self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return
571                 if self[TRIGGER_LEVEL_KEY] < self.get_y_min():
572                         self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return
573                 #disable the trigger channel
574                 if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE:
575                         self.plotter.clear_waveform(channel='Trig')
576                 else: #show trigger channel
577                         trigger_level = self[TRIGGER_LEVEL_KEY]
578                         trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0
579                         self.plotter.set_waveform(
580                                 channel='Trig',
581                                 samples=(
582                                         [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()],
583                                         [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level]
584                                 ),
585                                 color_spec=TRIGGER_COLOR_SPEC,
586                         )
587                 #update the plotter
588                 self.plotter.update()
589
590         def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY]
591         def get_t_min(self): return self[T_OFF_KEY]
592         def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY]
593         def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
594         def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
595         def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
596         def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
597
598         def update_grid(self, *args):
599                 """
600                 Update the grid to reflect the current settings:
601                 xy divisions, xy offset, xy mode setting
602                 """
603                 if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return
604                 if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return
605                 if self[XY_MODE_KEY]:
606                         #update the x axis
607                         self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1))
608                         self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY])
609                         #update the y axis
610                         self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1))
611                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
612                 else:
613                         #update the t axis
614                         self.plotter.set_x_label('Time', 's')
615                         self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True)
616                         #update the y axis
617                         self.plotter.set_y_label('Counts')
618                         self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
619                 #redraw current sample
620                 self.handle_samples()