Example usage ============= .. currentmodule:: ktl 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 .. _kpython: Importing the :mod:`ktl` module ------------------------------- In order to successfully import the :mod:`ktl` module, the correct installation path for the library must be in Python's search path. In order to streamline the use of the :mod:`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`` and ``$LROOT/lib`` 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 :attr:`sys.path` before importing the :mod:`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-service: Basic use of :class:`Service` and :class:`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 :class:`Service` instances for a given KTL service as you like; while they will *not* share any visible metadata or :class:`Keyword` instances, they *will* share a single open instance of the actual KTL client library. You can also use the :func:`cache` function to consistently retrieve the same :class:`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') .. _basic-keyword: :class:`Keyword` instances are always created/retrieved via a :class:`Service` instance:: service = ktl.Service('servicename') keyword = service['keywordname'] You can likewise use :func:`cache` to consistently retrieve the same :class:`Keyword` instance:: keyword = ktl.cache('servicename', 'keywordname') keyword = ktl.cache('servicename.keywordname') .. _service-reference: A :class:`Keyword` instance cannot exist independently of a :class:`Service` instance; if you do not retain a reference to the parent :class:`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 :class:`Service` for a given KTL service is cached for future reference; relying on this behavior is not recommended. Either store a reference to the :class:`Service` instance, or always use :func:`cache` to retrieve cached instances. Reading and writing with :class:`Keyword` instances --------------------------------------------------- A basic read operation will block until it returns the ascii representation of the keyword value; refer to :func:`Keyword.read` for other possibilities. :: keyword = ktl.cache('servicename', 'keywordname') value = keyword.read() The value returned by this type of :func:`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 :class:`ktlError`. Write operations are handled in a similar manner; continuing from the example immediately above:: keyword.write('newvalue') A call to :func:`Keyword.write` returns the sequence number associated with the underlying :func:`ktl_write` call. If the operation fails, it will always raise an exception, likely a :class:`ktlError`. With non-blocking writes, the returned sequence number can be used with :func:`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 :class:`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 :class:`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 :class:`Keyword` instances ----------------------------------------- In order to trigger a callback, one of two things must occur: * A :class:`Keyword` was subscribed to broadcasts via :func:`Keyword.monitor`. * :func:`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, :func:`Keyword.monitor` will automatically invoke :func:`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. .. _keyword-callbacks: Callbacks registered via :func:`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 :class:`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 :func:`Keyword.callback` does not immediately trigger an invocation of the callback. If you are in a circumstance where a :class:`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() .. _non-keyword-callbacks: Callbacks with :func:`monitor` ------------------------------ The :func:`monitor` function pre-dates the availability of :class:`Keyword` objects; it is now a backwards-compatible wrapper to :func:`Keyword.monitor` and :func:`Keyword.callback`. Any applications leveraging the top-level :func:`monitor` function tended to track per-keyword metadata that is commonly available as part of the :class:`Keyword` class; applications of this type can often be simplified by the use of :class:`Keyword` instances retrieved via :func:`cache`. A properly constructed callback for use with :func:`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 :func:`Keyword.callback` safely allows multiple callbacks to be established for a single KTL keyword; this was not true for the previous implementation. Using :func:`waitFor` --------------------- :func:`waitFor` allows a program to block (with an optional timeout) while waiting for a specific condition to be true. The related member functions, :func:`Service.waitFor` and :func:`Keyword.waitFor` are wrappers to :func:`waitFor` with varying default options. The basic use of :func:`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 :doc:`expressions` 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...'