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