.. _scpiDevice: *************************** How to write a SCPI device *************************** Introduction ============ From `wikipedia `_: 'The Standard Commands for Programmable Instruments (SCPI) (often pronounced "skippy") defines a standard for syntax and commands to use in controlling programmable test and measurement devices.' The terms 'instrument' and 'device' will be used when when referring to hardware instruments and software (Karabo) devices, respectively. Supported interfaces ==================== The base class currently supports TCP/IP, USB and serial communication with the SCPI instrument. The interface can be set-up by means of the ``Instrument URL`` device property. The URL must contain a scheme and a path. If no scheme is specified, 'socket' is assumed. In case of TCP/IP communication the scheme to be used is 'socket' or 'tcp' and the path must contain the hostname and port separated by a colon, namely 'socket://hostname:port' or 'tcp://hostname:port'. In case of serial interface the scheme to be used is 'serial' and the path must contain the file pointer to the serial port (e.g. /dev/ttyUSB0). The other mandatory parameter is the baud rate, to be specified in the query part of the URL. A minimal serial URL would be for example 'serial:///dev/ttyUSB0?baudRate=57600'. Optional parameters like 'dataBits', 'stopBits' and 'parity' can be specified in the query, e.g. 'serial:///dev/ttyUSB0?baudRate=57600&dataBits=7&stopBits=1&parity=O'. In case of a USB interface the scheme shall be 'usb' and the path shall contain the vendor and product ID separated by a colon, namely 'usb://idVendor:idProduct'. In case more than one USB device of the same kind is attached to the computer, you must also give its serial number or the bus and address in the query part of the URL to unambiguously identify one of them. For example 'usb://2457:1012?serialNumber=SN12345' or 'usb://2457:1012? bus=003&address=002'. In order to find out the correct values of the parameters, you can run in a terminal the ``lsusb -v`` command . How to Create a SCPI Karabo Device ================================== Create a python Karabo Device, called for example myScpiDevice: .. code-block:: bash ./karabo new myScpiDevice python Go into the base directory of the project: .. code-block:: bash cd karabo/devices/myScpiDevice Add a plaintext file named DEPENDS that contains the scpi version tag, like this: .. code-block:: bash #package, Branch scpi, 2.1.0-2.16.5 Do not forget to add the DEPENDS file to the repository: .. code-block:: bash git add DEPENDS Now you can open the source file, MyScpiDevice.py, for editing. Base Device Classes =================== There are currently three base classes available for SCPI devices: ``ScpiDevice``, ``ScpiOnOff``, ``ScpiStartStop``. ``ScpiDevice`` is the simplest one. It has only three states: ``UNKNOWN`` (i.e. the software device is not connected to the hardware), ``NORMAL`` (i.e. the hardware is functional) and ``ERROR`` (i.e. the hardware is returning some error). ``ScpiOnOff`` has five states: ``UNKNOWN``, ``ERROR``, ``ON``, ``OFF`` and ``CHANGING`` (e.g. for when the hardware is switching between ``ON`` and ``OFF``). This class is suitable for example for power supplies. ``ScpiStartStop`` has also five states: ``UNKNOWN``, ``ERROR``, ``STARTED``, ``STOPPED`` and ``CHANGING`` (e.g. for when the hardware is switching between ``STARTED`` and ``STOPPED``). This class is suitable for example for measurement devices. .. _scpi-writing-a-device: Writing a Device ================ Writing a Simple SCPI Device ---------------------------- The base class to be used is ``ScpiDevice``. The device is very simple and should look like: .. code-block:: python ############################################################################ # Author: # Created on March 29, 2023 # Copyright (C) European XFEL GmbH Schenefeld. All rights reserved. ############################################################################ from karabo.bound import KARABO_CLASSINFO from scpi.scpi_device import ScpiDevice from ._version import version as deviceVersion @KARABO_CLASSINFO("MyScpiDevice", deviceVersion) class MyScpiDevice(ScpiDevice): def __init__(self, configuration): # always call superclass constructor first! super().__init__(configuration) self.commandTerminator = "\r\n" # The command terminator @staticmethod def expectedParameters(expected): ( # Fill here with the list of expected parameters ) The SCPI parameters can be accessed using Karabo expected parameters (see the :ref:`scpi-expected-parameters` Section for details). Writing an On/Off Device ------------------------ The base class to be used is ``ScpiOnOff``. The device should look pretty much like the Ok/Error one, except for the base class to be used. You will also need to define an ``alias`` for the ``on`` and ``off`` slots, see the :ref:`scpi-expected-parameters` Section for details). .. code-block:: python ############################################################################ # Author: # Created on March 29, 2023 # Copyright (C) European XFEL GmbH Schenefeld. All rights reserved. ############################################################################ from karabo.bound import ( KARABO_CLASSINFO, OVERWRITE_ELEMENT ) from scpi.scpi_on_off import ScpiOnOff from ._version import version as deviceVersion @KARABO_CLASSINFO("MyScpiDevice", deviceVersion) class MyScpiDevice(ScpiOnOff): def __init__(self, configuration): # always call superclass constructor first! super().__init__(configuration) self.commandTerminator = "\r\n" # The command terminator @staticmethod def expectedParameters(expected): ( # Define alias for the "on" slot OVERWRITE_ELEMENT(expected).key("on") .setNewAlias("OUTP ON;;;;") .commit(), # Define alias for the "off" slot OVERWRITE_ELEMENT(expected).key("off") .setNewAlias("OUTP OFF;;;;") .commit(), # Fill here with the list of expected parameters ) By default the ``on`` and ``off`` slots will send the corresponding SCPI command and immediately change device state (to respectively ``ON`` and ``OFF``). In case the switch takes some time, and the instrument has some property which can be used to evaluate its actual state, this default behaviour shall be overridden, e.g. .. code-block:: python @staticmethod def expectedParameters(expected): ( [...] # This parameter is tagged as 'poll', thus the query 'OUTP?' will # be sent periodically and the Karabo device updated accordingly. BOOL_ELEMENT(expected).key("isOn") .tags("scpi,poll") .alias(";;OUTP?;{isOn:b};") .displayedName("Device is ON") .description("True when the device is ON.") .readOnly() .commit(), ) # Overrides ScpiOnOff.on() def on(self): self["status"] = "Being switched on" self.updateState(State.CHANGING) self.sendCommand("on") # followHardwareState() will take care of updating the state when # the hardware is done. # Overrides ScpiOnOff.off() def off(self): self["status"] = "Being switched off" self.updateState(State.CHANGING) self.sendCommand("off") # followHardwareState() will take care of updating the state when # the hardware is done. # This function will be called at the end periodic device poll def followHardwareState(self): is_on = self["isOn"] state = self["state"] if state in (State.CHANGING, State.OFF) and is_on: self.log.INFO('Follow hardware state -> ON') self.updateState(State.ON) elif state in (State.CHANGING, State.ON) and not is_on: self.log.INFO('Follow hardware state -> OFF') self.updateState(State.OFF) Writing a Start/Stop Device --------------------------- The base class to be used is ``ScpiStartStop``. Similarly to the On-Off one, you will need to define here aliases for the ``start`` and ``stop`` slots. You will also need to provide a ``processAsyncData`` function to process the data that are received asynchronously during a measurement. The device should look like .. code-block:: python ############################################################################ # Author: # Created on March 29, 2023 # Copyright (C) European XFEL GmbH Schenefeld. All rights reserved. ############################################################################ from karabo.bound import ( KARABO_CLASSINFO, OVERWRITE_ELEMENT ) from scpi.scpi_start_stop import ScpiStartStop from ._version import version as deviceVersion @KARABO_CLASSINFO("MyScpiDevice", deviceVersion) class MyScpiDevice(ScpiStartStop): def __init__(self, configuration): # always call superclass constructor first! super().__init__(configuration) self.commandTerminator = "\r\n" # The command terminator @staticmethod def expectedParameters(expected): ( # Define alias for the "start" slot OVERWRITE_ELEMENT(expected).key("start") .setNewAlias("INIT;;;;") .commit(), # Define alias for the "stop" slot OVERWRITE_ELEMENT(expected).key("stop") .setNewAlias("ABORT;;;;") .commit(), # Fill here with the list of expected parameters ) # Use this one to process asynchronously received data and possibly # update device properties. def processAsyncData(self, data): pass Also in the case of ``ScpiStartStop`` the ``start`` and ``stop`` will send the respective SCPI command and change immediately the device state. The default behaviour can be changed as described in the previous section. How to read/write parameters from/to the instrument --------------------------------------------------- Each parameter on the instrument you want to expose in the Karabo device must have a corresponding expected parameter in the Karabo device. The expected parameter must be tagged as ``scpi``. Please have a look at the :ref:`scpi-expected-parameters` Section for the details. How to deal with asynchronous instrument messages ------------------------------------------------- To parse unexpected messages (e.g. error message) that may come while instrument is polled [Note that this violates SCPI specifications] the method ``parseAsyncStrings()`` can be implemented in the derived class. .. _scpi-expected-parameters: Expected Parameters =================== .. _scpi-tags: Tags ---- * ``scpi`` tag: Parameters to be read from (written to) the SCPI instrument must have the ``scpi`` tag. * ``readOnConnect`` and ``writeOnConnect`` tags: Parameters having the ``readOnConnect`` (respectively ``writeOnConnect``) flag will be read from (written to) the instrument when the Karabo device connects to it. * ``poll`` tag: Parameters having the ``poll`` tag will be polled periodically. The poll interval is a parameter of the base class. The ``sendOnConnect`` Parameter ------------------------------- Commands to be sent to the instrument when the Karabo device connects to it (for example some initial configuration), can be listed in the ``__init__`` function. For example: .. code-block:: python self.sendOnConnect = ['TRIG:LEV 10', 'TRIG:SOURCE EXT', 'SYST:COMM:SER:BAUD 19200'] These commands will be sent before the expected parameters with ``writeOnConnect`` tag (see :ref:`scpi-tags` Section). The ``sendOnReset`` Parameter ----------------------------- Commands to be sent to the instrument when executing the ``reset`` command, can be listed in the ``__init__`` function. For example: .. code-block:: python self.sendOnReset = ['Reset Uc'] Aliases ------- The SCPI commands and queries corresponding to writing and reading any parameter must be written in the parameter ``alias``. Different fields in the ``alias`` have to be separated by semicolons (;) or a different separator (as explained in :ref:`scpi-alias-separator` Section). For example: .. code-block:: python INT32_ELEMENT(expected).key("resolutionMode") .tags("scpi poll") .alias(">S1H {resolutionMode};E0;>S1H?;S1H:{resolutionMode:d};") .displayedName("Current Resolution Mode") .description("Set the current resolution mode (0=normal 1=high resolution).") .assignmentOptional().defaultValue(0) .options("0 1") .allowedStates(State.ON, State.OFF) .reconfigurable() .commit(), The first field in the ``alias`` contains the set command (ie ``>S1H``) and its parameters (ie ``{resolutionMode}``) for the ``resolutionMode``. This string will be parsed, and ``{resolutionMode}`` will be replaced by the configuration value corresponding to the key. The second field (ie ``E0``) is the expected reply to the set command; it is also parsed to extract parameters (none in this example). The third field contains the query command (ie ``>S1H?``) and its parameters (none). The fourth field (ie ``{resolutionMode:d}``) is the expected reply to the query; it is parsed and ``resolutionMode`` is extracted as integer (``d``). The parsing is done by using the python parse package (see `documentation `_), therefore all types defined there can be used: +------+---------------------------------------------------------+----------+ | Type | Characters Matched | Output | +======+=========================================================+==========+ | w | Letters and underscore | str | +------+---------------------------------------------------------+----------+ | W | Non-letter and underscore | str | +------+---------------------------------------------------------+----------+ | s | Whitespace | str | +------+---------------------------------------------------------+----------+ | S | Non-whitespace | str | +------+---------------------------------------------------------+----------+ | d | Digits (effectively integer numbers) | int | +------+---------------------------------------------------------+----------+ | D | Non-digit | str | +------+---------------------------------------------------------+----------+ | n | Numbers with thousands separators (, or .) | int | +------+---------------------------------------------------------+----------+ | % | Percentage (converted to value/100.0) | float | +------+---------------------------------------------------------+----------+ | f | Fixed-point numbers | float | +------+---------------------------------------------------------+----------+ | e | Floating-point numbers with exponent e.g. 1.1e-10, | float | | | NAN (all case insensitive) | | +------+---------------------------------------------------------+----------+ | g | General number format (either d, f or e) | float | +------+---------------------------------------------------------+----------+ | b | Binary numbers | int | +------+---------------------------------------------------------+----------+ | o | Octal numbers | int | +------+---------------------------------------------------------+----------+ | x | Hexadecimal numbers (lower and upper case) | int | +------+---------------------------------------------------------+----------+ | ti | ISO 8601 format date/time | datetime | | | e.g. 1972-01-20T10:21:36Z (“T” and “Z” optional) | | +------+---------------------------------------------------------+----------+ | te | RFC2822 e-mail format date/time | datetime | | | e.g. Mon, 20 Jan 1972 10:21:36 +1000 | | +------+---------------------------------------------------------+----------+ | tg | Global (day/month) format date/time | datetime | | | e.g. 20/1/1972 10:21:36 AM +1:00 | | +------+---------------------------------------------------------+----------+ | ta | US (month/day) format date/time | datetime | | | e.g. 1/20/1972 10:21:36 PM +10:30 | | +------+---------------------------------------------------------+----------+ | tc | ctime() format date/time | datetime | | | e.g. Sun Sep 16 01:03:52 1973 | | +------+---------------------------------------------------------+----------+ | th | HTTP log format date/time | datetime | | | e.g. 21/Nov/2011:00:07:11 +0000 | | +------+---------------------------------------------------------+----------+ | ts | Linux system log format date/time | datetime | | | e.g. Nov 9 03:37:44 | | +------+---------------------------------------------------------+----------+ | tt | Time e.g. 10:21:36 PM -5:30 | time | +------+---------------------------------------------------------+----------+ In addition, two extra types can be used for the SCPI devices: +------+---------------------------------------------------------+----------+ | Type | Characters Matched | Output | +======+=========================================================+==========+ | p | All printable characters | str | +------+---------------------------------------------------------+----------+ | P | All non-printable characters | str | +------+---------------------------------------------------------+----------+ .. _scpi-alias-separator: The ``aliasSeparator`` Parameter -------------------------------- The separator for the fields in the ``alias`` is by default the semicolon (;), but can be changed to a different one in the ``__init__`` function; for example: .. code-block:: python self.aliasSeparator = "|" will change it to the pipe character (\|). The ``initialConnectedState`` Parameter --------------------------------------- It defines which will be the device state after successful connection to the remote instrument. It is set to ``State.NORMAL`` for a ``ScpiDevice``, to ``State.OFF`` for a ``ScpiOnOffDevice`` and to ``State.STOPPED`` for a ``ScpiStartStopDevice``. The ``commandPrefix`` and ``terminator`` Parameters --------------------------------------------------- The command prefix and terminator - to be used in the communications between the Karabo device and the SCPI instrument - can be set in two different ways. For a given device, the command prefix and terminator are usually known and fixed, therefore should be hard-coded in the Karabo device. This can be done by adding lines like these to the ``__init__`` function: .. code-block:: python self.commandTerminator = "\r\n" # The command terminator self.commandPrefix = "*" # all command strings starts with # asterisk ('*') char The second way to set command prefix and terminator is by adding the ``commandPrefix`` and/or ``terminator`` expected parameters. This should be done for "generic" devices, for which different prefixes and terminators should be available at instantiation time. For example: .. code-block:: python # Re-define default value and options STRING_ELEMENT(expected).key("terminator") .displayedName("Command Terminator") .description("The command terminator.") .assignmentOptional().defaultValue("\\n") .options("\\n \\r \\r\\n") .init() .commit(), # Re-define default value and options STRING_ELEMENT(expected).key("commandPrefix") .displayedName("Command Prefix") .description("The command prefix.") .assignmentOptional().defaultValue("") .options('* > ""') .init() .commit(), If the terminator is not set in the Karabo device, the default one will be used for communications with the SCPI instrument: ``\n``. If the command prefix is not set in the Karabo device, the default one will be used for communications with the SCPI instrument: ``''`` (empty string i.e. no prefix). The ``responsePrefix`` and ``responseTerminator`` Parameters ------------------------------------------------------------ In addition to the command prefix and terminator described in the previous section, prefix and terminator for responses can be optionally set. If response prefix/terminator value is not set, the command prefix/terminator will be used for responses as well. Also for the response prefix and terminator there are two ways of setting. You can use either: .. code-block:: python self.responseTerminator = "\0" self.responsePrefix = ">" in the ``__init__`` function, or add the ``responsePrefix`` and/or ``responseTerminator`` expected parameter. This second option should be used for "generic" devices, as explained in the previous section. .. _scpi-timeout-parameter: The ``scpiTimeout`` Parameter ----------------------------- The default scpi communication timeout used in the base class is 1 second. This value is normally ok, but some instruments (eg the agilentMultimeterPy) may need a longer time to give back a measurement. The scpi timeout (in seconds) can be changed in ``__init__`` with something like: .. code-block:: python self.scpiTimeout = 5.0 # New timeout value in seconds A second way to set it is by adding the ``scpiTimeout`` expected parameter. In this way the timeout can be changed during the lifetime of the Karabo device. For example: .. code-block:: python FLOAT_ELEMENT(expected).key("scpiTimeout") .displayedName("SCPI Timeout") .description("The scpi communication timeout.") .unit(Unit.SECOND) .assignmentOptional().defaultValue(1.0) .reconfigurable() .commit(), If the scpi timeout is not set in the Karabo device, the default value of 1 s will be used. The ``scpiWaitAfterRequest`` Parameter -------------------------------------- By default there is no wait time between two consecutive queries or commands, in the base class. This means: as soon as the reply comes from the instrument, the next query or command is sent. There is the possibility to modify the wait time in ``__init__`` by doing .. code-block:: python self.scpiWaitAfterRequest = 0.01 # New wait time in seconds or by adding a ``scpiWaitAfterRequest`` element to the expected parameters. The ``socketTimeout`` Parameter ------------------------------- The default TCP socket timeout used in the base class is 1 second. Similarly to the scpi communication timeout, also the TCP socket timeout for the device can be set to a differene value, either in the ``__init__`` by doing .. code-block:: python self.socketTimeout = 2.0 # New timeout value in seconds or by adding a ``socketTimeout`` element to the expected parameters. The ``usbTimeout`` Parameter ---------------------------- The default USB timeout used in the base class is 1 second. Similarly to the other timeouts, it can be changed, either in the ``__init__`` by doing .. code-block:: python self.usbTimeout = 2.0 # New timeout value in seconds or by adding a ``usbTimeout`` element to the expected parameters. On/Off (and Start/Stop) Slots ----------------------------- For On/Off (Start/Stop) devices, the on/off (start/stop) slots are already defined. What you have to do, is to set the SCPI command in the slots's ``alias``. For example, for the start/stop: .. code-block:: python # Define alias for the "start" slot OVERWRITE_ELEMENT(expected).key("start") .setNewAlias("INIT;;;;") .commit(), # Define alias for the "stop" slot OVERWRITE_ELEMENT(expected).key("stop") .setNewAlias("ABORT;;;;") .commit(), A Complete Example ------------------ Here is a complete example of expected parameters for a Start/Stop device: .. code-block:: python # Define alias for the "start" slot OVERWRITE_ELEMENT(expected).key("start") .setNewAlias("INIT;;;;") # No query available .commit(), # Define alias for the "stop" slot OVERWRITE_ELEMENT(expected).key("stop") .setNewAlias("ABORT;;;;") # No query available .commit(), # Re-define default value and options STRING_ELEMENT(expected).key("terminator") .displayedName("Command Terminator") .description("The command terminator.") .assignmentOptional().defaultValue("\\n") .options("\\n") .init() .commit(), STRING_ELEMENT(expected).key("handshake") .tags("scpi") .alias("SYST:COMM:HAND {handshake};;SYST:COMM:HAND?;{handshake:w};") .displayedName("Handshake") .description("Set the state of the message roundtrip handshaking.") .assignmentOptional().defaultValue("OFF") .options("OFF ON") .allowedStates(State.STOPPED) .reconfigurable() .commit(), STRING_ELEMENT(expected).key("baudRate") .tags("scpi") .alias("SYST:COMM:SER:BAUD {baudRate};;SYST:COMM:SER:BAUD?;{baudRate:w};") .displayedName("Serial Baud Rate") .description("Set the transmit and receive baud rates on the RS-232 port.") .assignmentOptional().defaultValue("9600") .options("DEFAULT 9600 19200 38400 57600 115200") .allowedStates(State.STOPPED) .reconfigurable() .commit(), INT32_ELEMENT(expected).key("errorCount") .tags("scpi poll") .alias(";;SYST:ERR:COUNT?;{errorCount:d};") # Only query available .displayedName("Error Count") .description("The number of error records in the queue.") .readOnly() .commit(), STRING_ELEMENT(expected).key("measureType") .tags("scpi writeOnConnect") # Write to h/w at initialization .alias("CONF:MEAS:TYPE {measureType};;CONF:MEAS:TYPE?;{measureType:w};") .displayedName("Measure Type") .description("Set the meter measurement mode (energy or power).") .assignmentOptional().defaultValue("J") .options("DEFAULT J W") .allowedStates(State.STOPPED) .reconfigurable() .commit(), STRING_ELEMENT(expected).key("serialNumber") .tags("scpi readOnConnect") # Read from h/w at initialization .alias(";;SYST:INF:SNUM?;\"{serialNumber:p}\";") # Only query available .displayedName("Serial Number") .description("The serial number.") .readOnly() .commit(), Polling Device Properties ========================= All the expected parameters having the ``poll`` tag will be automatically polled (see :ref:`scpi-tags` Section). The refresh interval is given by the ``pollInterval`` device parameter. An immediate refresh can be triggered by the ``pollNow`` command. The list of parameters to be polled can be reconfigured be means of the ``propertiesToPoll`` property. For example, if you set it to ``handshake,baudRate`` these two properties will be polled. The access level for ``propertiesToPoll`` property is expert. A user's hook is also provided by the base class, allowing the post-processing of the polled properties. For example, if you read some temperature in Fahrenheit degrees and you want to display it in Celsius, you can define two expected parameters, like in the following: .. code-block:: python FLOAT_ELEMENT(expected).key("temperature") .displayedName("Temperature") .description("Blah blah.") .unit(Unit.DEGREE_CELSIUS) .readOnly() .commit(), FLOAT_ELEMENT(expected).key("temperatureFahrenheit") .tags("scpi poll") .alias(";;GETTEMP;{temperatureHex:g};") .displayedName("Temperature Fahrenheit") .description("Blah blah.") .expertAccess() # Only visible to expert .readOnly() .commit(), Then, you can postprocess the polled data this way: .. code-block:: python def pollInstrumentSpecific(self): # 'temperatureFahrenheit' is a polled property tF = self.get('temperatureFahrenheit') # Convert temperatureFahrenheit into Celsius degrees tC = (tF - 32.) / 1.8 # Set 'temperature' on the Karabo device, which is a derived property self.set('temperature', tC) The ``gotoErrorOnPollFailure`` Parameter ---------------------------------------- If the poll of a parameter fails, an error message will be logged, but no further action will be by default taken. By setting the ``gotoErrorOnPollFailure`` parameter to ``True``, if **all** parameter polls fail, the device will go to error state. Reading and Writing Properties on Connection ============================================ As mentioned above (see :ref:`scpi-tags` Section) parameters to be sent to hardware upon connection (e.g. stored configuration values) are marked with ``writeOnConnect`` tag, while parameters to be retrieved from the hardware at connection are marked with ``readOnConnect`` tag. The base class provides the ``readOnConnectInstrumentSpecific`` hook that can be optionally implemented in the derived device to perform any parsing or post processing of these parameters, similarly to ``pollInstrumentSpecific``. Preprocessing the Incoming Reconfiguration ========================================== The base class provides a user's hook to preprocess the incoming reconfiguration, before any command is sent to the instrument. It can be used for example when the instrument expects a parameter in some unusual units, and you would like to allow the user to input the parameter with a more standard unit. This can be done by using two expected parameters, like in the following: .. code-block:: python FLOAT_ELEMENT(expected).key("temperature") .tags("scpi") .alias("SETTEMP {temperatureHex};;;;") .displayedName("Temperature") .description("Blah blah.") .unit(Unit.DEGREE_CELSIUS) .reconfigurable() .commit(), STRING_ELEMENT(expected).key("temperatureHex") .displayedName("TemperatureHex") .description("Blah blah.") .expertAccess() # Only visible to expert .readOnly() .commit(), and then by coding the relation between temperature and temperatureHex in the preprocessConfiguration function, .. code-block:: python def preprocessConfiguration(self, inputConfig): if inputConfig.has('temperature'): # Get temperature from inputConfig, change unit, # represent it as bytes temp = inputConfig.get('temperature') # eg -23.15 temp100 = np.int16(100*temp) # -2315 tempBytes = temp100.tostring() # b'\xf5\xf6' tempHex = '%02X%02X'%(tempBytes[0], tempBytes[1]) # 'F5F6' self.set('temperatureHex', tempHex) Setting a new value for ``temperature`` will make preprocessConfiguration called. A new value for ``temperatureHex`` will be set in the device, and only then the command ``SETTEMP`` will be sent to the instrument, with ``temperatureHex`` as an additional parameter. Postprocessing the Incoming Reconfiguration =========================================== The base class provides an additional user's hook called when all actions defined in an incoming reconfiguration have been performed. This can be used to immediately trigger property polling after a property, e.g. 'xyz', has been reconfigured which is known to cause side-effects on other properties. Manipulating the polling event loop in this way reduces the period when misalignment of property values are exposed to clients. .. code-block:: python def postprocessConfiguration(self, inputConfig): if inputConfig.has('xyz'): self.pollTrigger = True Enabling the Heartbeat ====================== The scpi Karabo device can periodically send a heartbeat query to the instrument. The default query is ``*IDN?``, which should be available for all SCPI-compliant instruments. The sending of the heartbeat query is by default disabled, but it can be enabled by setting the ``enableHeartbeat`` property to ``True``. Once a reply to the query is received, it is published in the ``heartbeatReply`` property, and the ``heartbeatTime`` is updated as well, with the current time. The query can be changed in the ``heartbeatCommand`` property, and also the time interval can be changed in the ``heartbeatPeriod`` property (default value is 5 s). Enabling Auto-Connect ===================== Setting the ``autoConnect`` property ``True`` will enable auto connecting to the instrument when the state is ``UNKNOWN``. The default property value is ``False``. Injecting into the Schema ========================= The ``postOnConnect`` hook is provided and can be used, amongst other actions, to inject properties into the schema. For schema injection the user must control in the derived class whether multiple injection, after each connect, is required or must be prevented. The base class always calls the hook. Name Swapping on Write and Read =============================== The ``addValueSwap`` function is used to associate a class with the key of a property. Calling addValueSwap(self, key, className) associates className with key. The list of user friendly device swvalue value and instrument side hwvalue value associations are defined in module imported into the device. The ValueSwap class is required to be iterable and have attributes swvalue and hwvalue, and can be provided most easily by using an enum as shown below. .. code-block:: python @unique class GasTypes(ValueSwapEnum): N2orAir = ('N2orAir', '0') Ar = ('Ar', '1') Unknown = ('Unknown', '2') , or are created inline .. code-block:: python aa = ['N2orAir', 'Ar', 'Unknown'] bb = ['0', '1', '2'] GasTypes = ValueSwapEnum('GasTypes', [(a, (a, b)) for a, b, c in zip(aa, bb)]) In either case the value swap must be registered. .. code-block:: python self.addValueSwap('xgsSensor{}.gas'.format(name), GasTypes) Both inherit from ValueSwapEnum .. code-block:: python from enum import Enum, unique @unique class ValueSwapEnum(Enum): def __init__(self, swvalue, hwvalue): self.swvalue = swvalue self.hwvalue = hwvalue The enum can also simplify creating expected properties. .. code-block:: python gas_desc = ', '.join(['{} = {}'.format(x.swvalue, x.hwvalue) for x in GasTypes]) gas_opts = ', '.join(['{}'.format(x.swvalue) for x in GasTypes]) gas_def = GasTypes.Unknown.swvalue ( STRING_ELEMENT(expected).key('xgsSensor{}.gas'.format(name)) .tags('scpi poll') .alias('4F{}{{xgsSensor{}.gas}};;4E{};{{xgsSensor{}.gas}};'.format(gauge, name, gauge, name)) .displayedName('Gas type') .description('Gas type = hw id: ({})'.format(gas_desc)) .allowedStates(State.NORMAL) .options(gas_opts) .assignmentOptional().defaultValue(gas_def) .reconfigurable() .commit(), ) Sending an Arbitrary Command to the Instrument ============================================== An arbitrary command (or query) can be sent to the instrument. To do so, it is enough to write the command (or query) in the ``sendCommand`` device property. This property is expert access level. Once a reply is received, it is published in the ``replyToCommand`` property. If no reply is received after the scpi timeout (see :ref:`scpi-timeout-parameter`), ``replyToCommand`` is left empty. Query and Command Statistics ============================ The query and command statistics are accessible at expert level, .. _scpi-queries_and_commands: .. figure:: queries_and_commands.png :alt: queries_and_commands.png For both queries and commands, the number of successes and failures is available, as well as the date/time of the last one. The counters will be reset on reconnection to the device. Initial Check of the Connection to Equipment ============================================ Some SCPI hardware equipments are connected via an intermediate interface. An example is the Lantronix, which is used to allow a remote access (via its TCP/IP address) to an RS-232-based EnergyMax instrument. If the TCP/IP and port of the Lantronix device are correctly assigned, the SCPI karabo device will successfully establish a TCP/IP connection to that port, but no error will be reported if no equipment is there connected. In this hardware configuration, the developer should set the parameter ``self.enableConnectionCheck`` to ``True`` in the ``__init__`` function. This will verify an established communication by requesting the heartbeat; the proper value ``Heartbeat text`` (``heartbeatCommand`` key) should be set according to the used equipment. Useful Tips for Write (Set) and Read (Get) Patterns to Use ========================================================== In this section patterns for write (set) and read (get) operations are described. The tips derive from coding scpi-like instruments which are not fully compatible with the scpi-1999 standard. Use Names Instead of Numbers ---------------------------- Using 'ON' and 'OFF' at the device interface and converting these to '1' and '0' when written to the instrument and reversing the procedure on reading produces an easily understandable interface for the user. This can be achieved by using name swapping as described in ``Name swapping on write and read`` which additionally simplifies filling in property definitions. Consider Minimizing the Number Device Buttons --------------------------------------------- Most SCPI instruments are relatively simple and require only a few command actions (e.g. 'on/off', 'factory reset', 'calibrate device', etc.) and implementing these commands as device buttons is clean and follows the Karabo principle that commands are buttons. However, when coding multi-channel devices, it may be more appropriate to perform actions through read/write properties. This technique is used by MPOD, where O(100) channels can be present. Minimize the Number of Properties Defined ----------------------------------------- Two methods to set an instrument property value are typically encountered, 1) where a set command followed by the value to set are written to the instrument, and 2) where different commands are written without a qualifying value, meaning each command represents a particular value to be set. A single command is defined to read the value set. In the following example the type 2 set commands defined are 10 = Torr, 11 = mBar and 12 = Pascal. The read command is 13. By moving the second character of the command into a name swap definition the number of properties defined is minimized. .. code-block:: python @unique class PressureUnits(ValueSwapEnum): Torr = ('Torr', '0') Mbar = ('mBar', '1') Pascal = ('Pascal', '2') punit_desc = ', '.join( ['{} = {}'.format(x.swvalue, x.hwvalue) for x in PressureUnits]) punit_opts = ', '.join( ['{}'.format(x.swvalue) for x in PressureUnits]) punit_def = PressureUnits.Mbar.value[0] STRING_ELEMENT(expected).key("pressureUnits") .tags("scpi writeOnConnect readOnConnect poll") .alias("1{pressureUnits};;13;0{pressureUnits};") .displayedName('Pressure units') .description('Choices are {}. Note 1: the unit is set to {}' ' when the connection is established.'.format( punit_names, punit_def)) .allowedStates(State.NORMAL) .options(punit_opts) .assignmentOptional().defaultValue(punit_def) .reconfigurable() .commit(), self.addValueSwap('pressureUnits', PressureUnits) Use Option Sets Whenever Possible --------------------------------- Name swapping is not a requirement of using option sets. If a property value written and the read value is an item of the same set (e.g. ['a', 'b', 'c']), i.e. no name swap is needed, then use the options attribute when defining the property. This has the advantage of exposing a pull down menu of the set at the gui and turning on property value validation on set and read. String Comments --------------- Command and queries exchanged between instruments and devices are character strings. It is good practise is to use the correct type (int, float...) in (Karabo) devices for a property as validation of the property type is performed automatically. Strings are only loosely validated and devices may have to enforce requirements on string properties required by the instrument in Karabo's preConfiguration() callback. Strings which fail requirements must not be set and an ERROR log message must be injected. Some requirements are softer, e.g. a supplied string is too long but can be set if truncated, in this case a WARN log message must be injected. It is good practice is to document format requirements in the properties description.