2 # Copyright 2008,2010 Free Software Foundation, Inc.
4 # This file is part of GNU Radio
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)
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.
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.
22 ##################################################
24 ##################################################
31 from constants import *
32 from gnuradio import gr #for gr.prefs, trigger modes
35 ##################################################
37 ##################################################
38 DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'scope_rate', 30)
39 PERSIST_ALPHA_MIN_EXP, PERSIST_ALPHA_MAX_EXP = -2, 0
41 DEFAULT_TRIG_MODE = gr.prefs().get_long('wxgui', 'trig_mode', gr.gr_TRIG_MODE_AUTO)
42 DEFAULT_WIN_SIZE = (600, 300)
48 ('Freerun', gr.gr_TRIG_MODE_FREE),
49 ('Auto', gr.gr_TRIG_MODE_AUTO),
50 ('Normal', gr.gr_TRIG_MODE_NORM),
51 ('Stripchart', gr.gr_TRIG_MODE_STRIPCHART),
54 ('Pos +', gr.gr_TRIG_SLOPE_POS),
55 ('Neg -', gr.gr_TRIG_SLOPE_NEG),
57 CHANNEL_COLOR_SPECS = (
66 TRIGGER_COLOR_SPEC = (1.0, 0.4, 0.0)
67 AUTORANGE_UPDATE_RATE = 0.5 #sec
75 DEFAULT_MARKER_TYPE = None
77 ##################################################
78 # Scope window control panel
79 ##################################################
80 class control_panel(wx.Panel):
82 A control panel with wx widgits to control the plotter and scope block.
84 def __init__(self, parent):
86 Create a new control panel.
87 @param parent the wx parent window
91 wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
92 parent[SHOW_CONTROL_PANEL_KEY] = True
93 parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show)
94 control_box = wx.BoxSizer(wx.VERTICAL)
96 ##################################################
98 ##################################################
101 sizer=control_box, parent=self, label='Persistence',
102 ps=parent, key=USE_PERSISTENCE_KEY,
104 #static text and slider for analog alpha
105 persist_alpha_text = forms.static_text(
106 sizer=control_box, parent=self, label='Analog Alpha',
107 converter=forms.float_converter(lambda x: '%.4f'%x),
108 ps=parent, key=PERSIST_ALPHA_KEY, width=50,
110 persist_alpha_slider = forms.log_slider(
111 sizer=control_box, parent=self,
112 min_exp=PERSIST_ALPHA_MIN_EXP,
113 max_exp=PERSIST_ALPHA_MAX_EXP,
114 num_steps=SLIDER_STEPS,
115 ps=parent, key=PERSIST_ALPHA_KEY,
117 for widget in (persist_alpha_text, persist_alpha_slider):
118 parent.subscribe(USE_PERSISTENCE_KEY, widget.Enable)
119 widget.Enable(parent[USE_PERSISTENCE_KEY])
120 parent.subscribe(USE_PERSISTENCE_KEY, widget.ShowItems)
121 #allways show initially, so room is reserved for them
122 widget.ShowItems(True) # (parent[USE_PERSISTENCE_KEY])
124 parent.subscribe(USE_PERSISTENCE_KEY, self._update_layout)
126 ##################################################
128 ##################################################
129 control_box.AddStretchSpacer()
130 axes_options_box = forms.static_box_sizer(
131 parent=self, sizer=control_box, label='Axes Options',
132 bold=True, orient=wx.VERTICAL,
134 ##################################################
136 ##################################################
137 scope_mode_box = wx.BoxSizer(wx.VERTICAL)
138 axes_options_box.Add(scope_mode_box, 0, wx.EXPAND)
140 forms.incr_decr_buttons(
141 parent=self, sizer=scope_mode_box, label='Secs/Div',
142 on_incr=self._on_incr_t_divs, on_decr=self._on_decr_t_divs,
145 y_buttons_scope = forms.incr_decr_buttons(
146 parent=self, sizer=scope_mode_box, label='Counts/Div',
147 on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
150 y_off_buttons_scope = forms.incr_decr_buttons(
151 parent=self, sizer=scope_mode_box, label='Y Offset',
152 on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
155 scope_mode_box.AddSpacer(5)
157 parent=self, sizer=scope_mode_box,
158 ps=parent, key=T_FRAC_OFF_KEY, label='T Offset',
159 minimum=0, maximum=1, num_steps=1000,
161 scope_mode_box.AddSpacer(5)
162 ##################################################
164 ##################################################
165 xy_mode_box = wx.BoxSizer(wx.VERTICAL)
166 axes_options_box.Add(xy_mode_box, 0, wx.EXPAND)
168 x_buttons = forms.incr_decr_buttons(
169 parent=self, sizer=xy_mode_box, label='X/Div',
170 on_incr=self._on_incr_x_divs, on_decr=self._on_decr_x_divs,
173 y_buttons = forms.incr_decr_buttons(
174 parent=self, sizer=xy_mode_box, label='Y/Div',
175 on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs,
178 x_off_buttons = forms.incr_decr_buttons(
179 parent=self, sizer=xy_mode_box, label='X Off',
180 on_incr=self._on_incr_x_off, on_decr=self._on_decr_x_off,
183 y_off_buttons = forms.incr_decr_buttons(
184 parent=self, sizer=xy_mode_box, label='Y Off',
185 on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off,
187 for widget in (y_buttons_scope, y_off_buttons_scope, x_buttons, y_buttons, x_off_buttons, y_off_buttons):
188 parent.subscribe(AUTORANGE_KEY, widget.Disable)
189 widget.Disable(parent[AUTORANGE_KEY])
190 xy_mode_box.ShowItems(False)
193 parent=self, sizer=axes_options_box, label='Autorange',
194 ps=parent, key=AUTORANGE_KEY,
196 ##################################################
198 ##################################################
199 TRIGGER_PAGE_INDEX = parent.num_inputs
200 XY_PAGE_INDEX = parent.num_inputs+1
201 control_box.AddStretchSpacer()
202 chan_options_box = forms.static_box_sizer(
203 parent=self, sizer=control_box, label='Channel Options',
204 bold=True, orient=wx.VERTICAL,
206 options_notebook = wx.Notebook(self)
207 options_notebook_args = list()
208 CHANNELS = [('Ch %d'%(i+1), i) for i in range(parent.num_inputs)]
209 ##################################################
211 ##################################################
212 for i in range(parent.num_inputs):
213 channel_menu_panel = wx.Panel(options_notebook)
214 options_notebook_args.append((channel_menu_panel, i, 'Ch%d'%(i+1)))
215 channel_menu_box = wx.BoxSizer(wx.VERTICAL)
216 channel_menu_panel.SetSizer(channel_menu_box)
218 channel_menu_box.AddStretchSpacer()
220 parent=channel_menu_panel, sizer=channel_menu_box,
221 ps=parent, key=common.index_key(AC_COUPLE_KEY, i),
222 choices=map(lambda x: x[1], COUPLING_MODES),
223 labels=map(lambda x: x[0], COUPLING_MODES),
224 label='Coupling', width=WIDTH,
227 channel_menu_box.AddStretchSpacer()
229 parent=channel_menu_panel, sizer=channel_menu_box,
230 ps=parent, key=common.index_key(MARKER_KEY, i),
231 choices=map(lambda x: x[1], MARKER_TYPES),
232 labels=map(lambda x: x[0], MARKER_TYPES),
233 label='Marker', width=WIDTH,
235 channel_menu_box.AddStretchSpacer()
236 ##################################################
238 ##################################################
239 trigger_menu_panel = wx.Panel(options_notebook)
240 options_notebook_args.append((trigger_menu_panel, TRIGGER_PAGE_INDEX, 'Trig'))
241 trigger_menu_box = wx.BoxSizer(wx.VERTICAL)
242 trigger_menu_panel.SetSizer(trigger_menu_box)
245 parent=trigger_menu_panel, sizer=trigger_menu_box,
246 ps=parent, key=TRIGGER_MODE_KEY,
247 choices=map(lambda x: x[1], TRIGGER_MODES),
248 labels=map(lambda x: x[0], TRIGGER_MODES),
249 label='Mode', width=WIDTH,
252 trigger_slope_chooser = forms.drop_down(
253 parent=trigger_menu_panel, sizer=trigger_menu_box,
254 ps=parent, key=TRIGGER_SLOPE_KEY,
255 choices=map(lambda x: x[1], TRIGGER_SLOPES),
256 labels=map(lambda x: x[0], TRIGGER_SLOPES),
257 label='Slope', width=WIDTH,
260 trigger_channel_chooser = forms.drop_down(
261 parent=trigger_menu_panel, sizer=trigger_menu_box,
262 ps=parent, key=TRIGGER_CHANNEL_KEY,
263 choices=map(lambda x: x[1], CHANNELS),
264 labels=map(lambda x: x[0], CHANNELS),
265 label='Channel', width=WIDTH,
268 hbox = wx.BoxSizer(wx.HORIZONTAL)
269 trigger_menu_box.Add(hbox, 0, wx.EXPAND)
270 hbox.Add(wx.StaticText(trigger_menu_panel, label='Level:'), 1, wx.ALIGN_CENTER_VERTICAL)
271 trigger_level_button = forms.single_button(
272 parent=trigger_menu_panel, sizer=hbox, label='50%',
273 callback=parent.set_auto_trigger_level, style=wx.BU_EXACTFIT,
275 hbox.AddSpacer(WIDTH-60)
276 trigger_level_buttons = forms.incr_decr_buttons(
277 parent=trigger_menu_panel, sizer=hbox,
278 on_incr=self._on_incr_trigger_level, on_decr=self._on_decr_trigger_level,
280 def disable_all(trigger_mode):
281 for widget in (trigger_slope_chooser, trigger_channel_chooser, trigger_level_buttons, trigger_level_button):
282 widget.Disable(trigger_mode == gr.gr_TRIG_MODE_FREE)
283 parent.subscribe(TRIGGER_MODE_KEY, disable_all)
284 disable_all(parent[TRIGGER_MODE_KEY])
285 ##################################################
287 ##################################################
288 if parent.num_inputs > 1:
289 xy_menu_panel = wx.Panel(options_notebook)
290 options_notebook_args.append((xy_menu_panel, XY_PAGE_INDEX, 'XY'))
291 xy_menu_box = wx.BoxSizer(wx.VERTICAL)
292 xy_menu_panel.SetSizer(xy_menu_box)
293 #x and y channel choosers
294 xy_menu_box.AddStretchSpacer()
296 parent=xy_menu_panel, sizer=xy_menu_box,
297 ps=parent, key=X_CHANNEL_KEY,
298 choices=map(lambda x: x[1], CHANNELS),
299 labels=map(lambda x: x[0], CHANNELS),
300 label='Channel X', width=WIDTH,
302 xy_menu_box.AddStretchSpacer()
304 parent=xy_menu_panel, sizer=xy_menu_box,
305 ps=parent, key=Y_CHANNEL_KEY,
306 choices=map(lambda x: x[1], CHANNELS),
307 labels=map(lambda x: x[0], CHANNELS),
308 label='Channel Y', width=WIDTH,
311 xy_menu_box.AddStretchSpacer()
313 parent=xy_menu_panel, sizer=xy_menu_box,
314 ps=parent, key=XY_MARKER_KEY,
315 choices=map(lambda x: x[1], MARKER_TYPES),
316 labels=map(lambda x: x[0], MARKER_TYPES),
317 label='Marker', width=WIDTH,
319 xy_menu_box.AddStretchSpacer()
320 ##################################################
321 # Setup Options Notebook
322 ##################################################
324 parent=self, sizer=chan_options_box,
325 notebook=options_notebook,
326 ps=parent, key=CHANNEL_OPTIONS_KEY,
327 pages=map(lambda x: x[0], options_notebook_args),
328 choices=map(lambda x: x[1], options_notebook_args),
329 labels=map(lambda x: x[2], options_notebook_args),
331 #gui handling for channel options changing
332 def options_notebook_changed(chan_opt):
334 parent[TRIGGER_SHOW_KEY] = chan_opt == TRIGGER_PAGE_INDEX
335 parent[XY_MODE_KEY] = chan_opt == XY_PAGE_INDEX
336 except wx.PyDeadObjectError: pass
337 parent.subscribe(CHANNEL_OPTIONS_KEY, options_notebook_changed)
338 #gui handling for xy mode changing
339 def xy_mode_changed(mode):
340 #ensure xy tab is selected
341 if mode and parent[CHANNEL_OPTIONS_KEY] != XY_PAGE_INDEX:
342 parent[CHANNEL_OPTIONS_KEY] = XY_PAGE_INDEX
343 #ensure xy tab is not selected
344 elif not mode and parent[CHANNEL_OPTIONS_KEY] == XY_PAGE_INDEX:
345 parent[CHANNEL_OPTIONS_KEY] = 0
346 #show/hide control buttons
347 scope_mode_box.ShowItems(not mode)
348 xy_mode_box.ShowItems(mode)
350 parent.subscribe(XY_MODE_KEY, xy_mode_changed)
351 xy_mode_changed(parent[XY_MODE_KEY])
352 ##################################################
354 ##################################################
356 control_box.AddStretchSpacer()
358 sizer=control_box, parent=self,
359 true_label='Stop', false_label='Run',
360 ps=parent, key=RUNNING_KEY,
363 self.SetSizerAndFit(control_box)
365 def on_mouse_wheel(event):
366 if not parent[XY_MODE_KEY]:
367 if event.GetWheelRotation() < 0: self._on_incr_t_divs(event)
368 else: self._on_decr_t_divs(event)
369 parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel)
371 ##################################################
373 ##################################################
375 def _on_incr_trigger_level(self, event):
376 self.parent[TRIGGER_LEVEL_KEY] += self.parent[Y_PER_DIV_KEY]/3.
377 def _on_decr_trigger_level(self, event):
378 self.parent[TRIGGER_LEVEL_KEY] -= self.parent[Y_PER_DIV_KEY]/3.
380 def _on_incr_t_divs(self, event):
381 self.parent[T_PER_DIV_KEY] = common.get_clean_incr(self.parent[T_PER_DIV_KEY])
382 def _on_decr_t_divs(self, event):
383 self.parent[T_PER_DIV_KEY] = common.get_clean_decr(self.parent[T_PER_DIV_KEY])
384 def _on_incr_x_divs(self, event):
385 self.parent[X_PER_DIV_KEY] = common.get_clean_incr(self.parent[X_PER_DIV_KEY])
386 def _on_decr_x_divs(self, event):
387 self.parent[X_PER_DIV_KEY] = common.get_clean_decr(self.parent[X_PER_DIV_KEY])
388 def _on_incr_y_divs(self, event):
389 self.parent[Y_PER_DIV_KEY] = common.get_clean_incr(self.parent[Y_PER_DIV_KEY])
390 def _on_decr_y_divs(self, event):
391 self.parent[Y_PER_DIV_KEY] = common.get_clean_decr(self.parent[Y_PER_DIV_KEY])
393 def _on_incr_x_off(self, event):
394 self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY]
395 def _on_decr_x_off(self, event):
396 self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY]
397 def _on_incr_y_off(self, event):
398 self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY]
399 def _on_decr_y_off(self, event):
400 self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]
402 ##################################################
403 # subscriber handlers
404 ##################################################
405 def _update_layout(self,key):
406 # Just ignore the key value we get
407 # we only need to now that the visability or size of something has changed
411 ##################################################
412 # Scope window with plotter and control panel
413 ##################################################
414 class scope_window(wx.Panel, pubsub.pubsub):
440 pubsub.pubsub.__init__(self)
442 assert num_inputs <= len(CHANNEL_COLOR_SPECS)
444 self.sampleses = None
445 self.num_inputs = num_inputs
446 autorange = not v_scale
447 self.autorange_ts = 0
448 v_scale = v_scale or 1
449 self.frame_rate_ts = 0
451 self.proxy(MSG_KEY, controller, msg_key)
452 self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key)
453 self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key)
454 self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key)
455 self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key)
456 self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key)
457 self.proxy(DECIMATION_KEY, controller, decimation_key)
459 self[RUNNING_KEY] = True
460 self[XY_MARKER_KEY] = 2.0
461 self[CHANNEL_OPTIONS_KEY] = 0
462 self[XY_MODE_KEY] = xy_mode
463 self[X_CHANNEL_KEY] = 0
464 self[Y_CHANNEL_KEY] = self.num_inputs-1
465 self[AUTORANGE_KEY] = autorange
466 self[T_PER_DIV_KEY] = t_scale
467 self[X_PER_DIV_KEY] = v_scale
468 self[Y_PER_DIV_KEY] = v_scale
470 self[X_OFF_KEY] = v_offset
471 self[Y_OFF_KEY] = v_offset
475 self[Y_AXIS_LABEL] = y_axis_label
476 self[FRAME_RATE_KEY] = frame_rate
477 self[TRIGGER_LEVEL_KEY] = 0
478 self[TRIGGER_CHANNEL_KEY] = 0
479 self[TRIGGER_MODE_KEY] = trig_mode
481 self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS
482 self[T_FRAC_OFF_KEY] = 0.5
483 self[USE_PERSISTENCE_KEY] = use_persistence
484 self[PERSIST_ALPHA_KEY] = persist_alpha
486 if self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_STRIPCHART:
487 self[T_FRAC_OFF_KEY] = 0.0
489 for i in range(num_inputs):
490 self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i))
492 wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER)
493 self.plotter = plotter.channel_plotter(self)
494 self.plotter.SetSize(wx.Size(*size))
495 self.plotter.set_title(title)
496 self.plotter.enable_legend(True)
497 self.plotter.enable_point_label(True)
498 self.plotter.enable_grid_lines(True)
499 self.plotter.set_use_persistence(use_persistence)
500 self.plotter.set_persist_alpha(persist_alpha)
501 #setup the box with plot and controls
502 self.control_panel = control_panel(self)
503 main_box = wx.BoxSizer(wx.HORIZONTAL)
504 main_box.Add(self.plotter, 1, wx.EXPAND)
505 main_box.Add(self.control_panel, 0, wx.EXPAND)
506 self.SetSizerAndFit(main_box)
507 #register events for message
508 self.subscribe(MSG_KEY, self.handle_msg)
509 #register events for grid
510 for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [
511 TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY,
512 T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY,
513 T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY,
514 T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY,
515 XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY,
516 TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY,
517 ]: self.subscribe(key, self.update_grid)
518 #register events for plotter settings
519 self.subscribe(USE_PERSISTENCE_KEY, self.plotter.set_use_persistence)
520 self.subscribe(PERSIST_ALPHA_KEY, self.plotter.set_persist_alpha)
524 def handle_msg(self, msg):
526 Handle the message from the scope sink message queue.
527 Plot the list of arrays of samples onto the grid.
528 Each samples array gets its own channel.
529 @param msg the time domain data as a character array
531 if not self[RUNNING_KEY]: return
533 if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return
534 #convert to floating point numbers
535 samples = numpy.fromstring(msg, numpy.float32)
536 #extract the trigger offset
537 self.trigger_offset = samples[-1]
538 samples = samples[:-1]
539 samps_per_ch = len(samples)/self.num_inputs
540 self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)]
542 self.handle_samples()
543 self.frame_rate_ts = time.time()
545 def set_auto_trigger_level(self, *args):
547 Use the current trigger channel and samples to calculate the 50% level.
549 if not self.sampleses: return
550 samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]]
551 self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2
553 def handle_samples(self):
555 Handle the cached samples from the scope input.
556 Perform ac coupling, triggering, and auto ranging.
558 if not self.sampleses: return
559 sampleses = self.sampleses
560 if self[XY_MODE_KEY]:
561 self[DECIMATION_KEY] = 1
562 x_samples = sampleses[self[X_CHANNEL_KEY]]
563 y_samples = sampleses[self[Y_CHANNEL_KEY]]
565 if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
566 x_min, x_max = common.get_min_max(x_samples)
567 y_min, y_max = common.get_min_max(y_samples)
568 #adjust the x per div
569 x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY])
570 if x_per_div != self[X_PER_DIV_KEY]: self[X_PER_DIV_KEY] = x_per_div; return
572 x_off = x_per_div*round((x_max+x_min)/2/x_per_div)
573 if x_off != self[X_OFF_KEY]: self[X_OFF_KEY] = x_off; return
574 #adjust the y per div
575 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
576 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
578 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
579 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
580 self.autorange_ts = time.time()
582 self.plotter.set_waveform(
584 samples=(x_samples, y_samples),
585 color_spec=CHANNEL_COLOR_SPECS[0],
586 marker=self[XY_MARKER_KEY],
588 #turn off each waveform
589 for i, samples in enumerate(sampleses):
590 self.plotter.clear_waveform(channel='Ch%d'%(i+1))
593 if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE:
594 bounds = [common.get_min_max(samples) for samples in sampleses]
595 y_min = numpy.min([bound[0] for bound in bounds])
596 y_max = numpy.max([bound[1] for bound in bounds])
597 #adjust the y per div
598 y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY])
599 if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return
601 y_off = y_per_div*round((y_max+y_min)/2/y_per_div)
602 if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return
603 self.autorange_ts = time.time()
604 #number of samples to scale to the screen
605 actual_rate = self.get_actual_rate()
606 time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY]
607 num_samps = int(round(time_span*actual_rate))
608 #handle the time offset
609 t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span)
610 if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return
611 samps_off = int(round(actual_rate*self[T_OFF_KEY]))
612 #adjust the decim so that we use about half the samps
613 self[DECIMATION_KEY] = int(round(
614 time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0]))
617 #num samps too small, auto increment the time
618 if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY])
619 #num samps in bounds, plot each waveform
620 elif num_samps <= len(sampleses[0]):
621 for i, samples in enumerate(sampleses):
623 self.plotter.set_waveform(
624 channel='Ch%d'%(i+1),
625 samples=samples[samps_off:num_samps+samps_off],
626 color_spec=CHANNEL_COLOR_SPECS[i],
627 marker=self[common.index_key(MARKER_KEY, i)],
628 trig_off=self.trigger_offset,
631 self.plotter.clear_waveform(channel='XY')
632 #keep trigger level within range
633 if self[TRIGGER_LEVEL_KEY] > self.get_y_max():
634 self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return
635 if self[TRIGGER_LEVEL_KEY] < self.get_y_min():
636 self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return
637 #disable the trigger channel
638 if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE:
639 self.plotter.clear_waveform(channel='Trig')
640 else: #show trigger channel
641 trigger_level = self[TRIGGER_LEVEL_KEY]
642 trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0
643 self.plotter.set_waveform(
646 [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()],
647 [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level]
649 color_spec=TRIGGER_COLOR_SPEC,
652 self.plotter.update()
654 def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY]
655 def get_t_min(self): return self[T_OFF_KEY]
656 def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY]
657 def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
658 def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY]
659 def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
660 def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY]
662 def update_grid(self, *args):
664 Update the grid to reflect the current settings:
665 xy divisions, xy offset, xy mode setting
667 if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return
668 if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return
669 if self[XY_MODE_KEY]:
671 self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1))
672 self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY])
674 self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1))
675 self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
678 self.plotter.set_x_label('Time', 's')
679 self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True)
681 self.plotter.set_y_label(self[Y_AXIS_LABEL])
682 self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY])
683 #redraw current sample
684 self.handle_samples()