Bring hooke.plugin.tutorial up to date with Plugin/Command architecture.
authorW. Trevor King <wking@drexel.edu>
Tue, 3 Aug 2010 15:18:54 +0000 (11:18 -0400)
committerW. Trevor King <wking@drexel.edu>
Tue, 3 Aug 2010 15:18:54 +0000 (11:18 -0400)
hooke/plugin/__init__.py
hooke/plugin/tutorial.py

index 54cce666b926fb86b0a604d03fbbd1c885f089d1..3d3efd9b93a48666a8cda0739d55fd199eae5881 100644 (file)
@@ -48,7 +48,7 @@ PLUGIN_MODULES = [
 #    ('showconvoluted', True),
 #    ('superimpose', True),
 #    ('tccd', True),
-#    ('tutorial', True),
+    ('tutorial', True),
     ('vclamp', True),
     ]
 """List of plugin modules and whether they should be included by
@@ -173,7 +173,6 @@ def load_graph(graph, config, include_section):
         except configparser.NoOptionError:
             include = True # non-optional include (e.g. a Builtin)
         enabled[item.name] = include
-        print item.name, include
         if include == True:
             for dependency in node:
                 if enabled.get(dependency.data.name, None) != True:
index 3fc9319930f821d0ea0ee501a409070bb03b43f9..a126e2cca36a6577fc43a936c041ef0c3097ba7e 100644 (file)
 Hooke plugin, including description of main Hooke internals.
 """
 
+import logging
+import StringIO
+
 from numpy import arange
 
 from ..command import Command, Argument, Failure
-from ..playlist import FilePlaylist
-from ..plugin import Plugin
+from ..config import Setting
+from ..interaction import PointRequest, PointResponse
+from ..util.si import ppSI, split_data_label
+from . import Plugin
+from .curve import CurveArgument
 
 
 class TutorialPlugin (Plugin):
@@ -77,8 +83,8 @@ class TutorialPlugin (Plugin):
         # we instantiate them in a list here, and rely on
         # :meth:`hooke.plugin.Plugin.commands` to return copies of
         # that list.
-        self._commands = [] #CommandInit]
-
+        self._commands = [DoNothingCommand(self), HookeInfoCommand(self),
+                          PointInfoCommand(self),]
 
     def dependencies(self):
         """Return a list  of names of :class:`hooke.plugin.Plugin`\s we
@@ -89,497 +95,210 @@ class TutorialPlugin (Plugin):
         enabled if it is missing dependencies.
         """
         return ['vclamp']
-        #Here we initialize a local configuration variable; see plotmanip_absvalue() for explanation.
-        self.config['tutorial_absvalue']=0
-
-    def do_nothing(self,args):
-        '''
-        This is a boring but working example of an actual Hooke command.
-        A Hooke command is a function of the xxxxCommands class, which is ALWAYS defined
-        this way:
-
-        def do_nameofcommand(self,args)
-
-        *do_            is needed to make Hooke understand this function is a command
-        *nameofcommand  is how the command will be called in the Hooke command line.
-        *self           is, well, self
-        *args           is ALWAYS needed (otherwise Hooke will crash executing the command). We will see
-                        later what args is.
-
-        Note that if you now start Hooke with this plugin activated and you type in the Hooke command
-        line "help nothing" you will see this very text as output. So the help of a command is a
-        string comment below the function definition, like this one.
-
-        Commands usually return None.
-        '''
-        print 'I am a Hooke command. I do nothing.'
-
-    def do_printargs(self,args):
-        '''
-        This command prints the args you give to it.
-        args is always a string, that contains everything you write after the command.
-        So if you issue "mycommand blah blah 12345" args is "blah blah 12345".
-
-        Again, args is needed in the definition even if your command does not use it.
-        '''
-        print 'You gave me those args: '+args
-
-    def help_tutorial(self):
-        '''
-        This is a help function.
-        If you want a help function for something that is not a command, you can write a help
-        function like this. Calling "help tutorial" will execute this function.
-        '''
-        print 'You called help_tutorial()'
-
-    def do_environment(self,args):
-        '''
-        This plugin contains a panoramic of the Hooke command line environment variables,
-        and prints their current value.
-        '''
-
-        '''self.current_list
-        TYPE: [ curve.HookeCurve ], len=variable
-        contains the actual playlist of Hooke curve objects.
-        Each HookeCurve object represents a reference to a data file.
-        We will see later in detail how do they work.
-        '''
-        print 'current_list length:',len(self.current_list)
-        print 'current_list 0th:',self.current_list[0]
-
-        '''self.pointer
-        TYPE: int
-        contains the index of
-        the current curve in the playlist
-        '''
-        print 'pointer: ',self.pointer
-
-        '''self.current
-        TYPE: curve.HookeCurve
-        contains the current curve displayed.
-        We will see later how it works.
-        '''
-        print 'current:',self.current
-
-        '''self.plots
-        TYPE: [ curve.PlotObject ], len=1,2
-        contains the current default plots.
-        Each PlotObject contains all info needed to display
-        the plot: apart from the data vectors, the title, destination
-        etc.
-        Usually self.plots[0] is the default topmost plot, self.plots[1] is the
-        accessory bottom plot.
-        '''
-        print 'plots:',self.plots
-
-        '''self.config
-        TYPE: { string:anything }
-        contains the current Hooke configuration variables, in form of a dictionary.
-        '''
-        print 'config:',self.config
-
-        '''self.plotmanip
-        TYPE: [ function ]
-        Contains the ordered plot manipulation functions.
-        These functions are called to modify the default plot by default before it is plotted.
-        self.plots contains the plot passed through the plot manipulators.
-        We will see it better later.
-        *YOU SHOULD NEVER MODIFY THAT*
-        '''
-        print 'plotmanip: ',self.plotmanip
-
-        '''self.drivers
-        TYPE: [ class ]
-        Contains the plot reading drivers.
-        *YOU SHOULD NEVER MODIFY THAT*
-        '''
-        print 'drivers: ',self.drivers
-
-        '''self.frame
-        TYPE: wx.Frame
-        Contains the wx Frame of the GUI.
-        ***NEVER, EVER TOUCH THAT.***
-        '''
-        print 'frame: ',self.frame
-
-        '''self.list_of_events
-        TYPE: { string:wx.Event }
-        Contains the wx.Events to communicate with the GUI.
-        Usually not necessary to use it, unless you want
-        to create a GUI plugin.
-        '''
-        print 'list of events:',self.list_of_events
-
-        '''self.events_from_gui
-        TYPE: Queue.Queue
-        Contains the Queue where data from the GUI is put.
-        Usually not necessary to use it, unless you want
-        to create a GUI plugin.
-        '''
-        print 'events from gui:',self.events_from_gui
-
-        '''self.playlist_saved
-        TYPE: Int (0/1) ; Boolean
-        Flag that tells if the playlist has been saved or not.
-        '''
-        print 'playlist saved:',self.playlist_saved
-
-        '''self.playlist_name
-        TYPE: string
-        Name of current playlist
-        '''
-        print 'playlist name:',self.playlist_name
-
-        '''self.notes_saved
-        TYPE: Int (0/1) ; Boolean
-        Flag that tells if the playlist has been saved or not.
-        '''
-        print 'notes saved:',self.notes_saved
-
-
-    def do_myfirstplot(self,args):
-        '''
-        In this function, we see how to create a PlotObject and send it to the screen.
-        ***Read the code of PlotObject in curve.py before!***.
-        '''
-
-        #We generate some interesting data to plot for this example.
-        xdata1=arange(-5,5,0.1)
-        xdata2=arange(-5,5,0.1)
-        ydata1=[item**2 for item in xdata1]
-        ydata2=[item**3 for item in xdata2]
-
-        #Create the object.
-        #The PlotObject class lives in the curve library.
-        myplot=lhc.PlotObject()
-        '''
-        The *data* of the plot live in the .vectors list.
-
-        plot.vectors is a multidimensional array:
-        plot.vectors[0]=set1
-        plot.vectors[1]=set2
-        plot.vectors[2]=sett3
-        etc.
-
-        2 curves in a x,y plot are:
-        [[[x1],[y1]],[[x2],[y2]]]
-        for example:
-            x1          y1              x2         y2
-        [[[1,2,3,4],[10,20,30,40]],[[3,6,9,12],[30,60,90,120]]]
-        x1 = self.vectors[0][0]
-        y1 = self.vectors[0][1]
-        x2 = self.vectors[1][0]
-        y2 = self.vectors[1][1]
-        '''
-        #Pour 0-th dataset into myplot:
-        myplot.add_set(xdata1,ydata1)
-
-        #Pour 1-st dataset into myplot:
-        myplot.add_set(xdata2,ydata2)
-
-        #Add units to x and y axes
-        #units=[string, string]
-        myplot.units=['x axis','y axis']
-
-        #Where do we want the plot? 0=top, 1=bottom
-        myplot.destination=1
-
-        '''Send it to the GUI.
-        Note that you *have* to encapsulate it into a list, so you
-        have to send [myplot], not simply myplot.
-
-        You can also send more two plots at once
-        self.send_plot([plot1,plot2])
-        '''
-        self._send_plot([myplot])
-
-
-    def do_myfirstscatter(self,args):
-        '''
-        How to draw a scatter plot.
-        '''
-        #We generate some interesting data to plot for this example.
-        xdata1=arange(-5,5,1)
-        xdata2=arange(-5,5,1)
-        ydata1=[item**2 for item in xdata1]
-        ydata2=[item**3 for item in xdata2]
-
-        myplot=lhc.PlotObject()
-        myplot.add_set(xdata1,ydata1)
-        myplot.add_set(xdata2,ydata2)
-
-
-        #Add units to x and y axes
-        myplot.units=['x axis','y axis']
-
-        #Where do we want the plot? 0=top, 1=bottom
-        myplot.destination=1
-
-        '''None=standard line plot
-        'scatter'=scatter plot
-        By default, the styles attribute is an empty list. If you
-        want to define a scatter plot, you must define all other
-        plots as None or 'scatter', depending on what you want.
-
-        Here we define the second set to be plotted as scatter,
-        and the first to be plotted as line.
-
-        Here we define also the colors to be the default Matplotlib colors
-        '''
-        myplot.styles=[None,'scatter']
-        myplot.colors=[None,None]
-        self._send_plot([myplot])
-
-
-    def do_clickaround(self,args):
-        '''
-        Here we click two points on the curve and take some parameters from the points
-        we have clicked.
-        '''
-
-        '''
-        points = self._measure_N_points(N=Int, whatset=Int)
-        *N = number of points to measure(1...n)
-        *whatset = data set to measure (0,1...n)
-        *points = a list of ClickedPoint objects, one for each point requested
-        '''
-        points=self._measure_N_points(N=2,whatset=1)
-        print 'You clicked the following points.'
-
-        '''
-        These are the absolute coordinates of the
-        point clicked.
-        [float, float] = x,y
-        '''
-        print 'Absolute coordinates:'
-        print points[0].absolute_coords
-        print points[1].absolute_coords
-        print
-
-        '''
-        These are the coordinates of the points
-        clicked, remapped on the graph.
-        Hooke looks at the graph point which X
-        coordinate is next to the X coordinate of
-        the point measured, and uses that point
-        as the actual clicked point.
-        [float, float] = x,y
-        '''
-        print 'Coordinates on the graph:'
-        print points[0].graph_coords
-        print points[1].graph_coords
-        print
-
-        '''
-        These are the indexes of the clicked points
-        on the dataset vector.
-        '''
-        print 'Index of points on the graph:'
-        print points[0].index
-        print points[1].index
-
-
-    def help_thedifferentplots(self):
-        '''
-        The *three* different default plots you should be familiar with
-        in Hooke.
-
-        Each plot contains of course the respective data in their
-        vectors attribute, so here you learn also which data access for
-        each situation.
-        '''
-        print '''
-        1. THE RAW, CURRENT PLOTS
-
-        self.current
-        ---
-        Contains the current curve.HookeCurve container object.
-        A HookeCurve object defines only two default attributes:
-
-        * self.current.path = string
-        The path of the current displayed curve
-
-        * self.current.curve = curve.Driver
-        The curve object. This is not only generated by the driver,
-        this IS a driver instance in itself.
-        This means that in self.current.curve you can access the
-        specific driver APIs, if you know them.
-
-        And defines only one method:
-        * self.current.identify()
-        Fills in the self.current.curve object.
-        See in the cycling tutorial.
-
-        *****
-        The REAL curve data actually lives in:
-        ---
-        * self.current.curve.default_plots() = [ libhooke.PlotObject ]
-        Contains the raw PlotObject-s, as "spitted out" by the driver, without any
-        intervention.
-        This is as close to the raw data as Hooke gets.
-
-        One or two plots can be spit out; they are always enclosed in a list.
-        *****
-
-        Methods of self.current.curve are:
-        ---
-
-        * self.current.curve.is_me()
-        (Used by identify() only.)
-
-        * self.current.curve.close_all()
-        Closes all driver open files; see the cycling tutorial.
-        '''
-
-        print '''
-        2. THE PROCESSED, DEFAULT PLOT
-
-        The plot that is spitted out by the driver is *not* the usual default plot
-        that is displayed by calling "plot" at the Hooke prompt.
-
-        This is because the raw, driver-generated plot is usually *processed* by so called
-        *plot processing* functions. We will see in the tutorial how to define
-        them.
-
-        For example, in force spectroscopy force curves, raw data are automatically corrected
-        for deflection. Other data can be, say, filtered by default.
-
-        The default plots are accessible in
-        self.plots = [ libhooke.PlotObject ]
-
-        self.plots[0] is usually the topmost plot
-        self.plots[1] is usually the bottom plot (if present)
-        '''
-
-        print '''
-        3. THE PLOT DISPLAYED RIGHT NOW.
-
-        Sometimes the plots you are displaying *right now* is different from the previous
-        two. You may have a fit trace, you may have issued some command that spits out
-        a custom plot and you want to rework that, whatever.
-
-        You can obtain in any moment the plot currently displayed by Hooke by issuing
-
-        PlotObject = self._get_displayed_plot(dest)
-        * dest = Int (0/1)
-        dest=0 : top plot
-        dest=1 : bottom plot
-        '''
-
-
-    def do_cycling(self,args):
-        '''
-        Here we cycle through our playlist and print some info on the curves we find.
-        Cycling through the playlist needs a bit of care to avoid memory leaks and dangling
-        open files...
-
-        Look at the source code for more information.
-        '''
-
-        def things_when_cycling(item):
-            '''
-            We encapsulate here everything has to open the actual curve file.
-            By doing it all here, we avoid to do acrobacies when deleting objects etc.
-            in the main loop: we do the dirty stuff here.
-            '''
-
-            '''
-            identify()
-
-            This method looks for the correct driver in self.drivers to use;
-            and puts the curve content in the .curve attribute.
-            Basically, until identify() is called, the HookeCurve object
-            is just an empty shell. When identify() is called (usually by
-            the Hooke plot routine), the HookeCurve object is "filled" with
-            the actual curve.
-            '''
-
-            item.identify(self.drivers)
 
-            '''
-            After the identify(), item.curve contains the curve, and item.curve.default_plots() behaves exactly like
-            self.current.curve.default_plots() -but for the given item.
-            '''
-            itplot=item.curve.default_plots()
-
-            print 'length of X1 vector:',len(itplot[0].vectors[0][0]) #just to show something
+    def default_settings(self):
+        """Return a list of :class:`hooke.config.Setting`\s for any
+        configurable plugin settings.
+
+        The suggested section setting is::
+
+            Setting(section=self.setting_section, help=self.__doc__)
+
+        You only need to worry about this if your plugin has some
+        "magic numbers" that the user may want to tweak, but that
+        won't be changing on a per-command basis.
+
+        You should lead off the list of settings with the suggested
+        section setting mentioned above.
+        """
+        return [
+            # We disable help wrapping, since we've wrapped
+            # TutorialPlugin.__doc__ ourselves, and it's more than one
+            # paragraph (textwrap.fill, used in
+            # :meth:`hooke.config.Setting.write` only handles one
+            # paragraph at a time).
+            Setting(section=self.setting_section, help=self.__doc__,
+                    wrap=False),
+            Setting(section=self.setting_section, option='favorite color',
+                    value='orange', help='Your personal favorite color.'),
+            ]
+
+
+# Define common or complicated arguments
+
+# Often, several commands in a plugin will use similar arguments.  For
+# example, many curves in the 'playlist' plugin need a playlist to act
+# on.  Rather than repeating an argument definition in several times,
+# you can keep your code DRY (Don't Repeat Yourself) by defining the
+# argument at the module level and referencing it during each command
+# initialization.
+
+def color_callback(hooke, command, argument, value):
+    """If `argument` is `None`, default to the configured 'favorite color'.
+
+    :class:`hooke.command.Argument`\s may have static defaults, but
+    for dynamic defaults, they use callback functions (like this one).
+    """
+    if value != None:
+        return value
+    return command.plugin.config['favorite color']
+
+ColorArgument = Argument(
+    name='color', type='string', callback=color_callback,
+    help="Pick a color, any color.")
+# See :func:`hooke.ui.gui.panel.propertyeditor.prop_from_argument` for
+# a situation where :attr:`type` is important.
+
+
+class DoNothingCommand (Command):
+    """This is a boring but working example of an actual Hooke command.
+    
+    As for :class:`hooke.plugin.Plugin`\s, the class name is not
+    important, but :attr:`name` is.  :attr:`name` is used (possibly
+    with some adjustment) as the name for accessing the command in the
+    various :class:`hooke.ui.UserInterface`\s.  For example the
+    `'do nothing'` command can be run from the command line UI with::
+
+       hooke> do_nothing
+
+    Note that if you now start Hooke with the command's plugin
+    activated and you type in the Hooke command line "help do_nothing"
+    you will see this very text as output. That is because we set
+    :attr:`_help` to this class' docstring on initialization.
+    """
+    def __init__(self, plugin):
+        # See the comments in TutorialPlugin.__init__ for details
+        # about super() and the docstring of
+        # :class:`hooke.command.Command` for details on the __init__()
+        # arguments.
+        super(DoNothingCommand, self).__init__(
+            name='do nothing',
+            arguments=[ColorArgument],
+            help=self.__doc__, plugin=plugin)
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        """This is where the command-specific magic will happen.
+
+        If you haven't already, read the Architecture section of
+        :file:`doc/hacking.txt` (also available `online`_).  It
+        explains the engine/UI setup in more detail.
+
+        .. _online:
+          http://www.physics.drexel.edu/~wking/rsrch/hooke/hacking.html#architecture
+
+        The return value (if any) of this method is ignored.  You
+        should modify the :class:`hooke.hooke.Hooke` instance passed
+        in via `hooke` and/or return things via `outqueue`.  `inqueue`
+        is only important if your command requires mid-command user
+        interaction.
+
+        By the time this method is called, all the argument
+        preprocessing (callbacks, defaults, etc.) have already been
+        handled by :meth:`hooke.command.Command.run`.
+        """
+        # On initialization, :class:`hooke.hooke.Hooke` sets up a
+        # logger to use for Hooke-related messages.  Please use it
+        # instead of debugging 'print' calls, etc., as it is more
+        # configurable.
+        log = logging.getLogger('hooke')
+        log.debug('Watching %s paint dry' % params['color'])
 
-            '''
-            The following three lines are a magic spell you HAVE to do
-            before closing the function.
-            (Otherwise you will be plagued by unpredicatable, system-dependent bugs.)
-            '''
-            item.curve.close_all() #Avoid open files dangling
-            del item.curve #Avoid memory leaks
-            del item #Just be paranoid. Don't ask.
 
+class HookeInfoCommand (Command):
+    """Get information about the :class:`hooke.hooke.Hooke` instance.
+    """
+    def __init__(self, plugin):
+        super(HookeInfoCommand, self).__init__(
+            name='hooke info',
+            help=self.__doc__, plugin=plugin)
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        outqueue.put('Hooke info:')
+        # hooke.config contains a :class:`hooke.config.HookeConfigParser`
+        # with the current hooke configuration settings.
+        config_file = StringIO.StringIO()
+        hooke.config.write(config_file)
+        outqueue.put('configuration:\n  %s'
+                     % '\n  '.join(config_file.getvalue().splitlines()))
+        # hooke.plugins contains :class:`hooke.plugin.Plugin`\s defining
+        # :class:`hooke.command.Command`\s.
+        outqueue.put('plugins: %s'
+                     % ', '.join([plugin.name for plugin in hooke.plugins]))
+        # hooke.drivers contains :class:`hooke.driver.Driver`\s for
+        # loading curves.
+        outqueue.put('drivers: %s'
+                     % ', '.join([driver.name for driver in hooke.drivers]))
+        # hooke.playlists contains a
+        # :class:`hooke.playlist.NoteIndexList` of
+        # :class:`hooke.playlist.Playlist`\s.  Each playlist may
+        # contain several :class:`hooke.curve.Curve`\s representing a
+        # grouped collection of data.
+        playlist = hooke.playlists.current()
+        if playlist == None:
             return
+        outqueue.put('current playlist: %s (%d of %d)'
+                     % (playlist.name,
+                        hooke.playlists.index(),
+                        len(hooke.playlists)))
+        curve = playlist.current()
+        if curve == None:
+            return
+        outqueue.put('current curve: %s (%d of %d)'
+                     % (curve.name,
+                        playlist.index(),
+                        len(playlist)))
+
 
+class PointInfoCommand (Command):
+    """Get information about two user-selected points.
 
-        c=0
-        for item in self.current_list:
-            print 'Looking at curve ',c,'of',len(self.current_list)
-            things_when_cycling(item)
-            c+=1
-
-        return
-
-
-
-    def plotmanip_absvalue(self, plot, current, customvalue=None):
-        '''
-        This function defines a PLOT MANIPULATOR.
-        A plot manipulator is a function that takes a plot in input, does something to the plot
-        and returns the modified plot in output.
-        The function, once plugged, gets automatically called everytime self.plots is updated
-
-        For example, in force spectroscopy force curves, raw data are automatically corrected
-        for deflection. Other data can be, say, filtered by default.
-
-        To create and activate a plot manipulator you have to:
-            * Write a function (like this) which name starts with "plotmanip_" (just like commands
-              start with "do_")
-            * The function must support four arguments:
-              self : (as usual)
-              plot : a plot object
-              current : (usually not used, deprecated)
-              customvalue=None : a variable containing custom value(s) you need for your plot manipulators.
-            * The function must return a plot object.
-            * Add an entry in hooke.conf: if your function is "plotmanip_something" you will have
-              to add <something/> in the plotmanips section: example
-
-            <plotmanips>
-                <detriggerize/>
-                <correct/>
-                <median/>
-                <something/>
-            </plotmanips>
-
-            Important: Plot manipulators are *in pipe*: each plot manipulator output becomes the input of the next one.
-            The order in hooke.conf *is the order* in which plot manipulators are connected, so in the example above
-            we have:
-            self.current.curve.default_plots() --> detriggerize --> correct --> median --> something --> self.plots
-        '''
-
-        '''
-        Here we see what is in a configuration variable to enable/disable the plot manipulator as user will using
-        the Hooke "set" command.
-        Typing "set tutorial_absvalue 0" disables the plot manipulator; typing "set tutorial_absvalue 1" will enable it.
-        '''
-        if not self.config['tutorial_absvalue']:
-            return plot
-
-        #We do something to the plot, for demonstration's sake
-        #If we needed variables, we would have used customvalue.
-        plot.vectors[0][1]=[abs(i) for i in plot.vectors[0][1]]
-        plot.vectors[1][1]=[abs(i) for i in plot.vectors[1][1]]
-
-        #Return the plot object.
-        return plot
-
-
-#TODO IN TUTORIAL:
-#how to add lines to an existing plot!!
-#peaks
-#configuration files
-#gui plugins
+    Ordinarily a command that knew it would need user selected points
+    would declare an appropriate argument (see, for example,
+    :class:`hooke.plugin.cut.CutCommand`).  However, here we find the
+    points via user-interaction to show how user interaction works.
+    """
+    def __init__(self, plugin):
+        super(PointInfoCommand, self).__init__(
+            name='point info',
+            arguments=[
+                CurveArgument,
+                Argument(name='block', type='int', default=0,
+                    help="""
+Data block that points are selected from.  For an approach/retract
+force curve, `0` selects the approaching curve and `1` selects the
+retracting curve.
+""".strip()),
+                ],
+            help=self.__doc__, plugin=plugin)
+
+    def _run(self, hooke, inqueue, outqueue, params):
+        data = params['curve'].data[params['block']]
+        while True:
+            # Ask the user to select a point.
+            outqueue.put(PointRequest(
+                    msg="Select a point",
+                    curve=params['curve'],
+                    block=params['block']))
+
+            # Get the user's response
+            result = inqueue.get()
+            if not isinstance(result, PointResponse):
+                inqueue.put(result)  # put the message back in the queue
+                raise Failure(
+                    'expected a PointResponse instance but got %s.'
+                    % type(result))
+            point = result.value
+
+            # Act on the response
+            if point == None:
+                break
+            values = []
+            for column_name in data.info['columns']:
+                name,unit = split_data_label(column_name)
+                column_index = data.info['columns'].index(column_name)
+                value = data[point,column_index]
+                si_value = ppSI(value, unit, decimals=2)
+                values.append('%s: %s' % (name, si_value))
+
+            outqueue.put('selected point %d: %s'
+                         % (point, ', '.join(values)))