Karabo middlelayer API

The Karabo middlelayer API is written in pure Python deriving its name from the main design aspect: controlling and monitoring driver devices while providing state aggregation and additional functionality. The same interface is also used for the macros. Hence, this api exposes an efficient interface for tasks such as interacting with other devices via device proxies to enable monitoring of properties, executing commands and waiting on their completion, either synchronously or asynchronously.

Start simple: Hello World Device!

Below is the source code of a Hello World! device:

from karabo.middlelayer import Device, Slot, String


class HelloWorld(Device):

    __version__ = "2.0"

    greeting = String(
        defaultValue="Hello World!",
        description="Message printed to console.")

    @Slot()
    async def hello(self):
        print(self.greeting)

    async def onInitialization(self):
        """ This method will be called when the device starts.

            Define your actions to be executed after instantiation.
        """

The middlelayer device is created by inheriting from the middlelayer’s Device base class. Below the device class definition are the expected parameters, containing the static schema of the device. A property __version__ can indicate the lowest Karabo version in which this device is supposed to run. In this device we create a Karabo property greeting which is also referred to as KaraboValue. This property has an assigned descriptor String containing the attributes. In this example the defaultValue and the description attributes are defined, which is rendered in the karabo GUI as a text box showing “Hello World!”.

Additionally, we create a single Slot hello by using a decorator. This slot will be rendered in the karabo GUI as a button enabling us to print the greeting string to console.

Properties: Karabo Descriptors

As shown by the example, every device has a schema, which contains all the details about the expected parameters, its types, comments, units, everything. In the middlelayer this schema is built by so-called karabo descriptors. A schema is only broadcasted rarely over the network, typically only during the initial handshake with the device. Once the schema is known, only configurations, or even only parts of configurations, are sent over the network in a tree structure called Hash (which is not a hash table).

These configurations know nothing anymore about the meaning of the values they contain, yet they are very strictly typed: even different bit sizes of integers are conserved.

Descriptors describe the content of a device property. As shown in the Hello World example, The description is done in their attributes, which come from a fixed defined set.

It may be useful to note that instances of this class do not contain any data, instead they are describing which values a device property may take and they are given as keyword arguments upon initialization.

../_images/descriptors.png

Attributes

Attributes of properties may be accessed during runtime as members of the property descriptor. Depending on the type of the property, some attributes might not be accessible.

The common descriptor attributes are:

Common Attribute

Example

displayType

e.g. oct, bin, dec, hex, directory

unitSymbol

e.g. Unit.METER

metricPrefixSymbol

e.g. MetricPrefix.MILLI

accessMode

e.g. AccessMode.READONLY

assignment

e.g. Assignment.OPTIONAL

defaultValue

the default value or None

requiredAccessLevel

e.g. AccessLevel.EXPERT

allowedStates

the list of allowed states

tags

a list of strings as property tags

alias

a string to be used as alias

daqPolicy

e.g. DaqPolicy.SAVE

The min and maxSize attributes are only available for vectors if they have been set before:

Vector Attribute

Example

minSize

the minimum size of vector

maxSize

the maximum size of vector

Attributes that are available for simple types only (Double, Int32, etc.):

Simple Attribute

Example

minInc

the inclusive-minimum value

minExc

the exclusive-minimum value

maxInc

the inclusive-maximum value

maxExc

the exclusive-maximum value

warnLow

warn threshold low

warnHigh

warn threshold high

alarmLow

alarm threshold low

alarmHigh

alarm threshold high

Handling timestamps

When a user operates on a KaraboValue, the timestamp of the result is the newest timestamp of all timestamps that take part in the operation, unless the user explicitly sets a different one. This is in line with the validity intervals described above: if a value is composed from other values, it is valid typically starting from the moment that the last value has become valid (this assumes that all values are still valid at composition time, but this is the responsibility of the user, and is typically already the case).

All properties in Karabo may have timestamps attached. In the middlelayer API they can be accessed from the timestamp attribute:

self.speed.timestamp

They are automatically attached and set to the current time upon assignment of a value that does not have a timestamp:

self.steps = 5  # current time as timestamp attached

A different timestamp may be attached using karabo.middlelayer.Timestamp`:

self.steps.timestamp = Timestamp("2009-09-01 12:34 UTC")

If a value already has a timestamp, it is conserved, even through calculations. If several timestamps are used in a calculation, the newest timestamp is used. In the following code, self.speed gets the timestamp of either self.distance or self.times, whichever is newer:

self.speed = 5 * self.distance / self.times[3]

Due to this behaviour, using in-place operators, such as += is discouraged, as the timestamp would be conserved:

self.speed = 5  # A new timestamp is attached

self.speed += 5  # The timestamp is kept

The above effectively is:

self.speed = self.speed + 5

And whilst the value is 10, we used the newest timestamp available from either component, here the previous one from self.speed, and the timestamp never gets incremented! In order to create a new timestamp, the raw value needs to be accessed:

self.speed = self.speed.value + 5

Since the value and 5 are both integers, no timestamp is available, and a new one is created.

Warning

Developers should be aware that automated timestamp handling defaults to the newest timestamp, i.e. the time at which the last assignment operation on a variable in a calculation occured. Additionally, these timestamps are not synchronized with XFEL’s timing system, but with the host’s local clock.

When dealing with several inputs, a function can use the karabo.middlelayer.removeQuantity() decorator, to ease the readability:

from karabo.middlelayer import removeQuantity

steps = Int32()
speed = Int32()
increment = Int32()

@removeQuantity
def _increment_all_parameters(self, steps, speed, increment):
    self.steps = steps + increment
    self.speed = speed + increment

@Slot()
async def incrementAllParameters(self):
    self._increment_all_params(self.steps, self.speed, self.increment)

Karabo Slots

karabo.middlelayer.Slot is the way to mark coroutines as actionable from the ecosystem, whether from the GUI, or other Middlelayer devices:

from karabo.middlelayer import Slot

 @Slot(displayedName="Start",
       description="Prints an integer",
       allowedStates={State.OFF})
 async def start(self):
     self.state = State.ON

     i = 0
     while True:
         await sleep(1)
         print(i)
         i = i + 1

A golden rule in a Middlelayer device is that Slot are coroutines The slot exits with the last state update and returns once the code is run through.

Slot has a number of arguments that are explained in Karabo Attributes

Holding a Slot (Return with correct state)

In certain cases, it may be useful to keep a slot, to prevent a user to interfere with current operation, for example. Since slots are asynchronous, some trickery is required. For simplicity, below is an example assuming we read out the motor states in a different task.

async def state_monitor():
    # A simple state monitor
    while True:
        await waitUntilNew(self.motor1.state, self.motor2.state)
        state = StateSignifier().returnMostSignificant(
            [self.motor1.state, self.motor2.state])
        if state != self.state:
            self.state = state

@Slot(displayedName="Move",
      description="Move a motor",
      allowedStates={State.ON})
async def move(self):

    # We move to motors at once
    await gather(self.motor1.move(), self.motor2.move())
    # We wait for our own state change here to exit this slot with
    # the expected state, e.g. ERROR or MOVING.
    await waitUntil(lambda: self.state != State.ON)

Karabo Attributes

This section describes how attributes can be used in Properties and Slots.

Slots have two important attributes that regulate their access using states and access modes.

Required Access Level

The requiredAccessLevel attribute allows to set at which access level this property may be reconfigured or Slot may be executed. The minimum requiredAccessLevel for a reconfigurable property or Slot is at least USER (level 1) if not explicitly specified.

Furthermore, this feature can be used to hide features from lower level users and some user interfaces might hide information depending on the access level.

User levels are defined in karabo.middlelayer.AccessLevel, and range from OBSERVER (level 0) to ADMIN (level 4). Consequently, a user with lower level access, such as OPERATOR (level 2), will have access to less information than EXPERT (level 3).

First, import AccessLevel:

from karabo.middlelayer import AccessLevel

In the following example, we create a Slot and a property for a voltage controller whose access is limited to the expert level, such that operators or users cannot modify the device. The definition of such a slot is then as follows:

targetVoltage = Double(
    defaultValue=20.0
    requiredAccessLevel=AccessLevel.EXPERT)

@Slot(displayedName="Ramp Voltage up",
      requiredAccessLevel=AccessLevel.EXPERT)
async def rampUp(self):
    self.status = "Ramping up voltage"

    ... do something

Note

The default requiredAccesslevel is AccessLevel.OBSERVER (level 0).

Allowed States

The middlelayer API of Karabo uses a simple state machine to protect slot execution and property reconfiguration. Therefore, it is possible to restrict slot calls to specific states using the allowedStates attribute in the Slot definition.

States are provided and defined in the Karabo Framework in karabo.middlelayer.State

In the example below, the voltage of the controller can only be ramped up if the device is in the state: State.ON. In the Slot rampUp we also switch the device state to State.RUNNING, since a ramp up action will be running after the first call. With this protection, the procedure of ramping up the device can only be executed again after it has finished.

targetVoltage = Double(
    defaultValue=20.0
    requiredAccessLevel=AccessLevel.EXPERT,
    allowedStates={State.ON})

@Slot(displayedName="Ramp Voltage up",
      requiredAccessLevel=AccessLevel.EXPERT
      allowedStates={State.ON})
async def rampUp(self):
    self.status = "Ramping up voltage"

    self.state = State.RUNNING
    ... do something

It is possible to define an arbitrary quantity of states:

allowedStates={State.ON, State.OFF}

Note

By default every property and Slot may be reconfigured or executed for all device states.

AccessMode

The accessMode attribute allows to set if a property in a device is a READONLY, RECONFIGURABLE or INITONLY.

Init only properties can only be modified during before instantiation of the device.

First, import AccessMode:

from karabo.middlelayer import AccessMode

Based on the previous example, we add a read only property for the current voltage of our voltage controller:

currentVoltage = Double(
    accessMode=AccessMode.READONLY,
    requiredAccessLevel=AccessLevel.OPERATOR)

targetVoltage = Double(
    defaultValue=20.0
    requiredAccessLevel=AccessLevel.EXPERT)

Note

The default accessMode is AccessMode.RECONFIGURABLE. The read only setting of a property has to be provided explicitly.

Assignment

The assignment attribute declares the behavior of the property on instantiation of the device. Its function is coupled to the accessMode. It can be either OPTIONAL, MANDATORY or INTERNAL.

Init only properties can only be modified during before instantiation of the device.

These assignments are very import in the configuration management.

  • INTERNAL assigned properties are always erased from configuration and indicate that they are provided from the device internals on startup. This is made visible to the operator, they cannot be edited for example in the graphical user interface.

  • MANDATORY assigned properties must be provided on instantiation. They are typically left blank, and the operator must provide a value (e.g. host, ip for a camera).

from karabo.middlelayer import AccessMode, Assignment, Double, String

# assignment have no effect
currentVoltage = Double(
    accessMode=AccessMode.READONLY,
    requiredAccessLevel=AccessLevel.OPERATOR)

# default assignment is OPTIONAL
targetVoltage = Double(
    defaultValue=20.0
    requiredAccessLevel=AccessLevel.EXPERT)

# default accessMode is RECONFIGURABLE
# on instantiation, this property is MANDATORY and must be provided
host = String(
    assignment = Assignment.MANDATORY,
    requiredAccessLevel=AccessLevel.EXPERT)

# default accessMode is RECONFIGURABLE
# on instantiation, this property is INTERNAL. In this case it is read
# from hardware, but it can be reconfigured on the online device
targetCurrent = Double(
    assignment = Assignment.INTERNAL,
    requiredAccessLevel=AccessLevel.ADMIN)

Note

The default assignment is Assignment.OPTIONAL.

DAQ Policy

Not every parameter of a device is interesting to record. As a workaround for a missing DAQ feature, the policy for each individual property can be set, on a per-class basis.

These are specified using the karabo.middlelayer.DaqPolicy enum:

  • OMIT: will not record the property to file;

  • SAVE: will record the property to file;

  • UNSPECIFIED: will adopt the global default DAQ policy. Currently, it is set to record, although this will eventually change to not recorded.

Legacy devices which do not specify a policy will have an UNSPECIFIED policy set to all their properties.

Note

This are applied to leaf properties. Nodes do not have DaqPolicy.

from karabo.middlelayer import DaqPolicy

 currentVoltage = Double(
     accessMode=AccessMode.READONLY,
     requiredAccessLevel=AccessLevel.OPERATOR,
     daqPolicy=DaqPolicy.SAVE)

Handling Units

You can define a unit for a property, which is then used in the calculations of this property. In the Middlelayer API units are implemented using the pint module.

A unit is declared using the unitSymbol and further extended with the metricPrefixSymbol attribute:

distance = Double(
    unitSymbol=Unit.METER,
    metricPrefixSymbol=MetricPrefix.MICRO)
times = VectorDouble(
    unitSymbol=Unit.SECOND,
    metricPrefixSymbol=MetricPrefix.MILLI)
speed = Double(
    unitSymbol=Unit.METER_PER_SECOND)
steps = Double()

Once declared, all calculations have correct units:

self.speed = self.distance / self.times[3]

In this code units are converted automatically. An error is raised if the units don’t match up:

self.speed = self.distance + self.times[2]  # Ooops! raises error

If you need to add a unit to a value which doesn’t have one, or remove it, there is the unit object which has all relevant units as its attribute:

self.speed = self.steps * (unit.meters / unit.seconds)
self.steps = self.distance / (3.5 * unit.meters)

Warning

While the Middlelayer API of Karabo in principle allows for automatic unit conversion, developers are strongly discouraged to use this feature for critical applications: the Karabo team simply cannot guarantee that pint unit handling is preserved in all scenarios, e.g. that a unit is not silently dropped.

Device States

Every device has a state, one of these defined in karabo.middlelayer.State. They are used to show what the device is currently doing, what it can do, and which actions are not allowed.

For instance, it can be disallowed to call the start slot if the device is in State.STARTED or State.ERROR. Such control can be applied to both slot calls and properties.

The states and their hierarchy are documented in the Framework.

Within the Middlelayer API, the State is an enumerable represented as string, with a few specific requirements, as defined in karabo.middlelayer_api.device.Device

Although not mandatory, a device can specify which states are used in the options attribute:

from karabo.middlelayer import Overwrite, State

state = Overwrite(
     defaultValue=State.STOPPED,
     displayedName="State",
     options={State.STOPPED, State.STARTED, State.ERROR})

If this is not explicitly implemented, all states are possible.

State Aggregation

If you have several proxies, you can aggregate them together and have a global state matching the most significant. In the example, this is called trumpState and makes use of karabo.middlelayer.StateSignifier().

from karabo.middlelayer import background, StateSignifier

async def onInitialization(self):
    self.trumpState = StateSignifier()
    monitor_task = background(self.monitor_states())

async def monitor_states(self):
    while True:
        # Here self.devices is a list of proxies
        state_list = [dev.state for dev in self.devices]
        self.state = self.trumpState.returnMostSignificant(state_list)
        await waitUntilNew(*state_list)

As well as getting the most significant state, it will attach the newest timestamp to the returned state.

It is also possible to define your own rules, as documented in karabo.common.states.StateSignifier

The following shows how to represent and query a remote device’s state and integrate it in a device:

from karabo.middlelayer import (
    AccessMode, Assignment, background, connectDevice, State, String,
    waitUntilNew)

remoteState = String(
    displayedName="State",
    enum=State,
    displayType="State",  # This type enables color coding in the GUI
    description="The current state the device is in",
    accessMode=AccessMode.READONLY,
    assignment=Assignment.OPTIONAL,
    defaultValue=State.UNKNOWN)

async def onInitialization(self):
    self.remote_device = await connectDevice("some_device")
    self.watch_task = background(self.watchdog())

async def watchdog(self):
   while True:
       await waitUntilNew(self.remote_device)
       state = self.remote_device.state
       self.remoteState != state:
         self.remoteState = state:

Tags and Aliases

It is possible to assign a property with tags and aliases.

  • Tags can be multiple per property and can therefore be used to group properties

together. - Aliases are unique and for instance used to map hardware commands to Karabo property names.

These are typically used both together without the need for keeping several lists of parameters and modes.

To begin, mark the properties as desired, here are properties that are polled in a loop, and properties that are read once, at startup, for instance:

from karabo middlelayer import AccessMode, Bool, String

isAtTarget = Bool(displayedName="At Target",
                  description="The hardware is on target position",
                  accessMode=AccessMode.READONLY,
                  alias="SEND TARGET",  # The hardware command
                  tags={"once", "poll"})  # The conditions under which to query

hwStatus = String(displayedName="HW status",
                  description="status, as provided by the hardware",
                  accessMode=AccessMode.READONLY,
                  alias="SEND STATUS",  # The hardware command
                  tags={"poll"})  # The conditions under which to query

hwVersion = String(displayedName="HW Version",
                   description="status, as provided by the hardware",
                   accessMode=AccessMode.READONLY,
                   alias="SEND VERSION",  # The hardware command
                   tags={"once"})  # The conditions under which to query

Tags of a property can be multiple, and are contained within a set.

Once this is defined, karabo.middlelayer.Schema.filterByTags() will return a hash with the keys of all properties having a specific tag:

async def onInitialization(self):
    schema = self.getDeviceSchema()

    # Get all properties that are to be queried once
    onces = schema.filterByTags("once")
    # This returns a Hash which is the subset of the current configuration,
    # with the property names that have 'once' as one of their tags.

    # Get the hardware commands, aliases, for each of the properties
    tasks = {prop: self.query_device(schema.getAliasAsString(prop)) for prop in once.keys()}

    # Query
    results = await gather(tasks)

    # Set the result
    for prop, value in results.items():
        setattr(self, prop, value)

whilst a background task can poll the other parameters in a loop:

from karabo.middlelayer import background, gather

async def onStart(self):
    schema = self.getDeviceSchema()
    # Get all properties that are to be polled
    to_poll = schema.filterByTags("poll")

    # Create a background loop
    self.poll_task = background(self.poll(to_poll))


 async def poll(self, to_poll):
     while True:
         # Get the hardware commands for each of the properties
         tasks = {prop: self.query_device(schema.getAliasAsString(prop)) for prop in to_poll.keys()}

         # Query
         results = await gather(tasks)

         # Set the result
         for prop, value in results.items():
             setattr(self, prop, value)

Note

The concepts of background and gather are explained later in chapter 2

  • OphirPowerMeter is a device interfacing with a meter over tcp making use of tags and aliases

Nodes

Nodes allow a device’s properties to be organized in a hierarchical tree-like structure: Devices have properties - node properties - which themselves have properties.

If a device has a node property with key x and the node has a property of type double with key y, then the device will have a property of type double with key x.y.

Defining a Node’s Properties

To create a device with node properties, first create a class which inherits from Configurable with the desired properties for the node. These are created as you would for properties of a device, at class scope, and understand the same attribute arguments.

For example, the following class is used to create a node for a (linear) motor axis with units in mm and actual and target position properties:

class LinearAxis(Configurable):
    actualPosition = Double(
        displayedName="Actual Position",
        description="The actual position of the axis.",
        unitSymbol=Unit.METER,
        metricPrefixSymbol=MetricPrefix.MILLI,
        accessMode=AccessMode.READONLY,
        absoluteError=0.01)

    targetPosition = Double(
        displayedName="Target Position",
        description="Position argument for move.",
        unitSymbol=Unit.METER,
        metricPrefixSymbol=MetricPrefix.MILLI,
        absoluteError=0.01)

Adding Node Properties to a Device

Nodes are added to a device in the same way as other properties, at class scope, using the Node class and understand the same attribute arguments as other properties where these make sense.

So the following creates a device with two node properties for two motor axes, using the LinearAxis class above:

class MultiAxisController(Device):
    axis1 = Node(
        LinearAxis,
        displayedName="Axis 1",
        description="The first motor axis.")

    axis2 = Node(
        LinearAxis,
        displayedName="Axis 2",
        description="The second motor axis.")

The resulting device will have, for example, a node property with key axis1 and a double property with key axis2.targetPosition.

Node: Required Access Level

To be able to access a property, a user must have access rights equal to or above the required level for the property, specified by the requiredAccessLevel descriptor. For properties belonging to nodes, the user must have the access rights for the property and all parent nodes above it in the tree structure.

Error Handling

Errors happen and When they happen in Python typically an exception is raised. The best way to do error handling is to use the usual Python try-except-finally statements.

There are two types of errors to take care of in Middlelayer API: CancelledError and TimeoutError

from asyncio import CancelledError, TimeoutError, wait_for
from karabo.middlelayer import connectDevice, Slot

@Slot()
async def doSomething(self):
    try:
        # start something here, e.g. move some motor
    except CancelledError:
        # handle error
    finally:
        # something which should always be done, e.g. move the motor
        # back to its original position

@Slot()
async def doOneMoreThing(self):
    try:
        await wait_for(connectDevice("some_device"), timeout=2)
    except TimeoutError:
        # notify we received a timeout error
    finally:
        # reconnect to the device

Note

Both CancelledError and TimeoutError are imported from asyncio.

Sometimes, however, an exception may be raised unexpectedly and has no ways of being handled better. onException() is a mechanism that can be overwritten for this usage:

async def onException(self, slot, exception, traceback):
    """If an exception happens in the device, and not handled elsewhere,
    it can be caught here.
    """
    self.logger.warn(f"An exception occured in {slot.method.__name__} because {exception}")
    await self.abort_action()

It is also possible that a user or Middlelayer device will cancel a slot call:

async def onCancelled(self, slot):
    """To be called if a user cancels a slot call"""
    tasks = [dev.stop() for dev in self.devices()]
    await allCompleted(*tasks)

Don’t use try … except Exception pattern

In the middlelayer API so-called tasks are created. Whenever a device is shutdown, all active tasks belonging to this device are cancelled. Tasks might be created by the device developer or are still active Slots. If a task is cancelled, an CancelledError is thrown and by using a try … except Exception pattern, the exception and underlying action will always be fired. In the bottom case, we want to log an error message. Since the device is already shutting down, the task created by the log message will never be retrieved nor cancelled leaving a remnant on the device server. Subsequently, the server cannot shutdown gracefully.

This changed on Python 3.8 where CancelledError is inherits from BaseException.

from asyncio import CancelledError, TimeoutError, wait_for
from karabo.middlelayer import connectDevice, Slot

async def dontDoThisTask(self):
    while True:
        try:
            # Some action here
        except Exception:
            self.logger.error("I got cancelled but I cannot log")
            # This will always be fired

Warning

Always catch a CancelledError explicitly when using a try … except Exception pattern!

Code Style

While PEP8, PEP20, and PEP257 are stylings to follow, there are a few Karabo-specific details to take care of to improve code quality, yet keep consistency, for better maintainability across Karabo’s 3 APIs.

An example of a major exception from PEP8 is that underscores should never be used for public device properties or slots.

Imports

Imports follow isort style: they are first in resolution order (built-ins, external libraries, Karabo, project), then in import style (from imports, imports), then in alphabetical order:

import sys
from asyncio import wait_for

import numpy as np

from karabo.middlelayer import (
    connectDevice, Device, Slot, String)

from .scenes import control, default

isort can automatically fix imports by calling it with the filename:

$ isort Keithley6514.py
Fixing /data/danilevc/Keithley6514/src/Keithley6514/Keithley6514.py

Class Definitions

Classes should be CamelCased:

class MyDevice(Device):
    pass

Abbreviations in class names should be capitalised:

class JJAttenutator(Device):
    pass

class SA3MirrorsWitch(Device):
    pass

Class Properties

Properties part of the device’s schema, which are exposed, should be camelCased, whereas non-exposed variables should have_underscores:

name = String(displayedName="Name")

someOtherString = String(
    displayedName="Other string",
    defaultValue="Hello",
    accessMode=AccessMode.READONLY)

valid_ids = ["44eab", "ff64d"]

Slots and Methods

Slots are camelCased, methods have_underscores. Slots must not take arguments, apart from self.

@Slot(displayedName='Execute')
async def execute(self):
    """This slot is exposed to the system"""
    self.state = State.ACTIVE
    await self.execute_action()

@Slot(displayedName='Abort',
      allowedStates={State.ACTIVE, State.ERROR})
async def abortNow(self):
    self.state = state.STOPPING
    await self.abort_action()

async def execute_action(self):
    """This is not exposed, and therefore PEP8"""
    pass

Mutable objects must not be used as default values in method definitions.

Printing and Logging

Logging is the way to share information to developers and maintainers. This allows for your messages to be stored to files for analysis at a later time, as well as being shared with the GUI under certain conditions.

The Middlelayer API has its own Logger implemented as a Configurable. It is part of the Device class and no imports are required.

Whilst it can be used either as self.log or self.logger, the preferred style is as follows:

from karabo.middlelayer import allCompleted

async def stop_all(self):
    self.logger.info("Stopping all devices")
    tasks = [device.stop() for device in self.devices]
    done, pending, failed = await allCompleted(*tasks)
    if failed:
        self.logger.error("Some devices could not be stopped!")

Note

Logging is disabled in the constructor __init__().

Inplace Operators

Inplace operations on Karabo types are discouraged for reasons documented in Handling timestamps.

Don’t do:

speed = Int32(defaultValue=0)

@Slot()
async def speedUp(self):
    self.speed += 5

But rather:

speed = Int32(defaultValue=0)

@Slot()
async def speedUp(self):
    self.speed = self.speed.value + 5

Exceptions

It is preferred to check for conditions to be correct rather than using exceptions. This defensive approach is to ensure that no device would be stuck or affect other devices running on the same server.

Therefore, the following is discouraged:

async def execute_action(self):
    try:
        await self.px.move()
    except:
        pass

But rather:

async def execute_action(self):
    if self.px.state not in {State.ERROR, State.MOVING}:
        await self.px.move()
    else:
        pass

If exceptions are a must, then follow the Error Handling

Use Double and NOT Float

The middlelayer API supports both Double and Float properties.

However, behind the scenes a Float value is casted as numpy’s float32 type. Casting this value back to float64 may lead to different value. Hence, services on top of the framework might cast this value to a string before casting to the built-in python float of 64 bit to prevent cast errors. Note, that a 32 bit float has a precision of 6, which might be of different expectation for a normal python developer.

Use `karabo.middlelayer.Double` instead of `Float`.