diff options
author | jcorgan | 2008-08-14 18:43:15 +0000 |
---|---|---|
committer | jcorgan | 2008-08-14 18:43:15 +0000 |
commit | 36649d4e472172fe840444ac0268c7b6b4da94b4 (patch) | |
tree | a3f8b35980e18f7faee2e82e079746f71b0ee02c /gr-wxgui/src/python/scope_window.py | |
parent | 5b09804605cd41bbc3fdcb917eda3f69a7598af9 (diff) | |
download | gnuradio-36649d4e472172fe840444ac0268c7b6b4da94b4.tar.gz gnuradio-36649d4e472172fe840444ac0268c7b6b4da94b4.tar.bz2 gnuradio-36649d4e472172fe840444ac0268c7b6b4da94b4.zip |
Merged changeset r9241:9289 from jblum/glwxgui into trunk. Adds OpenGL versions of fftsink, waterfallsink, and scopesink, and new constsink. See README.gl for use. (Josh Blum)
git-svn-id: http://gnuradio.org/svn/gnuradio/trunk@9290 221aa14e-8319-0410-a670-987f0aec2ac5
Diffstat (limited to 'gr-wxgui/src/python/scope_window.py')
-rw-r--r-- | gr-wxgui/src/python/scope_window.py | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/gr-wxgui/src/python/scope_window.py b/gr-wxgui/src/python/scope_window.py new file mode 100644 index 000000000..0c7326b1d --- /dev/null +++ b/gr-wxgui/src/python/scope_window.py @@ -0,0 +1,482 @@ +# +# Copyright 2008 Free Software Foundation, Inc. +# +# This file is part of GNU Radio +# +# GNU Radio is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GNU Radio is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GNU Radio; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +################################################## +# Imports +################################################## +import plotter +import common +import wx +import numpy +import time +import pubsub +from constants import * + +################################################## +# Constants +################################################## +DEFAULT_FRAME_RATE = 30 +DEFAULT_WIN_SIZE = (600, 300) +DEFAULT_V_SCALE = 1000 +TRIGGER_MODES = ( + ('Off', 0), + ('Neg', -1), + ('Pos', +1), +) +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), +) +CHANNEL_COLOR_SPECS = ( + (0, 0, 1), + (0, 1, 0), + (1, 0, 0), + (1, 0, 1), +) +AUTORANGE_UPDATE_RATE = 0.5 #sec + +################################################## +# Scope window control panel +################################################## +class control_panel(wx.Panel): + """! + A control panel with wx widgits to control the plotter and scope block. + """ + def __init__(self, parent): + """! + Create a new control panel. + @param parent the wx parent window + """ + 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 + control_box.AddStretchSpacer() + control_box.Add(common.LabelText(self, 'Axes Options'), 0, wx.ALIGN_CENTER) + control_box.AddSpacer(2) + ################################################## + # Scope Mode Box + ################################################## + self.scope_mode_box = wx.BoxSizer(wx.VERTICAL) + control_box.Add(self.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) + #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 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) + ################################################## + # 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) + ################################################## + # End Special Boxes + ################################################## + #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) + #run/stop + 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) + #run/stop + self.run_button = common.ToggleButtonController(self, parent, RUNNING_KEY, 'Stop', 'Run') + control_box.Add(self.run_button, 0, wx.EXPAND) + #set sizer + self.SetSizerAndFit(control_box) + + ################################################## + # 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() + #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])) + def _on_decr_t_divs(self, event): + self.parent.set_t_per_div( + 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])) + def _on_decr_x_divs(self, event): + self.parent.set_x_per_div( + 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])) + def _on_decr_y_divs(self, event): + self.parent.set_y_per_div( + 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]) + def _on_decr_x_off(self, event): + self.parent.set_x_off( + 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]) + def _on_decr_y_off(self, event): + self.parent.set_y_off( + self.parent[Y_OFF_KEY] - self.parent[Y_PER_DIV_KEY]) + +################################################## +# Scope window with plotter and control panel +################################################## +class scope_window(wx.Panel, pubsub.pubsub, common.prop_setter): + def __init__( + self, + parent, + controller, + size, + title, + frame_rate, + num_inputs, + sample_rate_key, + t_scale, + v_scale, + ac_couple, + xy_mode, + scope_trigger_level_key, + scope_trigger_mode_key, + scope_trigger_channel_key, + msg_key, + ): + pubsub.pubsub.__init__(self) + #check num inputs + assert num_inputs <= len(CHANNEL_COLOR_SPECS) + #setup + self.ext_controller = controller + self.num_inputs = num_inputs + self.sample_rate_key = sample_rate_key + autorange = v_scale is None + self.autorange_ts = 0 + if v_scale is None: v_scale = 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 + #init panel and plot + wx.Panel.__init__(self, parent, -1, 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) + #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) + #register events + self.ext_controller.subscribe(msg_key, self.handle_msg) + for key in ( + 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, + ): self.subscribe(key, self.update_grid) + #initial update, dont do this here, wait for handle_msg #HACK + #self.update_grid() + + def handle_msg(self, msg): + """! + Handle the message from the scope sink message queue. + Plot the list of arrays of samples onto the grid. + Each samples array gets its own channel. + @param msg the time domain data as a character array + """ + if not self[RUNNING_KEY]: return + #check time elapsed + 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) + 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 handle_samples(self): + """! + Handle the cached samples from the scope input. + Perform ac coupling, triggering, and auto ranging. + """ + 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]] + #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) + #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) + #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) + #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) + 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], + ) + #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], + ) + else: + #autorange + if self[AUTORANGE_KEY] and time.time() - self.autorange_ts > AUTORANGE_UPDATE_RATE: + bounds = [common.get_min_max(samples) for samples in sampleses] + y_min = numpy.min(*[bound[0] for bound in bounds]) + 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) + #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) + 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: + #plot samples + self.plotter.set_waveform( + channel='Ch%d'%(i+1), + samples=samples[:num_samps], + color_spec=CHANNEL_COLOR_SPECS[i], + ) + #turn XY channel off + self.plotter.set_waveform( + channel='XY', + samples=[], + color_spec=CHANNEL_COLOR_SPECS[0], + ) + #update the plotter + self.plotter.update() + + 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]: + #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, + ) + #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, + ) + 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), + ) + #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, + ) + #redraw current sample + self.handle_samples() |