}

Introduction

This tutorial introduces you to the joys and pains of writing scripts for STUI.

Scripts are written in Python, and this tutorial does not attempt to teach the language. I strongly recommend the python tutorial at Python's home. Python is a simple and powerful language, and a little knowledge can be a tremendous asset.

The topics are as follows:

  • Basics (1 Hello script) Your first script. What's with "yield"?

    Basics

    Let's start with the traditional greeting. Create a text file "hello.py" in your home directory (or anywhere convenient) containing the following text. Use tabs for indentation:

    1 Hello script


                    class ScriptClass(object):
                        def __init__(self, sr):
                            pass
                    
                        def run(self, sr):
                            sr.showMsg("Hello")
                            yield sr.waitMS(1000)
                    

    Start STUI (if it is not already running) and load your script using Open... in the Scripts menu. A new script window should appear; press "Start" to run your script. This causes the status bar to show "Hello" for 1 second, after which your script ends and the status bar shows "Done".

    To reload a script (i.e. after modifying it), select Reload from the contextual pop-up menu for the status bar or any of the buttons along the bottom (Start, etc.). This is useful for developing scripts: you can modify the script in your favorite editor, then reload it in STUI and try it out.

    What's With "yield"?

    Whenever your script calls an sr.wait... function (i.e. wants to wait for anything), it must use yield, as in:

        yield sr.wait...(...)
                    

    This is a painful, but it could be worse. Most languages would force you to break your script into many small functions, each of which would have to be registered as a separate callback function. That style of programming is fine for user interfaces but is needlessly complicated for scripts.

    If you forget the "yield", your script will plow ahead instead of waiting, which is a recipe for trouble. However, STUI will catch this problem the next time you call an sr.wait... function, at which point STUI will kill your script, print a message to the status bar and print details to the error log.

  • Send Commands (2 Simple Ping script) Control an actor.

    Send Commands

    Sending commands to an instrument or other actor is straightforward. You need to know the name of the actor (e.g. "boss", "sop", "tcc") and the command you wish to send. See APO Documentationfor documentation for the instruments, TCC and hub.

    This script simply pings boss.

    2 Simple Ping script


                    class ScriptClass(object):
                        """Ping two actors"""
                        def __init__(self, sr):
                            pass
                    
                        def run(self, sr):
                            yield sr.waitCmd(
                                actor = "alerts",
                                cmdStr = "ping",
                            )
                            yield sr.waitCmd(
                                actor = "boss",
                                cmdStr = "ping",
                            )
                    

    Notes:

    • yield sr.waitCmd(...) starts a command and waits for it to finish.
    • If a command fails then the script will terminate. Usually this is what you want, but it can be avoided as we will see later.
  • Loops (3 Looping Ping script) Use a loop to simplify repetitive commands.

    Loops

    Often you will want to execute the a set of commands, or the same command multiple times with slight variations. Loops make this easy.

    This version will halt at the first failure and display that failure in the status bar. We can do better, as shown by the next version of this script.

    3 Looping Ping script


    class ScriptClass(object):
        """Tutorial script to test the aliveness of several actors.
        
        This version uses a loop.
        """
        def __init__(self, sr):
            pass
    
        def run(self, sr):
            for actorCmd in (
                "alerts ping",
                "boss ping",
                "gcamera ping",
                "guider ping",
                "mcp ping",
                "platedb ping",
                "sop ping",
                "tcc show time",
            ):
                actor, cmdStr = actorCmd.split(None, 1)
                yield sr.waitCmd(
                    actor = actor,
                    cmdStr = cmdStr,
                )
    

    Notes:

    • split(None, 1) splits a string at the first occurrence of whitespace (None means split on any amount of whitespace). In this case it separates the actor from the command to send to that actor.
    • The script will fail if any one of the commands fails, and an appropriate error message will be displayed in the status bar. Unfortunately this means the rest of the actors will not be tested. We'll improve that in the next version of the script.
  • Refinements (4 Nice Ping script) Log the results of your commands.

    Refinements

    Adding a few refinements to the ping script makes a script worth adapting for your own uses.

    The improvements are as follows:

    • Displays a log of the results of each command.
    • It checks all actors in the list, even if a command fails.

    The resulting script still has no provision for user input, but often that is fine. We'll deal with user input in the next lesson.

    4 Nice Ping script


    import RO.Constants
    
    class ScriptClass(object):
        """Tutorial script to test the aliveness of various actors.
    
        This is the recommended version. Unlike 3 Looping Ping:
        - It outputs the results to a log window
        - It uses checkFail=False to continue and check all actors even if one command fails.
        """
        def __init__(self, sr):
            """Display the exposure status panel.
            """
            self.actorCmdList = (
                "alerts ping",
                "boss ping",
                "gcamera ping",
                "guider ping",
                "mcp ping",
                "platedb ping",
                "sop ping",
                "tcc show time",
            )
            
            self.logWdg = RO.Wdg.LogWdg(
                master = sr.master,
                width = 30,
                height = len(self.actorCmdList) + 1, # avoids scrolling
            )
            self.logWdg.grid(row=0, column=0, sticky="news")
        
        def run(self, sr):
            """Run the script"""
            for actorCmd in self.actorCmdList:
                actor, cmdStr = actorCmd.split(None, 1)
                yield sr.waitCmd(
                    actor = actor,
                    cmdStr = cmdStr,
                    checkFail = False,
                )
                cmdVar = sr.value
                if cmdVar.didFail:
                    self.logWdg.addMsg("%s FAILED" % (actor,), severity=RO.Constants.sevError)
                else:
                    self.logWdg.addMsg("%s OK" % (actor,))
    

    Notes:

    • This is the first script that displays any widgets. In this case it is just one widget, and one that is smart enough to take care of its own display. But the pattern is always the same:
      • Use "sr.master" as the master widget. This is an empty frame intended for your script's widgets. It appears above the status bar and control buttons.
      • Grid or pack widgets to make them show up. Never mix gridding and packing in the same master widget; it usually causes Tk to go into an infinite loop.
      • To pass data between __init__ and run, use self, as in self.actorCmdList and self.logWdg.
    • The list of command is defined in __init__ simply because it is used to set the height of the log window to avoid any need to scroll. however, if the number of commands gets too large (such that the window gets too long) then you should think about a more efficient way to display the information, or accept the need to scroll and set height to a constant, such as 20.
    • The result of yield sr.wait... is saved in sr.value. In particular, yield sr.waitCmd puts the command variable (opscore.actor.CmdVar) in sr.value. You can test a command variable to see if the command succeeded or failed (as shown above), access keyword variables that were set in response to the command (see the keyVars argument to waitCmd for details).
  • Getting Info (5 Actor Versions script) Get information from actors.

    Get Info

    It is easy to incorporate information from an actor (e.g. instrument or the tcc) into your scripts. Each actor has a model that contains a set of keyword variables you can query. In this simple example we get and display the version of several actors.

    The models are in the actorkeys product. You should get a copy of actorkeys (or browse it on the Trac wiki) before trying to read data from an instrument. Each model contains a description of each keyword output by the actor (at least those keywords you can get data for) and the description usually includes documentation. (One exception: documentation for TCC keywords is in the TCC Message Keywords Dictionary).

    5 Actor Versions script


    import RO.Wdg
    import TUI.Models
    
    class ScriptClass(object):
        """Tutorial script to print the version of some actors
        """
        def __init__(self, sr):
            self.bossModel = TUI.Models.getModel("boss")
            self.gcameraModel = TUI.Models.getModel("gcamera")
            self.guiderModel = TUI.Models.getModel("guider")
            self.tccModel = TUI.Models.getModel("tcc")
            
            self.actorKeyVarList = (
                ("boss", self.bossModel.version),
                ("gcamera", self.gcameraModel.version),
                ("guider", self.guiderModel.version),
            )
            
            self.logWdg = RO.Wdg.LogWdg(
                master = sr.master,
                width = 40,
                height = len(self.actorKeyVarList) + 2, # avoids scrolling
            )
            self.logWdg.grid(row=0, column=0, sticky="news")
            
        def run(self, sr):
            for actor, keyVar in self.actorKeyVarList:
                versionStr = sr.getKeyVar(keyVar, ind=0, defVal="?")
                self.logWdg.addMsg("%s\t%s" % (actor, versionStr))
    
            yield sr.waitCmd(actor="tcc", cmdStr="show version", keyVars=[self.tccModel.version])
            tccVers = sr.value.getLastKeyVarData(self.tccModel.version)[0]
            self.logWdg.addMsg("%s\t%s" % ("tcc", tccVers))
    

    Notes:

    • sr.getKeyVar returns the current value of the specified "keyword variable" without waiting. That is fully appropriate for this script, because the hub gets the version and status from every actor when it connects (and the status is updated as it changes). Thus you should never have to send an explicit status command unless you suspect the actor is broken in such a way that it is not reliably outputting status as it changes. There are, however, other techniques to get information, which are appropriate to other situations:
      • Sometimes you want to wait until the keyword is next updated. In that case use yield sr.waitKeyVar.
      • Sometimes you need the value of a keyword in response to a particular command. For example suppose you are converting a position from one coordinate system to another using the "tcc convert" command. In that case you want to the keywords output by "tcc convert" in response to your particular command, with no chance of mixing them up with a reply to somebody else's command. A simpler but silly example is shown here: the script sends the "show version" command to the TCC and retrieves the "version" keyword that is output as a result of that command. The technique is the same in any case: supply the keywords you want to watch as the keyVars argument to sr.waitCmd, then obtain the data using the CmdVar's getLastKeyVarData or getKeyVarData method (when sr.waitCmd succeeds it puts the CmdVar used to execute the command in sr.value).
  • Conclusion Where to go for more information.
  • Conclusion

    You have seen how to write a variety of scripts, including scripts that use current status information from instruments and the telescope and that add widgets for user output. Now what?

    • Write some scripts! Nothing beats experience, and scripts can save you precious observing time.
    • Read the dictionaries and command manuals for the actors you want to control. The dictionaries are in actorkeys.
    • Look through existing scripts and the source code for STUI itself, especially anything that solves a problem similar to yours or shows controls you want to use.
    • Read the source code for RO.Wdg widgets of interest. Read the source code for oppscore.actor to learn more about KeyVar, CmdVar and ScriptRunner. The source includes extensive comments.
    • Read the Scripting Manual. This is a detailed manual for the script runner sr and includes such handy topics as:
      • Where to put your scripts so they are automatically loaded.
      • A simple, handy debug mode.
      • How your script can reliably clean up after itself using the end function.
      • How you can break your script into sub-tasks (functions that wait that you write).
    • Look through Programming. It gives you an overview of what is going on "under the hood" in STUI and helps you learn more. It also describes how to create your own STUI windows and where to put them so they load automatically. Writing your own STUI windows is not hard, once you have written some scripts.

The sample scripts from this tutorial can be found in tui_root/STUI/Scripts/Tutorial (you may have to ask your sysadmin for the location of tui_root).