General Concepts

Devices

The device is the core concept of Karabo. Basically, Karabo consists of a set of devices running somewhere on the network. Every device is an instance of an object-oriented class which implements the device interface and logic. Each device serves one logical and encapsulated service, i.e. 1 device = 1 service.

A device can represent:

  • A single IO channel (e.g. a digital output for switching something on or off)
  • A single piece of equipment (e.g. a motor or a pump)
  • A controller driving a set of equipment (e.g. a pump controller)
  • A group of equipment that together forms a larger component (e.g. a slit using two underlying motors)
  • A software algorithm (e.g. image processing)
  • A connection to a service, file system or database (e.g. data archive reader, calibration database adapter)

The main purpose of devices is to hide the implementation details of the underlying service from the user and provide a standardized interface to the outside world.

In order to unambiguously address a device running somewhere in the network, each device is identified by a unique name, its device id. A Karabo distributed system will not allow a second device to be started with an instance name that already exists somewhere in its managed topology.

Device instance ids are strings that must not be empty. Allowed characters are upper or lower case letters of the English alphabet, digits, or the special characters ‘_’, ‘/’ and ‘-‘. Preferrably, ids have three parts domain, device type and member separated by ‘/’:

<domain>/<type>/<member>

Device Implementation

As previously mentioned devices are classes, with methods and properties. All devices inherit from a base class in the respective API, ensuring that a common core functionality in terms of inter-device communication, data types, self-description and logging is provided.

Device Slots

Device slots can conceptually be seen as member functions of a C++ or Python class which are additionally exposed to all other devices in the control system. Slots may be called with up to four arguments of the types described in Section Karabo Data Types (although many more are possible using a Hash as a container). They may have zero to four return values of the a Karabo-known types.

Slots that are part of the device self-description and are thus exposed to the graphical user interface, do not take arguments. As commands they should return the state that the device is in after slot execution.

The Call & Request/Reply Patterns

At its core Karabo uses a combination of signals and slots to provide for (inter-)device communication. This low-level interface can be directly used if a large degree of message passing patterns and the (a)synchronicity of events is needed. In the C++ API and Bound API it is exposed as part of the Device interface. If an exception occurs during execution of a slot on the remote device, an exception will be thrown in case of synchronous operations. In the asynchronous case, one can specify a failure handler in addition to the normal handler.

In the simplest case a device method is called (possibly from another device) and any return value is not expected. This is the call pattern.

class RemoteDevice(PythonDevice):
        ...
        def __init__(self, configuration):
                super(RemoteDevice, self).__init__(configuration)
                self.KARABO_SLOT(self.foo)

        def foo(self, a):
                self.log.INFO(a)

# code on caller
self.call("a/remote/device", "foo", 1)

Note

A special case of the call pattern is the global call. The idea is to call a specific slot function irrespective of the device that carries it. This is expressed by using a “*” instead of a specific device name. Global calls should not be used in device code but are mentioned here for completeness.

The call follows a fire-and-forget mentality and any potential reply statement on the remote function will be ignored and not sent back to the callee. Neither are any failures reported like non-existence of the called device or slot. Calling a remote slot will never block the caller.

If return values are expected the request and reply pattern is used. A request to a method may be called in two different ways:

  • synchronously, as a direct call: the caller will block until the method execution returns or fails by throwing an exception. A timeout may be configured if a reply is expected.

    class RemoteDevice(PythonDevice):
            ...
            def initialization(self):
                    self.KARABO_SLOT(self.bar)
    
            def bar(self, b):
                    c = b + 1
                    #this is a slot which should send out a reply
                    self.reply(c)
    
    # code on caller
    result = self.request("/a/remote/device", "bar", 1).waitForReply(1000)
    
  • asynchronously, with callback: the call to the method directly returns to the caller. Upon completion of the call the callback is executed (in a separate thread) and any return values are supplied as arguments.

    class RemoteDevice(PythonDevice):
            ...
            #as before
    
    #code on caller
    def onBar(self, response):
            self.log.INFO(response)
    
    self.request("a/remote/device", "bar, 2).receiveAsync(self.onBar)
    

In C++ the syntax is slightly different and the callbacks are bound in runtime, using karabo::util::bind_weak:

string txt(“The answer is: ”);
request(“some/device/1”, “slotFoo”, 21)
    .receiveAsynce<int>(bind_weak(&onReply, this, txt, _1),
                        bind_weak(&onError, this));

void onReply(const std::string& arg1, int arg2) {
    std::cout << arg1 << arg2 << std::endl; // Prints: "The answer is: 42"
}

void onError() {
    try {
            throw;
    } catch (const std::exception& e) {
            std::cout << An error occurred when calling 'slotFoo': "
            << e.what() << std::endl;
    }
}

// Replying instance ("some/device/1"):
void slotFoo(const int arg1) {
    reply(arg1 + arg1);
}

Note

Using karabo::util::weak_bind ensures that while the callback is being executed it is protected from destruction of this, while at the same time a bound but not executed callback will not prevent destruction of this.

A signal can directly be used to initiate action: the method is attached to a signal and is executed when this signal is emitted. This is especially useful if the update of a parameter should trigger different actions on multiple devices with multiple methods.

class RemoteDevice(PythonDevice)
    ...
    def initialization(self):
        self.registerSignal("foo", int)

    def bar(self):
        self.emit("foo", 1)

class Receiver1(Python):
    ...
    def initialization(self):
        self.KARABO_SLOT(self.onFoo)
        self.connect("remote/device/1", "foo", "", "onFoo")

    def onFoo(self, a):
        self.log.INFO(a)

class Receiver2(Python):
    ...
    def initialization(self):
        self.KARABO_SLOT(self.onBar)
        self.connect("remote/device/1", "foo", "", "onBar")

    def onBar(self, b):
        self.log.INFO(b+1)

Technical Implementation

Every device is subscribed as a client to a central message broker. All devices subscribe with their device names. The broker uses these names for message routing during the request / reply communication. The requesting instance generates a unique ID for each request, which is shipped with the message and is used for blocking and unblocking or registering and finding a provided callback, respectively.

Device Properties

Note

The below writing addresses the C++ and Bound APIs, property access is simplified in middle-layer devices.

Device properties are the equivalent to public members in C++ or properties in Python, i.e. they are class member variables which you would like to expose to the outside world, or in the context of a distributed control system, to other devices. In the Tango world they directly correspond to attributes; in the DOOCS world they correspond to properties.

In Karabo they are defined statically in the so-called expectedParameters section. Properties may be of any of the types specified in Section Karabo Data Types and may have received further specification using attributes. Alongside methods, properties constitute an integral part of a device’s self description, as defined by its Schema. By defining a property the following is implied

  • the property is readable (get) and possibly writable (set) from within the distributed system using a combination of device id and property key and given the user has appropriate access rights.
  • the combination of device id and key is unique across the distributed system installation.
  • the GUI provides basic functionality for displaying the property
  • the GUI provides basic functionality for altering the property
  • the property is available to middle-layer devices and macros via proxies
  • the property can be serialized in Karabo’s serialization and DAQ formats.

Properties can be any of the Karabo data types described in Section Karabo Data Types. They are defined in the so-called expected parameters definition of a device and are known to the system at static time.

@staticmethod
def expectedParameters(expected):

    (
        STRING_ELEMENT(expected).key("stringProperty")
            .displayedName("A string property")
            .assignmentMandatory()
            .commit()
            ,
        UINT32_ELEMENT(expected).key("integerProperty")
            .displayedName("An integer property")
            .assignmentOptional().defaultValue(1)
            .commit()
            ,
    )

As shown in the code, properties are defined by creating an element, identified by the Karabo type with the suffix _ELEMENT. They need to be given a unique key, and may be further specified through attributes.

Node Elements

Karabo allows grouping of properties into hierarchical tree structures. This is done using node elements. A node element can be seen as an intermediate component in the path uniquely identifying a property. It is a natural consequence of allowing nested Hash structures. Accordingly, requesting the value of a node element will return a Hash with the node’s inner elements as members.

A device may give different options on which kind of node to use, this is called a choice of nodes element:

@staticmethod
def expectedParameters(expected):
    (
        CHOICE_ELEMENT(expected).key("connection")
        .appendNodesOfConfigurationBase(ConnectionBase)
        .commit()
    )

In some occasions, it may be useful to have an entire list of different nodes, which is the list of nodes element. The device programmer defines node types which can be used in this list:

@staticmethod
def expectedParameters(expected):
    (
        LIST_ELEMENT(expected).key("categories")
        .appendNodesOfConfigurationBase(CategoryBase)
        .commit()
    )

Device version

Each device declares in its configuration the Karabo Framework version as well as its package version. The automation of this feature allows to seamlessly store the software configuration in the logging system.

See the respective API sections on examples of how this is done for the C++ and python APIs.

Device Hooks

Karabo devices provide a set of common hooks in both the Python and C++ APIs (but not the middle-layer API). Developers can use these hooks to trigger special functionality on events common to all devices. They are as follows:

  • preReconfigure(incomingReconfiguration): allows an incoming re-configuration to the device to be altered before actually updating device properties. This hook can be used to perform more sophisticated validity checks or to alter the configuration before its application. The configuration is passed as a Karabo Hash which contains all altered properties.

    Note

    The incoming configuration can contain one to many altered properties, depending on whether apply or apply all was executed from the GUI.

  • postReconfigure: this parameterless hook is called after a new configuration has been applied. One can use this hook to perform some action on hardware after configuration has been validated and set.

  • preDestruction: this parameterless hook is executed before a device instance is destroyed. You should use it to clean up, close any open sockets or connections or possible bring the hardware back into a specified safe state.

  • onTimeUpdate(trainId, sec, frac, period): is executed when the device receives an update from the timing system.

    • The registerInitialFunction(func) method can be used to register a function to be called at the end of device initialization, i.e. after the device properties’ initial values have been set and are available through the get and set methods. Usually, this function should bring the device into an initial known state.

Events vs. Polling on bound devices

In the context of bound devices Karabo imposes no restrictions if values from hardware are introduced into the distributed control system in an event-driven fashion or through polling. Hardware interaction may thus occur via the hardware sending event messages via a defined channel, i.e. an open socket, to the device, possibly with a PLC system mediating between both sides, or by actively polling the hardware on an interface or connection at a predefined update interval.

In either case new values (from the hardware) are made available to the distributed system in a standardized fashion by assigning (setting) to the corresponding property, defined as an expected parameter. Possibly, some sort of computation has occurred prior to this, e.g. if a histogram is computed from digitizer output and the individual samples are not further used.

Assigning to a property is an atomic, blocking operation, i.e. the rest of the distributed system is only made aware of the property change if the assignment succeeded. Similarly, retrieval of a property value is an atomic, blocking operation: during retrieval it is guaranteed that the current value is not altered by an assignment operation.

Note

This does not mean that there may not be a more up-to-date value available from the hardware. It only means that the distributed system returns the most current value it is aware of.

A device polling hardware should usually implement its own worker thread as is shown in the following code example.

from karabo.bound.worker import Worker
from karabo.bound.decorators import KARABO_CLASSINFO
from karabo.bound.device import PythonDevice, launchPythonDevice
from ._version import version as deviceVersion

@KARABO_CLASSINFO("HardwarePollingDevice", deviceVersion)
class HardwarePollingDevice(PythonDevice):

    def __init__(self, configuration):
        super(HardwarePollingDevice).__init__(self, configuration)
        self.pollWorker = None
        self.registerInitialFunction(self.initialization)

    def preDestruction(self):
        if self.pollWorker is not None:
            if self.pollWorker.is_running():
                self.pollWorker.stop()
            self.pollWorker.join()
            self.pollWorker = None

    @staticmethod
    def expectedParameters(expected):
        (
            INT32_ELEMENT(expected).key("polledValue)
                .readOnly().noInitialValue()
                .commit()
                ,
                ...
        )

    def initialization(self):
        if self.pollWorker is None:
            # Create and start poll worker
            timeout = 1000 # milliseconds
            self.pollWorker = Worker(self.pollingFunction, timeout, -1).start()

    def pollingFunction(self)
        #do something useful
        .....
        self.set("polledValue", value)

Synchronous and Asynchronous Communication via the Client Interface

As was mentioned in the Device Slots section, Karabo devices support two types of calls to slots on devices: synchronous calls and asynchronous calls on the lower-level signal-slot interface. Often such a detailed level of control over (a)synchronicity of communication is not needed. In such cases the DeviceClient interface can be used. The device client is accessible using the remote() function:

self.remote().execute("/a/remote/device", "foo", 1)

will block on the caller until the call either returns or fails by throwing an exception, the latter could e.g. happen if you called to a wrong id, gave the wrong type or number of arguments or there was a problem with the network connection. Optionally, you can specify a timeout as last parameter, after which an exception is thrown if the call has not completed by then.

In contrast,

self.remote().executeNoWait("/a/remote/device", "foo", 1)

will directly return to the caller if no exception is thrown. Similarly, you can alter properties on a remote device using

self.remote().set("/a/remote/device", "A", 1)
self.remote().setNoWait("/a/remote/device", "B", 2)

and retrieve them

self.remote().get("/a/remote/device", "A")

If you depend on executing some code whenever a property on a device changes property monitors come into use. They allow you to register a callback to be executed whenever the property changes:

def myCallBack(self, a, timestamp):
    self.log.INFO("Value has changed: {} at {}".format(a,t))

self.remote().registerPropertyMonitor("/a/remote/device", "A", self.myCallBack)

Callbacks can also be registered to receive notifications if a device has generally changed, i.e. its properties or state were altered:

def myCallBack(self, a, timestamp):
    #do something useful
    ...

self.remote().registerDeviceMonitor("/a/remote/device", "A", self.myCallBack)

Note

While communication via the client interface offers some degree of convenience for bound device development, it is recommended that such devices which do not need low-level event control are programmed in the middle-layer API instead, where a more concise interface for the client functionality as just described is available.

The Simple State Machine

All device APIs in Karabo provide state-awareness via so-called simple state machines. The underlying assumption is that for (bound) devices, where strict state transition rules need to be enforced, these will have been implemented in hardware or in firmware on PLCs. Bound devices thus need to be able to follow or reflect the hardware state, but not enforce strict state transition rules. In other words: state-violating input to the hardware is caught by the hardware, preserving hardware safety, not by the software device.

Leveraging this policy software state handling can be more relaxed: slots are state aware in that it can be defined for which states they may be executed, but no transition rules need to be enforced. Instead state transition is programmatically driven using

def expectedParameters(expected)
    (
        SLOT_ELEMENT(expected).key("start")
            .displayedName("Start")
            .allowedStates([States.STOPPED, States.IDLE])
            .commit()
            ,
        SLOT_ELEMENT(expected).key("stop")
            .displayedName("Stop")
            .allowedStates(States.MOVING)
            .commit()
            ,
    )

#...

def start(self):
    #...
    self.updateState(states.MOVING)

The available states are consistent across the distributed system and defined in the states enumerator. Details can be found in Section States.

Karabo Data Types

Karabo properties can have a number of common data types, ranging from simple and complex scalars, vectors of these, as well as composite types such as arrays of arbitrary rank and tables, i.e. 2-d arrays with a fixed column schema.

Additionally, Karabo implements a key-value container which preserves insertion order and can be iterated over: the Karabo Hash.

Karabo datatypes “live” in the Karabo Hash. They are converted to the native types of the programming language upon retrieval (get) from the Hash and from the native types upon assignment to the Hash. In C++ this is explicitly done using template mechanisms, in Python an implicit conversion is performed. Casting is supported using the ``getAs` method:

h = Hash("foo", 1) # assigned an integer to foo
f = h.getAs("foo", float) # f is a float
s = h.getAs("foo", str) # s is a string

h2 = Hash("bar", "Hello World!) #assigned a string to bar
i = h2.getAs("bar", int)
# will raise an exception as Hello World cannot be converted to int

In C++ templating mechanisms are used:

Hash h("foo", 1)
float f = h.getAs<float>("foo")
std::string s = h.getAs<std::string>("foo")

Hash h2("bar", "Hello World!")
int i = h2.getAs<int>("bar") // will throw

The Karabo Hash

The Karabo Hash is a key-value container. This means the (values of) elements in a Hash can be addressed by a string key.

h = Hash()
h.set("foo", 1)
v = h.get("foo")

Insertion order into the Hash is preserved and iteration supported:

h = Hash()
h.set("foo", 1)
h.set("bar", 2)

for key in h.getKeys():
    print(key, h.get(key))

#will print
# foo, 1
# bar, 2

Hash key-value pairs can have attributes assigned to them, allowing to specify e.g. validity bounds:

def checkBounds(h,k):
    if h.hasAttribute(k, "warnLow") and h.hasAttribute(k, "warnHigh"):
        if h.get(k) < h.getAttribute(k, "warnLow") or \
            h.get(k) > h.getAttribute(k, "warnHigh"):

            raise AttributeError("Value out of bounds")

h = Hash()
h.set("foo", 1)
h.setAttribute("foo", "warnLow", 0)
h.setAttribute("foo", "warnHigh", 2)

checkBounds(h, "foo")
#all good
h.set("foo", 3)
checkBounds(h, "foo")
#raises AttributeError

In fact bound-checking is already included in Karabo and can be enabled upon property definition. It is implemented using attributes.

From the Python perspective a Hash corresponds to something like an ordered dict() which allows attribute assignment to each key. C++ programmers by think of it as an ordered std::map.

Finally, Hashes may contain other Hashes, adding hierarchy to the container. Values are thus identifiable by paths, separated with “.” characters:

h1 = Hash()
h2 = Hash("a", 1)
h1.set("b", h2)

h3 = Hash("c", h1)

print(h1.get("b.a"))
# will print 1
print(h3.get("c.b.a"))
# will print 1

h3.setAttribute("c.b.a", "myAttribute", "Test")
print(h3.getAttribute("c.b.a", "myAttribute"))
#will print "Test"

Note that in the above examples copies of h2 and h1 are created upon insertion. The following call will thus fail, as h2 has not been set an attribute:

print(h2.getAttribute("a", "myAttribute"))

Note

While the above examples are Python code, having to access items of a dictionary-like container by key, instead of iterating over key-value pairs, seems unnecessary complex. In the middle-layer API a more pythonic solution is available using Hash.iteritems().

Note

In both Python APIs requesting a non-existing key from the Hash will return None.

Scalar Types

Karabo support the most common scalar data types:

Boolean type: BOOL
Character type (raw byte): CHAR
Signed integer types: INT8, INT16, INT32, INT64
Unsigned integer types: UINT8, UINT16, UINT32, UINT64
Floating point types: FLOAT, DOUBLE

Note

There is purposely no INT or LONG type in Karabo. Depending on the host and operating system these type can either be 32 bits or 64 bits long, leading to ambiguity. Instead use the INT32 type if you need a 32 bit integer and the INT64 type if you need a 64 bit integer. Out of similar reasons try to avoid using size_t for counters and rather use the explicit uint64_t, which is assured to of 64 bits length.

Complex Types

Complex types are available in Karabo. They are available for float and double scalar and vector types described in the previous section by prepending COMPLEX.

Complex scalar types: COMPLEX_FLOAT, COMPLEX_DOUBLE

In C++ the underlying type is std::complex<>, in Python the complex type is used. The following two examples are equivalent:

using namespace std::complex_literals;
std::complex<double> z1 = 1i * 1i;
std::cout<<z1.real<<" "<<z1.imag;
z1 = 1j*1j
print(z1.real, z1.imag)

Vector Types

Karabo supports vectors of all scalar and complex types as well as vectors of Hashes. Vector types are specified by prepending VECTOR_ to the scalar property name or to the Hash:

Boolean type: VECTOR_BOOL
Signed integer types: VECTOR_INT8, VECTOR_INT16, VECTOR_INT32, VECTOR_INT64
Unsigned integer types: VECTOR_UINT8, VECTOR_UINT16, VECTOR_UINT32, VECTOR_UINT64
Floating point types: VECTOR_FLOAT, VECTOR_DOUBLE
Complex vector types: VECTOR_COMPLEX_FLOAT, VECTOR_COMPLEX_DOUBLE
Hash type: VECTOR_HASH

NDArray Types

Multidimensional types are represented using the NDArray type and the associated NDARRAY_ELEMENT. The element itself is untyped. Rather it will always internally store data as a ByteArray alongside an attribute for type information.

For images the ImageData and IMAGEDATA_ELEMENT build on-top of the NDArray, adding additional standardized meta-data.

Both types derive from the Karabo Hash and thus can fully be serialized. You can set and retrieve objects of these types using the standard get and set interfaces.

Attributes

Attributes have already briefly been introduced. In Karabo they can be used to further specify the characteristics of a property. The can be set for any key in a Karabo Hash.

While attributes are freely assignable and may consist of all scalar, vector and complex types, Karabo comes with a set of standardized attributes, used for common tasks such as bound checking or defining the unit and order of magnitude of a value. These are exposed via a dedicated interface, in addition to being accessible via the setAttribute and getAttribute methods.

Numerical Representation

Karabo allows to set the numerical representation of a value in the GUI.

UINT8_ELEMENT(expected).key("binaryRep)
    .displayedName("As binary")
    .bin()
    .assignmentOptional().defaultValue(128)
    .commit()

#is displayed as 0b10000000

UINT8_ELEMENT(expected).key("hexRep)
    .displayedName("As hex")
    .hex()
    .assignmentOptional().defaultValue(128)
    .commit()

#is displayed as 0x80

UINT8_ELEMENT(expected).key("octalRep)
    .displayedName("As octal")
    .oct()
    .assignmentOptional().defaultValue(128)
    .commit()

#is displayed as 0o200

The following representations are available:

Binary mask bin()
Hexadecimal hex()
Octal oct()

Bounds & Alarm Conditions

Bounds may be set as inclusive or exclusive bounds indicating setting, warning and alarm bounds and ranges. Karabo allows for setting lower (minimum) and upper (maximum) bounds, and any set operation or property change using the GUI will check against these before updating the property value. Bounds are specified when defining a devices expected parameters:

UINT32_ELEMENT(expected).key("bounded")
    .displayedName("Has bounds")
    .minIncl(100).maxExcl(600)
    .alarmLow(200).needsAcknowledging(True)
    .alarmHigh(500).description("Foo").needsAcknowledging(True)
    .warnLow(300).needsAcknowledging(False)
    .warnHigh(400).needsAcknowledging(False)
.assignmentOptional().defaultValue(128)
.commit()

self.set("bounded", 30)  # raises exception, too low
self.set("bounded", 100)  # works, but shows alarm
self.set("bounded", 200)  # works, but shows alarm
self.set("bounded", 300)  # works, but shows warning
self.set("bounded", 350)  # just works
self.set("bounded", 400)  # works, but shows warning
self.set("bounded", 500)  # works, but shows alarm
self.set("bounded", 600)  # raises exception~

Additionally, alarm conditions may be set in the variance of a parameter, evaluated in a defined rolling window:

UINT32_ELEMENT(expected).key("bounded")
    .displayedName("Has bounds")
    .warnVarHigh(10).needsAcknowledging(True)
    .alarmVarLow(10).needsAcknowledging(True)
.assignmentOptional().defaultValue(128)
.commit()

Note

Alarm condition definitions need to always be closed of by stating if the alarm needs acknowledging on the alarm service to disappear.

Units

It is considered best practice to always assign a unit if a property represents a physical observable. Karabo provides for assigning SI (System International) and selected derived and historical units as property attributes. The following units are available:

Unit Symbol Karabo Used for
unitless NUMBER Values without a clearly defined unit
count COUNT Counters, iteration variable
meter m METER Length measurements, wavelength
gram g GRAM Weight measurements
second s SECOND Time measurement
ampère A AMPERE Electrical currents
kelvin K KELVIN Temperature measurements
mole mol MOLE Molecular amounts
candela cd CANDELA Luminous intensity
litre l LITRE Volume
hertz Hz HERTZ Frequency measurements
radian rad RADIAN Angular distances
degree ° DEGREE Angular distances
steradian sr STERADIAN Solid angles
newton N NEWTON Force
pascal Pa PASCAL Pressure
joule J JOULE Energy
electron volt eV ELECTRONVOLT Energy, \(1\,\text{eV} = 1.602176\times 10^{-19}\,\text{J}\)
watt W WATT Power
coulomb C COULOMB Charge
volt V VOLT Voltage
farad F FARAD Capacity
ohm Ω OHM Resistance
siemens S SIEMENS Electric conductance, admittance, susceptance
weber Wb WEBER Magnetic flux
tesla T TESLA Magnetic flux density
henry H HENRY Inductivity
degree celsius °C DEGREE_CELSIUS Temperature measurements
lumen lm LUMEN Luminous flux
lux lx LUX Luminous emittance
becquerel Bq BECQUEREL Radioactivity
gray Gy GRAY Ionizing dose
sievert Sv SIEVERT Effective dose
katal kat KATAL Catalytic activity (in enzymes)
minute min MINUTE Time measurement
hour h HOUR Time measurement
day d DAY Time measurement
year yr YEAR Time measurement
bar bar BAR Pressure measurement (consider using pascal)
pixel px PIXEL Image display
byte B BYTE Computer memory and storage
bit b BIT Computer memory and storage, architecture
meter per second m/s METER_PER_SECOND Velocity
volt per second V/s VOLT_PER_SECOND Voltage ramping
ampère per second A/s AMPERE_PER_SECOND Current ramping
percent % PERCENT Relative quantification

Note

While support for some historic, non-SI units is provided, please consider using SI units as much as possible.

Warning

While Karabo allows for specifying units it does not take these into account in any calculations: it is up to the programmer to make sure that algebra on different properties in compatible in terms of units and to determine the unit of the result!

Metric Prefixes

Frequently, it is favorable to not represent a value in SI-units, but with a multiplication factor in powers of 10 of that unit. This is called the metric prefix and commonly expressed by adding a prefix to the unit, e.g. 1 km, instead of 1000 m. In every day usage we do this to not have to deal with overly large or small numbers when comparisons or calculations are made with value which have the same order of magnitude. In terms of computer processing there is an additional benefit: the value range of integer values is limited, as is the precision of floating point numbers. By introducing a metric prefix attribute we can shift values back into a specified range, without sacrificing precision:

UINT8_ELEMENT(expected).key("prefixedValue")
    .displayedName("Prefixed value")
    .metricPrefix(MetricPrefix.MEGA)
    .assignmentOptional().defaultValue(128)
    .commit()

A 1B unsigned int value has a maximum value of 255. By assigning the prefix we can express that we actually mean :\(128\times10^{6}\). The following metric prefixes are available in Karabo:

prefix y z a f p n \(\mu\) m c d
factor \(10^{-24}\) \(10^{-21}\) \(10^{-18}\) \(10^{-15}\) \(10^{-12}\) \(10^{-9}\) \(10^{-6}\) \(10^{-3}\) \(10^{-2}\) \(10^{-1}\)
Karabo YOCTO ZEPTO ATTO FEMTO PICO NANO MICRO MILLI CENTI DECI
prefix da h k M G T P E Z Y
factor \(10^{1}\) \(10^{2}\) \(10^{3}\) \(10^{6}\) \(10^{9}\) \(10^{12}\) \(10^{15}\) \(10^{18}\) \(10^{21}\) \(10^{24}\)
Karabo DECA HECTO KILO MEGA GIGA TERA PETA EXA ZETTA YOTTA

No prefix does not need an explicit specification but can be specified as MetricPrefix.NONE. It corresponds to a multiplication by 1.

Note

While Karabo allows for specifying metric prefixes it does not take these into account in any calculations: whenever you retrieve a Karabo property it is converted to the programming language’s native type, which has no notion of prefixes! You can however use the getPrefixFactor() method to return a multiplicative factor depending on the assigned prefix.

a = self.get("prefixedValue")*self.getPrefixFactor("prefixedValue")
# a = 128e6 as given in the previous example

Advantages of Using Units, Metric Prefixes

Adding units, metric prefixes and unit scales to values may seem like a nuisance at first. It has two major benefits though:

  • Persons not intimately familiar with a device can get a better understanding of its properties in a much shorter time, ambiguity of a properties meaning is avoided and proper understanding of critical values enforced.
  • Karabo can (in the future) offer you a much richer plotting experience. Karabo plots allow you to drag multiple properties into the same plot to display them against each other. By using units and metric prefixes Karabo can decide which values can share the same y-axis, and add new axes if data in a new unit is dragged onto the plot.

Timestamps

Karabo’s properties have timestamps, which are either passed up from hardware interfaced to the control system or set to the current time upon property assignment. A central timing service assures synchronization across the distributed system. Alternatively, developers may set an arbitrary timestamp upon assignment as an optional parameter in set commands:

now = self.getActualTimestamp()
timeNow = Epochstamp() # this is only a time
train = 12 # we also need a train id
now2 = Timestamp(timeNow, train) # a timestamp consists of a time and train id
self.set("a", 1, now)

You can convert Karabo’s internal timestamps to other representations using the following functions:

toIso8601(precision = MICROSEC, extended = False)

Generates a sting (respecting ISO-8601) for object time for INTERNAL usage (“%Y%m%dT%H%M%S%f” => “20121225T132536.789333[123456789123]”)

precision - Indicates the precision of the fractional seconds (e.g. MILLISEC, MICROSEC, NANOSEC, PICOSEC, FEMTOSEC, ATTOSEC)

extended - “true” returns ISO8601 extended string; “false” returns ISO8601 compact string

toIso8601Ext(precision = MICROSEC, extended = False)

Generates a string (respecting ISO-8601) for object time for EXTERNAL usage (“%Y%m%dT%H%M%S%f%z” => “20121225T132536.789333[123456789123]Z”)

precision - Indicates the precision of the fractional seconds (e.g. MILLISEC, MICROSEC, NANOSEC, PICOSEC, FEMTOSEC, ATTOSEC)

extended - “true” returns ISO8601 extended string; “false” returns ISO8601 compact string

toFormattedString(format = "%Y-%b-%d %H:%M:%S", localTimeZone = "Z")

Formats to specified format time stored in the object

format the format of the time point (visit strftime for more info).

localTimeZone - String that represents an ISO8601 time zone.

getSeconds()

Returns the seconds of the unix epoch for this timestamp

Timestamps are given by seconds of the UNIX epoch alongside fractional seconds used to provide additional accuracy for resolving the XFEL pulse-structure in the femtosecond range.

getFractionalSeconds()

Returns the fractional seconds of this timestamp

getTrainId()

Returns the train id for this timestamp

The Karabo Schema

Karabo stores a static description of a device as part of the device’s schema. The schema contains information on the expected parameters of the device, including property types and default values. Underneath, the schema uses the same technology as the Karabo Hash to construct a hierarchical, ordered key- value representation. It is serializable to XML. Currently, Karabo does not support schema evolution.

The TABLE_ELEMENT

The TABLE_ELEMENT internally is a VECTOR_HASH property which has a rowSchema attribute defining the cells a row consists of. As this is the same for all rows, the schema defines the columns of the table. Columns may be of any Karabo data type, although the GUI will only render scalar types and fail gracefully for others. A TABLE_ELEMENT is defined as follows:

tableSchema = Schema()
(
    UINT32_ELEMENT(tableSchema).key("col1)
       .displayedName("Column One")
       .assignmentOptional().noDefaultValue()
       .commit()
       ,
        STRING_ELEMENT(tableSchema).key("a)
       .displayedName("A")
       .assignmentOptional().defaultValue("Hello World!")
       .commit()
       ,
    FLOAT_ELEMENT(tableSchema).key("b)
       .displayedName("Float Val")
       .assignmentMandatory()
       .commit()
)

tableDefault = [Hash("col1", 1, "b", 2.0)]

TABLE_ELEMENT(expected).key("table")
    .displayedName("A Table Element")
    .setRowSchema(tableSchema)
    .assignmentOptional().defaultValue(tableDefault)
    .commit()

This will render to

Column 1 A Float Val
1 Hello World! 2.0

in the GUI. Entries of the element are validated according to the validation rules specified in the property definition: col1 may stay undefined and will not if initialized to a default value in this case, a is initialized to “Hello World!” if it is undefined, and b needs to be defined, otherwise an exception is thrown.

Default values are passed to the element as a vector/list of Hashes, where each Hash validates against the row schema.