Example usage

This documentation assumes that a kroot environment was successfully installed, and that the configure script in kroot/etc found an acceptable Python interprter. In order to ensure predictable results, make sure that the following environment variables are correctly set; here is an example for Bourne shell:

KROOT=/path/to/kroot
RELNAM=default
RELDIR=$KROOT/rel/$RELNAM

export KROOT RELNAM RELDIR

Importing the ktl module

In order to successfully import the ktl module, the correct installation path for the library must be in Python’s search path. In order to streamline the use of the ktl module, kpython was written to eliminate this specific concern. kpython is a small binary compiled from C, minimizing the additional runtime overhead. Here is an example using kpython:

#! @KPYTHON@

import ktl

This specific example assumes that the script is actually a .sin file, and will be processed by subst.tcl to substitute for values established by kroot/etc/defs.mk. Another portable approach would be:

#! /usr/bin/env kpython

import ktl

This second approach assumes that the correct $RELDIR/bin is encountered first in the user’s default search path (the PATH environment variable).

kpython will check and set values for the KROOT, LROOT, and RELDIR environment variables, and will then append $RELDIR/lib/python and $LROOT/lib/python to the PYTHONPATH environment variable. Having carefully constructed this runtime environment, kpython then executes the Python interpreter identified by kroot/etc/configure.

If not using kpython, a user may choose instead to set the PYTHONPATH to an appropriate value as described above, or the script may choose to manipulate sys.path before importing the ktl module. Here is an example of the latter, again using substitutions for key values, but still allowing the user’s environment to take precedence:

#! @PYTHON2@

import os
import sys

RELDIR = os.environ.get ('RELDIR')

if RELDIR is None or RELDIR == '':
        RELDIR = '@RELDIR@'

RELDIR_LIB = os.path.join (RELDIR, 'lib')

if RELDIR_LIB in sys.path:
        pass
else:
        sys.path.append (RELDIR_LIB)


# It is now safe to import ktl.
import ktl

Basic use of Service and Keyword classes

KTL service names are case-sensitive. Opening and otherwise preparing to use a KTL service is a one-line operation:

service = ktl.Service ('servicename')

You can create as many Service instances for a given KTL service as you like; while they will not share any visible metadata or Keyword instances, they will share a single open instance of the actual KTL client library.

You can also use the cache() function to consistently retrieve the same Service instance; this eliminates the need to retain a unique local reference to the instance, which can simplify code spread out in multiple namespaces. Example:

service = ktl.cache ('servicename')

Keyword instances are always created/retrieved via a Service instance:

service = ktl.Service ('servicename')
keyword = service['keywordname']

You can likewise use cache() to consistently retrieve the same Keyword instance:

keyword = ktl.cache ('servicename', 'keywordname')
keyword = ktl.cache ('servicename.keywordname')

A Keyword instance cannot exist independently of a Service instance; if you do not retain a reference to the parent Service instance, it will be deallocated, and as a result the open KTL handle for that instance will be closed:

service = ktl.Service ('servicename')
keyword = service['keywordname']

del service                # The service closed here.

keyword.read ()            # This will fail.
keyword.write ('value')    # This will fail.

There is some small uncertainty in when such a failure might occur, as there is no way to predict when Python’s garbage collector is invoked. There is some wiggle room here as the first instance of a Service for a given KTL service is cached for future reference; relying on this behavior is not recommended. Either store a reference to the Service instance, or always use cache() to retrieve cached instances.

Reading and writing with Keyword instances

A basic read operation will block until it returns the ascii representation of the keyword value; refer to Keyword.read() for other possibilities.

keyword = ktl.cache ('servicename', 'keywordname')
value = keyword.read ()

The value returned by this type of Keyword.read() call will always be a string regardless of the base keyword type. If the operation fails, it will always raise an exception, likely a ktlError.

Write operations are handled in a similar manner; continuing from the example immediately above:

keyword.write ('newvalue')

A call to Keyword.write() returns the sequence number associated with the underlying ktl_write() call. If the operation fails, it will always raise an exception, likely a ktlError.

With non-blocking writes, the returned sequence number can be used with Keyword.wait() if an application intends to block at a different control point:

sequence = keyword.write ('newvalue', wait=False)
keyword.wait (timeout=60, sequence=sequence)

Write operations will also be performed with in-place operators. For example, incrementing an integer keyword or performing in-place string concatenation could be done with the += operator:

numeric_keyword += 1
string_keyword  += ' appended string'

All of the normal in-place operators are supported for appropriate keyword types. Before writing the keyword value, monitoring for that keyword will be enabled; when writing the keyword value, a blocking write (with no timeout) will be performed. The in-place operation only returns after the Keyword instance receives a broadcast of a new value; depending on the KTL implementation this extra delay may be significant compared to the amount of time required to execute the blocking write.

Different Keyword types will accept different Python-native values. For example, boolean KTL keyword will accept the typical Python True and False objects; array keyword types will accept arbitrary sequences (of the correct length) or dictionaries. Care should be taken to use or specify the correct write type for keywords that have distinct meanings for their ascii and binary representations. For ascii representations, the KTL client library’s native routine(s) will be used to convert the caller-supplied argument to its binary form; for binary representations the supplied value will be interpreted directly to its binary form with minimal translation beyond basic type conversion.

Callbacks with Keyword instances

In order to trigger a callback, one of two things must occur:

A typical callback would be established in the following fashion:

service = ktl.Service ('servicename')
interest = ('keyword1', 'keyword2')

for name in interest:

        keyword = service[name]
        keyword.callback (myCallback)
        keyword.monitor ()

If a KTL keyword does not support broadcasts, Keyword.monitor() will automatically invoke Keyword.poll() as a fallback. The callback mechanism is the same in either case. Keywords that do not support broadcasts and do not support read operations cannot be monitored and cannot trigger callbacks.

Callbacks registered via Keyword.callback() must take care not to directly execute time consuming operations. The motivation for the concern is the single-threaded processing of underlying KTL events; if events cannot be popped off the queue fast enough, an unbounded backlog of pending events will accumulate.

A typical callback function should also be mindful of the possibility that an exception was raised as part of the low-level processing; one way this could occur is if the broadcast keyword value is invalid. Any such exception will be raised when attempting to use accessor functions to retrieve the value of the Keyword instance. The following code block is one approach for safe handling during a callback context:

def receiveCallback (keyword):

        # Confirm that a Keyword object has a value before
        # attempting to access the ascii value. If it had
        # no value, invoking keyword['ascii'] would trigger
        # a just-in-time ktl_read() operation.

        if keyword['populated'] == False:
                return

        try:
                value = keyword['ascii']
        except:
                # An exception would be raised here if the
                # Python/C interpretation of the broadcast
                # value failed. This example will do nothing
                # to handle such an exception except to abort
                # further processing.

                return

Note that invoking Keyword.callback() does not immediately trigger an invocation of the callback. If you are in a circumstance where a Keyword instance may already be monitored you may want to manually prime the callback. A helper function like the following may be useful to guarantee that every registered callback gets called at least once:

def registerCallback (keyword, callback):

        keyword.callback (callback)

        if keyword['monitored'] == True:
                # Prime the callback to ensure it gets called at least
                # once with the current value of the ktl.Keyword.
                callback (keyword)
        else:
                # The priming read done by default in Keyword.monitor()
                # will automatically result in an invocation of the
                # newly registered callback.
                keyword.monitor ()

Callbacks with monitor()

The monitor() function pre-dates the availability of Keyword objects; it is now a backwards-compatible wrapper to Keyword.monitor() and Keyword.callback(). Any applications leveraging the top-level monitor() function tended to track per-keyword metadata that is commonly available as part of the Keyword class; applications of this type can often be simplified by the use of Keyword instances retrieved via cache().

A properly constructed callback for use with monitor() will look like one of the following:

def callback (keyword, value, mydata):

        print "Keyword '%s' has value '%s'" % (keyword, value)
        print "Object '%s' was provided as requested" % (mydata)


class myclass ():

        def callback (self, keyword, value, mydata)

                print "myclass instance '%s' has a callback" % (self)
                print "Keyword '%s' has value '%s'" % (keyword, value)
                print "Object '%s' was provided as requested" % (mydata)

Establishing monitoring with one of the above callbacks is done like so:

ktl.monitor ('servicename', 'keywordname', callback, mydata)

The wrapper to Keyword.callback() safely allows multiple callbacks to be established for a single KTL keyword; this was not true for the previous implementation.

Using waitFor()

waitFor() allows a program to block (with an optional timeout) while waiting for a specific condition to be true. The related member functions, Service.waitFor() and Keyword.waitFor() are wrappers to waitFor() with varying default options.

The basic use of waitFor() will include one or more KTL keywords, and should set a timeout to avoid blocking indefinitely. A simple example, using keyword BAR from service foo:

success = ktl.waitFor ('($foo.BAR > 15)', timeout=5)

if success == True:
        print 'foo.BAR exceeded 15 within 5 seconds'

See the section on Expressions in KTL Python for additional details on expression syntax.

Putting it all together

The example shown here is a modest expansion of what one could otherwise accomplish with cshow. The objective is to print a single line, with a timestamp, showing two different representations for a single physical parameter, and to print that line every time the values change. In this case, the corrected elevation angle for an atmospheric dispersion corrector (ADC), and the load encoder value for that angle.

What little complexity is present in this script is almost entirely in the callback function.

#! /usr/bin/env kpython
#
# Watch the ADC position, and print its position when it changes.

import ktl              # provided by svn/kroot/ktl/keyword/python
import time


# Definitions.

SERVICE = 'apfmot'
HEARTBEATS = ('DISP0CLK', 'DISP1CLK', 'DISP2CLK', 'DISP9CLK')

# The following values will be filled in after the service is opened.

RAW = None
ANGLE = None


# Callback to print values.

def callback (keyword):

        # Take no action if this keyword does not have a value.

        if keyword['populated'] == False:
                return

        try:
                value = keyword['ascii']
        except:
                return

        if RAW is None or ANGLE is None:
                return


        if keyword == RAW:
                other_keyword = ANGLE

        elif keyword == ANGLE:
                other_keyword = RAW

        else:
                raise RuntimeError, "unexpected keyword handled by callback: '%s'" % (keyword['name'])


        # Initial condition: one of the keywords will receive its
        # callback first. Don't proceed until both keywords have
        # values.

        if other_keyword['populated'] == False:
                return


        # Only print output when the broadcast timestamps for the RAW
        # and ANGLE keywords are in close agreement. The apfmot
        # service will broadcast these values on 1 Hz boundaries,
        # and one should follow closely after the other, certainly
        # in less than 0.2 seconds.

        window = 0.2

        this_keyword = keyword

        this_event  = this_keyword['history'][-1]
        other_event = other_keyword['history'][-1]

        this_timestamp  = this_event['timestamp']
        other_timestamp = other_event['timestamp']

        if abs (this_timestamp - other_timestamp) < window:

                # Use the "oldest" timestamp for the display, since
                # it is closer to the actual event time. Ideally,
                # the timestamps for the two broadcasts would be
                # identical, since they correspond to the same
                # reading of the same physical parameter.

                timestamp = min (this_timestamp, other_timestamp)

                print "%.6f ADC angle: %s %s (raw: %s %s)" % (timestamp, ANGLE['ascii'], ANGLE['units'], RAW['ascii'], RAW['units'])


# Get started.

service = ktl.Service (SERVICE)

# Watch all the heartbeats for this service, to mitigate the possibility
# of missing any broadcasts.

for heartbeat in HEARTBEATS:
        service.heartbeat (heartbeat)

ANGLE = service['ADCVAX']
RAW   = service['ADCRAW']


for keyword in ANGLE, RAW:

        # Query the keyword units now, so they will be cached
        # before the callback gets invoked.

        keyword['units']

        keyword.callback (callback)
        keyword.monitor ()


# Callback are registered, and we are now subscribed to broadcasts.
# Wait until the user exits with ctrl-c.

while True:
        try:
                time.sleep (300)
        except:
                break

print 'Exiting...'