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.
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. |
metricPrefixSymbol |
e.g. |
accessMode |
e.g. |
assignment |
e.g. |
defaultValue |
the default value or None |
requiredAccessLevel |
e.g. |
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. |
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`.