Bread’N’Butter Features¶
The following section describes the Bread’N’Butter features of the middlelayer api.
Configurables¶
Devices can have Nodes with properties. Node
structures are, as devices, created with Configurable
classes.
It might be necessary to access the top-level device within a Node and this can be straightforwardly done with Configurable.get_root() as shown below.
from karabo.middlelayer import (
Configurable, Device, Double, MetricPrefix, Node, Slot, State, Unit,
background)
class FilterNode(Configurable):
filterPosition = Double(
displayedName="Filter Position",
unitSymbol=Unit.METER,
metricPrefixSymbol=MetricPrefix.MILLI,
absoluteError=0.01)
@Slot(allowedStates=[State.ON])
async def move(self):
# access main device via get_root to notify a state change
root = self.get_root()
root.state = State.MOVING
# Move the filter position in a background task
background(self._move_filter())
async def _move_filter(self):
try:
pass
# do something
finally:
root = self.get_root()
root.state = State.ON
class GlobalDevice(Device):
node = Node(FilterNode, displayedName="Filter")
Furthermore, it is possible to do a bulkset of a Hash container on a Configurable and only consider the changes. Note that, if a single value cannot be validated, the whole Hash is not set. In all cases the timestamp information is preserved, either from the value of the Hash element or an eventual KaraboValue that is coming from a Proxy, e.g. via connectDevice.
from karabo.middlelayer import (
Configurable, Device, Double, Hash, Int32, MetricPrefix, Node,
QuantityValue, Slot, State, Unit, minutesAgo)
class FilterNode(Configurable):
filterPosition = Double(
displayedName="Filter Position",
unitSymbol=Unit.METER,
metricPrefixSymbol=MetricPrefix.MILLI,
absoluteError=0.01)
class MotorDevice(Device):
channel = Int32(
defaultValue=0)
targetPosition = Double(
defaultValue=0.0)
velocity = Double(
defaultValue=0.0,
minInc=0.0,
maxInc=10)
node = Node(FilterNode, displayedName="Filter")
def updateFromExternal(self):
# get external data, everytime the timestamp is considered, either
# via Hash attributes or `KaraboValue`
h = Hash("targetPosition", 4.2,
"velocity", QuantityValue(4.2, timestamp=minutesAgo(2)),
"node.filterPosition", 2)
# 1. All values will be applied only if they changed
self.set(h, only_changes=True)
# 2. All values will be applied
self.set(h)
h = Hash("targetPosition", 4.2,
"velocity", QuantityValue(100.2, timestamp=minutesAgo(2)),
"node.filterPosition", 12)
# NO VALUE will be applied as velocity is outside limits!
self.set(h, only_changes=True)
Util functions¶
Remove Quantity Values from core functions¶
A good practice is to work with fixed expectations about units and timestamps and strip them away in critical math operations, especially when relying on the external libray numpy. Decorate a function to remove KaraboValue input with removeQuantity.
This function works as well with async declarations and can be used as
@removeQuantity
def calculate(x, y):
assert not isinstance(x, KaraboValue)
assert not isinstance(y, KaraboValue)
return x, y
@removeQuantity
def calculate(boolean_x, boolean_x):
assert not isinstance(x, KaraboValue)
assert not isinstance(y, KaraboValue)
# Identity comparison possible!
return x is y
Note
This decorator does not cast to base units! Retrieving the magnitude of the KaraboValue allows for identity comparison.
Maximum and minimum¶
Typically, values in devices and proxies are a KaraboValue which comes with a timestamp and a unit. However, not every simple mathmatical operation with KaraboValues is supported by common packages. In order to take the maximum and minimum of an iterable please have a look:
from karabo.middlelayer import maximum, minimum, QuantityValue, minutesAgo
t1 = minutesAgo(1)
t2 = minutesAgo(10)
a = QuantityValue(3, "m", timestamp=t1)
b = QuantityValue(1000, "mm", timestamp=t2)
m = maximum([a, b])
assert m == 3 * unit.meter
m = minimum([a, b])
assert m == 1000 * millimeter
# Timestamp is the newest one -> 1
assert m.timestamp == t1
Get and Set Property¶
Sometimes in scripting it is very convenient to get properties from devices
or proxies in a getattr
fashion, especially with noded structures.
Use get_property as equivalent to python’s builtin getattr
. Similarly,
the set_property function is the equivalent to pythons setattr
.
from karabo.middlelayer import get_property, set_property
prop = get_property(proxy, "node.subnode.property")
# This is equivalent to accessing
prop = proxy.node.subnode.property
# Set a value
set_property(proxy, "node.subnode.property", 5)
proxy.node.subnode.property = 5
Alarms in Devices¶
Each device is equipped with a globalAlarmCondition
property which can
set if an alarm should be highlighted. All alarm levels are provided by
an enum of AlarmCondition
according to the severity of the alarm level.
In the middlelayer API the globalAlarmCondition
does NOT require an
acknowledging of the alarm setting.
There are different ways of alarm monitoring depending on the environment. The device developer can write an own alarm monitor as shown below observing the difference of temperatures:
from karabo.middlelayer import (
AlarmCondition, background, connectDevice, Device, waitUntilNew)
class AlarmDevice(Device):
async def onInitialization(self):
self.temp_upstream = await connectDevice("REMOTE_UPSTREAM")
self.temp_downstream = await connectDevice("REMOTE_DOWNSTREAM")
background(self.monitor())
async def monitor(self):
while True:
await waitUntilNew(self.temp_upstream.value,
self.temp_downstream.value)
diff = abs(self.temp_upstream.value - self.temp_downstream.value)
if diff > 5:
level = AlarmCondition.WARN
elif diff > 10:
level = AlarmCondition.ALARM
else:
level = AlarmCondition.NONE
# Only set the new value if there is a difference!
if level != self.globalAlarmCondition:
self.globalAlarmCondition = level
Note
The default value of the globalAlarmCondition
property is AlarmCondition.NONE
.
Other simple settings are AlarmCondition.WARN
, AlarmCondition.ALARM
and
AlarmCondition.INTERLOCK
.
The alarm monitoring can also be automatically configured within a property with different steps and information.
from karabo.middlelayer import (
AlarmCondition, background, connectDevice, Device, Double, waitUntilNew)
class AlarmDevice(Device):
temperatureDiff = Double(
displayedName="Temperature Difference",
accessMode=AccessMode.READONLY,
defaultValue=0.0,
warnLow=-5.0,
alarmInfo_warnLow="A temperature warnLow",
alarmNeedsAck_warnLow=True,
warnHigh=5.0,
alarmInfo_warnHigh="A temperature warnHigh",
alarmNeedsAck_warnHigh=True,
alarmLow=-10.0,
alarmInfo_alarmLow="A temperature alarmLow",
alarmNeedsAck_alarmLow=True,
alarmHigh=10.0,
alarmInfo_alarmHigh="Alarm: The temperature is critical",
alarmNeedsAck_alarmHigh=True)
async def onInitialization(self):
self.temp_upstream = await connectDevice("REMOTE_UPSTREAM")
self.temp_downstream = await connectDevice("REMOTE_DOWNSTREAM")
background(self.monitor())
async def monitor(self):
while True:
await waitUntilNew(self.temp_upstream.value,
self.temp_downstream.value)
diff = self.temp_upstream.value - self.temp_downstream.value
self.temperatureDiff = diff
Table Element (VectorHash)¶
Known as TABLE_ELEMENT
in the bound API, VectorHash
allows
users to specify custom entries, in the form of a table, that are,
programmatically, available later in the form of an iterable.
Like other karabo properties, VectorHash
is initialized by
displayedName, description, defaultValue, and accessMode. As well, it has a
rows field that describes what each row in the table contains.
This rows field expects a class that inherits from Configurable
.
class RowSchema(Configurable):
deviceId = String(
displayedName="DeviceId",
defaultValue="")
instanceCount = Int(
displayedName="Count")
This class can have as many parameters as desired, and these will be represented as columns in the table.
With RowSchema
, the definition of the VectorHash is as follows:
class MyMLDevice(Device):
userConfig = VectorHash(
rows=RowSchema,
displayedName="Hot Initialisation",
defaultValue=[],
minSize=1, maxSize=4)
The user will now be presented with an editable table:
Note that it is possible to provide the user with predefined entries, such as
default values or reading a configuration file, by providing a populated array
in the defaultValue
option.
The minSize
and maxSize
arguments can limit the table’s size if
needed.
class MyMLDevice(Device):
userConfig = VectorHash(
rows=RowSchema,
displayedName="Default Value Example",
defaultValue=[Hash("deviceId", "XHQ_EG_DG/MOTOR/1",
"instanceCount", 1),
Hash("deviceId", "XHQ_EG_DG/MOTOR/2",
"instanceCount", 2)],
minSize=1, maxSize=4)
The Table Element can be a bit customized with the displayType attribute. Specifying for example
class RowSchema(Configurable):
deviceId = String(
displayedName="DeviceId",
defaultValue="")
state = String(
defaultValue="UNKNOWN",
displayType="State",
displayedName="State")
will have a table schema that describes the state column with a displayType State
. The graphical user
interface can then color the column according to state color.
With Karabo 2.14.X, the table offers more customization, e.g.
class RowSchema(Configurable):
progress = Double(
displayedName="ProgressBar",
displayType="TableProgressBar"
defaultValue=0.0,
minInc=0.0,
maxInc=100.0)
stringColor = String(
defaultValue="anystring",
displayType="TableColor|default=white&xfel=orange&desy=blue",
displayedName="stringColor")
numberColor = Int32(
displayedName="Number Color",
displayType="TableColor|0=red&1=orange&2=blue"
defaultValue=0)
Hence, for different displayTypes more options are available.
A progressbar can be declared with
TableProgressBar
on a number descriptor.Background coloring can be provided for strings and numbers with the
TableColor
displayType. The coloring is then appended in an URI scheme (separator &) which is append to the displayType after |. Declaration of a default background brush can be set with the default setting.
class RowSchema(Configurable):
progress = Bool(
displayedName="Bool Button",
displayType="TableBoolButton",
defaultValue=True)
For read only table element a button can be declared via TableBoolButton
.
The button is enabled depending on the boolean setting.
Clicking the button will send a Hash to the device via the slot requestAction.
The hash contains keys with data:
action: TableButton
path: the property key
table: the table data
The table data itself is a Hash with:
data = Hash(
"rowData", h,
"row", row,
"column", column,
"header", header)
The rowData is a Hash of the row of the table button. The elements row and column provide the indexes of the button and the header the column string.
Another option (since Karabo 2.15.X) for a button can be the TableStringButton
. Besides access level considerations
this button is always enabled to provide an action on a deviceScene or url.
class RowSchema(Configurable):
description = String(
displayedName="Description",
defaultValue="")
view = String(
displayedName="View",
displayType="TableStringButton",
defaultValue="")
The value for both protocols are strings and an example to set a table
device_scene = "deviceScene|deviceId=YOURDEVICE&name=YOURSCENENAME"
open_url = "url|www.xfel.eu"
self.table = [Hash("description", "Important device", "view", device_scene),
Hash("description", "Important device doc", "view", open_url)]
Vector handling in tables is significantly increased in Karabo 2.16.X.
Specifiy a button with TableVectorButton
to launch a list edit dialog
via a button in the table element.
class RowSchema(Configurable):
devices = VectorString(
displayedName="View",
displayType="TableVectorButton",
defaultValue=[])
Once the VectorHash has been populated, it is possible to iterate through its
rows, which are themselves internally stored as a TableValue
, which itself
encapsulates a numpy array.
From Karabo 2.14.0 onwards it is possible to convert the np.array value
to a list of Hashes with
table = self.userConfig.to_hashlist()
Moreover, iterating over the encapsulated numpy array can be done like
@Slot(displayedName="Do something with table")
async def doSomethingTable(self):
# This loops over the array (.value)
for row in self.userConfig.value:
# do something ..., e.g. check the first column
first_column = row[0]
If an action is required on VectorHash update, e.g. a row is added or removed, then the VectorHash should be defined within a decorator:
@VectorHash(rows=RowSchema,
displayedName="Hot Initialisation",
defaultValue=[])
async def tableUpdate(self, updatedTable):
self.userConfig = updatedTable
# This loops over the array (.value)
for row in updatedTable.value:
# do something ...
Overwrite Properties¶
Classes in Karabo may have default properties. A type of motors may have a default speed, a camera may have a default frame rate, and so forth.
Let’s say a base class for a motor has a default max speed of 60 rpms:
class MotorTemplate(Configurable):
maxrpm = Int32(
displayedName="Max Rotation Per Minutes",
accessMode=AccessMode.READONLY,
allowedStates={State.ON},
unitSymbol=Unit.NUMBER)
maxrpm = 60
...[much more business logic]...
All instances of that MotorTemplate will have a fixed maximum rpm 60, that can be seen when the state is State.ON, and is read only.
We now would like to create a custom motor, for a slightly different usage, where users can set this maximum, according to their needs, but all other parameters and functions remain the same.
It is possible to do so by inheriting from MotorTemplate
, and using the
karabo.middlelayer.Overwrite
element:
class CustomMotor(MotorTemplate):
maxrpm = Overwrite(
minExc=1,
accessMode=AccessMode.RECONFIGURABLE,
allowedStates={State.OFF, State.INIT})
Note that only the required fields are modified. Others, such as
displayedName
will retain their original values.
Using Overwrite
allows inheritance whilst replacing pre-existing
parameters, keeping the namespace clean, and avoiding confusion between the
local and inherited scope, thus providing a simpler codebase.
Schema injection¶
A parameter injection is a modification of the class of an object. It is used to add new parameters to an instantiated device or to update the attributes of already existing parameters. An example for the latter is the change of the size of an array depending on a user specified Karabo parameter.
The following code shows an example for the injection of a string and a node into the class of a device:
from karabo.middlelayer import Device
class MyDevice(Device):
async def onInitialization(self):
# should it be needed to test that the node is there
self.my_node = None
async def inject_something(self):
# inject a new property into our personal class:
self.__class__.injected_string = String()
self.__class__.my_node = Node(MyNode, displayedName="position")
await self.publishInjectedParameters()
# use the property as any other property:
self.injected_string = "whatever"
# the test that the node is there is superfluous here
if self.my_node is not None:
self.my_node.reached = False
class MyNode(Configurable):
reached = Bool(
displayedName="On position",
description="On position flag",
defaultValue=True,
accessMode=AccessMode.RECONFIGURABLE
)
Note that calling :Python:`inject_something` again resets the values of properties to their defaults.
Middlelayer class based injection differs strongly from C++ and bound api parameter injection, and the following points should be remembered:
classes can only be injected into the top layer of the empty class and, consequently, of the schema rendition
the order of injection defines the order in schema rendition
classes injected can be simple (Double, Bool, etc.) or complex (Node, an entire class hierarchies, etc.)
later modification of injected class structure is not seen in the schema. Modification can only be achieved by overwriting the top level assignment of the class and calling
publishInjectedParameters()
injected classes are not affected by later calls to
publishInjectedParameters()
used to inject other classesdeleted (del) injected classes are removed from the schema by calling
publishInjectedParameters()
In the above example, new properties are added to the top level class of the device. Next, we consider the case where we want to update a property inside a child of the top level class.
:Python:`Overwrite` is used to update the attributes of existing Karabo property descriptors. Similar to the description in Chapter overwrite for the creation of a class, the mechanism can be used in schema injection.
class Configurator(Device):
beckhoffComs = String(
displayedName="BeckhoffComs in Topic",
options=[])
@Slot("Find BeckhoffComs")
async def findBeckhoffComs(self):
# Get a list of beckhoffCom deviceIds
options = await self.get_beckhoff_coms()
self.__class__.beckhoffComs = Overwrite(options=options)
await self.publishInjectedParameters()
Karabo Devices are instances of a device class (classId). Hence, all instances of the same class on the same device server share the same static schema and a modification to any class object of the schema is propagated to all device children. However, as described above, it is possible to modify the top-layer device class. This leads to, that a change in a noded structure requires a full reconstruction (using a new class) and injection into the device’s top level class under the same property key.
Below is a MotorDevice with two different motor axes, rotary and linear represented in a Node. In the following, we would like to change on runtime the minInc and maxInc attribute of the targetPosition property. However, Schema injection for runtime attributes in mostly not worth it and should be avoided. Nevertheless, this example shows the technical possiblity.
from karabo.middlelayer import (
Configurable, Device, Double, isSet, Node, String, Slot, VectorDouble)
def get_axis_schema(key, limits=None):
class AxisSchema(Configurable):
node_key = key
@Slot()
async def updateLimits(self):
await self.get_root().updateAxisLimits(
self.node_key, self.targetLimits.value)
@VectorDouble(
defaultValue=None,
minSize=2, maxSize=2,
displayedName="Target Limits")
async def targetLimits(self, value):
if not isSet(value):
self.targetLimits = value
return
# Setter function always called in initialization
self.targetLimits = value
targetPosition = Double(
displayedName="Value",
minInc=limits[0] if limits is not None else None,
maxInc=limits[1] if limits is not None else None)
return AxisSchema
class MotorDevice(Device):
# Node's take classes to build up
rotary = Node(get_axis_schema("rotary", None))
linear = Node(get_axis_schema("linear", [0, 90]))
async def updateAxisLimits(self, key, limits):
# 1. Get the previous configuration from the node under `key`
h = self.configurationAsHash()[key]
# 2. Create a new configurable schema for `key` with `limits`
conf_schema = get_axis_schema(key, limits)
# 3. Set the new node on `key` and inject with previous
# configuration `h`
setattr(self.__class__, key, Node(conf_schema))
await self.publishInjectedParameters(key, h)
The factory function :Python:`get_limit_schema` provides each Node with a Configurable class. During the creation, the minInc and maxInc attributes can be assigned to the targetPosition property. Here, the class itself has a Slot that propagates to the MotorDevice to inject a new Axis under the same key with different limits. During a runtime schema injection via the Slot updateLimits, we again create a new updated Configurable class for the Node - and - to make sure that the configuration of the MotorDevice is preserved, the existing configuration is passed to the :Python:`publishInjectedParameters` method. During the initialization, all eventual setters are called as usual.
Slots are decorating functions. If you want to add a Slot, or change the function it is bound to (decorating), the following will do the trick:
async def very_private(self):
self.log.INFO("This very private function is now exposed!!")
@Slot("Inject a slot")
async def inject_slot(self):
# Inject a new slot in our schema
self.__class__.injectedSlot = Slot(displayedName="Injected Slot")
self.__class__.injectedSlot.__call__(type(self).very_private)
await self.publishInjectedParameters()
Note
The key to that slot will not be very_private but instead injectedSlot So yes, cool that we can change the behaviour of a slot on the fly by changing the function the slot calls, but the key won’t reflect that.
If you do change the functions that are called, do put in a log message.
Warning
Consider instead injecting a node with a proper Slot definition.
Injected Properties and the DAQ need some ground rules in order to record these properties correctly.
In order for the DAQ to record injected properties, the DAQ needs to request the
updated schema again, using the Run Controller’s applyConfiguration()
slot.
This can be prone to operator errors, and therefore it is recommended that only properties injected at instantiation to be recorded.
Device Scenes¶
Karabo provides a protocol for devices to share predefined scenes. These allows the author of a device to provide what they think are a good starting point. Moreover, these are easily accessible by from the topology panel in the GUI:
A default scene can also be accessed by double-clicking on a device.
This section shows how to enable your device to have builtin scenes.
Implementing this functionality requires the creation of a scene, in the scene editor, conversion to Python, and adding the requestScene framework slot.
Begin by drawing an adequate scene in the GUI’s scene editor, and save it locally on your computer as SVG (right-click on scene -> Save to File).
Use the karabo-scene2py utility to convert the SVG file to Python code:
$ karabo-scene2py scene.svg SA1_XTD2_UND/MDL/GAINCURVE_SCAN > scenes.py
The first argument is the scene file, the second is an optional deviceId to be substituted.
As it is generated code, make sure the file is PEP8 compliant. The final result should look more or less like the following:
from karabo.common.scenemodel.api import (
IntLineEditModel, LabelModel, SceneModel, write_scene
)
def get_scene(deviceId):
input = IntLineEditModel(height=31.0,
keys=['{}.config.movingAverageCount'.format(deviceId)],
parent_component='EditableApplyLaterComponent',
width=67.0, x=227.0, y=18.0)
label = LabelModel(font='Ubuntu,11,-1,5,50,0,0,0,0,0', foreground='#000000',
height=27.0, parent_component='DisplayComponent',
text='Running Average Shot Count',
width=206.0, x=16.0, y=15.0)
scene = SceneModel(height=1017.0, width=1867.0, children=[input, label])
return write_scene(scene)
Add this file to your project.
In your device, add a read-only VectorString property called availableScenes, and implement the requestScene framework slot. This is a predefined slot, which allows various actors to understand the scene protocol.
The slot takes a Hash params and returns a Hash with the origin, its datatype (deviceScene), and the scene itself:
from karabo.middlelayer import AccessMode, DaqPolicy, VectoString, slot
from .scenes import get_scene
availableScenes = VectorString(
displayedName="Available Scenes",
displayType="Scenes",
accessMode=AccessMode.READONLY,
defaultValue=["overview"],
daqPolicy=DaqPolicy.OMIT)
@slot
def requestScene(self, params):
name = params.get('name', default='overview')
payload = Hash('success', True, 'name', name,
'data', get_scene(self.deviceId))
return Hash('type', 'deviceScene',
'origin', self.deviceId,
'payload', payload)
Note
Note that we use here slot, and not Slot(). These are two different functions. slot provides framework-level slots, whereas Slot are device-level.
Would you want to provide several scenes (e.g., simple overview and control scene), you can define several functions in scenes.py, and modify requestScene to check params[‘name’]:
from karabo.middlelayer import AccessMode, DaqPolicy, VectoString, slot
import .scenes
availableScenes = VectorString(
displayedName="Available Scenes",
displayType="Scenes",
accessMode=AccessMode.READONLY,
defaultValue=["overview", "controls"],
daqPolicy=DaqPolicy.OMIT)
@slot
def requestScene(self, params):
payload = Hash('success', False)
name = params.get('name', default='overview')
if name == 'overview':
payload.set('success', True)
payload.set('name', name)
payload.set('data', scenes.overview(self.deviceId))
elif name == 'controls':
payload.set('success', True)
payload.set('name', name)
payload.set('data', scenes.controls(self.deviceId))
return Hash('type', 'deviceScene',
'origin', self.deviceId,
'payload', payload)
Note
There is the convention that the default scene (of your choice) should be first in the availableScenes list.
As described in Table Element (VectorHash), table elements are vectors of hash, the schema is specified as Hash serialized to XML, (which karabo-scene2py takes care of).
In this case, it’s fine to break the PEP8 80 characters limit. A table element looks like:
table = TableElementModel(
column_schema='TriggerRow:<root KRB_Artificial="">CONTENT</root>',
height=196.0, keys=['{}.triggerEnv'.format(deviceId)],
klass='DisplayTableElement',
parent_component='DisplayComponent',
width=436.0, x=19.0, y=484.0
)
The following applies whether you want to link to another of your scenes or to another device’s scene.
Let’s say that you want to add links in your overview scene to your controls scene.
The DeviceSceneLinkModel
allows you to specify links to other
dynamically provided scenes.
In your scenes.py, import DeviceSceneLinkModel and SceneTargetWindow from
karabo.common.scenemodel.api and extend overview(deviceId)()
:
from karabo.common.scenemodel.api import DeviceSceneLinkModel, SceneTargetWindow
def overview(deviceId):
# remaining scene stays the same
link_to_controls = DeviceSceneLinkModel(
height=40.0, width=314.0, x=114.0, y=227.0,
parent_component='DisplayComponent',
keys=['{}.availableScenes'.format(deviceId)], target='controls',
text='Controls scene',
target_window=SceneTargetWindow.Dialog)
children = [label, input, link_to_controls]
scene = SceneModel(height=1017.0, width=1867.0, children=children)
return write_scene(scene)
If you want to link to another device, make overview()
accept another
remoteDeviceId parameter, and point the link to that device:
def overview(deviceId, remoteDeviceId):
# remaining scene stays the same
link_to_remote = DeviceSceneLinkModel(
height=40.0, width=314.0, x=114.0, y=267.0,
parent_component='DisplayComponent',
text='Link to other device',
keys=['{}.availableScenes'.format(remoteDeviceId)], target='scene',
target_window=SceneTargetWindow.Dialog
)
children = [label, input, link_to_controls, link_to_remote]
scene = SceneModel(height=1017.0, width=1867.0, children=children)
return write_scene(scene)
Note
remoteDeviceId is merely the deviceId, here. If you have a proxy, you may want to rethink the arguments to overview and pass it self or the proxy object. Then you can find out exactly what scenes are available there, e.g.:
target = ‘controls’ if ‘controls’ in px.availableScenes else ‘scene’ keys=[‘{}.availableScenes’.format(px.deviceId)], target=target,
GainCurveScan: provides a single default scene
Karabacon: provides several scenes
KEP21: definition of the scene protocol