X-Git-Url: https://git.gag.com/?a=blobdiff_plain;f=gr-wxgui%2Fsrc%2Fpython%2Fscope_window.py;h=aace8688f2a2e46ed65bace7e711b493bd019b8e;hb=331542fc51eb47a65161ab6d41c6dc8fd1dd1f22;hp=7d4f97113e12b5fbf7a8db5e5a528c8dc8346fa5;hpb=f6d78800dc5b9db2025ffc2855f09f5cc79b16a6;p=debian%2Fgnuradio diff --git a/gr-wxgui/src/python/scope_window.py b/gr-wxgui/src/python/scope_window.py index 7d4f9711..aace8688 100644 --- a/gr-wxgui/src/python/scope_window.py +++ b/gr-wxgui/src/python/scope_window.py @@ -29,41 +29,46 @@ import numpy import time import pubsub from constants import * -from gnuradio import gr #for gr.prefs +from gnuradio import gr #for gr.prefs, trigger modes +import forms ################################################## # Constants ################################################## DEFAULT_FRAME_RATE = gr.prefs().get_long('wxgui', 'scope_rate', 30) +ANALOG_ALPHA_MIN_EXP, ANALOG_ALPHA_MAX_EXP = -2, 0 +SLIDER_STEPS = 100 DEFAULT_WIN_SIZE = (600, 300) -DEFAULT_V_SCALE = 1000 +COUPLING_MODES = ( + ('DC', False), + ('AC', True), +) TRIGGER_MODES = ( - ('Off', 0), - ('Neg', -1), - ('Pos', +1), + ('Freerun', gr.gr_TRIG_MODE_FREE), + ('Auto', gr.gr_TRIG_MODE_AUTO), + ('Normal', gr.gr_TRIG_MODE_NORM), ) -TRIGGER_LEVELS = ( - ('Auto', None), - ('+High', 0.75), - ('+Med', 0.5), - ('+Low', 0.25), - ('Zero', 0.0), - ('-Low', -0.25), - ('-Med', -0.5), - ('-High', -0.75), +TRIGGER_SLOPES = ( + ('Pos +', gr.gr_TRIG_SLOPE_POS), + ('Neg -', gr.gr_TRIG_SLOPE_NEG), ) CHANNEL_COLOR_SPECS = ( - (0, 0, 1), - (0, 1, 0), - (1, 0, 0), - (1, 0, 1), + (0.3, 0.3, 1.0), + (0.0, 0.8, 0.0), + (1.0, 0.0, 0.0), + (0.8, 0.0, 0.8), + (0.7, 0.7, 0.0), + (0.15, 0.90, 0.98), + ) +TRIGGER_COLOR_SPEC = (1.0, 0.4, 0.0) AUTORANGE_UPDATE_RATE = 0.5 #sec MARKER_TYPES = ( - ('Dot Small', 1.0), - ('Dot Medium', 2.0), - ('Dot Large', 3.0), ('Line Link', None), + ('Dot Large', 3.0), + ('Dot Med', 2.0), + ('Dot Small', 1.0), + ('None', 0.0), ) DEFAULT_MARKER_TYPE = None @@ -79,175 +84,332 @@ class control_panel(wx.Panel): Create a new control panel. @param parent the wx parent window """ + WIDTH = 90 self.parent = parent wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER) - self.control_box = control_box = wx.BoxSizer(wx.VERTICAL) - #trigger options - control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Trigger Options'), 0, wx.ALIGN_CENTER) - control_box.AddSpacer(2) - #trigger mode - self.trigger_mode_chooser = common.DropDownController(self, 'Mode', TRIGGER_MODES, parent, TRIGGER_MODE_KEY) - control_box.Add(self.trigger_mode_chooser, 0, wx.EXPAND) - #trigger level - self.trigger_level_chooser = common.DropDownController(self, 'Level', TRIGGER_LEVELS, parent, TRIGGER_LEVEL_KEY) - parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_level_chooser.Disable(x==0)) - control_box.Add(self.trigger_level_chooser, 0, wx.EXPAND) - #trigger channel - choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)] - self.trigger_channel_chooser = common.DropDownController(self, 'Channel', choices, parent, TRIGGER_CHANNEL_KEY) - parent.subscribe(TRIGGER_MODE_KEY, lambda x: self.trigger_channel_chooser.Disable(x==0)) - control_box.Add(self.trigger_channel_chooser, 0, wx.EXPAND) - #axes options - SPACING = 15 + parent[SHOW_CONTROL_PANEL_KEY] = True + parent.subscribe(SHOW_CONTROL_PANEL_KEY, self.Show) + control_box = wx.BoxSizer(wx.VERTICAL) + + ################################################## + # Emulate Analog + ################################################## + + forms.check_box( + sizer=control_box, parent=self, label='Emulate Analog', + ps=parent, key=EMULATE_ANALOG_KEY, + ) + #static text and slider for analog alpha + analog_alpha_text = forms.static_text( + sizer=control_box, parent=self, label='Analog Alpha', + converter=forms.float_converter(lambda x: '%.4f'%x), + ps=parent, key=ANALOG_ALPHA_KEY, width=50, + ) + analog_alpha_slider = forms.log_slider( + sizer=control_box, parent=self, + min_exp=ANALOG_ALPHA_MIN_EXP, + max_exp=ANALOG_ALPHA_MAX_EXP, + num_steps=SLIDER_STEPS, + ps=parent, key=ANALOG_ALPHA_KEY, + ) + for widget in (analog_alpha_text, analog_alpha_slider): + parent.subscribe(EMULATE_ANALOG_KEY, widget.Enable) + widget.Enable(parent[EMULATE_ANALOG_KEY]) + parent.subscribe(EMULATE_ANALOG_KEY, widget.ShowItems) + #allways show initially, so room is reserved for them + widget.ShowItems(True) # (parent[EMULATE_ANALOG_KEY]) + + parent.subscribe(EMULATE_ANALOG_KEY, self._update_layout) + + ################################################## + # Axes Options + ################################################## control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Axes Options'), 0, wx.ALIGN_CENTER) - control_box.AddSpacer(2) + axes_options_box = forms.static_box_sizer( + parent=self, sizer=control_box, label='Axes Options', + bold=True, orient=wx.VERTICAL, + ) ################################################## # Scope Mode Box ################################################## - self.scope_mode_box = wx.BoxSizer(wx.VERTICAL) - control_box.Add(self.scope_mode_box, 0, wx.EXPAND) + scope_mode_box = wx.BoxSizer(wx.VERTICAL) + axes_options_box.Add(scope_mode_box, 0, wx.EXPAND) #x axis divs - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Secs/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - x_buttons = common.IncrDecrButtons(self, self._on_incr_t_divs, self._on_decr_t_divs) - hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + forms.incr_decr_buttons( + parent=self, sizer=scope_mode_box, label='Secs/Div', + on_incr=self._on_incr_t_divs, on_decr=self._on_decr_t_divs, + ) #y axis divs - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Units/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs) - parent.subscribe(AUTORANGE_KEY, y_buttons.Disable) - hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + y_buttons_scope = forms.incr_decr_buttons( + parent=self, sizer=scope_mode_box, label='Counts/Div', + on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs, + ) #y axis ref lvl - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.scope_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' Y Offset '), 1, wx.ALIGN_CENTER_VERTICAL) - y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off) - parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable) - hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(SPACING) + y_off_buttons_scope = forms.incr_decr_buttons( + parent=self, sizer=scope_mode_box, label='Y Offset', + on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off, + ) + #t axis ref lvl + scope_mode_box.AddSpacer(5) + forms.slider( + parent=self, sizer=scope_mode_box, + ps=parent, key=T_FRAC_OFF_KEY, label='T Offset', + minimum=0, maximum=1, num_steps=1000, + ) + scope_mode_box.AddSpacer(5) ################################################## # XY Mode Box ################################################## - self.xy_mode_box = wx.BoxSizer(wx.VERTICAL) - control_box.Add(self.xy_mode_box, 0, wx.EXPAND) - #x and y channel - CHOOSER_WIDTH = 60 - CENTER_SPACING = 10 - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - choices = [('Ch%d'%(i+1), i) for i in range(parent.num_inputs)] - self.channel_x_chooser = common.DropDownController(self, 'X Ch', choices, parent, SCOPE_X_CHANNEL_KEY, (CHOOSER_WIDTH, -1)) - hbox.Add(self.channel_x_chooser, 0, wx.EXPAND) - hbox.AddSpacer(CENTER_SPACING) - self.channel_y_chooser = common.DropDownController(self, 'Y Ch', choices, parent, SCOPE_Y_CHANNEL_KEY, (CHOOSER_WIDTH, -1)) - hbox.Add(self.channel_y_chooser, 0, wx.EXPAND) - #div controls - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' X/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - x_buttons = common.IncrDecrButtons(self, self._on_incr_x_divs, self._on_decr_x_divs) - parent.subscribe(AUTORANGE_KEY, x_buttons.Disable) - hbox.Add(x_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(CENTER_SPACING) - hbox.Add(wx.StaticText(self, -1, ' Y/Div '), 1, wx.ALIGN_CENTER_VERTICAL) - y_buttons = common.IncrDecrButtons(self, self._on_incr_y_divs, self._on_decr_y_divs) - parent.subscribe(AUTORANGE_KEY, y_buttons.Disable) - hbox.Add(y_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - #offset controls - hbox = wx.BoxSizer(wx.HORIZONTAL) - self.xy_mode_box.Add(hbox, 0, wx.EXPAND) - hbox.Add(wx.StaticText(self, -1, ' X Off '), 1, wx.ALIGN_CENTER_VERTICAL) - x_off_buttons = common.IncrDecrButtons(self, self._on_incr_x_off, self._on_decr_x_off) - parent.subscribe(AUTORANGE_KEY, x_off_buttons.Disable) - hbox.Add(x_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) - hbox.AddSpacer(CENTER_SPACING) - hbox.Add(wx.StaticText(self, -1, ' Y Off '), 1, wx.ALIGN_CENTER_VERTICAL) - y_off_buttons = common.IncrDecrButtons(self, self._on_incr_y_off, self._on_decr_y_off) - parent.subscribe(AUTORANGE_KEY, y_off_buttons.Disable) - hbox.Add(y_off_buttons, 0, wx.ALIGN_CENTER_VERTICAL) + xy_mode_box = wx.BoxSizer(wx.VERTICAL) + axes_options_box.Add(xy_mode_box, 0, wx.EXPAND) + #x div controls + x_buttons = forms.incr_decr_buttons( + parent=self, sizer=xy_mode_box, label='X/Div', + on_incr=self._on_incr_x_divs, on_decr=self._on_decr_x_divs, + ) + #y div controls + y_buttons = forms.incr_decr_buttons( + parent=self, sizer=xy_mode_box, label='Y/Div', + on_incr=self._on_incr_y_divs, on_decr=self._on_decr_y_divs, + ) + #x offset controls + x_off_buttons = forms.incr_decr_buttons( + parent=self, sizer=xy_mode_box, label='X Off', + on_incr=self._on_incr_x_off, on_decr=self._on_decr_x_off, + ) + #y offset controls + y_off_buttons = forms.incr_decr_buttons( + parent=self, sizer=xy_mode_box, label='Y Off', + on_incr=self._on_incr_y_off, on_decr=self._on_decr_y_off, + ) + for widget in (y_buttons_scope, y_off_buttons_scope, x_buttons, y_buttons, x_off_buttons, y_off_buttons): + parent.subscribe(AUTORANGE_KEY, widget.Disable) + widget.Disable(parent[AUTORANGE_KEY]) + xy_mode_box.ShowItems(False) + #autorange check box + forms.check_box( + parent=self, sizer=axes_options_box, label='Autorange', + ps=parent, key=AUTORANGE_KEY, + ) ################################################## - # End Special Boxes + # Channel Options ################################################## - #misc options - control_box.AddStretchSpacer() - control_box.Add(common.LabelText(self, 'Range Options'), 0, wx.ALIGN_CENTER) - #ac couple check box - self.ac_couple_check_box = common.CheckBoxController(self, 'AC Couple', parent, AC_COUPLE_KEY) - control_box.Add(self.ac_couple_check_box, 0, wx.ALIGN_LEFT) - #autorange check box - self.autorange_check_box = common.CheckBoxController(self, 'Autorange', parent, AUTORANGE_KEY) - control_box.Add(self.autorange_check_box, 0, wx.ALIGN_LEFT) - #marker + TRIGGER_PAGE_INDEX = parent.num_inputs + XY_PAGE_INDEX = parent.num_inputs+1 control_box.AddStretchSpacer() - self.marker_chooser = common.DropDownController(self, 'Marker', MARKER_TYPES, parent, MARKER_KEY) - control_box.Add(self.marker_chooser, 0, wx.EXPAND) - #xy mode - control_box.AddStretchSpacer() - self.scope_xy_mode_button = common.ToggleButtonController(self, parent, SCOPE_XY_MODE_KEY, 'Scope Mode', 'X:Y Mode') - parent.subscribe(SCOPE_XY_MODE_KEY, self._on_scope_xy_mode) - control_box.Add(self.scope_xy_mode_button, 0, wx.EXPAND) + chan_options_box = forms.static_box_sizer( + parent=self, sizer=control_box, label='Channel Options', + bold=True, orient=wx.VERTICAL, + ) + options_notebook = wx.Notebook(self) + options_notebook_args = list() + CHANNELS = [('Ch %d'%(i+1), i) for i in range(parent.num_inputs)] + ################################################## + # Channel Menu Boxes + ################################################## + for i in range(parent.num_inputs): + channel_menu_panel = wx.Panel(options_notebook) + options_notebook_args.append((channel_menu_panel, i, 'Ch%d'%(i+1))) + channel_menu_box = wx.BoxSizer(wx.VERTICAL) + channel_menu_panel.SetSizer(channel_menu_box) + #ac couple check box + channel_menu_box.AddStretchSpacer() + forms.drop_down( + parent=channel_menu_panel, sizer=channel_menu_box, + ps=parent, key=common.index_key(AC_COUPLE_KEY, i), + choices=map(lambda x: x[1], COUPLING_MODES), + labels=map(lambda x: x[0], COUPLING_MODES), + label='Coupling', width=WIDTH, + ) + #marker + channel_menu_box.AddStretchSpacer() + forms.drop_down( + parent=channel_menu_panel, sizer=channel_menu_box, + ps=parent, key=common.index_key(MARKER_KEY, i), + choices=map(lambda x: x[1], MARKER_TYPES), + labels=map(lambda x: x[0], MARKER_TYPES), + label='Marker', width=WIDTH, + ) + channel_menu_box.AddStretchSpacer() + ################################################## + # Trigger Menu Box + ################################################## + trigger_menu_panel = wx.Panel(options_notebook) + options_notebook_args.append((trigger_menu_panel, TRIGGER_PAGE_INDEX, 'Trig')) + trigger_menu_box = wx.BoxSizer(wx.VERTICAL) + trigger_menu_panel.SetSizer(trigger_menu_box) + #trigger mode + forms.drop_down( + parent=trigger_menu_panel, sizer=trigger_menu_box, + ps=parent, key=TRIGGER_MODE_KEY, + choices=map(lambda x: x[1], TRIGGER_MODES), + labels=map(lambda x: x[0], TRIGGER_MODES), + label='Mode', width=WIDTH, + ) + #trigger slope + trigger_slope_chooser = forms.drop_down( + parent=trigger_menu_panel, sizer=trigger_menu_box, + ps=parent, key=TRIGGER_SLOPE_KEY, + choices=map(lambda x: x[1], TRIGGER_SLOPES), + labels=map(lambda x: x[0], TRIGGER_SLOPES), + label='Slope', width=WIDTH, + ) + #trigger channel + trigger_channel_chooser = forms.drop_down( + parent=trigger_menu_panel, sizer=trigger_menu_box, + ps=parent, key=TRIGGER_CHANNEL_KEY, + choices=map(lambda x: x[1], CHANNELS), + labels=map(lambda x: x[0], CHANNELS), + label='Channel', width=WIDTH, + ) + #trigger level + hbox = wx.BoxSizer(wx.HORIZONTAL) + trigger_menu_box.Add(hbox, 0, wx.EXPAND) + hbox.Add(wx.StaticText(trigger_menu_panel, label='Level:'), 1, wx.ALIGN_CENTER_VERTICAL) + trigger_level_button = forms.single_button( + parent=trigger_menu_panel, sizer=hbox, label='50%', + callback=parent.set_auto_trigger_level, style=wx.BU_EXACTFIT, + ) + hbox.AddSpacer(WIDTH-60) + trigger_level_buttons = forms.incr_decr_buttons( + parent=trigger_menu_panel, sizer=hbox, + on_incr=self._on_incr_trigger_level, on_decr=self._on_decr_trigger_level, + ) + def disable_all(trigger_mode): + for widget in (trigger_slope_chooser, trigger_channel_chooser, trigger_level_buttons, trigger_level_button): + widget.Disable(trigger_mode == gr.gr_TRIG_MODE_FREE) + parent.subscribe(TRIGGER_MODE_KEY, disable_all) + disable_all(parent[TRIGGER_MODE_KEY]) + ################################################## + # XY Menu Box + ################################################## + if parent.num_inputs > 1: + xy_menu_panel = wx.Panel(options_notebook) + options_notebook_args.append((xy_menu_panel, XY_PAGE_INDEX, 'XY')) + xy_menu_box = wx.BoxSizer(wx.VERTICAL) + xy_menu_panel.SetSizer(xy_menu_box) + #x and y channel choosers + xy_menu_box.AddStretchSpacer() + forms.drop_down( + parent=xy_menu_panel, sizer=xy_menu_box, + ps=parent, key=X_CHANNEL_KEY, + choices=map(lambda x: x[1], CHANNELS), + labels=map(lambda x: x[0], CHANNELS), + label='Channel X', width=WIDTH, + ) + xy_menu_box.AddStretchSpacer() + forms.drop_down( + parent=xy_menu_panel, sizer=xy_menu_box, + ps=parent, key=Y_CHANNEL_KEY, + choices=map(lambda x: x[1], CHANNELS), + labels=map(lambda x: x[0], CHANNELS), + label='Channel Y', width=WIDTH, + ) + #marker + xy_menu_box.AddStretchSpacer() + forms.drop_down( + parent=xy_menu_panel, sizer=xy_menu_box, + ps=parent, key=XY_MARKER_KEY, + choices=map(lambda x: x[1], MARKER_TYPES), + labels=map(lambda x: x[0], MARKER_TYPES), + label='Marker', width=WIDTH, + ) + xy_menu_box.AddStretchSpacer() + ################################################## + # Setup Options Notebook + ################################################## + forms.notebook( + parent=self, sizer=chan_options_box, + notebook=options_notebook, + ps=parent, key=CHANNEL_OPTIONS_KEY, + pages=map(lambda x: x[0], options_notebook_args), + choices=map(lambda x: x[1], options_notebook_args), + labels=map(lambda x: x[2], options_notebook_args), + ) + #gui handling for channel options changing + def options_notebook_changed(chan_opt): + try: + parent[TRIGGER_SHOW_KEY] = chan_opt == TRIGGER_PAGE_INDEX + parent[XY_MODE_KEY] = chan_opt == XY_PAGE_INDEX + except wx.PyDeadObjectError: pass + parent.subscribe(CHANNEL_OPTIONS_KEY, options_notebook_changed) + #gui handling for xy mode changing + def xy_mode_changed(mode): + #ensure xy tab is selected + if mode and parent[CHANNEL_OPTIONS_KEY] != XY_PAGE_INDEX: + parent[CHANNEL_OPTIONS_KEY] = XY_PAGE_INDEX + #ensure xy tab is not selected + elif not mode and parent[CHANNEL_OPTIONS_KEY] == XY_PAGE_INDEX: + parent[CHANNEL_OPTIONS_KEY] = 0 + #show/hide control buttons + scope_mode_box.ShowItems(not mode) + xy_mode_box.ShowItems(mode) + control_box.Layout() + parent.subscribe(XY_MODE_KEY, xy_mode_changed) + xy_mode_changed(parent[XY_MODE_KEY]) + ################################################## + # Run/Stop Button + ################################################## #run/stop - self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') - control_box.Add(self.run_button, 0, wx.EXPAND) + control_box.AddStretchSpacer() + forms.toggle_button( + sizer=control_box, parent=self, + true_label='Stop', false_label='Run', + ps=parent, key=RUNNING_KEY, + ) #set sizer self.SetSizerAndFit(control_box) + #mouse wheel event + def on_mouse_wheel(event): + if not parent[XY_MODE_KEY]: + if event.GetWheelRotation() < 0: self._on_incr_t_divs(event) + else: self._on_decr_t_divs(event) + parent.plotter.Bind(wx.EVT_MOUSEWHEEL, on_mouse_wheel) ################################################## # Event handlers ################################################## - def _on_scope_xy_mode(self, mode): - self.scope_mode_box.ShowItems(not mode) - self.xy_mode_box.ShowItems(mode) - self.control_box.Layout() + #trigger level + def _on_incr_trigger_level(self, event): + self.parent[TRIGGER_LEVEL_KEY] += self.parent[Y_PER_DIV_KEY]/3. + def _on_decr_trigger_level(self, event): + self.parent[TRIGGER_LEVEL_KEY] -= self.parent[Y_PER_DIV_KEY]/3. #incr/decr divs def _on_incr_t_divs(self, event): - self.parent.set_t_per_div( - common.get_clean_incr(self.parent[T_PER_DIV_KEY])) + self.parent[T_PER_DIV_KEY] = common.get_clean_incr(self.parent[T_PER_DIV_KEY]) def _on_decr_t_divs(self, event): - self.parent.set_t_per_div( - common.get_clean_decr(self.parent[T_PER_DIV_KEY])) + self.parent[T_PER_DIV_KEY] = common.get_clean_decr(self.parent[T_PER_DIV_KEY]) def _on_incr_x_divs(self, event): - self.parent.set_x_per_div( - common.get_clean_incr(self.parent[X_PER_DIV_KEY])) + self.parent[X_PER_DIV_KEY] = common.get_clean_incr(self.parent[X_PER_DIV_KEY]) def _on_decr_x_divs(self, event): - self.parent.set_x_per_div( - common.get_clean_decr(self.parent[X_PER_DIV_KEY])) + self.parent[X_PER_DIV_KEY] = common.get_clean_decr(self.parent[X_PER_DIV_KEY]) def _on_incr_y_divs(self, event): - self.parent.set_y_per_div( - common.get_clean_incr(self.parent[Y_PER_DIV_KEY])) + self.parent[Y_PER_DIV_KEY] = common.get_clean_incr(self.parent[Y_PER_DIV_KEY]) def _on_decr_y_divs(self, event): - self.parent.set_y_per_div( - common.get_clean_decr(self.parent[Y_PER_DIV_KEY])) + self.parent[Y_PER_DIV_KEY] = common.get_clean_decr(self.parent[Y_PER_DIV_KEY]) #incr/decr offset - def _on_incr_t_off(self, event): - self.parent.set_t_off( - self.parent[T_OFF_KEY] + self.parent[T_PER_DIV_KEY]) - def _on_decr_t_off(self, event): - self.parent.set_t_off( - self.parent[T_OFF_KEY] - self.parent[T_PER_DIV_KEY]) def _on_incr_x_off(self, event): - self.parent.set_x_off( - self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY]) + self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] + self.parent[X_PER_DIV_KEY] def _on_decr_x_off(self, event): - self.parent.set_x_off( - self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY]) + self.parent[X_OFF_KEY] = self.parent[X_OFF_KEY] - self.parent[X_PER_DIV_KEY] def _on_incr_y_off(self, event): - self.parent.set_y_off( - self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY]) + self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] + self.parent[Y_PER_DIV_KEY] def _on_decr_y_off(self, event): - self.parent.set_y_off( - self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]) + self.parent[Y_OFF_KEY] = self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY] + + ################################################## + # subscriber handlers + ################################################## + def _update_layout(self,key): + # Just ignore the key value we get + # we only need to now that the visability or size of something has changed + self.parent.Layout() + #self.parent.Fit() ################################################## # Scope window with plotter and control panel ################################################## -class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): +class scope_window(wx.Panel, pubsub.pubsub): def __init__( self, parent, @@ -259,79 +421,95 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): sample_rate_key, t_scale, v_scale, - ac_couple, + v_offset, xy_mode, - scope_trigger_level_key, - scope_trigger_mode_key, - scope_trigger_channel_key, + ac_couple_key, + trigger_level_key, + trigger_mode_key, + trigger_slope_key, + trigger_channel_key, + decimation_key, msg_key, + emulate_analog, + analog_alpha, ): pubsub.pubsub.__init__(self) #check num inputs assert num_inputs <= len(CHANNEL_COLOR_SPECS) #setup self.sampleses = None - self.ext_controller = controller self.num_inputs = num_inputs - self.sample_rate_key = sample_rate_key - autorange = v_scale is None + autorange = not v_scale self.autorange_ts = 0 - if v_scale is None: v_scale = 1 + v_scale = v_scale or 1 self.frame_rate_ts = 0 - self._init = False #HACK - #scope keys - self.scope_trigger_level_key = scope_trigger_level_key - self.scope_trigger_mode_key = scope_trigger_mode_key - self.scope_trigger_channel_key = scope_trigger_channel_key + #proxy the keys + self.proxy(MSG_KEY, controller, msg_key) + self.proxy(SAMPLE_RATE_KEY, controller, sample_rate_key) + self.proxy(TRIGGER_LEVEL_KEY, controller, trigger_level_key) + self.proxy(TRIGGER_MODE_KEY, controller, trigger_mode_key) + self.proxy(TRIGGER_SLOPE_KEY, controller, trigger_slope_key) + self.proxy(TRIGGER_CHANNEL_KEY, controller, trigger_channel_key) + self.proxy(DECIMATION_KEY, controller, decimation_key) + #initialize values + self[RUNNING_KEY] = True + self[XY_MARKER_KEY] = 2.0 + self[CHANNEL_OPTIONS_KEY] = 0 + self[XY_MODE_KEY] = xy_mode + self[X_CHANNEL_KEY] = 0 + self[Y_CHANNEL_KEY] = self.num_inputs-1 + self[AUTORANGE_KEY] = autorange + self[T_PER_DIV_KEY] = t_scale + self[X_PER_DIV_KEY] = v_scale + self[Y_PER_DIV_KEY] = v_scale + self[T_OFF_KEY] = 0 + self[X_OFF_KEY] = v_offset + self[Y_OFF_KEY] = v_offset + self[T_DIVS_KEY] = 8 + self[X_DIVS_KEY] = 8 + self[Y_DIVS_KEY] = 8 + self[FRAME_RATE_KEY] = frame_rate + self[TRIGGER_LEVEL_KEY] = 0 + self[TRIGGER_CHANNEL_KEY] = 0 + self[TRIGGER_MODE_KEY] = gr.gr_TRIG_MODE_AUTO + self[TRIGGER_SLOPE_KEY] = gr.gr_TRIG_SLOPE_POS + self[T_FRAC_OFF_KEY] = 0.5 + self[EMULATE_ANALOG_KEY] = emulate_analog + self[ANALOG_ALPHA_KEY] = analog_alpha + for i in range(num_inputs): + self.proxy(common.index_key(AC_COUPLE_KEY, i), controller, common.index_key(ac_couple_key, i)) #init panel and plot - wx.Panel.__init__(self, parent, -1, style=wx.SIMPLE_BORDER) + wx.Panel.__init__(self, parent, style=wx.SIMPLE_BORDER) self.plotter = plotter.channel_plotter(self) self.plotter.SetSize(wx.Size(*size)) self.plotter.set_title(title) self.plotter.enable_legend(True) self.plotter.enable_point_label(True) + self.plotter.enable_grid_lines(True) + self.plotter.set_emulate_analog(emulate_analog) + self.plotter.set_analog_alpha(analog_alpha) #setup the box with plot and controls self.control_panel = control_panel(self) main_box = wx.BoxSizer(wx.HORIZONTAL) main_box.Add(self.plotter, 1, wx.EXPAND) main_box.Add(self.control_panel, 0, wx.EXPAND) self.SetSizerAndFit(main_box) - #initial setup - self._register_set_prop(self, RUNNING_KEY, True) - self._register_set_prop(self, AC_COUPLE_KEY, ac_couple) - self._register_set_prop(self, SCOPE_XY_MODE_KEY, xy_mode) - self._register_set_prop(self, AUTORANGE_KEY, autorange) - self._register_set_prop(self, T_PER_DIV_KEY, t_scale) - self._register_set_prop(self, X_PER_DIV_KEY, v_scale) - self._register_set_prop(self, Y_PER_DIV_KEY, v_scale) - self._register_set_prop(self, T_OFF_KEY, 0) - self._register_set_prop(self, X_OFF_KEY, 0) - self._register_set_prop(self, Y_OFF_KEY, 0) - self._register_set_prop(self, T_DIVS_KEY, 8) - self._register_set_prop(self, X_DIVS_KEY, 8) - self._register_set_prop(self, Y_DIVS_KEY, 8) - self._register_set_prop(self, SCOPE_X_CHANNEL_KEY, 0) - self._register_set_prop(self, SCOPE_Y_CHANNEL_KEY, num_inputs-1) - self._register_set_prop(self, FRAME_RATE_KEY, frame_rate) - self._register_set_prop(self, TRIGGER_CHANNEL_KEY, 0) - self._register_set_prop(self, TRIGGER_MODE_KEY, 1) - self._register_set_prop(self, TRIGGER_LEVEL_KEY, None) - self._register_set_prop(self, MARKER_KEY, DEFAULT_MARKER_TYPE) - #register events - self.ext_controller.subscribe(msg_key, self.handle_msg) - for key in ( + #register events for message + self.subscribe(MSG_KEY, self.handle_msg) + #register events for grid + for key in [common.index_key(MARKER_KEY, i) for i in range(self.num_inputs)] + [ + TRIGGER_LEVEL_KEY, TRIGGER_MODE_KEY, T_PER_DIV_KEY, X_PER_DIV_KEY, Y_PER_DIV_KEY, T_OFF_KEY, X_OFF_KEY, Y_OFF_KEY, T_DIVS_KEY, X_DIVS_KEY, Y_DIVS_KEY, - SCOPE_XY_MODE_KEY, - SCOPE_X_CHANNEL_KEY, - SCOPE_Y_CHANNEL_KEY, - AUTORANGE_KEY, - AC_COUPLE_KEY, - MARKER_KEY, - ): self.subscribe(key, self.update_grid) - #initial update, dont do this here, wait for handle_msg #HACK - #self.update_grid() + XY_MODE_KEY, AUTORANGE_KEY, T_FRAC_OFF_KEY, + TRIGGER_SHOW_KEY, XY_MARKER_KEY, X_CHANNEL_KEY, Y_CHANNEL_KEY, + ]: self.subscribe(key, self.update_grid) + #register events for plotter settings + self.subscribe(EMULATE_ANALOG_KEY, self.plotter.set_emulate_analog) + self.subscribe(ANALOG_ALPHA_KEY, self.plotter.set_analog_alpha) + #initial update + self.update_grid() def handle_msg(self, msg): """ @@ -345,15 +523,23 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): if time.time() - self.frame_rate_ts < 1.0/self[FRAME_RATE_KEY]: return #convert to floating point numbers samples = numpy.fromstring(msg, numpy.float32) + #extract the trigger offset + self.trigger_offset = samples[-1] + samples = samples[:-1] samps_per_ch = len(samples)/self.num_inputs self.sampleses = [samples[samps_per_ch*i:samps_per_ch*(i+1)] for i in range(self.num_inputs)] - if not self._init: #HACK - self._init = True - self.update_grid() #handle samples self.handle_samples() self.frame_rate_ts = time.time() + def set_auto_trigger_level(self, *args): + """ + Use the current trigger channel and samples to calculate the 50% level. + """ + if not self.sampleses: return + samples = self.sampleses[self[TRIGGER_CHANNEL_KEY]] + self[TRIGGER_LEVEL_KEY] = (numpy.max(samples)+numpy.min(samples))/2 + def handle_samples(self): """ Handle the cached samples from the scope input. @@ -361,52 +547,37 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): """ if not self.sampleses: return sampleses = self.sampleses - #trigger level (must do before ac coupling) - self.ext_controller[self.scope_trigger_channel_key] = self[TRIGGER_CHANNEL_KEY] - self.ext_controller[self.scope_trigger_mode_key] = self[TRIGGER_MODE_KEY] - trigger_level = self[TRIGGER_LEVEL_KEY] - if trigger_level is None: self.ext_controller[self.scope_trigger_level_key] = '' - else: - samples = sampleses[self[TRIGGER_CHANNEL_KEY]] - self.ext_controller[self.scope_trigger_level_key] = \ - trigger_level*(numpy.max(samples)-numpy.min(samples))/2 + numpy.average(samples) - #ac coupling - if self[AC_COUPLE_KEY]: - sampleses = [samples - numpy.average(samples) for samples in sampleses] - if self[SCOPE_XY_MODE_KEY]: - x_samples = sampleses[self[SCOPE_X_CHANNEL_KEY]] - y_samples = sampleses[self[SCOPE_Y_CHANNEL_KEY]] + if self[XY_MODE_KEY]: + self[DECIMATION_KEY] = 1 + x_samples = sampleses[self[X_CHANNEL_KEY]] + y_samples = sampleses[self[Y_CHANNEL_KEY]] #autorange if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE: x_min, x_max = common.get_min_max(x_samples) y_min, y_max = common.get_min_max(y_samples) #adjust the x per div x_per_div = common.get_clean_num((x_max-x_min)/self[X_DIVS_KEY]) - if x_per_div != self[X_PER_DIV_KEY]: self.set_x_per_div(x_per_div) + if x_per_div != self[X_PER_DIV_KEY]: self[X_PER_DIV_KEY] = x_per_div; return #adjust the x offset x_off = x_per_div*round((x_max+x_min)/2/x_per_div) - if x_off != self[X_OFF_KEY]: self.set_x_off(x_off) + if x_off != self[X_OFF_KEY]: self[X_OFF_KEY] = x_off; return #adjust the y per div y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY]) - if y_per_div != self[Y_PER_DIV_KEY]: self.set_y_per_div(y_per_div) + if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return #adjust the y offset y_off = y_per_div*round((y_max+y_min)/2/y_per_div) - if y_off != self[Y_OFF_KEY]: self.set_y_off(y_off) + if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return self.autorange_ts = time.time() #plot xy channel self.plotter.set_waveform( channel='XY', samples=(x_samples, y_samples), color_spec=CHANNEL_COLOR_SPECS[0], - marker=self[MARKER_KEY], + marker=self[XY_MARKER_KEY], ) #turn off each waveform for i, samples in enumerate(sampleses): - self.plotter.set_waveform( - channel='Ch%d'%(i+1), - samples=[], - color_spec=CHANNEL_COLOR_SPECS[i], - ) + self.plotter.clear_waveform(channel='Ch%d'%(i+1)) else: #autorange if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE: @@ -415,86 +586,90 @@ class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): y_max = numpy.max([bound[1] for bound in bounds]) #adjust the y per div y_per_div = common.get_clean_num((y_max-y_min)/self[Y_DIVS_KEY]) - if y_per_div != self[Y_PER_DIV_KEY]: self.set_y_per_div(y_per_div) + if y_per_div != self[Y_PER_DIV_KEY]: self[Y_PER_DIV_KEY] = y_per_div; return #adjust the y offset y_off = y_per_div*round((y_max+y_min)/2/y_per_div) - if y_off != self[Y_OFF_KEY]: self.set_y_off(y_off) + if y_off != self[Y_OFF_KEY]: self[Y_OFF_KEY] = y_off; return self.autorange_ts = time.time() - #plot each waveform - for i, samples in enumerate(sampleses): - #number of samples to scale to the screen - num_samps = int(self[T_PER_DIV_KEY]*self[T_DIVS_KEY]*self.ext_controller[self.sample_rate_key]) - #handle num samps out of bounds - if num_samps > len(samples): - self.set_t_per_div( - common.get_clean_decr(self[T_PER_DIV_KEY])) - elif num_samps < 2: - self.set_t_per_div( - common.get_clean_incr(self[T_PER_DIV_KEY])) - num_samps = 0 - else: + #number of samples to scale to the screen + actual_rate = self.get_actual_rate() + time_span = self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + num_samps = int(round(time_span*actual_rate)) + #handle the time offset + t_off = self[T_FRAC_OFF_KEY]*(len(sampleses[0])/actual_rate - time_span) + if t_off != self[T_OFF_KEY]: self[T_OFF_KEY] = t_off; return + samps_off = int(round(actual_rate*self[T_OFF_KEY])) + #adjust the decim so that we use about half the samps + self[DECIMATION_KEY] = int(round( + time_span*self[SAMPLE_RATE_KEY]/(0.5*len(sampleses[0])) + ) + ) + #num samps too small, auto increment the time + if num_samps < 2: self[T_PER_DIV_KEY] = common.get_clean_incr(self[T_PER_DIV_KEY]) + #num samps in bounds, plot each waveform + elif num_samps <= len(sampleses[0]): + for i, samples in enumerate(sampleses): #plot samples self.plotter.set_waveform( channel='Ch%d'%(i+1), - samples=samples[:num_samps], + samples=samples[samps_off:num_samps+samps_off], color_spec=CHANNEL_COLOR_SPECS[i], - marker=self[MARKER_KEY], + marker=self[common.index_key(MARKER_KEY, i)], + trig_off=self.trigger_offset, ) #turn XY channel off + self.plotter.clear_waveform(channel='XY') + #keep trigger level within range + if self[TRIGGER_LEVEL_KEY] > self.get_y_max(): + self[TRIGGER_LEVEL_KEY] = self.get_y_max(); return + if self[TRIGGER_LEVEL_KEY] < self.get_y_min(): + self[TRIGGER_LEVEL_KEY] = self.get_y_min(); return + #disable the trigger channel + if not self[TRIGGER_SHOW_KEY] or self[XY_MODE_KEY] or self[TRIGGER_MODE_KEY] == gr.gr_TRIG_MODE_FREE: + self.plotter.clear_waveform(channel='Trig') + else: #show trigger channel + trigger_level = self[TRIGGER_LEVEL_KEY] + trigger_point = (len(self.sampleses[0])-1)/self.get_actual_rate()/2.0 self.plotter.set_waveform( - channel='XY', - samples=[], - color_spec=CHANNEL_COLOR_SPECS[0], + channel='Trig', + samples=( + [self.get_t_min(), trigger_point, trigger_point, trigger_point, trigger_point, self.get_t_max()], + [trigger_level, trigger_level, self.get_y_max(), self.get_y_min(), trigger_level, trigger_level] + ), + color_spec=TRIGGER_COLOR_SPEC, ) #update the plotter self.plotter.update() + def get_actual_rate(self): return 1.0*self[SAMPLE_RATE_KEY]/self[DECIMATION_KEY] + def get_t_min(self): return self[T_OFF_KEY] + def get_t_max(self): return self[T_PER_DIV_KEY]*self[T_DIVS_KEY] + self[T_OFF_KEY] + def get_x_min(self): return -1*self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY] + def get_x_max(self): return self[X_PER_DIV_KEY]*self[X_DIVS_KEY]/2.0 + self[X_OFF_KEY] + def get_y_min(self): return -1*self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY] + def get_y_max(self): return self[Y_PER_DIV_KEY]*self[Y_DIVS_KEY]/2.0 + self[Y_OFF_KEY] + def update_grid(self, *args): """ Update the grid to reflect the current settings: xy divisions, xy offset, xy mode setting """ - #grid parameters - t_per_div = self[T_PER_DIV_KEY] - x_per_div = self[X_PER_DIV_KEY] - y_per_div = self[Y_PER_DIV_KEY] - t_off = self[T_OFF_KEY] - x_off = self[X_OFF_KEY] - y_off = self[Y_OFF_KEY] - t_divs = self[T_DIVS_KEY] - x_divs = self[X_DIVS_KEY] - y_divs = self[Y_DIVS_KEY] - if self[SCOPE_XY_MODE_KEY]: + if self[T_FRAC_OFF_KEY] < 0: self[T_FRAC_OFF_KEY] = 0; return + if self[T_FRAC_OFF_KEY] > 1: self[T_FRAC_OFF_KEY] = 1; return + if self[XY_MODE_KEY]: #update the x axis - self.plotter.set_x_label('Ch%d'%(self[SCOPE_X_CHANNEL_KEY]+1)) - self.plotter.set_x_grid( - -1*x_per_div*x_divs/2.0 + x_off, - x_per_div*x_divs/2.0 + x_off, - x_per_div, - ) + self.plotter.set_x_label('Ch%d'%(self[X_CHANNEL_KEY]+1)) + self.plotter.set_x_grid(self.get_x_min(), self.get_x_max(), self[X_PER_DIV_KEY]) #update the y axis - self.plotter.set_y_label('Ch%d'%(self[SCOPE_Y_CHANNEL_KEY]+1)) - self.plotter.set_y_grid( - -1*y_per_div*y_divs/2.0 + y_off, - y_per_div*y_divs/2.0 + y_off, - y_per_div, - ) + self.plotter.set_y_label('Ch%d'%(self[Y_CHANNEL_KEY]+1)) + self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY]) else: #update the t axis - coeff, exp, prefix = common.get_si_components(t_per_div*t_divs + t_off) - self.plotter.set_x_label('Time', prefix+'s') - self.plotter.set_x_grid( - t_off, - t_per_div*t_divs + t_off, - t_per_div, - 10**(-exp), - ) + self.plotter.set_x_label('Time', 's') + self.plotter.set_x_grid(self.get_t_min(), self.get_t_max(), self[T_PER_DIV_KEY], True) #update the y axis self.plotter.set_y_label('Counts') - self.plotter.set_y_grid( - -1*y_per_div*y_divs/2.0 + y_off, - y_per_div*y_divs/2.0 + y_off, - y_per_div, - ) + self.plotter.set_y_grid(self.get_y_min(), self.get_y_max(), self[Y_PER_DIV_KEY]) #redraw current sample self.handle_samples() +