Welcome to SILF Experiment API’s documentation!

Contents:

Api of the device

device Package

It packages api for single device.

_const Module

silf.backend.commons.device._const.DEVICE_STATES = ('off', 'stand-by', 'ready', 'running', 'cleaned_up')

Touple containing all allowable device states.

_device Module

This is a api for a device.

class silf.backend.commons.device._device.Device(device_id='default', config_file=None)

Bases: object

Defines plugin for a particular device in the experiment.

All methods are blocking, that is should block the current thread until finished.

Note

Instances of this object don’t need to use any synchronization, they will always be called from single thread, This instance will be constructed used and destroyed on single process.

Warning

All methods should exit relatively fast.

Warning

Both method parameters and responses should be pickleable, these will be travelling between process boundaries.

MAIN_LOOP_INTERVAL = 0.1

Interval between invocations of main loop. Represents number of seconds as float.

apply_settings(settings)

Applies some set of settings to this device.

Parameters:

settings (dict) – Settings to be applied, it is already validated by the IDeviceManager.

Raises:
Returns:

None

Return type:

None

logger
Returns:Logger instance attached to this device. Utility method, you may use whatsoever logger you want
loop_iteration()

Perform an iteration of main experiment loop. Should terminate quickly,

Raises:DeviceRuntimeException – If any exception occours
Returns:If returned value is False or None next iteration of this method will be scheduled after MAIN_LOOP_INTERVAL seconds, it result is true it will be sheduled earlier (after at most one command from controller was performed);
perform_diagnostics(diagnostics_level='short')

Performs diagnostics on the device. Can be ran if this device is OFF. or STAND_BY.

Parameters:

diagnostics_level (str) – Whether diagnostisc should be thororough or not, must be in DEVICE_STATES

Raises:
pop_results()

This method returns list of recently acquired points, it should clear this list so next calls won’t return the same result points.

Raises:
Returns:

Returns results for (possibly) many points.

Return type:

list (or any other iterable) of dict.

post_power_up_diagnostics(diagnostics_level='short')
power_down()

Call to this method moves this class to OFF state.

Raises:
power_up()

Call to this method enables consecutive apply_settings().

It also should power up the device (if this action makes any sense for this particular device see also: Power management).

Raises:
pre_power_up_diagnostics(diagnostics_level='short')
start()

Starts the acquisituon on the device (that is starts the measurements).

Blocks until this device is stared.

Raises:InvalidStateException – If device is in invalid state (that is not READY
Returns:None
Return type:None
state = None

State of this device should be in DEVICE_STATES, full state chart is avilable in: Device state chart.

stop()

Stops the acquisituon on the device (that is stops the measurements).

Blocks until this device is stared.

Raises:
Returns:

None

Return type:

None

tearDown()
tear_down()

Called when current process is being disabled.

This method can be called multiple times.

Note

do not override this method, override _tear_down().

Raises:DeviceRuntimeException – If any exception occours
Returns:None
Return type:None
exception silf.backend.commons.device._device.InvalidCallToAssertState

Bases: Warning

API fine print

_images/device_state_chart.svg

Device state chart

Power management

Note

If your device does not need to power itself up or down, please just ignore power_up() and power_down() methods.

Devices should be powered up when we start call power_up(), but needn`t do so, they must be powered up when after we exit from start(). So there are three methods in which devices should power up:

  • power_up(), this method is called relatively early in during the experiment, and should allow plenty of time to initialize everyhing
  • apply_settings(), use this method if your device powers up quickly.
  • start(), if your device is volatile and you want to minimize the time it is powered up use this.

You can power down the device when following methods are called:

Threading considerations

Devices are accessed from single thread. All methods sould exit relatively fast, you should not use loops that are infinite (or can be infinite — for example if hardware will not respond).

Change device state

It is quite important to change state of your device after appropriate method calls.

How to test the devices according to the API

There are two ways in which you can test it: start ipython interpreter create device and manage it by hand:

Use DeviceWorkerWrapper from interpreter

Import classes:

>>> from silf.backend.commons_test.device.test_device import *
>>> from silf.backend.commons.device_manager import start_worker_interactive

Start the device:

>>> work = start_worker_interactive('foo', MockDevice,
... configure_logging=False, auto_pull_results=False)

>>> work.state
'off'

>>> work.power_up() 
UUID(...)

Let’s setup the device:

>>> work.apply_settings({"foo": 3, "bar": 2}) 
UUID(...)

>>> work.start() 
UUID(...)

This device will perform own acquisition in separete process, well wait for results to be acquired:

>>> time.sleep(1.2)

First pop_results() will return stale data, and schedule acquisition of new data:

>>> work.state
'running'
>>> work.pop_results() == []
True

Wait for results to get processed (will be faster on server!)

>>> time.sleep(0.5)
>>> results = work.pop_results()
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True

Kill it without waiting;

>>> work.kill(wait_time=None)

Auto result pooling

You can configure this to auto poll for results:

>>> work = start_worker_interactive('foo', MockDevice,
... configure_logging=False, auto_pull_results=True)


>>> work.power_up() 
UUID(...)

As in last test:

>>> work.apply_settings({"foo": 3, "bar": 2}) 
UUID(...)
>>> work.start() 
UUID(...)

Wait for results to be gathered

>>> time.sleep(2)

Notice that results are avilable at once (no need to query)

>>> results = work.pop_results()
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True

>>> work.kill(wait_time=None)
>>> results == [{'foo_result': 3, 'bar_result': 2}]
True

>>> work.kill(wait_time=None)

Device Api examples

This is pseudocode

Engine driver

This imaginary device implements an engine. This is not actual experiment code, sxperiment will not be doing any waiting!

engine = ImaginaryDriver()

assert engine.state == 'off'

engine.power_up() # Powers up the device

assert engine.state == 'stand-by'

engine.apply_settings({"position" : 512})

assert engine.state == 'ready'

engine.start() # Start the engine

assert engine.state == 'acquiring'

# Silnik ruszył i teraz jest w stanie `acquiring`

# .. wait

while engine.state != 'ready':
    time.sleep(0.1)

# Silnik doszedł do końca i jest w stanie `ready`

# Następny pukt

engine.apply_settings({"position" : 1024})

engine.start() # Start the engine

Engine driver

Imaginary voltimeter

volt = ImaginaryVoltimeter()

assert volt.state == 'off'

volt.power_up() # Powers up the device

assert volt.state == 'stand-by'

volt.apply_settings({'range' : 15})

assert volt.state == 'ready'

volt.start() # Start the volt

assert volt.state == 'acquiring'

while volt.state != 'ready':
    time.sleep(0.1)

assert volt.pop_results() == [{'voltage' : 243.11}]

Engine and voltimeter connected

It works that so voltimeter measures single point after position is set by the engine.

engine = ImaginaryDriver()
volt = ImaginaryVoltimeter()

engine.power_up() # Powers up the device
volt.power_up() # Powers up the device

engine.apply_settings({"position" : 512})

engine.start();

while engine.state != 'ready':
    time.sleep(0.1)

volt.apply_settings({'range' : 15})

volt.start() # Start the volt

while volt.state != 'ready':
    time.sleep(0.1)

assert volt.pop_results() == [{'voltage' : 243.11}]

engine.apply_settings({"position" : 1024})

engine.start();

while engine.state != 'ready':
    time.sleep(0.1)

while volt.state != 'ready':
    time.sleep(0.1)

assert volt.pop_results() == [{'voltage' : 123.123}]

Howto create experiment (in four easy steps!)

Create device drivers for all needed devices

See device_api.rst

Create device managers for all the devices

Single mode devices

Device managers are wrappers around devices that have following repsonsibilities:

  1. Provide metadata about the device it holds
  2. Validate input
  3. Provide any neccessary per-experiment customisation

For example you need to create a DeviceManager for HantekPPS2116ADevice that can be found in this repository: https://bitbucket.org/silf/silf-backend-driver-power-hantek. This is a simple programmable power source. This manager will also allow user to directly set temperature of light-bulb that is beinbg controlled by this power source, so we will need to convert both user settinga as well as experiment responses.

We’ll start with the following:

class HantekManager(SingleModeDeviceManager):

    DEVICE_ID = "hantek"
    DEVICE_CONSTRUCTOR = HantekPPS2116ADevice
    CONTROLS = ControlSuite(
        NumberControl("temperature", "Temperatura włókna żarówki", default_value=2700, min_value=300, max_value=2800)
    )

    OUTPUT_FIELDS = OutputFieldSuite(
        blackbody_temperature = OutputField(
            "integer-indicator", "temperature", label="Aktualna temperatura włókna żarówki"
        )
    )

    RESULT_CREATORS = [
        ReturnLastElementResultCreator("temperature")
    ]

Following points are important:

  1. DEVICE_ID is programmer-readable unique for experiment name of the device, it is not visible to the end-user.
  2. DEVICE_CONSTRUCTOR is a callable that creates the Device. Mostly it will be just a subtype of Device. It should accept the same arguments as meth: Device.__init__()
  3. CONTROLS Controls define controls for user for this device. For more information about how controls work and how they are represented read: Input fields and :mod:`silf.backend.commons.
  4. Result creators allows to customize how results are sent to user (there will be more about it in this document)
Converting settings

For now the problem is that device needs voltage and current, and student is providing the temperature. To convert settings you need to override silf.backend.commons.device_manager.DefaultDeviceManager._convert_settings_to_device_format().

class HantekManager(SingleModeDeviceManager):

    ...

    def __get_hantek_settings_from_temperature(self, temperature):
        return {'voltage' : ( 1.55155e-6 *temperature**2 - 0.00045359 ) }  #Temperature [K] approx. conv. to volt.

    def _convert_settings_to_device_format(self, converted_settings):
        return self.__get_hantek_settings_from_temperature(converted_settings['temperature'])
Converting results

Also resuls need to be converted:

class HantekManager(SingleModeDeviceManager):

    ...

    def __get_temperature_from_hantek_results(self, voltage, current):
        return {"temperature" : (math.sqrt( 684732 * voltage) + 86.5226)} #Voltage approx. conv. to temp. K

    def _convert_result(self, results):
        return self.__get_temperature_from_hantek_results(
            results['voltage'], results['current']
        )
Results creators

Devices produce results asynchroneusly, that is while DeviceManager was doing something else Device could procudce many results (each of these results being a dictionary holding many values) or none at all. RESULT_CREATORS are designed to convert that unknown number of results to something that can be sent to users.

If experiment produced N result dictionaries, first we convert each of them using _convert_results then we fire result creators.

Create the experiment manager

Complicated experiment manager

class BlackbodyExperiment(EventExperimentManager):

    LOOP_TIMEOUT = 1

    DEVICE_MANAGERS = {
        'hantek': HantekManager,
        'strawberry': StrawberryManager,
        'voltimeter': RigolDM3000UsbtmcManager
    }

Configuration:

LOOP_TIMEOUT
As everywhere — will controls wait after each iteration of main loop
DEVICE_MANAGERS
Dictionary mapping name to IDeviceManager.

Logic of this experiment is as follows:

  • For each point:
  • Move detector to the desired position
  • Set parameters for the light source
  • Capture voltage

We need to implement this logic using EventExperimentManager. In this class each lifecycle method launches an event, rest of the logic may be added via custom events.

All lifecycle events will be handled by default handlers, with the exception of ‘start’ which will invoke new logic.

These default handlers have following meaning

power_up
Powers up all the devices
stop
Stops all the devices
power_down
Powers down all the devices
apply_settings
Applies settings to all the devices
tick
Calls Device.loop_iteration()
pop_results
If device is running gets the results from all the devices and sends them to client.

Here are custom handlers for this device:

start
Clears internal state, starts engine driver and powersource, then and rises next_point event
next_point

Moves engine to the next point and schedules check_position to be fired after one second. If this is the last point fires stop event.

def _on_next_point(self, event):
    try:
        self['strawberry'].move_to_next_series_point() # Moves the engine to the next series point
        self.schedule_event('check_position') # Schedule event for self.LOOP_TIMEOUT
    except OnFinalSeriesPoint:  # This is raised if we are at last series point
        self.experiment_callback.send_series_done() # Send users that series is finished
        self.stop() # Stop all devices
check_position

If engine arrived on target position starts voltimeter and fires check_voltage if not schedules check_position to be fired after one second.

def _on_check_position(self, event):
    # This is how we detect that device finished working -- it should
    # switch it's state
    if self['strawberry'].device_state == DEVICE_STATES[READY]:
        # Purge all cached results
        self['voltimeter'].clear_current_results()
        # Start the voltimeter
        self['voltimeter'].start()
        # Schedule next event for immediate execution
        self.schedule_event('check_voltage', 0)
    else:
        # If engines are working schedule check position once more
        self.schedule_event('check_position')
check_voltage

If voltimeter finished the measurement fires next_point event if not check_voltage to be fired after one second.

Also if voltimeter has finished it sends next point to the user.

def _on_check_voltage(self, event):
    voltimeter = self['voltimeter']

    # If volrimter has results let's send them!
    if voltimeter.has_result('voltage'):
        voltage = voltimeter.current_result_map['voltage']
        wavelength = self['strawberry'].current_wavelength
        results = {'voltage': voltage, 'wavelength': wavelength}
        self.chart_generator.aggregate_results(results)
        chart_item = self.chart_generator.pop_results()
        self.experiment_callback.send_results(ResultSuite(
            **{self.chart_generator.result_name: chart_item}
        ))
        self.schedule_event('next_point', 0)
    else:
        # Wait for results
        self.schedule_event('check_voltage')

Then you need to hook events to this instance:

def initialize(self, experiment_callback):
    super().initialize(experiment_callback)

    # Load default events

    self.install_default_event_managers_for('power_up')
    self.install_default_event_managers_for('stop')
    self.install_default_event_managers_for('power_down')
    self.install_default_event_managers_for('apply_settings')
    self.install_default_event_managers_for('tick')
    self.install_default_event_managers_for('pop_results')

    #Load additional events

    self.register_listener('start', self._on_series_started)
    self.register_listener('next_point', self._on_next_point)
    self.register_listener('check_position', self._on_check_position)
    self.register_listener('check_voltage', self._on_check_voltage)

    # Clear results for chart
    self.register_listener('stop', lambda evt: self.chart_generator.clear())
class BlackbodyExperiment(EventExperimentManager):

    def initialize(self, experiment_callback):
        super().initialize(experiment_callback)

        self.install_default_event_managers_for('power_up')
        self.install_default_event_managers_for('stop')
        self.install_default_event_managers_for('power_down')
        self.install_default_event_managers_for('apply_settings')
        self.install_default_event_managers_for('tick')
        self.install_default_event_managers_for('pop_results')

        self.register_listener('start', self._on_series_started)
        self.register_listener('next_point', self._on_next_point)
        self.register_listener('check_position', self._on_check_position)
        self.register_listener('check_voltage', self._on_check_voltage)
        self.register_listener('stop', lambda evt: self.chart_generator.clear())

    def _on_series_started(self, event):
        self['hantek'].start() # Starts the power source
        self['strawberry'].start() # Starts the engine
        self._on_next_point(event)

    def _on_next_point(self, event):
        try:
            self['strawberry'].move_to_next_series_point() # Moves the engine to the next series point
            self.schedule_event('check_position')
        except OnFinalSeriesPoint:
            self.experiment_callback.send_series_done()
            self.stop()

    def _on_check_position(self, event):
        if self['strawberry'].device_state == DEVICE_STATES[READY]:
            self['voltimeter'].clear_current_results()
            self['voltimeter'].start()
            self.schedule_event('check_voltage', 0)
        else:
            self.schedule_event('check_position')

    def _on_check_voltage(self, event):
        voltimeter = self['voltimeter']

        if voltimeter.has_result('voltage'):
            voltage = voltimeter.current_result_map['voltage']
            wavelength = self['strawberry'].current_wavelength
            results = {'voltage': voltage, 'wavelength': wavelength}
            self.chart_generator.aggregate_results(results)
            chart_item = self.chart_generator.pop_results()
            self.experiment_callback.send_results(ResultSuite(
                **{self.chart_generator.result_name: chart_item}
            ))
            self.schedule_event('next_point', 0)
        else:
            self.schedule_event('check_voltage')

Configure the experiment

With experiment manager you need to create configuration file, please see Experiment configuration file.

[Experiment]
#Experiment name
ExperimentName=MockExperiment
#Name of the experiment manager (one created in last step)
ExperimentManagerClass=silf.backend.commons_test.experiment.mock_experiment_manager.MockExperimentManager #Ścieżka do klasy zarządzającej eksperymentem
#Client class
ClientClass=silf.backend.client.mock_client.Client #Klasa klienta

# How long this experiment will wait before it shuts itself down after last series did end
shutdown_experiment_timeout=3600
# How long this experiment will wait before it kills current session
stop_series_timeout=600


[XMPPClient]

#Configutation for XMPP client
nick = experiment
jid = geiger@pip.ilf.edu.pl/experiment
password = NepjotmirkOdofruebcajigIaheHuSka
room = test-geiger@muc.pip.ilf.edu.pl
port = 5222
host = pip.ilf.edu.pl

[Logging]
config_type=file

Experiment configuration file

This file contains experiment description.

[Experiment] Section

Contains configuration for the whole experiment:

ExperimentName
Name of the experiment
ExperimentManagerClass
Class that manages the experiment (see: Complicated experiment manager and BaseExperimentManager).
ClientClass
XMPPClient to use.

Other entries in this section are optional, and are dependent ExperimentManagerClass.

LoopTimeout
Timeout of main experiment loop [in seconds]
series_pattern

Experiment series will get label according to this pattern (unless ExperimentManagerClass overries it ;).

This pattern can contain names of any settings sent to the experiment in str.format() format.

stop_series_timeout
Timeout (in seconds) after which we stop series that was not stopped by the user or the experiment (and it does not send any results)
shutdown_experiment_timeout
Timeout (in seconds) after which we stop experiment if there are no series active.
AdditionalConfigFiles
See Multi File Config

Multi File Config

You might want to split experiment configuration into several config files, valid examples for such setup are, we just wanted to store config files inside VCS, and have passwords stored elsewhere.

Config entry: AdditionalConfigFiles in section [Config] contains semicolon separated list of paths to config files, these config files will be loaded during experiment startup and will override any entries in original config file.

If any of specified files is missing exception will be raised.

[XMPPClient] Section

Contains configuration for the XMPP client.

It is mostly obvious:

jid
JID (part before @ symbol)
nick
nick in XMPP groupchat, any string
password
password for XMPP account
room
XMPP groupchat room name (part before @ symbol)
server_config_section
Name of section that contains XMPP server configuration, by default it is [Server]

[Server] Section

These properties govern connection to tigase server

host
server address Due to some Sleek bug you should provide IP rather than a hostname
port
server port

These properties are added used to create JID

domain
Domain (added to JID after @ symbol)
groupchat_domain
Domain (added to room name @ symbol)

[Logging] Section

Configures logging for this experiment.

config_type
Either file or basic. basic calls logging.basicConfig(DEBUG, file allows use ini based logging configuration.
logging_file
Usable for config_type=file. Specifies the logging configuration to use. Defaults to prepared config file.

In both configs a we send e-mails with errors to experiment administrators, to configure this behaviour in your logging config you might use logging section that looks like:

[handler_exception]
class: silf.backend.commons.util.mail.ExceptionHandler
formatter: detailed
args: []

And then add handler configured in this way to root logger.

[SMTP] Section

This section is optional. It configures functionality of sending emails to administratiors of the experiment.

smtp_enabled

Boolean value is false or missing altogether thhis functionality is disabled.

test_smtp

Boolean value if true will use mock smtp (no emails will be sent).

smtp_server, smtp_port

Host to which we connect via SMTP

smtp_user, smtp_password

Login credentials

smtp_client_class

SMTP client class to use. This entry contains path to module and type to use, for example: smtplib.SMTP_SSL. This class has to have similar api to smtplib.SMTP_SSL.

from_email

FROM address for emails.

admin_list

List of users to which we send error emails.

Other sections

This file may also contain other sections that define particular devices. Device uses configuratuion defined in section named by it’s deviceId.

While device can use any property in it’s own section this sections also contains:

single_task_timeout

Float value, maximal time for each operation on the remote device in seconds. If operation takes longer time error is raised.

XMPPClient api

For functions descriptions refer to silf.backend.client Package

Client is low-level component used by experiment that encapsulates XMPP communication, it’s based on SleekXMPP library with minor modifications.

Client allows sending and receiving custom XMPP substanzas (labdata) embedded in basic Message stanzas.

Functionality

Client can send XMPP groupchat messages to two separate XMPP rooms. One room is used for data transfer between parts of experiment, second one is chat room for users.

Messages sent to main room must contain labdata substanza. Client provides callbacks fired when specific labdata namespace or type arrives.

Config options:

  • nick - Nick in XMPP room used by client
  • jid - first part of jid (login) on XMPP server
  • password - password
  • room - XMPP groupchat room to use as main room (for data transfer)
  • chatroom - XMPP groupchat room to use as chat room (for user messages)
  • server_config_section - Name of config file section that contains XMPP server config

Server config options:

  • host - Host used for connection (or IP address)
  • domain - Second part of JIDs of users (domain configured in XMPP server)
  • groupchat_domain - Domain configured in XMPP server used in room JIDs
  • port - Port used for connection

Example config:

[Client]
    nick = Echo
    jid = echo
    password = jhfjkkdutgouhitg

    room = testroom
    chatroom = testchatroom
    server_config_section = Server

[Server]
    host = pip.ilf.edu.pl
    domain = pip.ilf.edu.pl
    groupchat_domain = muc.pip.ilf.edu.pl
    port = 5222

You can specify parsed config file section by passing ‘section’ parameter to client constructor ([XMPPClient] in default experiment implementation)

Basic usage in experiment

You do not have to touch anything inside client when writing standard experiment, specify client class in experiment config file (in [Experiment] section)

[Experiment]
    (...)
    ClientClass=silf.backend.client.client.Client
    (...)

Usage as standalone class

c = Client(config='default.ini', section='Client')
c.initialize()  # make XMPP connection

c.send_chatmessage('Hello, XMPP client here')  # send message to chatroom

l = format_labdata(...)
c.send_labdata(l)  # Send message with labdata substanza to main room

c.register_callback(callback_func, namespace=None, type=None) # register callback function for ALL labdata messages
c.register_callback(callback_get, namespace='silf:mode:get', type=None) # register callback for all labdata messages in 'silf:mode:get' namespace
c.register_callback(callback_set_query, namespace='silf:mode:set', type='query') # register callback for labdata messages of type query in namespace 'silf:mode:set'

c.disconnect() # terminate XMPP connection

Refer to protocol documentation for list of available namespaces and labdata types

Module index

setup Module

silf.backend.client Package

api Module

exception silf.backend.client.api.ClientException

Bases: Exception

Base class for all Client related exceptions

exception silf.backend.client.api.ClientNoErrorNamespaceException

Bases: silf.backend.client.api.ClientException

Thrown when send_error is called in a way that does not provide namespace (all args None)

class silf.backend.client.api.IClient(config='config/default.ini', section='Client', block=False)

Bases: object

Message
copy() → silf.backend.client.api.IClient

Creates a copy of this client

create_random_id()
disconnect(wait)
initialize()
make_labdata_message(namespace=None, type=None, suite=None, labdata=None)

Create XMPP message stanza with labdata substanza using suppliend parameters

Parameters:
  • namespace – Namespace of labdata substanza
  • type – Type of labdata substanza
  • suite – Data suite that will be inserted into labdata substanza
  • labdata – Labdata substanza

Note

if labdata parameter is None, new labdata substanza is created using namespace, type and suite parameters, otherwise they are ignored.

make_message(mto, labdata, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None)
random_resource(jid)

Add random resource to JID

register_callback(callback, namespace=None, type=None)
register_error_callback(func)
send(namespace, suite, ltype='query', id=None, incoming_stanza=None)

Send labdata stanza with body containing JSON from suite, take namespace and id from state if serializing/sending labdata fails for some reason send error labdata instead

Parameters:
  • state (IncomingStanza) – IncomingStanza object (see IncomingStanza class description)
  • suite – Data to be sent (JSON serializable object - suite/stanza)
  • ltype (str) – Labtata type to use (string)
Returns:

None

send_error(suite, incoming_stanza=None, lnamespace=None)

Send error labdata

Parameters:
  • suite (ErrorSuite) – ErrorSuite object (contains info about errors - seraializable to JSON)
  • incoming_stanza – IncomingStanza object
  • lnamespace – Labdata namespace to use
Returns:

None

Note

labdata namespace is taken from state object, if state is None, namespace is taken from lnamespace parameter, if both are None exception is thrown.

send_labdata(labdata)
send_results(suite)

Send experiment results in ‘data’ labdata stanza :param suite: DataSuite object (serializable to JSON)

send_standalone_stanza(namespace, suite=None)

Sends standalone stanza (results, stop etc).

Parameters:
  • namespace (str) – Namespace to
  • suite
Returns:

client Module

class silf.backend.client.client.CallbackWrapper(name, matcher, pointer, client, **kwargs)

Bases: sleekxmpp.xmlstream.handler.callback.Callback

SleekXMPP does not allow passing additional arguments to _callbacks CallbackWrapper adds one layer between sleekxmpp and actual callback function.

run(payload, instream=False)
class silf.backend.client.client.Client(config='config/default.ini', section='Client', block=False)

Bases: silf.backend.client.api.IClient

XMPP client

Initiates connection to XMPP server, joins multi user chat.

After initialization use register_*_callback functions to add handlers for labdata messages in specific namespaces.

Message
copy() → silf.backend.client.client.Client

Creates a copy of this client

disconnect(wait=None)

Terminate connection to XMPP server. Both clients will be disconnected

initialize(time_to_wait_for_connection=0)

Connect to XMPP server, register necessary plugins

Create SleekXMPP client, for sending labdata stanzas. Use unencrypted plain authentication, because Tigase has problem with SSL. Insert self.error_handler into modified SleekXMPP clinets which will be called when XML perser error occurs.

make_message(mto, labdata, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None)

Create XMPP message stanza and insert labdata into it

Parameters:
  • labdata – Labdata substanza which will be inserted into message
  • mbody – Body of message (text)
  • msubject – Message subject
  • mtype – Message type (None for user to user messages, ‘groupchat’ for muc messages)
  • mhtml – HTML body content (optional)
  • mfrom – JID of sender (some servers require full client JID)
  • mnick – Nick of sender (optional)
register_error_callback(func)

Register callback function executed before client reset when XML parser error occurs

send_labdata(stanza)

Send message with labdata substanza to groupchat Add dummy <body> tag, XMPP server (openfire) does not pass groupchat messages without it :param stanza: received message.

start(event)

Fired automatically after successful connection to XMPP server Join groupchat.

wait_until_online(timeout=15)

Wait until client gets online :param timeout: max time to wait

exception silf.backend.client.client.DisconnectedException

Bases: Exception

Raised when Client couldn’t connect during startup.

We treat all connection errors as fatal errors, that force experiments to be terminated and then resurrected by systemd.

class silf.backend.client.client.SleekErrorHandler

Bases: object

SleekXMPP sometimes receives XMPP conflict, XML interpreter (expat?) throws SyntaxError exception, and tries to reconnect (XML stream is stuck in some unmanageable condition, and reconnect never succeeds). This triggers endless stream of reconnect attempts, experinment server becomes unusable. Resetting XML stream from inside of SleekXMPP (abort and then connect) does not work (triggers SocketError). XML processing thread must be killed (we must do this from Client context)

add_callable(c)

Add callback function (called on stream error from main experiment thread) avoid using this function directly, use Client.register_error_handler

check_flag()
fire()

Called when error is caught, calls all stored callback functions, flag marks client as ‘broken’ and can be read by other threads

const Module

echo Module

class silf.backend.client.echo.Echo(config, section='Echo')

Bases: silf.backend.client.listener.Listener

Test XMPP echo bot - bounces back all messages Used for labdata/Client debugging

muc_message(msg)

labdata Module

Labdata XMPP stanza (substanza of Message)

exception silf.backend.client.labdata.InvalidLabdataContentException

Bases: silf.backend.client.labdata.LabdataException

Thrown when we find labdata with invalid content.

class silf.backend.client.labdata.LabdataBase(xml=None, parent=None, type=None, suite=None, id=None)

Bases: sleekxmpp.xmlstream.stanzabase.ElementBase

body_as_dict
content
get_body()
has_body
insert_into_message(msg)

Insert Labdata substanza into message

interfaces = {'id', 'type', 'body'}
name = 'labdata'
namespace = 'silf'
plugin_attrib = 'labdata'
set_body(value)

Set body of labdata element in XML: <labdata …> HERE </labdata>

set_type(value)

Set type of Labdata substanza, ‘result’, ‘ack’ and ‘done’ need ‘id’ attribute set before type

sub_interfaces = set()
suite
suite_type
type
exception silf.backend.client.labdata.LabdataException

Bases: Exception

Base class for all Labdata stanza related exceptions

class silf.backend.client.labdata.LabdataExperimentStop(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:experiment:stop'
plugin_attrib = 'silf:experiment:stop'
class silf.backend.client.labdata.LabdataLangSet(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:lang:set'
plugin_attrib = 'silf:lang:set'
class silf.backend.client.labdata.LabdataMatcher(lnamespace=None, ltype=None)

Bases: sleekxmpp.xmlstream.matcher.base.MatcherBase

match(stanza)

Test if message stanza contains proper labdata substanza, check if labdata satisfies conditions set in __init__

class silf.backend.client.labdata.LabdataMiscError(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:misc:error'
plugin_attrib = 'silf:misc:error'
class silf.backend.client.labdata.LabdataModeGet(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:mode:get'
plugin_attrib = 'silf:mode:get'
class silf.backend.client.labdata.LabdataModeSet(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:mode:set'
plugin_attrib = 'silf:mode:set'
class silf.backend.client.labdata.LabdataResults(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:results'
plugin_attrib = 'silf:results'
class silf.backend.client.labdata.LabdataSeriesStart(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:series:start'
plugin_attrib = 'silf:series:start'
class silf.backend.client.labdata.LabdataSeriesStop(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:series:stop'
plugin_attrib = 'silf:series:stop'
class silf.backend.client.labdata.LabdataSettingsCheck(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:settings:check'
plugin_attrib = 'silf:settings:check'
class silf.backend.client.labdata.LabdataSettingsUpdate(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:settings:update'
plugin_attrib = 'silf:settings:update'
class silf.backend.client.labdata.LabdataState(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:state'
plugin_attrib = 'silf:state'
class silf.backend.client.labdata.LabdataVersionGet(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:protocol:version:get'
plugin_attrib = 'silf:protocol:version:get'
class silf.backend.client.labdata.LabdataVersionSet(xml=None, parent=None, type=None, suite=None, id=None)

Bases: silf.backend.client.labdata.LabdataBase

namespace = 'silf:protocol:version:set'
plugin_attrib = 'silf:protocol:version:set'
exception silf.backend.client.labdata.NoLabdataException

Bases: silf.backend.client.labdata.LabdataException

Thrown when reply_labdata() is called on message without labdata substanza.

exception silf.backend.client.labdata.NoLabdataIdException

Bases: silf.backend.client.labdata.LabdataException

Thrown when user tries to set labdata type to ack, result or done wtithout setting id first

exception silf.backend.client.labdata.NoLabdataNamespaceException

Bases: silf.backend.client.labdata.LabdataException

Thrown when matcher encounters labdata stanza without namespace

exception silf.backend.client.labdata.UnknownLabdataNamespaceException

Bases: silf.backend.client.labdata.LabdataException

Thrown when user tries to format_labdata with namespace not in LABDATA_NAMESPACES

exception silf.backend.client.labdata.UnknownLabdataTypeException

Bases: silf.backend.client.labdata.LabdataException

Thrown when user tries to create labdata of unknown type (not in LABDATA_TYPES array)

silf.backend.client.labdata.check_labdata(stanza)

Check if stanza is valid labdata

silf.backend.client.labdata.check_message_labdata(stanza)

Check if message contains valid labdata

silf.backend.client.labdata.format_labdata(lnamespace, ltype='ack', lid=None, lbody=None, suite=None)

Prepare labdata stanza using supplied data

silf.backend.client.labdata.labdata_subclass

alias of LabdataSettingsUpdate

silf.backend.client.labdata.reply_labdata(labdata, ltype='ack', lbody=None)

Reply to labdata stanza (create another labdata)

labdata_sender Module

class silf.backend.client.labdata_sender.LabdataSender(args, config='config/default.ini', section='Sender')

Bases: object

Connects to XMPP server, joins groupchat, sends single labdata stanza with parameters specified by command line args and disconnects. Used only for labdata/Client debugging

initialize(block=True)
muc_message(msg)
send_labdata(stanza)
start(event)

listener Module

class silf.backend.client.listener.Listener(config, section='Listener')

Bases: object

Test XMPP client - Sits quietly on room and dumps everything it hears to stdout Used oinly for labdata/Client debugging

disconnect()
initialize(block)
muc_message(msg)
muc_online(presence)
start(event)

mock_client Module

class silf.backend.client.mock_client.Client(config='config/default.ini', section='Client', block=False, log_file=None)

Bases: silf.backend.client.api.IClient

>>> mock = Client()
>>> mock.sent_stanzas
{}

We are registering callback for all incoming stanzas:

>>> mock.register_callback(lambda x: print("Reclieved {}".format(x)))
>>> mock.fire_synthetic_event( "silf:mode:get", "query")
Reclieved <IncomingStanza type='query' namespace='silf:mode:get' contents=<<empty>> >

Sent stanzas contains stanzas sent by this client so it is empty:

>>> mock.sent_stanzas
{}
>>> mock.register_send_callback(lambda  x: print("Sent {}".format(x)))
>>> mock.send_standalone_stanza('silf:experiment:stop')
Sent <IncomingStanza type='data' namespace='silf:experiment:stop' contents=<<empty>> >
>>> mock.sent_stanzas
{'silf:experiment:stop': [<labdata data silf:experiment:stop None>]}
assert_and_get_stanza(namespace, types=None)

Returns stanza with proper namespace and type if it was sent by the remote side (experiment)

Parameters:
  • namespace (string) – What namespace to return
  • types (list) – Is not None we will return only stanzas of specified type. This can be either a string or a list.
Raises:

AssertionError – If stanza was not found

Returns:

Specified labdata

Return type:

LabdataBase

assert_no_errors()
clear_client()
copy() → silf.backend.client.mock_client.Client
disconnect(wait=None)
fire_synthetic_event(namespace, type, contents=None, suite=None)
Parameters:incoming_stanza (LabdataState) – Event to be fired.
Returns:
get_and_clear_errors()
get_stanza(namespace, types=None)
initialize()
register_error_callback(func)
register_send_callback(callback, *, type=None, namespace=None)
send_labdata(labdata)
sent_errors
sent_stanzas
class silf.backend.client.mock_client.TestClient(config='config/default.ini', section='Client', block=False, log_file=None)

Bases: silf.backend.client.mock_client.Client

send_set_mode(mode)
send_set_mode_and_wait(mode, wait=2)
send_start_series(**kwargs)
send_start_series_and_wait(settings, wait_time=3)
send_stop_experiment()
send_stop_experiment_and_wait(wait_time=3)
send_stop_series()
send_stop_series_and_wait(wait_time=5)
watch_for_results(max_time, on_result_callback=None, do_print=False)

api Package

api Package

const Module

exceptions Module

exception silf.backend.commons.api.exceptions.ConfigurationException

Bases: silf.backend.commons.api.exceptions.SilfException

exception silf.backend.commons.api.exceptions.DeviceException(message, fixable_by_user=False, **kwargs)

Bases: silf.backend.commons.api.exceptions.SilfException

Exception raised by the device.

fixable_by_user = None

bool. True if this erroc can be fixed by user.

exception silf.backend.commons.api.exceptions.DeviceRuntimeException

Bases: silf.backend.commons.api.exceptions.SilfException

Wrapper for any exception raised by the device.

exception silf.backend.commons.api.exceptions.DiagnosticsException

Bases: silf.backend.commons.api.exceptions.DeviceRuntimeException

Thrown if intialization of this device encounters an error.

exception silf.backend.commons.api.exceptions.ExperimentBackendUnresponsive

Bases: silf.backend.commons.api.exceptions.SILFProtocolError

exception silf.backend.commons.api.exceptions.ExperimentCreationError

Bases: silf.backend.commons.api.exceptions.ConfigurationException

Raised when we can’t create an experiment

exception silf.backend.commons.api.exceptions.InvalidModeException

Bases: silf.backend.commons.api.exceptions.SilfException

Raised when experiment or device wrapper is in invalid mode.

exception silf.backend.commons.api.exceptions.InvalidStateException(message, fixable_by_user=False, **kwargs)

Bases: silf.backend.commons.api.exceptions.DeviceException

Raised when device is in invalid state

exception silf.backend.commons.api.exceptions.MissingRequiredControlException(errors)

Bases: silf.backend.commons.api.exceptions.ValidationError

exception silf.backend.commons.api.exceptions.SILFProtocolError(errors)

Bases: silf.backend.commons.api.exceptions.SilfException

Exception that should be propagated to the enduser, it should be caught by the experiment class and trensformed to error stanza.

classmethod from_args(severity, error_type, message, field=None, **kwargs)
>>> raise SILFProtocolError.from_args("error", "system", "Test error") 
Traceback (most recent call last):
exceptions.SILFProtocolError: Test error

Constructs SILFProtocolError from Error constructed from args and kwargs. :rtype: SILFProtocolError

classmethod join(*args)

Constructs SILFProtocolError from other SILFProtocolError newly created exception will contain all errors from exception it was created from. :param args: Iterable of SILFProtocolError :rtype: SILFProtocolError

exception silf.backend.commons.api.exceptions.SerializationException

Bases: silf.backend.commons.api.exceptions.SilfException

Exception raised during serialization to or from json. It normally signifies programming error.

exception silf.backend.commons.api.exceptions.SettingNonLiveControlException(errors)

Bases: silf.backend.commons.api.exceptions.ValidationError

exception silf.backend.commons.api.exceptions.SilfException

Bases: Exception

exception silf.backend.commons.api.exceptions.ValidationError(errors)

Bases: silf.backend.commons.api.exceptions.SILFProtocolError

Subpackages

stanza_content Package
stanza_content Package
_error Module
class silf.backend.commons.api.stanza_content._error.Error(severity, error_type, message, field=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Represents error message.

>>> err = Error("warning", "device", "foo")
>>> err
<Error severity='warning' error_type='device' metadata='[('message', 'foo')]'>
>>> err.field = "Foo"
>>> err
<Error severity='warning' error_type='device' metadata='[('field_name', 'Foo'), ('message', 'foo')]'>
>>> err.__getstate__() == {'metadata': {'field_name': 'Foo', 'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'}
True
>>> Error.from_dict(err.__getstate__()) == err
True
DEFAULTS = {'metadata': {}}
DICT_CONTENTS = {'metadata', 'severity', 'error_type'}
field

Field to which this error is attached

message
class silf.backend.commons.api.stanza_content._error.ErrorSuite(errors=None)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

>>> err = ErrorSuite(errors=[
... Error("warning", "device", "foo"), Error("warning", "device", "bar")
... ])
>>> err.__getstate__() == {'errors': [{'metadata': {'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'}, {'metadata': {'message': 'bar'}, 'severity': 'warning', 'error_type': 'device'}]}
True
>>> err.setstate({'errors': []})
>>> err.errors
[]
>>> err.__setstate__({'errors': [{'metadata': {'message': 'foo'}, 'severity': 'warning', 'error_type': 'device'}, {'metadata': {'message': 'bar'}, 'severity': 'warning', 'error_type': 'device'}]})
>>> err.errors
[<Error severity='warning' error_type='device' metadata='[('message', 'foo')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'bar')]'>]
>>> ErrorSuite.from_dict(err.__getstate__()) == err
True
DICT_CONTENTS = {'errors'}
getstate()
classmethod join(*args)

Returns ErrorSuite constructed from all errors in *args.

>>> error1 = Error("warning", "device", "foo")
>>> error2 = Error("warning", "device", "bar")
>>> error3 = Error("warning", "device", "baz")
>>> ErrorSuite.join(ErrorSuite(errors=[error1, error2]), ErrorSuite(errors=[error3]))
<ErrorSuite errors=[<Error severity='warning' error_type='device' metadata='[('message', 'foo')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'bar')]'>, <Error severity='warning' error_type='device' metadata='[('message', 'baz')]'>] >
Parameters:args – List of ErrorSuite.
Return type:ErrorSuite
setstate(state)
_json_mapper Module

Does serialization to json.

Serialization example

>>> from silf.backend.commons.api import *
>>> import json
>>> selection = ModeSelection(mode = "foo")
>>> json.dumps(selection.__getstate__())
'{"mode": "foo"}'

Deserialization example

>>> ModeSelection.from_dict(json.loads('{"mode": "foo"}'))
<ModeSelection mode=foo>
exception silf.backend.commons.api.stanza_content._json_mapper.SerializationException

Bases: silf.backend.commons.api.exceptions.SilfException

Exception raised during serialization to or from json. It normally signifies programming error.

class silf.backend.commons.api.stanza_content._json_mapper.FromStateMixin

Bases: object

classmethod from_dict(state)

Creates instance of this class using provided state dictionatry :param state: state from which to create instance :return: Created instance :rtype:cls

class silf.backend.commons.api.stanza_content._json_mapper.JsonMapped(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FromStateMixin

Json class that has well-known set of properties. Each property has known type (that can optionally be checked).

This class should be subclassed like that:

>>> class TmpTestJsonMapped(JsonMapped):
...
...   DICT_CONTENTS = {'foo'}
...
...   def __init__(self, foo):
...      super().__init__()
...      self.foo = foo
>>> foo = TmpTestJsonMapped(foo="bar")
>>> foo.__getstate__()
{'foo': 'bar'}

This class can automagically set atributes in __init__ method

>>> class TmpTestJsonMapped(JsonMapped):
...
...   DICT_CONTENTS = {'foo'}
...
...   def __init__(self, **kwargs):
...      super().__init__(**kwargs)
>>> foo = TmpTestJsonMapped(foo="bar")
>>> foo.__getstate__()
{'foo': 'bar'}
>>> foo = TmpTestJsonMapped(bar="bar")
Traceback (most recent call last):
ValueError: Invalid __init__ parameter bar
DEFAULTS = {}

Dictionaty containing default values for some of the properties.

DICT_CONTENTS = set()

Defines set of properties that need to be in state dicts (for both serialization and deserialization)

getstate()

Iternal function that should be overriden, it returns state dictionary, that will be transformed to json:

>>> foo = TestJsonMapped(foo="bar")
>>> foo.getstate() == {'foo': 'bar'}
True

Default implementation just filters the __dict__ property, so any additional items are removed:

>>> foo.bar = 'baz'
>>> foo.getstate() == {'foo': 'bar'}
True
setstate(state)

Iternal function that should be overriden by subclasses.

Note

This function should not be called directly, rather use __setstate__() that checks state dicitonary for invalid keys.

Default implementation just does:

for k, v in state.items():
    setattr(self, k, v)
>>> foo = TestJsonMapped("foo")

This function performs no checks whatsoever!

>>> foo.setstate({'foo': 'bar', 'bar': 'baz'})
>>> foo.bar
'baz'
class silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FromStateMixin

Represents type that serializes to json object that can contain any property, but all properties are of the same type.

>>> class TmpTestFlatDictJsonMapped(FlatDictJsonMapped):
...    INTERNAL_DICT_NAME = "foos"
...    INTERNAL_VALUE_CLASS = TestJsonMapped

Init method can take optional keyword named as INTERNAL_DICT_NAME >>> flat = TmpTestFlatDictJsonMapped( … foo = TestJsonMapped(foo=’bar’), … bar = TestJsonMapped(foo=’baz’)) >>> sorted(flat.foos.items()) # doctest: +ELLIPSIS [(‘bar’, <Foo foo=baz>), (‘foo’, <Foo foo=bar>)]

It implements get state:

>>> flat.__getstate__() == {'foo': {'foo': 'bar'}, 'bar': {'foo': 'baz'}}
True
>>> flat.foos = {}
>>> flat.foos
{}

And set state:

>>> flat.__setstate__({'foo': {'foo': 'bar'}, 'bar': {'foo': 'baz'}})
>>> sorted(flat.foos.items()) 
[('bar', <Foo foo=baz>), ('foo', <Foo foo=bar>)]
INTERNAL_DICT_NAME = None

Name of property that contains internal dictionary that will hold mapping.

INTERNAL_VALUE_CLASS = None

Class to which all properties will be deserialized

deserialize_internal_item(item)

Deserializes :attr:INTERNAL_VALUE_CLASS. by default it calls item.from_dict.

get(*args, **kwargs)
internal_dict
classmethod join(*args)

Joins series of instances of this class into single instance, checking for eventual duplicate keys;

>>> from silf.backend.commons.api import ResultSuite, Result
>>> a = ResultSuite(foo=Result(value=[5]), foobar=Result(value=[1, 2, 3]))
>>> b = ResultSuite(bar=Result(value=[10]), barbar=Result(value=[1, 2, 3]))
>>> ResultSuite.join(a, b)
<ResultSuite bar=<Result pragma=append value=[10] > barbar=<Result pragma=append value=[1, 2, 3] > foo=<Result pragma=append value=[5] > foobar=<Result pragma=append value=[1, 2, 3] > >
>>> ResultSuite.join(a, b, a) 
Traceback (most recent call last):
ValueError: We are joining instances of <class 'silf.backend.commons.api.misc.ResultSuite'>, these instances should not contain     duplicate data, however they did. Duplicate keys were ['foo', 'foobar'].     Debug data is: [{"foo": {"pragma": "append", "value": [5]}, "foobar": {"pragma": "append", "value": [1, 2, 3]}}, {"bar": {"pragma": "append", "value": [10]}, "barbar": {"pragma": "append", "value": [1, 2, 3]}}, {"foo": {"pragma": "append", "value": [5]}, "foobar": {"pragma": "append", "value": [1, 2, 3]}}].
serialize_internal_item(item)

Serializes :attr:INTERNAL_VALUE_CLASS. by default it calls item.__getstate__.

_misc Module
class silf.backend.commons.api.stanza_content._misc.Setting(value=None, current=False)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Single setting object. It serializes to following JSON:

>>> setting = Setting(value=15.4, current=True)

Serialization format

>>> setting.__getstate__() == {'current': True, 'value': 15.4}
True
>>> setting.__setstate__({'current': True, 'value': 15.4})
>>> setting.current
True
>>> setting.value
15.4

It can deserialize from dictionary without current item: >>> setting.__setstate__({‘value’: 15.4}) >>> setting.current False >>> setting.value 15.4

Check equality:

>>> setting2 = Setting.from_dict(setting.__getstate__())
>>> setting2 == setting
True

Setting incorrect type for current raises an exception >>> setting = Setting(value=15.4, current=”dupa”) Traceback (most recent call last): silf.backend.commons.api.exceptions.SerializationException: Current property must be either True or False >>> setting.__setstate__({“value” :5, ‘current’:”dupa”}) Traceback (most recent call last): silf.backend.commons.api.exceptions.SerializationException: Current property must be either True or False

DEFAULTS = {'current': False}
DICT_CONTENTS = {'current', 'value'}
class silf.backend.commons.api.stanza_content._misc.SettingSuite(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped

Contains a set of settings.

Serializes to following JSON:

>>> settings_set = SettingSuite(
...    foo = Setting(value = 13),
...    bar = Setting(value= "Kotek!", current=True)
... )

<SettingSuite bar=<Setting current=True value=Kotek! > foo=<Setting current=False value=13 > >

Serialization format
>>> settings_set.__getstate__() == {'foo': {'current': False, 'value': 13}, 'bar': {'current': True, 'value': 'Kotek!'}}
True

Test equality:

>>> setting_set2 = SettingSuite.from_dict(settings_set.__getstate__())
>>> setting_set2 == settings_set
True
INTERNAL_DICT_NAME = 'settings'
INTERNAL_VALUE_CLASS

alias of Setting

settings = {}
class silf.backend.commons.api.stanza_content._misc.InputFieldSuite(controls=(), **kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped

Contains a set of controls.

>>> from silf.backend.commons.api import *
>>> control_set = InputFieldSuite(
...     controls = [NumberControl("foo", "Insert foo"),
...                 NumberControl("bar", "Insert bar")]
... )
>>> control_set.__getstate__() == {
...     'foo': {'validations': {}, 'live': False, 'type': 'number', 'name': 'foo', 'metadata': {'label': 'Insert foo'}, "order": 0},
...     'bar': {'live': False, 'type': 'number', 'name': 'bar', 'metadata': {'label': 'Insert bar'}, 'validations': {}, "order": 1}
... }
True

Test equality:

>>> InputFieldSuite.from_dict(control_set.__getstate__()) == control_set
True
>>> control_set.__setstate__({"foo" : 15})
Traceback (most recent call last):
ValueError: Can deserialize only dict, got 15 (type: <class 'int'>)
INTERNAL_DICT_NAME = 'controls'
INTERNAL_VALUE_CLASS = None
controls = {}
deserialize_internal_item(item)
serialize_internal_item(item)
class silf.backend.commons.api.stanza_content._misc.OutputField(html_class, name, label=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Represents a Result field.

Default values are applied when constructing, and while using __setstate__

>>> result = OutputField("chart", "betaArray")
>>> result
<Result class=chart name=['betaArray'] type=array metadata={} settings={}>
>>> result.__setstate__({"class": "chart", "name": ["betaArray"]})
>>> result
<Result class=chart name=['betaArray'] type=array metadata={} settings={}>
>>> result.settings
{}

Some metadata properties can ve accessed as properties:

>>> result.label = "foo"
>>> result
<Result class=chart name=['betaArray'] type=array metadata={'label': 'foo'} settings={}>

Json property named class is avilable as html_class property

>>> result.html_class = "integer-indicator"

Serialization format

>>> result.__getstate__() == {'metadata': {'label': 'foo'}, 'class': 'integer-indicator', 'settings': {}, 'type': 'array', 'name': ['betaArray']}
True
>>> result.html_class
'integer-indicator'

Check equality:

>>> OutputField.from_dict(result.__getstate__()) == result
True
>>> result = OutputField("chart", "betaArray")
>>> result.__getstate__() == {'class': 'chart', 'name': ['betaArray'], 'metadata': {}, 'type': 'array', 'settings': {}}
True
>>> result.setstate( {'class': 'chart', 'name': ['gammaArray'], 'metadata': {}, 'type': 'array', 'settings': {}})
>>> result
<Result class=chart name=['gammaArray'] type=array metadata={} settings={}>
DEFAULTS = {'type': 'array', 'metadata': {}, 'settings': {}}
DICT_CONTENTS = {'type', 'metadata', 'class', 'name', 'settings'}
getstate()

Override parent implementation to dynamically translate labels inside of metadata dict :return:

html_class
label
class silf.backend.commons.api.stanza_content._misc.ChartField(html_class, name, label=None, axis_x_label=None, axis_y_label=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._misc.OutputField

axis_x_label
axis_y_label
getstate()

Override parent implementation to dynamically translate labels inside of metadata dict :return:

class silf.backend.commons.api.stanza_content._misc.OutputFieldSuite(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped

Holds suite of output results:

>>> suite = OutputFieldSuite(foo = OutputField("foo", ["foo"]))
>>> suite.output_fields
{'foo': <Result class=foo name=['foo'] type=array metadata={} settings={}>}
>>> suite.__getstate__() ==  {'foo': {'class': 'foo', 'metadata': {}, 'name': ['foo'], 'settings': {}, 'type': 'array'}}
True

Check equality:

>>> suite == OutputFieldSuite.from_dict(suite.__getstate__())
True
>>> suite.__setstate__({'bar': {'class': 'foo', 'metadata': {}, 'name': ['bar'], 'settings': {}, 'type': 'array'}})
>>> suite.output_fields
{'bar': <Result class=foo name=['bar'] type=array metadata={} settings={}>}

TEST ERRORS

>>> suite = OutputFieldSuite(foo = OutputField("foo", ["foo", "baz"]))
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ConfigurationException: Following errors encountered: ["Error --- control foo takes input from more than one result (that is: ['foo', 'baz']) this is disallowed"]
INTERNAL_DICT_NAME = 'output_fields'
INTERNAL_VALUE_CLASS

alias of OutputField

output_fields = None
class silf.backend.commons.api.stanza_content._misc.Mode(label, description=None, order=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Encapsulates single mode.

>>> mode = Mode("foo", description="Foo mode", order=1)
>>> mode.__getstate__() == {'description': 'Foo mode', 'label': 'foo', 'order': 1}
True
>>> mode.setstate({'description': 'Bar mode', 'label': 'bar'})
>>> mode
<Mode 'bar' description='Bar mode'>

Check Equality:

>>> mode == Mode.from_dict(mode.__getstate__())
True
DEFAULTS = {'order': None, 'description': None}
DICT_CONTENTS = {'order', 'label', 'description'}
getstate()
class silf.backend.commons.api.stanza_content._misc.ModeSuite(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped

Encapsulates suite of modes.

>>> suite = ModeSuite(foo= Mode("Foo Mode", "descr"), bar = Mode("Bar Mode", "descr"))
>>> suite.__getstate__() == {
...     'foo': {'label': 'Foo Mode', 'description': 'descr', 'order': None},
...     'bar': {'label': 'Bar Mode', 'description': 'descr', 'order': None}
... }
True
>>> suite.__setstate__({'bar': {'label': 'Bar Mode', 'description': 'descr'}})
>>> suite.modes
{'bar': <Mode 'Bar Mode' description='descr'>}

Check equality:

>>> suite == ModeSuite.from_dict(suite.__getstate__())
True

Nice example:

>>> suite = ModeSuite(time = Mode("Pomiar czasu", "Mierz zliczenia, i kończ pomiar po upływie ustalonego czasu.", order=0), counts = Mode("Pomiar zliczeń", "Mierz czas potrzebny do uzyskania ustalonej liczby zliczeń", order=1))
>>> suite.__getstate__() == {
...       'counts': {
...               'description': 'Mierz czas potrzebny do uzyskania ustalonej liczby zliczeń',
...               'label': 'Pomiar zliczeń',
...               'order' : 1
...       },
...       'time': {
...               'description': 'Mierz zliczenia, i kończ pomiar po upływie ustalonego czasu.',
...               'label': 'Pomiar czasu',
...               'order': 0
...             }
...       }
True
INTERNAL_DICT_NAME = 'modes'
INTERNAL_VALUE_CLASS

alias of Mode

modes = {}
class silf.backend.commons.api.stanza_content._misc.ModeSelection(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Encapsulates mode selection:

>>> mode = ModeSelection(mode = "foo")
>>> mode.__getstate__()
{'mode': 'foo'}
>>> mode.setstate( {'mode': 'bar'})
>>> mode.mode
'bar'

Check Equality:

>>> mode == ModeSelection.from_dict(mode.__getstate__())
True
DICT_CONTENTS = {'mode'}
class silf.backend.commons.api.stanza_content._misc.ModeSelected(mode, experimentId=None, settings=None, resultDescription=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Response after user selects mode:

>>> from silf.backend.commons.api import ControlSuite, NumberControl
>>> from silf.backend.commons.api.stanza_content._misc import *
>>> c = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10),
...   NumberControl("bar", "Insert bar")
... )
>>> resp = ModeSelected("foo", 'dfae1382-1746-4b5e-851e-93dff62b01ba',
...  settings=c.to_input_field_suite(),
...  resultDescription=OutputFieldSuite(betaArray = OutputField("array", name=["betaArray"]))
... )
>>> resp.resultDescription
<OutputFieldSuite betaArray=<Result class=array name=['betaArray'] type=array metadata={} settings={}> >
>>> resp.__getstate__() == {
...     'settings': {
...         'foo': {'validations': {'max_value': 10}, 'live': False, 'metadata': {'label': 'Insert foo'}, 'name': 'foo', 'type': 'number', "order": 0},
...         'bar': {'live': False, 'metadata': {'label': 'Insert bar'}, 'name': 'bar', 'type': 'number', 'validations': {}, "order": 1}
...          },
...      'experimentId': 'dfae1382-1746-4b5e-851e-93dff62b01ba',
...      'mode': 'foo',
...      'resultDescription': {
...         'betaArray': {'metadata': {}, 'class': 'array', 'settings': {}, 'name': ['betaArray'], 'type': 'array'}}}
True

Check Equality:

>>> resp == ModeSelected.from_dict(resp.__getstate__())
True
>>> resp = ModeSelected("foo", 'dfae1382-1746-4b5e-851e-93dff62b01ba',
...  settings=c,
...  resultDescription=OutputFieldSuite(betaArray = OutputField("array", name=["betaArray"]))
... )
DICT_CONTENTS = {'resultDescription', 'experimentId', 'mode', 'settings'}
getstate()
setstate(state)
class silf.backend.commons.api.stanza_content._misc.SeriesStartedStanza(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Message sent when we start the single measurement session..

>>> settings_set = SettingSuite(
...    foo = Setting(value = 13),
...    bar = Setting(value= "Kotek!", current=True)
... )
>>> series_started = SeriesStartedStanza(seriesId = "fac96014-e88f-4bab-af2c-32fd0cf14d20", initialSettings=settings_set, label="Seria z foo=12")
>>> series_started
<SeriesStartedStanza initialSettings=<SettingSuite bar=<Setting current=True value=Kotek! > foo=<Setting current=False value=13 > > metadata={'label': 'Seria z foo=12'} seriesId=fac96014-e88f-4bab-af2c-32fd0cf14d20 >
>>> series_started.__getstate__() == {'initialSettings': {'foo': {'current': False, 'value': 13}, 'bar': {'current': True, 'value': 'Kotek!'}}, 'metadata': {'label': 'Seria z foo=12'}, 'seriesId': 'fac96014-e88f-4bab-af2c-32fd0cf14d20'}
True

Test equality:

>>> series_started == SeriesStartedStanza.from_dict(series_started.__getstate__())
True
DEFAULTS = {'metadata': {}}
DICT_CONTENTS = {'initialSettings', 'metadata', 'seriesId'}
getstate()
initialSettings = <SettingSuite >
label
seriesId = ''
setstate(state)
class silf.backend.commons.api.stanza_content._misc.Result(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Single result type.

>>> res = Result(value = [1, 2, 3, 4], pragma = "transient")
>>> res.__getstate__() == {'pragma': 'transient', 'value': [1, 2, 3, 4]}
True
>>> res.__setstate__({"value" : "foobar"})
>>> res
<Result pragma=append value=foobar >
>>> res.__getstate__() ==  {'pragma': 'append', 'value': 'foobar'}
True
>>> res = Result(value = [[1, 1], [2, 2]], pragma="transient")
>>> res.__getstate__() == {'pragma': 'transient', 'value': [[1, 1], [2, 2]]}
True
DEFAULTS = {'pragma': 'append'}
DEFAULT_PRAGMA = 'append'
DICT_CONTENTS = {'pragma', 'value'}
class silf.backend.commons.api.stanza_content._misc.ResultSuite(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.FlatDictJsonMapped

Set of results.

>>> suite = ResultSuite(foo = Result(value = [1, 2, 3, 4]), bar = Result(value = True))
>>> suite.__getstate__() == {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}}
True
INTERNAL_DICT_NAME = 'results'
INTERNAL_VALUE_CLASS

alias of Result

class silf.backend.commons.api.stanza_content._misc.ResultsStanza(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Message containing a set of results sent to user.

>>> results_stanza = ResultsStanza(seriesId = "96beeb7e-3209-400a-892a-bf3201aa4658", results = ResultSuite(foo = Result(value = [1, 2, 3, 4]), bar = Result(value = True)))
>>> results_stanza.__getstate__() == {'seriesId': '96beeb7e-3209-400a-892a-bf3201aa4658', 'results': {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}}}
True
>>> results_stanza2 = ResultsStanza.from_dict({'seriesId': '96beeb7e-3209-400a-892a-bf3201aa4658', 'results': {'foo': {'value': [1, 2, 3, 4], 'pragma': 'append'}, 'bar': {'value': True, 'pragma': 'append'}}})
>>> results_stanza == results_stanza2
True
DICT_CONTENTS = {'seriesId', 'results'}
getstate()
setstate(state)
class silf.backend.commons.api.stanza_content._misc.CheckStateStanza(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Response for checking of experiment state.

>>> state = CheckStateStanza(state = "off")
>>> state.__getstate__() == {'seriesId': None, 'state': 'off', 'experimentId': None}
True
>>> state = CheckStateStanza(state = "off", experimentId = 'f5d1762e-f528-4ca1-bee4-97512c22ae6a', seriesId = 'eeb23307-69dc-4745-83da-db22f3dfa557')
>>> state.__getstate__() == {'seriesId': 'eeb23307-69dc-4745-83da-db22f3dfa557', 'state': 'off', 'experimentId': 'f5d1762e-f528-4ca1-bee4-97512c22ae6a'}
True

Check equality:

>>> state == CheckStateStanza.from_dict(state.__getstate__())
True
DEFAULTS = {'seriesId': None, 'experimentId': None}
DICT_CONTENTS = {'experimentId', 'seriesId', 'state'}
class silf.backend.commons.api.stanza_content._misc.SeriesStopped(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Sent when someone stops series

>>> state = SeriesStopped(seriesId = "f5d1762e-f528-4ca1-bee4-97512c22ae6a")
>>> state.__getstate__() == {'seriesId': 'f5d1762e-f528-4ca1-bee4-97512c22ae6a'}
True
DEFAULTS = {'seriesId': None}
DICT_CONTENTS = {'seriesId'}
class silf.backend.commons.api.stanza_content._misc.VideoElement(video_type=None, camera_url=None, label=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content._misc.OutputField

>>> video = VideoElement("image", "http://someserver/path")
>>> video.__getstate__() == {'name': [], 'type': 'array', 'metadata': {}, 'settings': {'camera_type': 'image', 'camera_url': 'http://someserver/path'}, 'class': 'camera'}
True
camera_type
camera_url
class silf.backend.commons.api.stanza_content._misc.ProtocolVersions(version_list)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

DEFAULTS = {'versions': None}
DICT_CONTENTS = {'versions'}
class silf.backend.commons.api.stanza_content._misc.ProtocolVersion(**kwargs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

DEFAULTS = {'version': None}
DICT_CONTENTS = {'version'}
class silf.backend.commons.api.stanza_content._misc.Language(lang)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Represents single language chosen by experiment in which user data is translated

DICT_CONTENTS = {'lang'}
class silf.backend.commons.api.stanza_content._misc.Languages(langs)

Bases: silf.backend.commons.api.stanza_content._json_mapper.JsonMapped

Represents stanza with list of possible languages used by user

DICT_CONTENTS = {'langs'}

control Package

control Package

_const Module

_control_api Module

class silf.backend.commons.api.stanza_content.control._control_api.Control(name, label, type, style=None, default_value=None, live=False, validators=None, required=True, description=None, order=None)

Bases: object

Class representing a input field in WEB UI.

>>> c = Control("foo", "Foo label", "Foo type")
>>> c
<Control name=foo type=Foo type live=False label='Foo label'>
>>> c.to_json_dict() == {'type': 'Foo type', 'metadata': {'label': 'Foo label'}, 'live': False, 'name': 'foo', 'order': None}
True
>>> c.default_value=3
>>> c.to_json_dict() ==  {'default_value': 3, 'name': 'foo', 'live': False, 'metadata': {'label': 'Foo label'}, 'type': 'Foo type', 'order': None}
True
>>> c.style = "indicator"
>>> c.description = "Foo Bar"
>>> c.to_json_dict() == {'name': 'foo', 'default_value': 3, 'type': 'Foo type', 'live': False, 'metadata': {'style': 'indicator', 'label': 'Foo label', 'description': 'Foo Bar'}, 'order': None}
True
convert_json_to_python(value)

Converts python value to json representation of this value.

Parameters:value (object) – Unparsed value reclieved from json schema.

Note

Do not override this method, rather override _json_to_python() as this method performs additional checks,

convert_python_to_json(value, omit_validation=False)

Converts json value to python object.

Note

Do not override this method, rather override _python_to_json() as this method performs additional checks,

get_value_type()
html_class
id = None

Html id of the field. Defaults to "id_{}".format(self.name)

live = None

Value of data-live attribute. This value governs when settings from this field are sent. Defaults to False

name

Html name of the field. Required.

post_construct_validate()

Overriden to perform any post-construction validation.

style = None

HTML class of the field. Defaults to None.

to_json_dict()
validators = None

List of validators attached to this field. Validators are python callables that should raise silf.backend.commons.api.error.SILFProtocolError.

value_is_empty(raw_value)

Checks whether raw_value shoule be considered empty. :param object raw_value: Unparsed value reclieved from json schema. :return:

_controls Module

class silf.backend.commons.api.stanza_content.control._controls.IntegerControl(name, label, type='number', style=None, default_value=None, live=False, validators=None, required=True, description=None, min_value=None, max_value=None, step=None)

Bases: silf.backend.commons.api.stanza_content.control._controls.NumberControl

class silf.backend.commons.api.stanza_content.control._controls.NumberControl(name, label, type='number', style=None, default_value=None, live=False, validators=None, required=True, description=None, min_value=None, max_value=None, step=None)

Bases: silf.backend.commons.api.stanza_content.control._control_api.Control

Control that take integer input, rendered as “number” field.

>>> control = NumberControl("foo", "Enter a number")
>>> control.to_json_dict() == {'type': 'number', 'name': 'foo', 'metadata': {'label': 'Enter a number'}, 'live': False, 'validations': {}, 'order': None}
True
>>> control.min_value = 1
>>> control.max_value = 10
>>> control.description = "Foo"
>>> control.style = "gauge"
>>> control.to_json_dict() ==  {
...     'type': 'number',
...     'validations': {'min_value': 1, 'max_value': 10},
...     'live': False,
...     'metadata': {'label': 'Enter a number', 'description': 'Foo', 'style': 'gauge'},
...     'name': 'foo',
...     'order': None}
True
>>> control.convert_json_to_python("5")
5.0
>>> control.convert_json_to_python("5.5")
5.5
>>> control.convert_json_to_python("aaa")
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Pole "Enter a number" powinno przyjmować wartość liczbową. Nie udało się skonwerować: aaa
do wartości liczbowej.
>>> control.convert_json_to_python(5)
5.0
>>> control.convert_json_to_python(11)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Enter a number" powinna być mniejsza niż 10 a wynosi 11.0.
>>> control.convert_json_to_python(-1)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Enter a number" powinna być większa niż 1 a wynosi -1.0.
get_value_type()
to_json_dict()
class silf.backend.commons.api.stanza_content.control._controls.TimeControlMinSec(name, label, type='interval', style=None, default_value=None, live=False, validators=None, required=True, description=None, max_time=None, min_time=datetime.timedelta(0))

Bases: silf.backend.commons.api.stanza_content.control._control_api.Control

Control that renders to silf-proprietary input field that allows input time deltas in format MM:SS.

>>> control = TimeControlMinSec("foo", "Insert time",
...    min_time=datetime.timedelta(seconds=30),
...    max_time=datetime.timedelta(days=60))
>>> control.to_json_dict() ==  {'type': 'interval', 'validations': {'max_value': 5184000.0, 'min_value': 30.0}, 'name': 'foo', 'metadata': {'label': 'Insert time'}, 'live': False, 'order': None}
True
>>> control.convert_json_to_python(75)
datetime.timedelta(0, 75)
>>> control.convert_python_to_json(datetime.timedelta(days = 10))
864000.0
>>> control.convert_json_to_python("bar")
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Podaj ilość sekund jako "Insert time". Nie udało się przekształcić tej wartości: "bar" na stałą ilczbową.
>>> control.convert_json_to_python(0)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Minimalny czas w polu "Insert time" wynosi 0:00:30. Ustawiono mniejszą wartość: 0:00:00.
>>> control.convert_python_to_json(datetime.timedelta(days = 60, seconds=1))
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Maksymalny czas w polu "Insert time" wynosi 60 days, 0:00:00. Ustawiono większą wartość: 60 days, 0:00:01.
get_value_type()
to_json_dict()
class silf.backend.commons.api.stanza_content.control._controls.BooleanControl(name, label, type='boolean', style=None, default_value=None, live=False, validators=None, required=True, description=None)

Bases: silf.backend.commons.api.stanza_content.control._control_api.Control

get_value_type()
class silf.backend.commons.api.stanza_content.control._controls.ComboBoxControl(name, label, item_type=<class 'str'>, choices=None, **kwargs)

Bases: silf.backend.commons.api.stanza_content.control._control_api.Control

choices
post_construct_validate()
to_json_dict()

_suite Module

class silf.backend.commons.api.stanza_content.control._suite.ControlSuite(*args)

Bases: object

check_and_convert_live_settings(setting_suite: silf.backend.commons.api.stanza_content._misc.SettingSuite, old_settings: dict) → dict
Checks settings and returns parsed values as a dictionaty.
>>> from silf.backend.commons.api import SettingSuite, Setting
>>> c = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10, live=True),
...   NumberControl("bar", "Insert bar", required=False)
... )
>>> old_settings = {'foo': 1.0, "bar":2.0}
>>> converted = c.check_and_convert_live_settings(SettingSuite(foo=Setting(5, False)), old_settings)
>>> converted == {'foo': 5.0, 'bar': 2.0}
True
>>> c.check_and_convert_live_settings(SettingSuite(bar=Setting(1, False)), old_settings)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Pole Insert bar zostało ustawione przez settings:update, ale nie zostało oznaczone jako live.
Parameters:
  • setting_suite
  • old_settings
Returns:

check_and_convert_settings(setting_suite)

Checks settings and returns parsed values as a dictionaty.

>>> from silf.backend.commons.api import SettingSuite, Setting
>>> c = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10),
...   NumberControl("bar", "Insert bar", required=False)
... )
>>> i = SettingSuite(
...    foo = Setting(1, False),
...    bar = Setting(2, False)
... )
>>> c.check_and_convert_settings(i) == {'bar': 2, 'foo': 1}
True
>>> i = SettingSuite(
...    foo = Setting(11, False),
...    bar = Setting(2, False)
... )
>>> c.check_and_convert_settings(i)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Insert foo" powinna być mniejsza niż 10 a wynosi 11.0.
>>> i = SettingSuite(
...    bar = Setting(9, True)
... )
>>> c.check_settings(i)
check_settings(setting_suite, ignore_unset_settings=True) → dict

Checks settings received from json and converts them to python domain.

>>> c = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10),
...   NumberControl("bar", "Insert bar")
... )
>>> from silf.backend.commons.api import SettingSuite, Setting
>>> i = SettingSuite(
...    foo = Setting(1, False),
...    bar = Setting(2, False)
... )
>>> c.check_settings(i)
>>> i = SettingSuite(
...    foo = Setting(11, True),
...    bar = Setting(2, False)
... )
>>> c.check_settings(i)
Traceback (most recent call last):
silf.backend.commons.api.exceptions.ValidationError: Wartość w polu "Insert foo" powinna być mniejsza niż 10 a wynosi 11.0.
>>> i = SettingSuite(
...    foo = Setting(9, True),
... )
>>> c.check_settings(i)
Parameters:
  • setting_suite (SettingSuite) – Settings read from json.
  • ignore_current_parameter (bool) – Whether this is called before applying settings (check_only``=False) or only to check them and display errors to user. If ``check_only is true this method might omit specific validations for fields that are not currently edited.
Raises:

ValidationError – If there is a validation error.

Returns:

None

controls = []
classmethod join(*args)
Parameters:args
Returns:
to_input_field_suite()

Converts this instance to misc.InputFieldSuite.

device_manager Package

High level overview

Conctete subclasses of IDeviceManager() represent a silf.backend.commons.device._device.Device to the ExperimentManager(), it has following responsibilities:

  • Manage modes of the Device
  • Store and return metadata that enables construction of experiment GUI
  • Aggregate and modify resuts from the Device to and return modivied results to the user.
  • Optionally separate Device inside its own subprocess.
Mode management

DeviceManager is aware of current mode as it gets it as a parameter for IDeviceManager.power_up() method.

Moreover methods that return metadata, also get mode as a parameter, in this case it means: “Get metadata for appripriate mode”.

Metadata management

Each DeviceManager should describe input fields it needs for particular mode. These input fields are returned from IDeviceManager.get_input_fields(). Output fielfs are retuned from IDeviceManager.get_output_fields().

Default DeviceManager implementations allow to define metadata declaratively: see: DefaultDeviceManager and SingleModeDeviceManager.

Result aggregation

All DeviceManager concrete implementation derive from ResultMixin that provides sensible way to aggregate results.

Basic device manager classes

class silf.backend.commons.device_manager._device_manager.IDeviceManager(experiment_name, config)

Bases: object

Instances of this class serve as proxy and translator between main experiment Experiment and Device.

TODO: add link do the experiment

It translated unfiltered input from user to input recognizable to device, it also contains metadata needed by the experiment.

applicable_modes

List of modes this device can be in. It is used purely for validation purposes, if we try to power_up() this device in a mode that is outside of applicable_modes exception should be raised.

If value returned is None it means that this device is mode agnostic,

Returns:Modes applicable for this device
Return type:list of strings or None.
apply_settings(settings)

Applies settings to the device.

Parameters:settings (dict) – settings to be applied. Can contain settings that are not interesting for this device, these should be filtered out. It contains raw input from json parser, values might need to be converted to python objects.
Raises:error.SILFProtocolError – if there are any errors in settings, or if there is any error during setting the equipement up.
check_settings(mode, settings)

Checks settings for a particular mode. This method is implemented, using native validation of class:.ControlSuite, using get_input_fields().

Parameters:
  • mode (str) – Mode for which settings are checked.
  • settings (SettingSuite) – Settings to be checked.
Raises:

SILFProtocolError – When settings are invalid

config_section = None

Section that customizes this device in config file

current_mode

Current mode this device is in. :return: Current mode :rtype: str

current_result_map

Current results, that is: all results produces in current series. This method returns a plain dictionary.

Returns:Current results
Return type:immutable dict
current_results

Current results, that is: all results produces in current series. This returns class that is serializable and ready to be sent. For developer friendly version use current_result_map().

This property is resetted during stop() method.

Returns:Current results
Return type:ResultSuite
current_settings

Currenty applied settings.

This property is resetted during power_down() method.

Returns:Currently applied settings. Mapping from setting name to setting value.
Return type:dict
device_name = 'default'

Name of the device. Should be unique in the experiment.

device_state

State of device managed by this manager.

get_input_fields(mode)

Returns _control suite for mode :param str mode: mode for which we get controls :rtype: ControlSuite

get_output_fields(mode)

Returns output fields for mode :param str mode: mode for which we get output fields

Returns:Returns output fields for mode
Return type:OutputFieldSuite
has_result(name)
Returns:Returns true if this instance has result named name in it’s current results. Result is considered valid if it’s value resolves to True (not None, if collection is must be not empty, etc).
Return type:bool
pop_results()

Return new results that could be sent to user. This method mutates the state of this instance, immidiately after a call to pop_results() next call will return empty ResultSuite.

Returns:Recently acquired results
Return type:ResultSuite
power_down(suspend=False)

Powers down the devie, if suspend is false it also cleans up the remote device kills and remote proces.

Parameters:suspend (bool) – If False will kill the remote process.
power_up(mode)

Powers up the device and sets it’s mode (if applicable).

Parameters:mode (str) – Mode to initialize device in.
query()

This method will be called periodically it should perform any neccesary communication with the device to refresh state of this instane.

It should exit in in less than 50ms.

running

If this device is running it means:

If this device is not running it means:

  • Results are None
Returns:Whether this device is running.
Return type:bool
start(callback=None)

Starts data acquisition for this device.

Parameters:callback – Optional callable to be called when devie is started.
state

State of this manager, may different from the device_state

Returns:State of this manager
Return type:str
stop()

Stops data acquisition for the device.

tearDown()

Cleans up this instance and releases all attached resources.

tear_down()

Cleans up this instance and releases all attached resources.

class silf.backend.commons.device_manager._device_manager.DefaultDeviceManager(*args, **kwargs)

Bases: silf.backend.commons.device_manager._device_manager.DeclarativeDeviceMixin, silf.backend.commons.device_manager._device_manager.DeviceWrapperMixin

To create instance of this class you need to specify following attributes:

>>> from silf.backend.commons_test.device.mock_voltimeter import MockVoltimeter
>>> class TestDeviceManager(DefaultDeviceManager):
...     DEVICE_CONSTRUCTOR = MockVoltimeter
...     DEVICE_ID = "voltimeter1"
...     CONTROL_MAP = {
...         "mode1" : ControlSuite(NumberControl("foo", "Foo Control")),
...         "mode2" : ControlSuite(NumberControl("foo", "Foo Control"))
...     }
...     OUTPUT_FIELD_MAP = {
...         "mode2" : OutputFieldSuite(),
...         "mode1" : OutputFieldSuite(bar = OutputField("foo", "bar"))
...     }
>>> mngr = TestDeviceManager("FooExperiment", None)
>>> sorted(mngr.applicable_modes)
['mode1', 'mode2']
>>> mngr.get_output_fields('mode1')
<OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> >
>>> mngr.get_input_fields('mode2')
<ControlSuite <Control name=foo type=number live=False label='Foo Control'>>
class silf.backend.commons.device_manager._device_manager.SingleModeDeviceManager(*args, **kwargs)

Bases: silf.backend.commons.device_manager._device_manager.SingleModeDeviceMixin, silf.backend.commons.device_manager._device_manager.DeviceWrapperMixin

To create instance of this class you need to specify following attributes:

>>> from silf.backend.commons_test.device.mock_voltimeter import MockVoltimeter
>>> class TestDeviceManager(SingleModeDeviceManager):
...     DEVICE_CONSTRUCTOR = MockVoltimeter
...     DEVICE_ID = "voltimeter1"
...     CONTROLS = ControlSuite(NumberControl("foo", "Foo Control"))
...     OUTPUT_FIELDS = OutputFieldSuite(bar = OutputField("foo", "bar"))
>>> mngr = TestDeviceManager("FooExperiment", None)
>>> mngr.applicable_modes
>>> mngr.get_output_fields('any-mode')
<OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> >
>>> mngr.get_input_fields('any-mode')
<ControlSuite <Control name=foo type=number live=False label='Foo Control'>>
class silf.backend.commons.device_manager._device_manager.ResultMixin(*args, **kwargs)

Bases: silf.backend.commons.device_manager._device_manager.IDeviceManager

This mixin handles getting results from the experiment,

Each entry in RESULT_CREATORS define single result that will be sent to the client. So class in example will send only two results: foo and bar, even if device itself will prowide other results.

Contents of particular result fields (foo and bar) are totally dependent on result creators.

Additionaly you can override ResultMixin._convert_result() that allows for for more extennsive costumistaion.

>>> from silf.backend.commons.device_manager import *
>>> from silf.backend.commons.device_manager._result_creator import *
>>> from silf.backend.commons.util.abc_utils import patch_abc
>>> p = patch_abc(ResultMixin)
>>> class TestResultMixin(ResultMixin):
...     RESULT_CREATORS = [
...         AppendResultCreator("foo"),
...         OverrideResultsCreator("bar")
...     ]
...
...     RESULTS = []
...
...     def _pop_results_internal(self):
...         try:
...             return self.RESULTS
...         finally:
...             self.RESULTS = []
>>> test = TestResultMixin(None, None)
>>> test.RESULTS = [{"foo": [1, 2, 3], "bar": 1.15}, {"foo": [4, 5], "bar" : 3.12}]
>>> test.current_results
<ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.current_results
<ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.pop_results()
<ResultSuite bar=<Result pragma=replace value=3.12 > foo=<Result pragma=append value=[1, 2, 3, 4, 5] > >
>>> test.pop_results()
<ResultSuite bar=<Result pragma=replace value=3.12 > >
>>> test.clear_current_results()
>>> test.pop_results()
<ResultSuite >
>>> test.current_results
<ResultSuite >

Clean up: >>> p.stop()

RESULT_CREATORS = []

A list that maps str (name of the result recieved from the device) to instance of :class:’result_appender.ResultAggregator`.

_convert_result(dict)

Enables to translate between device and experiment results.

_pop_results_internal()

Returns raw results from the device. :return: list of dicts.

clear_current_results()

Clears current results (that is after this call it is guaranteed that both current_results and pop_results will be empty. :return:

current_results
Return type:misc.ResultSuite
pop_results()
Returns:
class silf.backend.commons.device_manager._device_manager.SingleModeDeviceMixin(*args, **kwargs)

Bases: silf.backend.commons.device_manager._device_manager.IDeviceManager

Mixin for single current_mode device.

get_input_fields(mode)

Returns _control suite for current_mode :param str mode: current_mode for which we get controls (Ignored) :return: :rtype: misc.ControlSuite

get_output_fields(mode)

Returns output fields for current_mode :param str mode: current_mode for which we get output fields (Ignored) :return: :rtype: _control.OutputFieldSuite

class silf.backend.commons.device_manager._device_manager.DeclarativeDeviceMixin(*args, **kwargs)

Bases: silf.backend.commons.device_manager._device_manager.IDeviceManager

>>> from silf.backend.commons.api import *
>>> from silf.backend.commons.util.abc_utils import patch_abc
>>> foo_mode = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10),
...   NumberControl("bar", "Insert bar")
... )
>>> bar_mode = ControlSuite(
...   NumberControl("foo", "Insert foo", max_value=10),
...   NumberControl("bar", "Insert bar")
... )
>>> output_fields = _misc.OutputFieldSuite(
...     bar=_misc.OutputField("foo", "bar")
... )

Enable instantiating DeclarativeDeviceMixin (it is an ABC so you shouldn’t be able to do this normallu >>> p = patch_abc(DeclarativeDeviceMixin)

>>> class DeclarativeDeviceMixinTest(DeclarativeDeviceMixin):
...     CONTROL_MAP = {'foo' : foo_mode, 'bar' : bar_mode}
...     OUTPUT_FIELD_MAP = {
...         'foo' : output_fields,
...         'bar' : output_fields,
...     }
>>> manager = DeclarativeDeviceMixinTest(None, None)

Basic operatoons

>>> manager.get_input_fields('foo')
<ControlSuite <Control name=bar type=number live=False label='Insert bar'> <Control name=foo type=number live=False label='Insert foo'>>
>>> manager.get_input_fields('bar')
<ControlSuite <Control name=bar type=number live=False label='Insert bar'> <Control name=foo type=number live=False label='Insert foo'>>
>>> manager.get_input_fields("baz") 
Traceback (most recent call last):
KeyError: 'baz'
>>> manager.get_output_fields("foo")
<OutputFieldSuite bar=<Result class=foo name=['bar'] type=array metadata={} settings={}> >

Clean up: >>> p.stop()

get_input_fields(mode)

Returns _control suite for current_mode :param str mode: current_mode for which we get controls :return: :rtype: misc.ControlSuite

get_output_fields(mode)

Returns output fields for current_mode :param str mode: current_mode for which we get output fields :return: :rtype: _control.OutputFieldSuite

class silf.backend.commons.device_manager._device_manager.DeviceWrapperMixin(experiment_name, config)

Bases: silf.backend.commons.device_manager._device_manager.ResultMixin

Device manager that wraps a device.

AUTOMATICALLY_POOL_RESULTS = True

Boolean value. If true device will automatically pool results on each iteration.

BIND_STATE_TO_DEVICE_STATE = True

If true state of this device will be synchronized with state of underlying device. State is updated on every call to query().

DEVICE_CONSTRUCTOR = None

Type of the device, or callable that takes single argument: the device id.

DEVICE_ID = ''

Id of the device

USE_WORKER = True

If this is set to true this instance will spawn device in dofferent process, and all methods will be executed asynchroneusly. If set to False methods will be executed in current thread.

Warning

This is sometimes usefull when debugging stuff, but may break stuff in weird ways. Never use it in production.

_apply_settings_to_device(converted_settings)

Applies settings to the device. :param dict converted_settings: Result of settings conversion. :return:

_construct_device()

Protected method that is used to construct the internal device. :return: WorkerWrapper

_convert_settings_to_device_format(converted_settings)

Converts settings from format readable to user, to format readable to device. :param dict converted_settings: Parsed, validated, settings from the user. :return: Converted settings :rtype: dict

static _exception_translator()

Wrapper that catches all DeviceException and raises them again as SilfProtocolError.

>>> with exception_translator():
...     raise DeviceException("Bar")
Traceback (most recent call last):
silf.backend.commons.api.exceptions.SILFProtocolError: Bar
>>> with exception_translator():
...     raise TypeError("Foo")
Traceback (most recent call last):
TypeError: Foo
Rtype None:
_load_settings_without_applying_to_device(settings, live)

Loads settings (updates self.current_settings) :param SetingsSuite settings: :return:

apply_settings(settings)

See IDeviceManager.apply_settings().

perform_diagnostics(diagnostics_level='short')

See IDeviceManager.perform_diagnostics().

power_down(suspend=False, wait_time=10)

Powers down the device, and optionally kills it.

See IDeviceManager.power_down().

power_up(mode)

Creates and powers up the device.

See IDeviceManager.power_up().

running

See IDeviceManager.running().

start(callback=None)

See IDeviceManager.start().

stop()

See IDeviceManager.stop().

tear_down(wait_time=2)

Kills the device if device is present.

See IDeviceManager.tearDown().

Contains classes that cope with merging results taken from the device prior to sending them to the client.

class silf.backend.commons.device_manager._result_creator.ResultCreator(result_name, pragma='append', *, read_from=None)

Bases: object

Instances of this class get results from the device and produce appropriate Result instances.

aggregate_results(results)

Adds results to results cache. :param dict results: dictionary containing many results values.

clear()

Clears the results cache.

has_results
pop_results() → silf.backend.commons.api.stanza_content._misc.Result

Returns current set of results and possibly removes them from results cache.

Returns:Experiment results or RESULTS_UNSET if there are no new results to return.
Return type:Result or None
result_name = None

Name of interesting results

class silf.backend.commons.device_manager._result_creator.AppendResultCreator(result_name, pragma='append', **kwargs)

Bases: silf.backend.commons.device_manager._result_creator.ResultCreator

Class that aggregates results from source, and empties cache on each call to pop_results().

It is useful if device provides series of points, each of these points is unique, and should not be resent to client. >>> agg = AppendResultCreator(“foo”) >>> agg.has_results False >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.aggregate_results({“foo” : [1, 2], “bar”: [3, 4]}) >>> agg.pop_results() <Result pragma=append value=[1, 2] > >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.aggregate_results({“foo” : [1, 2], “bar”: [3, 4]}) >>> agg.aggregate_results({“foo” : [2, 3], “bar”: [3, 4]}) >>> agg.has_results True >>> agg.pop_results() <Result pragma=append value=[1, 2, 2, 3] > >>> agg.pop_results() <Result pragma=append value=[] > >>> agg.has_results False

aggregate_results(results)
clear_results(results_to_clear=0)
has_results
pop_results()
class silf.backend.commons.device_manager._result_creator.OverrideResultsCreator(result_name, pragma='replace', *, read_from=None)

Bases: silf.backend.commons.device_manager._result_creator.ResultCreator

Class that always returns last result. Useful for returning device state to user, and when single result compromises whole result set.

>>> agg = OverrideResultsCreator("foo")
>>> agg.has_results
False
>>> agg.pop_results()
<Result pragma=replace value=[] >
>>> agg.aggregate_results({"foo": [1, 2, 3, 4, 5, 6], "bar": "ignored"})
>>> agg.has_results
True
>>> agg.pop_results()
<Result pragma=replace value=[1, 2, 3, 4, 5, 6] >
>>> agg.pop_results()
<Result pragma=replace value=[1, 2, 3, 4, 5, 6] >
>>> agg.aggregate_results({"foo":  [7, 8, 9, 10, 11, 12], "bar": "ignored"})
>>> agg.pop_results()
<Result pragma=replace value=[7, 8, 9, 10, 11, 12] >
>>> agg.aggregate_results({"bar": "ignored"})
>>> agg.pop_results()
<Result pragma=replace value=[7, 8, 9, 10, 11, 12] >
>>> agg = OverrideResultsCreator("bar", read_from="foo")
>>> agg.has_results
False
>>> agg.pop_results()
<Result pragma=replace value=[] >
>>> agg.aggregate_results({"foo": [1]})
>>> agg.pop_results()
<Result pragma=replace value=[1] >
aggregate_results(results)
clear()
has_results
pop_results()
class silf.backend.commons.device_manager._result_creator.XYChartResultCreator(result_name, pragma='replace', *, read_from=())

Bases: silf.backend.commons.device_manager._result_creator.ResultCreator

Create this result creator

>>> creator = XYChartResultCreator(result_name="baz", read_from=['foo', 'bar'])

At the beginning it is empty

>>> creator.pop_results()
<Result pragma=replace value=[] >
>>> creator.has_results
False

Now let’s append some results

>>> creator.aggregate_results({"foo": [1, 2, 3], "bar": [-1, -2, -3]})
>>> creator.has_results
True
>>> creator.pop_results()
<Result pragma=replace value=[[1, -1], [2, -2], [3, -3]] >

Results are stored

>>> creator.has_results
True
>>> creator.pop_results()
<Result pragma=replace value=[[1, -1], [2, -2], [3, -3]] >

Results are cleared after call to clear:

>>> creator.clear()
>>> creator.has_results
False
>>> creator.pop_results()
<Result pragma=replace value=[] >

After appending only one coordinate we don’t produce results

>>> creator.aggregate_results({"foo": [1, 2, 3]})
>>> creator.has_results
False
>>> creator.pop_results()
<Result pragma=replace value=[] >

After adding second coordinate we produce them:

>>> creator.aggregate_results({"bar": [1]})
>>> creator.has_results
True
>>> creator.pop_results()
<Result pragma=replace value=[[1, 1]] >
>>> creator = XYChartResultCreator(result_name="baz", read_from=['foo', 'bar'])
>>> creator.aggregate_results({"foo": [1, 1, 1], "bar": [-1, -2, -3]})
>>> creator.pop_results()
<Result pragma=replace value=[[1, -3]] >
aggregate_results(results)
clear()
has_results
pop_results()
class silf.backend.commons.device_manager._result_creator.ReturnLastElementResultCreator(result_name, pragma='append', *, read_from=None)

Bases: silf.backend.commons.device_manager._result_creator.ResultCreator

>>> agg = ReturnLastElementResultCreator("foo")
>>> agg.has_results()
False
>>> agg.pop_results()
<Result pragma=append value=None >
>>> agg.aggregate_results({'foo' : [1, 2, 3]})
>>> agg.pop_results()
<Result pragma=append value=3 >
>>> agg.pop_results()
<Result pragma=append value=3 >
aggregate_results(results)
clear()
has_results()
pop_results()

Series manager

This package contains contains device managers for one use-case, we want student to capture series of points, and device is able to capture only single point at a time.

class silf.backend.commons.device_manager._series_manager.ISeriesManager(experiment_name, config)

Bases: silf.backend.commons.device_manager._series_manager.AbstractSeriesManager

apply_settings(settings)
get_number_of_points_in_series(settings)

Calculates number of points in series

Parameters:settings (dict) – Mapping from setting name to setting value.
Returns:Number of points in series.
Return type:int
is_series_finished
class silf.backend.commons.device_manager._series_manager.SingleModeSeriesManager(experiment_name, config)

Bases: silf.backend.commons.device_manager._series_manager.ISeriesManager, silf.backend.commons.device_manager._device_manager.SingleModeDeviceMixin

class silf.backend.commons.device_manager._series_manager.MultiModeSeriesManager(experiment_name, config)

Bases: silf.backend.commons.device_manager._series_manager.ISeriesManager, silf.backend.commons.device_manager._device_manager.DeclarativeDeviceMixin

exception silf.backend.commons.device_manager._series_manager.OnFinalSeriesPoint

Bases: Exception

Device Manger worlker module

This module contains plumbing that allows

Here we have defined a wrapper that creates device on separate process and allows to all methods of this device

class silf.backend.commons.device_manager._worker.DeviceWorkerWrapper(device_id, device_constructor, multiprocess=<object object>, config_file=None)

Bases: object

This a wrapper for the device it launched it works that way:

  • Creates a process on this machine
  • Constructs the device inside this process
  • Enables more-or-less transparent operations on the remote device.
  • Enables us to kill the remote process when really needed

Support for remote process should be transparent, with one important exception: all mehods are asynchroneous, for example when making a get_results() call you actually end up doing:

  • Sheduling a request to refresh results from remote side
  • Reading all messages from input queue
  • Returning last used results
append_results(results)
auto_pull_results = None

If true the experiment will automatically pull results.

cleanup(callback=None)

Schedules a task to turn off the device kill the remote thread. :return: task_id

config_file = None

Congig parser instance for this experiment.

device_constructor = None

Remote process.

execute_task(task, callback=None, sync=False)

Schedules task execution on remote process.

Parameters:
  • task – Task to be executed
  • callback – A callable that will be called upon completion of this task. It should accept single argument: instance of TaskResponse that contains the results for this task.
Returns:

Task id

in_queue = None

Queue that contains responses for tasks from remote process. Contains instances of TaskResponse.

kill(wait_time=2)

Kills remote process, optionally waiting. This method will block until remote process is killed.

Parameters:wait_time (float) – Wait time in seconds. If wait_time is None, then terminate process without additional waiting.
Returns:None
logger
loop_iteration(max_read=None)

Reads all messages from the input queue, updating self.state and self.results.

Returns:None
out_queue = None

Queue that contains tasks to be executed on remote process, Should contain instances of QueueTask, though any callable with proper signature will do.

ping_results(callback=None, sync=False)

Schedules getting of new results/ :param callback: See execute_task()

pop_results(sync=False)

Schedules getting of new results and then returns cached results. :return:

process_alive

Checks if remote process is alive.

Returns:True if remote process is alive, False otherwise
Return type:bool
purge_queue(queue)

Removes all pending operations from a queue. :return: None`

results = None

Current results, list

start(callback=None, sync=False)
start_subprocess()

Starts subprocess attached to this instance.

state

Returns most recent state of remote process. :return: State :rtype: str

stop(callback=None, sync=True)

Schedules a task to stop the device. :param callback: See execute_task() :return: Returns task id

wait_for_response(task_id)

Gets responses from queue until response for task with id equal to task_id param.

Note

This method will block until we get response for task_id

Warning

It may introduce deadlock if response for task_id has already been processed. To be sure it will not cause deadlock please execute this function just after sheduling the task. Much safer solution is to attach a callback for particular task, please see execute_task().

Parameters:task_id – Task id we are looking for.
Returns:Response for task with id equal to task_id.
silf.backend.commons.device_manager._worker.set_multiprocessing(do_multiprocess)
silf.backend.commons.device_manager._worker.reset_multiprocessing()
silf.backend.commons.device_manager._worker.start_worker_interactive(device_id, device_class, auto_pull_results=True, configure_logging=True, config_file=None)

silf.backend.commons.experiment package

Module contents

io Package

io Package

i2c Module

class silf.backend.commons.io.i2c.DeclarativeDriver

Bases: silf.backend.commons.io.i2c.I2CDriver

ADDR = None
REGISTER_MAPPER = {}
USER_READABLE_NAME = ''
class silf.backend.commons.io.i2c.I2CDriver(addr)

Bases: object

perform_diagnostics(diagnostics_level=None)

Performs diangostics for this device.

This method checks whether i2cdetect detects address of this device.

Note

Should call superclass method.

read_bool(register)
read_byte(register, min_value=None, max_value=None)
read_int(register, num_bytes=4, fmt='<I', min_value=None, max_value=None)
read_raw(register, num_bytes=1)
wait_until_byte(register, desired_value, max_time=3, iter_time=0.1)
write_byte(register, value)
exception silf.backend.commons.io.i2c.I2CError

Bases: Exception

silf.backend.commons.io.i2c.long_transaction(tries=5, timeout=0.5, caught_exceptions=(<class 'OSError'>, <class 'silf.backend.commons.io.i2c.I2CError'>))

Executes decorated function. If this function raises OSError with errno == 5 (raised by quick2wire when i2c error occours) it will wait timeout and retry. It will retry tries time. :param int tries: Maximal number of retrues :param float timeout: Timeout between retries in seconds. :return:

silf.backend.commons Package

version Module

class silf.backend.commons.version.ProtocolVersionUtil

Bases: object

classmethod is_version_in_list(ver, version_list)
>>> ProtocolVersionUtil.is_version_in_list("1.0.0", [])
False
>>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.0.1"])
False
>>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.0.1","0.5.0-0.9.0"])
False
>>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["1.0.0","0.5.0-0.9.0"])
True
>>> ProtocolVersionUtil.is_version_in_list("1.0.0", ["0.7.0","0.9.0-1.9.0"])
True
classmethod validate_protocol_version(ver)
>>> ProtocolVersionUtil.validate_protocol_version("0.0.0")
True
>>> ProtocolVersionUtil.validate_protocol_version("2.1.0")
True
>>> ProtocolVersionUtil.validate_protocol_version("2.1.0-2.2.0")
True
>>> ProtocolVersionUtil.validate_protocol_version("1.0")
Traceback (most recent call last):
ValueError: Wrong syntax for protocol version: 1.0
>>> ProtocolVersionUtil.validate_protocol_version("1.0.a")
Traceback (most recent call last):
ValueError: Wrong syntax for protocol version: 1.0.a
>>> ProtocolVersionUtil.validate_protocol_version("ala")
Traceback (most recent call last):
ValueError: Wrong syntax for protocol version: ala
>>> ProtocolVersionUtil.validate_protocol_version("2.a.0-2.2.0")
Traceback (most recent call last):
ValueError: Wrong syntax for protocol range: 2.a.0-2.2.0
>>> ProtocolVersionUtil.validate_protocol_version("2.1.0-2.2.bb")
Traceback (most recent call last):
ValueError: Wrong syntax for protocol range: 2.1.0-2.2.bb
classmethod validate_protocol_versions(version_list)
version_pattern = re.compile('\\d+\\.\\d+\\.\\d+')
version_range_pattern = re.compile('\\d+\\.\\d+\\.\\d+-\\d+\\.\\d+\\.\\d+')

Subpackages

silf.backend.commons.device package
Module contents

It packages api for single device.

silf.backend.commons.util package
Subpackages
silf.backend.commons.util.test_util package
Submodules
silf.backend.commons.util.test_util.config_test_utils module
Module contents
Submodules
silf.backend.commons.util.abc_utils module
>>> import abc
>>> class A(metaclass = abc.ABCMeta):
...     @abc.abstractmethod
...     def foo(self): pass
>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo
>>> A.__abstractmethods__=set()
>>> A() 
<....A object at 0x...>
>>> class B(object): pass
>>> B() 
<....B object at 0x...>
>>> B.__abstractmethods__={"foo"}
>>> B()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class B with abstract methods foo
>>> class A(metaclass = abc.ABCMeta):
...     @abc.abstractmethod
...     def foo(self): pass
>>> from unittest.mock import patch
>>> p = patch.multiple(A, __abstractmethods__=set())
>>> p.start()
{}
>>> A() 
<....A object at 0x...>
>>> p.stop()
>>> A()
Traceback (most recent call last):
TypeError: Can't instantiate abstract class A with abstract methods foo
silf.backend.commons.util.abc_utils.patch_abc(to_patch)
silf.backend.commons.util.config module
silf.backend.commons.util.config.prepend_current_dir_to_config_file(file_name, caller_frame_offset=1)

Todo

Use proper packaging tool, and get rid of assumption that files are laying on the fs directories.

Prepends directory name of caller file_name

Parameters:
  • file_name (str) – File name ot be appended
  • caller_frame_offset (int) – Frame from which to take the dirname :default:1
Returns:

Absolute path to file

silf.backend.commons.util.config.open_configfile(*config_files)

Opens configfile.

Parameters:config_file (str) – Absolute path to open
Returns::class:SilfConfigParser
class silf.backend.commons.util.config.SilfConfigParser(*args, **kwargs)

Bases: configparser.ConfigParser

validate_mandatory_config_keys(section, required_cofig_keys)
exception silf.backend.commons.util.config.ConfigValidationError(msg='')

Bases: configparser.Error

silf.backend.commons.util.mail module
class silf.backend.commons.util.mail.BorgSender

Bases: object

This is a borg class containing e-mail sender for currenly tunning expeirment we have to have global state containing e-mail sender because without it confguiring logging by ini file would be hard.

borg = {}
set_sender(sender)
class silf.backend.commons.util.mail.EmailSender(config)

Bases: object

Class that sends e-mails with errors to experiment admins.

email_throttling_dict = None

A dictionary that maps tracebak to time when exception with this traceback was sent last time.

install_error_function()
send_current_exception_to_admins(message=None, subject=None, excinfo=None)
send_email_to_admins(body, subject=None)
should_send_exception(traceback)
class silf.backend.commons.util.mail.ExceptionHandler(level=40)

Bases: logging.Handler

Sends e-mail with logging messahes

emit(record)
silf.backend.commons.util.mail.RESEND_EXCEPTION_TIMEOUT_SEC = 300.0

How long we weit between sending emails with exceptions with the same stacktrace.

silf.backend.commons.util.reflect_utils module
silf.backend.commons.util.reflect_utils.load_item_from_module(path)
silf.backend.commons.util.reflect_utils.pythonpath_entries(pythonpath_string)
silf.backend.commons.util.uniqueify module
silf.backend.commons.util.uniqueify.uniquefy_last(seq, idfun=None)
silf.backend.commons.util.uniqueify.uniqueify(seq, idfun=None)
>>> uniqueify([1, 2, 3, 1, 1, 4, 3 , 5])
[1, 2, 3, 4, 5]

Pasted from: www.peterbe.com/plog/uniqifiers-benchmark

Parameters:
  • seq (iterable) –
  • idfun
Returns:

Module contents

device Package

device Package

device_tests Module

mock_engine Module

mock_voltimeter Module

test_device Module

device_manager Package

device_manager Package

device_manager_tests Module

mock_engine_manager Module

mock_engine_tests Module

mock_voltimeter_manager Module

mock_voltimeter_tests Module

worker_tests Module

experiment Package

experiment Package

experiment_manager_tests Module

mock_experiment_manager Module

mock_experiment_manager_tests Module

silf.backend.commons_test Package

silf.backend.commons_test Package

Subpackages

util Package
util Package
config_tests Module

silf.backend.experiment Package

experiment Module

exception silf.backend.experiment.experiment.ClientResetException

Bases: Exception

class silf.backend.experiment.experiment.ClientResetToken

Bases: object

Object of this class is put into the queue if client encounters fatal error and must be reset

class silf.backend.experiment.experiment.Experiment(cp, ManagerClass, client: silf.backend.client.client.Client)

Bases: object

LOOP_TIMEOUT = 0.5
add_exception_callback(callback)
configure()
experiment_loop()
initialize()
logger
schedule_client_reset()
sender = None

Sends emails to admins of this experiment if any error happens

tear_down()
class silf.backend.experiment.experiment.ExperimentManagerCallback(client, experiment)

Bases: silf.backend.commons.experiment._experiment_manager.ExperimentCallback

send_experiment_done(message=None)
send_results(results)
send_series_done(message=None)
silf.backend.experiment.experiment.load_experiment(cp)

expmain Module

Utility that launches experiment.

Usage:
expmain.py <configfile> –test-smtp-sending expmain.py <configfile> [–no-multiprocessing] [–no-default-configfiles] [–additional-cf=<cf>]… expmain.py <configfile> [–no-multiprocessing] [–no-default-configfiles] [–additional-cf=<cf>]… debug <host> <port>
silf.backend.experiment.expmain.configure_email_sender(cp)
silf.backend.experiment.expmain.configure_logging(cp)
silf.backend.experiment.expmain.main(commands, run_loop=True)
silf.backend.experiment.expmain.run_new(configfile, client, ExperimentManagerClass)
silf.backend.experiment.expmain.run_old(cp, commands, client, ExperimentManagerClass)

reflect_utils Module

silf.backend.experiment.reflect_utils.load_item_from_module(path)
silf.backend.experiment.reflect_utils.pythonpath_entries(pythonpath_string)

silf.backend.experiment_test Package

silf.backend.experiment_test Package

experiment_tests Module

Authors:

  • Jacek Bzdak <jbzdak[at]gmail.com> – did

most of the work and maintains this. Blame this guy. * Daniel Kowalski <kowal256[at]gmail.com> – created XMPP client * Mikołaj Kubicki .... – some fixes

Release notes

Version 1.4.2

Enchancements

  • You can now split experiment ini file into several files. when calling expmain.py experiment.ini, Multi File Config.

  • You can now configure experiments to send e-mails with every uncaught exception, and every logging message with error or exception level (latter ones contain stacktrace). See [SMTP] Section.

    Warning

    Please beware that if you use not default logging you’ll need to add additional configuration: Experiment configuration file .

  • You should upgrade requirements (as SleekXMPP was changed)

  • Please change host in [Server] section to use IP (workaround for SleekXMPP bug).

Version 1.4.1

Incompatible changes

  • If any task on the device takes longer than 30 seconds it is considered an error. This can be configured using single_task_timeout parameter in a device section in config file.

    Default is to wait for 30 seconds, so I don’t expect anyting will be broken.

Version 1.4.0

Incompatible changes

  • Renamed all packages from silf_backend_* to silf.backend.*, you need to change
    • Imports of these packages
    • Experiment ini files (if they contain references to changed classes).
  • IntegerControl class is now deprecated, please use NumberControl instead.
  • Diagnostics api has changed (and is at least! actually used). perform_diagnostics method is now deprecated. Please use: pre_power_up_diagnostics and post_power_up_diagnostics.

Enchancements

  • Experiment now uses randomly assigned XMPP resources.
  • You can now sent chatroom messages
  • We can reconnect when XMPP server fails.

Version 1.2.0

Incompatible changes

  1. You need to change client config, to reflect changes in how we configure XMPP client

Sending e-mails with errors

SILF now has ability to automatically send emails with:

  • Uncaught exceptions
  • Logged messages with error level

Configuration is described in: Experiment configuration file.

Warning

To log exceptions you must also configure logging properly.

Protocol (current previous):

Conversation in protocol v0.5

In this document we will define what stanzas are exchanged during the experiment.

  • Operator sends silf:mode:get
    • Experiment responds with message containing possible modes.
  • Operator sends silf:mode:set
    • Experiment sets the mode, and then sends description of the interface.
  • Operator may send silf:settings:check (possibly many times), this message contains settings set by the user. This settings will be validated
    • Experiments validates the settings and sends response containing validation results
  • Operator sends silf:series:start. It contains settings set by the user.
    • Experiment response varies on the validity of settings:
      • If settings are valid measurement series is stated.
      • If settings are invalid user get’s an error and must send them via silf:series:start again
  • Experiment sends silf:results until the session ends.
  • During experiment session user may change settings that are live (see Input fields). It should use: silf:settings:update.
  • Data series ends when experiment sends silf:series:stop, it may end it in following circumstances:
    • After series is finished (in this case experiment sends: silf:series:stop on its own)
    • After operator requests it by sending silf:series:stop
    • When experiment detects series that take to long, and drops it (shouldn’t occour).
    • When experiment is ending.
  • After end of series user may either:
    • Start a next one
    • Change mode
    • End experiment
  • Experiment ends when experiment server sends: silf:series:stop.
    • Experiment is idle for too long time.
    • Operator requests it by sending silf:series:stop.
    • Bot that is operating the experiment detects end of reservation, and requests stop by sending: silf:series:stop.

SILF Protocol

Version note

This document describes protocol version 0.5 which will be implemented in silf laboratory up to version 1.0.0.

Version 1.0 of this protocol will be significantly simpler.

Namespaces

silf:mode:get

This namespace is used query modes from the experiment.

First client sends:

>>> write(LabdataModeGet(id="query1", type="query"))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="query" />

Then server responds:

>>> write(LabdataModeGet(id="query1", type="result", suite=ModeSuite(default=Mode("Default mode", "This is the default mode description"))))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="result">{"default": {"label": "Default mode", "description": "This is the default mode description"}}</ns0:labdata>
Stanza content

Contents of silf:mode:get in type result are formatted as follows:

{
    "mode-keyname": {
        "label": "Mode label", "description": "Mode description",
        "order" : "1"
    },
    "mode-keyname-2": {
        "label": "Mode label", "description": "Mode description",
        "order": "1"
    }
}

Each property in this object contains description of one mode, and name of this property is name of the mode.

Modes should be sorded ascending with order attribute, if order is equal sort them alphabetically.

Warning

Before we upgrade experiment software, client should not expect order attribute to be defined. If it is undefined sort it so all input fields with undefined should be below those with defined order.

Backend behaviour

In current implementation experiment does not change state when reclieving this message

Client behaviour

Client allows user to choose mode.

silf:mode:set

This namespace is used to set mode of the experiment, and to return experiment metadata to the users.

This namespace is used query modes from the experiment.

Client sends
>>> write(LabdataModeSet(id="query1", type="query", suite=ModeSelection(mode="default")))
<ns0:labdata xmlns:ns0="silf:mode:set" id="query1" type="query">{"mode": "default"}</ns0:labdata>

Code sent by client contains Json object contains single property “mode” and it’s value must be “mode-keyname” (that is name of property under which particular mode is found in silf:mode:get).

Server responds

Server responds with metadata containing description of selected mode.

Object will have following properties:

mode
name of selected mode — usefull for visitor users
experimentId
Globally unique identifier of experiment session
settings
Description of user controllable input fields, described in Input fields
resultDescription
Description of fields that will present results to the user Output fields

Example response:

{
    "experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c",
    "mode": "aluminium",
    "settings": {
        "acquisition_time": {
            "live": false, "metadata": {
            "label": "Czas pomiaru dla jednej grubo\u015bci materia\u0142u"},
            "name": "acquisition_time", "validations": {"min_value": 5.0, "max_value": 600.0}, "type": "interval",
            "sort" : "1"},
        "light": {"live": false, "metadata": {"label": "O\u015bwietlenie", "style": "light_control"}, "name": "light", "type": "boolean", "sort" : "1"}
    },
    "resultDescription": {
        "time_left": {"name": ["tile_left"], "type": "array", "class": "interval-indicator", "settings": {}, "metadata": {"label": "Czas pozosta\u0142y do ko\u0144ca bierz\u0105cego punktu"}},
        "chart": {"name": ["chart"], "type": "array", "class": "chart", "settings": {}, "metadata": {"chart.axis.y.label": "Liczba zlicze\u0144 zarejestrowana przez licznik", "chart.axis.x.label": "Grubo\u015b\u0107 przes\u0142ony [mm]"}
   }
}

silf:settings:check

Namespace: silf:settings:check issed to check settings for validity, prior to submiting them.

Client sends:

{
    "acquisition_time": {"value": 150, "current": false},
    "light": {"value": false, "current": false}
}

Property names in this object correspond to appropriate property names from settings proprerty in silf:mode:set.

Server responds either with empty tag of type result or with an error message (see: Errors).

When user finishes edition of a control (for example when focus is lost) Client should send values of all controls that were touched by user in current experiment run, and set current to true for control that triggered rsult sending, and to false for all others.

Settings

Settings themselves have following properties

value

Value of the settings. Type of the setting varies and is based on type send in silf:mode:set

current

Whether this setting was most recently updated by user. This helps to customise displayed erros and validation, in case of controls that are dependend on each other.

silf:series:start

Sets all the parameters and starts the measurement series, contents sent by the client are the same as for silf:settings:check.

Note

Currently current property in this stanza is ignored by the server. But I think it is easier to have the same format (despite ignored information).

Server response

Server responds either with an error: Errors, or with following structure:

{
    "metadata": {"label": "Czas pomiaru 90s."},
    "initialSettings": {
        "acquisition_time": {"current": false, "value": 90},
        "light": {"current": false, "value": false}
    },
    "seriesId": "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}

Where properties have following meaning:

seriesId
Unique id of current series
metadata
Dictionary containing additional metadata of the series. For now only key in this dictionary is series label (ie. non-unique human-readable name of the series)
initialSettings
Structure containing settings for the series (as it was set by the user)

silf:settings:update

Note

It’s a planned feaure and details might change.

During experiment session user my update input fields which are labeled as live (see: Input fields). In this case client should send silf:settings:update containing only the changed settings.

For example if only light was changed user should send:

{
    "light": {"value": false, "current": true}
}

If settings validate server should respond with silf:settings:update containting changed settings. If there are erros in settings, server should respod with proper error.

silf:results

Results are send as an object, where each result is sent as a distinct property, name of this property is also name of associated result.

So in following example:

{
    'foo' : {value: [1, 2, 3], pragma: "append"}.
    'bar' : {value: [1], pragma: "transient"}.
}

two results are sent, one named foo other named bar.

Single result

Result type has following properties:

pragma
Controls method in which result series is reconstructed.
value
Result value
Result series reconstruction

Experiment user wants to see whole result series, but in most cases we don’t want to send whole series each time (in some cases we need to!). We also don’t want to send new result stanza for each point if it is unnecessary.

Pragma property defines how series is reconstructed from series of result messages.

We support following pragmas:

append

Initially experiment series is empty, after each result contents of value property is appended to series.

For example if we sent following results:

{ "value": [1, 2, 3], "pragma": "append"}
{ "value": [4, 5], "pragma": "append"}

Series will be reconstructed as: {“value”: [1, 2, 3, 4, 5], “pragma”: “append”}

replace
Initially experiment series is empty, after reclieving of each result we replace series contents with this result.
transient
As append but will not be stored. Usefull when sending status variables.

silf:series:stop

Stops the series.

User sends following object:

{
    "seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}

seriesId property signified series to stop.

Client can omit this seriesId property.

Experiment responds with the same object:

{
    "seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}

silf:experiment:stop

Stops the experiment. User sends empty tag to the server.

Structures used in many stanzas

Input fields

Control fields contain at least following fields:

name
name of the control, it is not visible to user, but used to in both: silf:settings:check, and silf:series:stop.
type
Type of data this controls sends.
live
Boolean value. Currently unused.
metatadata
A dictionary that contains data visble to users: label Label of conntrol.
validations
Dictionary containing validations ot be done at client side.
default_value
Default value. It has the same type.
order

Used to sort input fields before they are rendered. Input fields with highter order should be presented on top of the input field list. In case of equal order fields should be sorted alphabetically on name attribute.

Warning

Before we upgrade experiment software, client should not expect order attribute to be defined. If it is undefined sort it so all input fields with undefined should be below those with defined order.

Currently we following types of input fields:

Number control

It allows user to submit an integer.

It has following possible validations:

min_value
minimal value present in the field
max_value
maximal value present in the field
step
increment in which to go from min_value to max_value
Value rendered to JSON Format

Client should create values that are json number objects, or dscimal strings.

{
    "number_control": {"value": 150, "current": false},
    "number_control_2": {"value": "36", "current": false}
}

Example of serialized control:

>>> control = NumberControl("foo", "Enter a number")
>>> control.to_json_dict() == {
... 'type': 'number', 'name': 'foo',
... 'metadata': {'label': 'Enter a number'},
... 'live': False}
True
>>> control.min_value = 1
>>> control.max_value = 10
>>> control.to_json_dict() ==  {
... 'type': 'number',
... 'validations': {'min_value': 1, 'max_value': 10},
... 'live': False,
... 'metadata': {'label': 'Enter a number'}, 'name': 'foo'}
True
Boolean control

It allows user to submit an boolean value.

Value rendered to JSON Format

Client should create values that are json boolean objects (any other objects will be passed to bool function.

{
    "number_control": {"value": 150, "current": false},
    "number_control_2": {"value": "36", "current": false}
}
Time interval control

Allows student to submit amount of time.

{
    "timedelta_control": {"value": 150, "current": false}
}

It has following validations:

min_value
minimal amout of time student could send. In the same format as it is send from client as settings.
max_value
maximal amount of time student can send.
Value rendered to JSON Format

Client should send to server amount of seconds (possibly as a float value).

ComboBox control

Renders a combo box.

It has no additional properties, but metadata property must contain choices property containing object defining the combo box.

Keys in the choices object define values that may will be sent from this controls, and values are user readable labels.

Example:

{
 'type': 'combo-box',
 'live': False,
 'metadata': {
     'label': 'Select material',
     'choices': {
         'lead': 'Use lead aperture',
         'cu': 'Use cooper aperture'
     }
 },
 'name': 'material',
 'default_value': 'cu'
}

This control would be rendered as a ComboBox with two choices: Use lead aperture and Use cooper aperture. And if user would choose Use cooper aperture, this control would send:

{
    ...
    'material: 'cu',
    ...
}

Output fields

Set of output fields is a plain object, each property denotes one output field, name of the property.

This block defines two output fields named foo and bar.

{
    'foo':   {'name': ['some_result'], ... },
    'bar':   {'name': ['other_result'], ... }
}

Format of object desceibing single output fields is defined in the next section.

Definitionamen of single output field

Output fields have following attributes:

name

names of result fields this output consumes. It is an array of strings.

Warning

Warning this is unimplemented in GUI, names is ignored, and follwing logic is used:

If experiment reclieves:

{
    'foo':   {'name': ['bar'], ... },
    'baz':   {'name': ['foobar'], ... }
}

Control that is under property foo will display results from property foo (and not bar as name would indicate).

This will be fixed sometime!

Until then Python API disallows sending name that is different than control name. So currently writen code is future proof.

type
Type of the resul field. This attribute defines how this field works. A string.
class
HTML class of result. This defines how does this field look. A string.
metadata
Dictionary od properties of this field that are visible
settings
Dictionary od properties of this field that are not visible to user
Applicable classes, and their behaviour

For now following classes are understood by the client.

Indicators, that display only the newest measurement in the series:

interval-indicator
Renders incoming data as a time interval, signifying (for example) time left end of current measurement point. It requires for
integer-indicator
Renders a number, for example number of registered counts on geiger counter.

Other classes:

chart
A chart.
Indicator example

Example of serialized indicator:

{
 'class': 'integer-indicator',
 'metadata': {'label': 'Current voltage [V]'},
 'name': ['current_voltage'],
 'settings': {},
 'type': 'array'
}
Chart example

Example of serialized chart:

{
 'class': 'chart',
 'metadata': {'chart.axis.x.label': 'Voltage between GM electrodes',
  'chart.axis.y.label': 'Counts in given time interval',
  'label': 'GM Counter characteristics'},
 'name': ['chart'],
 'settings': {},
 'type': 'array'}
How are results send and displayed

Each output field defines from which result field (or fields) it pulls the results, this defined in the name property.

Errors

Results are send as an object with single property errors that contains a list of error objects.

Each error object has following properties:

severity
A string describing how severe is this error. Following values are acceptable: error, warning, info.
error_type
A string. Following values are accetable: device, user. If value is user it means that user generated this error, and (so) can be fixed, device means that there is some error in the device.
metadata

Object with other error info:

message
Message for user
field
If it is present it is the name of an input field that is cause of this error (used when validating input fields).

Example:

{'errors':[
   {
       'severity':'error',
       'error_type': 'user',
       'metadata':{
           'message': 'Invalid value in field foo',
           'field': 'foo',
   },{
       'severity':'error',
       'error_type': 'user',
       'metadata':{
           'message': 'Invalid value in field bar',
           'field': 'bar',
    }
])

Protocol (current version):

High level overviev of protocol v. 1.1 DRAFT

Warning

This document describes protocol after establishing version. Part of protocol for establishing version is described in Establishing protocol version.

Whole conversation is done inside labdata tag, see: Protocol labdata stanza.

Experiment protocol

  • Operator sends silf:lang:set along with list of preferred languages for GUI
    • Experiment responds with language code which determines language tha will be used for GUI texts in following stanzas
  • Operator sends silf:mode:get
    • Experiment responds with message containing possible modes.
  • Operator sends silf:mode:set
    • Experiment sets the mode, and then sends description of the interface.
  • Operator may send silf:settings:check (possibly many times), this message contains settings set by the user. This settings will be validated
    • Experiments validates the settings and sends response containing validation results
  • Operator sends silf:series:start. It contains settings set by the user.
    • Experiment response varies on the validity of settings:
      • If settings are valid measurement series is stated.
      • If settings are invalid user get’s an error and must send them via silf:series:start again
  • Experiment sends silf:results until the session ends.
  • During experiment session user may change settings that are live (see Input fields). It should use: silf:settings:update.
  • Data series ends when experiment sends silf:series:stop, it may end it in following circumstances:
    • After series is finished (in this case experiment sends: silf:series:stop on its own)
    • After operator requests it by sending silf:series:stop
    • When experiment detects series that take to long, and drops it (shouldn’t occour).
    • When experiment is ending.
  • After end of series user may either:
    • Start a next one
    • Change mode
    • End experiment
  • Experiment ends when experiment server sends: silf:series:stop.
    • Experiment is idle for too long time.
    • Operator requests it by sending silf:series:stop.
    • Bot that is operating the experiment detects end of reservation, and requests stop by sending: silf:series:stop.

Joining groupchat late

Note

This is implemented in protocol version 1.1.0 and above.

When an user (irrevelant whether he is operator or not) joins groupchat he is updated with current experiment state.

First thing he gets information about protocol version used, see: version_selection. Then Athena bot updates client’s state in the following manner:

  • First Athena sends current series using following namespaces:

These messages have the same content and metadata as messages that experiment sent during normal (i.e. not joining late) operation.

If any of these messages were not sent during current session they are not sent now either.

See also: Result compression

  • After sending all above stanzas (that is syncing with current experiment state) Athena should give voice (allow him to write to the room) to user if he is an operator.

  • Then athena sends results from finished series belonging to current session. Each session is sent by sending three stanzas:

    All three stanzas should be send consecutively, however client can join them by comparing experimentId and seriesId.

    See also: Result compression

Note

Results compression There is no requirement to send archival results in single stanza, but athena should compress these results so data is send in concise way.

SILF Protocol version 1.1 DRAFT

Version note

This document describes protocol version 1.1.0

Namespaces

silf:lang:set

This namespace is used to set preferred language for the user interface texts send by experiment server. So content contains ordered list of preferred languages by user.

First client sends:

<ns0:labdata xmlns:ns0="silf:lang:set" id="query1" type="query">
    {
        "langs": ["en", "pl"]
    }
</ns0:labdata>

Then server responds:

<ns0:labdata xmlns:ns0="silf:lang:set" id="query1" type="result">
    {
        "lang": "en"
    }
</ns0:labdata>

Response contains single language code. This language will be used in further communication in texts which are provided by experiment server for GUI.

silf:mode:get

This namespace is used query modes from the experiment.

First client sends:

>>> write(LabdataModeGet(id="query1", type="query"))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="query" />

Then server responds:

>>> write(LabdataModeGet(id="query1", type="result", suite=ModeSuite(default=Mode("Default mode", "This is the default mode description"))))
<ns0:labdata xmlns:ns0="silf:mode:get" id="query1" type="result">{"default": {"label": "Default mode", "description": "This is the default mode description"}}</ns0:labdata>
Stanza content

Contents of silf:mode:get in type result are formatted as follows:

{
    "mode-keyname": {
        "label": "Mode label", "description": "Mode description",
        "order" : "1"
    },
    "mode-keyname-2": {
        "label": "Mode label", "description": "Mode description",
        "order": "1"
    }
}

Each property in this object contains description of one mode, and name of this property is name of the mode.

Modes should be sorded ascending with order attribute, if order is equal sort them alphabetically.

Warning

Before we upgrade experiment software, client should not expect order attribute to be defined. If it is undefined sort it so all input fields with undefined should be below those with defined order.

Backend behaviour

In current implementation experiment does not change state when reclieving this message

Client behaviour

Client allows user to choose mode.

silf:mode:set

This namespace is used to set mode of the experiment, and to return experiment metadata to the users.

This namespace is used query modes from the experiment.

Client sends
>>> write(LabdataModeSet(id="query1", type="query", suite=ModeSelection(mode="default")))
<ns0:labdata xmlns:ns0="silf:mode:set" id="query1" type="query">{"mode": "default"}</ns0:labdata>

Code sent by client contains Json object contains single property “mode” and it’s value must be “mode-keyname” (that is name of property under which particular mode is found in silf:mode:get).

Server responds

Server responds with metadata containing description of selected mode.

Object will have following properties:

mode
name of selected mode — usefull for visitor users
experimentId
Globally unique identifier of experiment session
settings
Description of user controllable input fields, described in Input fields
resultDescription
Description of fields that will present results to the user Output fields

Example response:

{
    "experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c",
    "mode": "aluminium",
    "settings": {
        "acquisition_time": {
            "live": false, "metadata": {
            "label": "Czas pomiaru dla jednej grubo\u015bci materia\u0142u"},
            "name": "acquisition_time", "validations": {"min_value": 5.0, "max_value": 600.0}, "type": "interval",
            "sort" : "1"},
        "light": {"live": false, "metadata": {"label": "O\u015bwietlenie", "style": "light_control"}, "name": "light", "type": "boolean", "sort" : "1"}
    },
    "resultDescription": {
        "time_left": {"name": ["tile_left"], "type": "array", "class": "interval-indicator", "settings": {}, "metadata": {"label": "Czas pozosta\u0142y do ko\u0144ca bierz\u0105cego punktu"}},
        "chart": {"name": ["chart"], "type": "array", "class": "chart", "settings": {}, "metadata": {"chart.axis.y.label": "Liczba zlicze\u0144 zarejestrowana przez licznik", "chart.axis.x.label": "Grubo\u015b\u0107 przes\u0142ony [mm]"}
   }
}

silf:mode:historical

Has the same contents as silf:mode:set, but sends interface details for historical series.

silf:settings:check

Namespace: silf:settings:check issed to check settings for validity, prior to submiting them.

Client sends:

{
    "acquisition_time": {"value": 150, "current": false},
    "light": {"value": false, "current": false}
}

Property names in this object correspond to appropriate property names from settings proprerty in silf:mode:set.

Server responds either with empty tag of type result or with an error message (see: Errors).

When user finishes edition of a control (for example when focus is lost) Client should send values of all controls that were touched by user in current experiment run, and set current to true for control that triggered rsult sending, and to false for all others.

Settings

Settings themselves have following properties

value

Value of the settings. Type of the setting varies and is based on type send in silf:mode:set

current

Whether this setting was most recently updated by user. This helps to customise displayed erros and validation, in case of controls that are dependend on each other.

silf:series:start

Sets all the parameters and starts the measurement series, contents sent by the client are the same as for silf:settings:check.

Note

Currently current property in this stanza is ignored by the server. But I think it is easier to have the same format (despite ignored information).

Server response

Server responds either with an error: Errors, or with following structure:

{
    "metadata": {"label": "Czas pomiaru 90s."},
    "initialSettings": {
        "acquisition_time": {"current": false, "value": 90},
        "light": {"current": false, "value": false}
    },
    "seriesId": "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f",
    "experimentId": "urn:uuid:a12e6562-5feb-4046-b98f-d8c40ca1609c"
}

Where properties have following meaning:

seriesId
Unique id of current series
experimentId
Unique id of experiment session to which this series belongs.
metadata
Dictionary containing additional metadata of the series. For now only key in this dictionary is series label (ie. non-unique human-readable name of the series)
initialSettings
Structure containing settings for the series (as it was set by the user)

silf:series:historical

Has the same contents as silf:series:start but is used by the athena bot to send historical results to the client.

silf:settings:update

Note

It’s a planned feaure and details might change.

During experiment session user my update input fields which are labeled as live (see: Input fields). In this case client should send silf:settings:update containing only the changed settings.

For example if only light was changed user should send:

{
    "light": {"value": false, "current": true}
}

If settings validate server should respond with silf:settings:update containting changed settings. If there are erros in settings, server should respod with proper error.

silf:results

Results are send as an object, where each result is sent as a distinct property, name of this property is also name of associated result.

So in following example:

{
    'sessionId':  "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
    'foo' : {value: [1, 2, 3], pragma: "append"}.
    'bar' : {value: [1], pragma: "transient"}.
}

two results are sent, one named foo other named bar.

Additionaly we send sessionId property which signifies to which series this result series belong

Single result

Result type has following properties:

pragma
Controls method in which result series is reconstructed.
value
Result value
Result series reconstruction

Experiment user wants to see whole result series, but in most cases we don’t want to send whole series each time (in some cases we need to!). We also don’t want to send new result stanza for each point if it is unnecessary.

Pragma property defines how series is reconstructed from series of result messages.

We support following pragmas:

append

Initially experiment series is empty, after each result contents of value property is appended to series.

For example if we sent following results:

{ "value": [1, 2, 3], "pragma": "append"}
{ "value": [4, 5], "pragma": "append"}

Series will be reconstructed as: {“value”: [1, 2, 3, 4, 5], “pragma”: “append”}

replace
Initially experiment series is empty, after reclieving of each result we replace series contents with this result.
transient
As append but will not be stored. Usefull when sending status variables.

silf:results:historical

Has the same contents as as silf:results, but ut is used by the athena bot to send historical results to the client.

silf:series:stop

Stops the series.

User sends following object:

{
    "seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}

seriesId property signified series to stop.

Client can omit this seriesId property.

Experiment responds with the same object:

{
    "seriesId" : "urn:uuid:6bd8a024-5a8b-4f04-be84-76dcad89d89f"
}

silf:experiment:stop

Stops the experiment. User sends empty tag to the server.

Structures used in many stanzas

Input fields

Control fields contain at least following fields:

name
name of the control, it is not visible to user, but used to in both: silf:settings:check, and silf:series:stop.
type
Type of data this controls sends.
live
Boolean value. Currently unused.
metatadata
A dictionary that contains data visble to users: label Label of conntrol.
validations
Dictionary containing validations ot be done at client side.
default_value
Default value. It has the same type.
order

Used to sort input fields before they are rendered. Input fields with highter order should be presented on top of the input field list. In case of equal order fields should be sorted alphabetically on name attribute.

Warning

Before we upgrade experiment software, client should not expect order attribute to be defined. If it is undefined sort it so all input fields with undefined should be below those with defined order.

Currently we following types of input fields:

Number control

It allows user to submit an integer.

It has following possible validations:

min_value
minimal value present in the field
max_value
maximal value present in the field
step
increment in which to go from min_value to max_value
Value rendered to JSON Format

Client should create values that are json number objects, or dscimal strings.

{
    "number_control": {"value": 150, "current": false},
    "number_control_2": {"value": "36", "current": false}
}

Example of serialized control:

>>> control = NumberControl("foo", "Enter a number")
>>> control.to_json_dict() == {
... 'type': 'number', 'name': 'foo',
... 'metadata': {'label': 'Enter a number'},
... 'live': False}
True
>>> control.min_value = 1
>>> control.max_value = 10
>>> control.to_json_dict() ==  {
... 'type': 'number',
... 'validations': {'min_value': 1, 'max_value': 10},
... 'live': False,
... 'metadata': {'label': 'Enter a number'}, 'name': 'foo'}
True
Boolean control

It allows user to submit an boolean value.

Value rendered to JSON Format

Client should create values that are json boolean objects (any other objects will be passed to bool function.

{
    "number_control": {"value": 150, "current": false},
    "number_control_2": {"value": "36", "current": false}
}
Time interval control

Allows student to submit amount of time.

{
    "timedelta_control": {"value": 150, "current": false}
}

It has following validations:

min_value
minimal amout of time student could send. In the same format as it is send from client as settings.
max_value
maximal amount of time student can send.
Value rendered to JSON Format

Client should send to server amount of seconds (possibly as a float value).

ComboBox control

Renders a combo box.

It has no additional properties, but metadata property must contain choices property containing object defining the combo box.

Keys in the choices object define values that may will be sent from this controls, and values are user readable labels.

Example:

{
 'type': 'combo-box',
 'live': False,
 'metadata': {
     'label': 'Select material',
     'choices': {
         'lead': 'Use lead aperture',
         'cu': 'Use cooper aperture'
     }
 },
 'name': 'material',
 'default_value': 'cu'
}

This control would be rendered as a ComboBox with two choices: Use lead aperture and Use cooper aperture. And if user would choose Use cooper aperture, this control would send:

{
    ...
    'material: 'cu',
    ...
}

Output fields

Set of output fields is a plain object, each property denotes one output field, name of the property.

This block defines two output fields named foo and bar.

{
    'foo':   {'name': ['some_result'], ... },
    'bar':   {'name': ['other_result'], ... }
}

Format of object desceibing single output fields is defined in the next section.

Definitionamen of single output field

Output fields have following attributes:

name

names of result fields this output consumes. It is an array of strings.

Warning

Warning this is unimplemented in GUI, names is ignored, and follwing logic is used:

If experiment reclieves:

{
    'foo':   {'name': ['bar'], ... },
    'baz':   {'name': ['foobar'], ... }
}

Control that is under property foo will display results from property foo (and not bar as name would indicate).

This will be fixed sometime!

Until then Python API disallows sending name that is different than control name. So currently writen code is future proof.

type
Type of the resul field. This attribute defines how this field works. A string.
class
HTML class of result. This defines how does this field look. A string.
metadata
Dictionary od properties of this field that are visible
settings
Dictionary od properties of this field that are not visible to user
Applicable classes, and their behaviour

For now following classes are understood by the client.

Indicators, that display only the newest measurement in the series:

interval-indicator
Renders incoming data as a time interval, signifying (for example) time left end of current measurement point. It requires for
integer-indicator
Renders a number, for example number of registered counts on geiger counter.

Other classes:

chart
A chart.
Indicator example

Example of serialized indicator:

{
 'class': 'integer-indicator',
 'metadata': {'label': 'Current voltage [V]'},
 'name': ['current_voltage'],
 'settings': {},
 'type': 'array'
}
Chart example

Example of serialized chart:

{
 'class': 'chart',
 'metadata': {'chart.axis.x.label': 'Voltage between GM electrodes',
  'chart.axis.y.label': 'Counts in given time interval',
  'label': 'GM Counter characteristics'},
 'name': ['chart'],
 'settings': {},
 'type': 'array'}
How are results send and displayed

Each output field defines from which result field (or fields) it pulls the results, this defined in the name property.

Errors

Results are send as an object with single property errors that contains a list of error objects.

Each error object has following properties:

severity
A string describing how severe is this error. Following values are acceptable: error, warning, info.
error_type
A string. Following values are accetable: device, user. If value is user it means that user generated this error, and (so) can be fixed, device means that there is some error in the device.
metadata

Object with other error info:

message
Message for user
field
If it is present it is the name of an input field that is cause of this error (used when validating input fields).

Example:

{'errors':[
   {
       'severity':'error',
       'error_type': 'user',
       'metadata':{
           'message': 'Invalid value in field foo',
           'field': 'foo',
   },{
       'severity':'error',
       'error_type': 'user',
       'metadata':{
           'message': 'Invalid value in field bar',
           'field': 'bar',
    }
])

Establishing protocol version

Version is negotiated between experiment and experiment operator. Version is established from when experiment sends silf:protocol:version:set until experiment is finished.

Note

Currently experiment is finished when experiment sends silf:experiment:stop with type done. But precise moment may depend on protocol version.

Version values

We use semantic versioning v. 2.0.0 to version the protocol.

Experiment may specify that it supports version ranges that have following syntax: 1.1.1-2.0.0 which specifies versions from (including) 1.1.1 up to (excluding) 2.0.0.

Conversation

In this document we will define what stanzas are exchanged during the protocol negotiation. All communication is done inside labdata messages, :see:`proto-1.0-labdata`.

Happy path

  • Operator sends labdata: silf:protocol:version:get
  • Server responds with : silf:protocol:version:get that contains list of avilable versions.
  • Operetor sends: silf:protocol:version:set, this stanza contains selected version.

    • Server responds with: silf:protocol:version:set with version selected by client.

      From now on version is set.

Other proper paths

Operator does not need to check avilable versions from the server, it can just send silf:protocol:version:set and hope for the best, if he sent version that can’t be handled by the experiment, experiment will send and error stanza.

Error conditions

Folowing error conditions are possible:

  • Client sends version that is not acceptable for the experiment (was not in avilable version list). In this case experiment sends silf:protocol:version:set with type: error.
  • Client can’t handle any version sent by the experiment. In this case client does not send silf:protocol:version:set and just disconects from the experiment room.
  • Client sends any other stanza not specified by the protocol. In this case experiment sends appropriate error stanza, or just ignores the message.

Joining experiment after version is established

If observer joins the room after session negotiation he is informed about protocol version by the athena bot before he joins the room.

If experiment operator joins the room after session negotiation has taken place Athena bot should send experiment version before operator is given voice in the room. If the operator is given voice in the room, and he didn’t get this information he can start version negotiation.

In both cases athena bot sends silf:protocol:version:set with type result and the same content that experiment sent earlier.

Stanza content

silf:protocol:version:get

Client sends empty labdata with type query.

Server responds with a message of type result, and list of protocol versions this experiment supports, in the following list:

{
    "versions": ["1.0", "1.0.2", "0.0.0-0.6.0"]
}

silf:protocol:version:set

Client sends labdata with type query and sends selected version:

{
    "version": "0.5.9"
}

Server responds with labdata with type result and the same content.

From now on up to the moment when experiment closes the experiment session protocol version siginified in before mentioned stanza is used.

If client sends improper version (one not present in list of versions), experiment responds with error.

Note

Server responds by confirming selected version just so all elements of the system can know version just by checking single stanza.

Example conversation

<message from='exp@muc.ilf/operator' to='exp@muc.ilf' type='groupchat'>
    <labdata xmlns="silf:protocol:version:get" type="query" id="q1"/>
</message>

<message from='exp@muc.ilf/experiment' to='exp@muc.ilf' type='groupchat'>
    <labdata xmlns="silf:protocol:version:get" type="result" id="q1">
    {
        "versions": ["1.0", "1.0.2", "0.5"]
    }
    </labdata>
</message>


<message from='exp@muc.ilf/operator' to='exp@muc.ilf' type='groupchat'>
    <labdata xmlns="silf:protocol:version:set" type="query" id="q2">
    {
        "version": "1.0"
    }
    </labdata>
</message>

<message from='exp@muc.ilf/experiment' to='exp@muc.ilf' type='groupchat'>
    <labdata xmlns="silf:protocol:version:set" type="result" id="q2">
        {
            "version": "1.0"
        }
    </labdata>
</message>

Indices and tables