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
Keyword
was subscribed to broadcasts viaKeyword.monitor()
.Keyword.read()
was invoked.
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...'