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