Getting to know the bare protocol#

Here we want to show you how to get started with SECoP. We will begin by having a look on how to manually interact with a running SEC node. In the end, you probably won’t be using it this way, instead choosing a client which handles a lot of the logic for you. Nevertheless, understanding what happens on the wire may be beneficial, so read on if you want to, or skip over to the sections about the clients.

Talking to a SEC node#

So you have never used SECoP, and want to interact with a node? For exploring the protocol, all you need is a program that can talk tcp or serial, depending on your device. Connect to your device and send the identification message *IDN? to start communication (‘>’ and ‘<’ show who is sending the message):

> *IDN?

The SEC node replies with the version number of the protocol that it wants to speak:

< ISSE,SECoP,V2019-09-16,v1.0

Great! So we know that we are talking to something that knows SECoP, but we do not know yet what we are talking to. That is what we will find out with next message.

The Description#

> describe

We will format the answer a bit, since it is longer than the usual messages we will encounter. We will step through it afterwards.

Click to show the whole description
< describing . {
    "equipment_id": "example_heater",
    "description": "a basic example temperature SEC node.",
    "firmware": "ExampleSECoPFirmware",
    "modules": {
      "outside": {
        "interface_classes": ["Readable"],
        "description": "Outside temperature monitor.",
        "features": [],
        "implementation": "example.sensors.Temperature",
        "accessibles": {
          "value": {
            "description": "current value of the module",
            "datainfo": {
              "type": "double",
              "unit": "°C"
            },
            "description": "temperature outside",
            "readonly": true
          },
          "status": {
            "description": "current status of the module",
            "datainfo": {
              "type": "tuple",
              "members": [
                {
                  "type": "enum",
                  "members": {
                    "IDLE": 100,
                    "WARN": 200,
                    "ERROR": 400
                  }
                },
                { "type": "string" }
              ]
            },
            "readonly": true
          }
        }
      },
      "heater": {
        "description": "Example Heater",
        "implementation": "example.actuators.Heater",
        "interface_classes": ["Drivable"],
        "features": []
        "accessibles": {
          "value": {
            "description": "current value of the module",
            "datainfo": {
              "type": "double",
              "unit": "°C"
            },
            "readonly": true
          },
          "status": {
            "description": "current status of the module",
            "datainfo": {
              "type": "tuple",
              "members": [
                {
                  "type": "enum",
                  "members": {
                    "IDLE": 100,
                    "WARN": 200,
                    "BUSY": 300,
                    "ERROR": 400
                  }
                },
                {
                  "type": "string"
                }
              ]
            },
            "readonly": true
          },
          "target": {
            "description": "target value of the module",
            "datainfo": {
              "unit": "°C",
              "type": "double"
            },
            "readonly": false
          },
          "stop": {
            "description": "Stop heating, stay at current temperature.",
            "datainfo": {
              "type": "command"
            }
          },
          "_maxheaterpower": {
            "description": "maximum allowed heater power",
            "datainfo": {
              "unit": "W",
              "min": 0.0,
              "max": 100.0,
              "type": "double"
            },
            "readonly": false
          },
          "_examplecommand": {
            "description": "Do some calculation.",
            "datainfo": {
              "type": "command",
              "argument": {
                "type": "struct",
                "members": {
                  "a": {
                    "min": 0.0,
                    "max": 10.0,
                    "type": "double"
                  },
                  "b": {
                    "type": "double"
                  }
                }
              },
              "result": {
                "type": "double"
              }
            }
          }
        },
      }
    }
  }

SEC node information#

< describing . {
    "equipment_id": "example_org.example_heater",
    "description": "a basic example temperature SEC node.",
    "firmware": "ExampleSECoPFirmware v0.5",

The first few elements here are describing the capabilities of the SEC node itself. They include the firmware and version, the exposed interfaces and the unique equipment ID. The description is intended for humans to read. It can be longer than the short example here, and in the best case should include information that is useful for the operator, like the most important modules, usage hints or whatever else could be needed by a human operator beyond the information that SECoP provides.

The next element contains all modules available on the SEC node: in this case outside and heater.

Module information#

"modules": { "outside": { ... }, "heater": { ... } }

We will fist have a look at the smaller outside module:

"implementation": "example.sensors.Temperature",
"description": "Outside temperature monitor.",
"interface_classes": ["Readable"],
"features": [],

The implementation string is not standardized, but gives a hint where to find the implementation for this Module for debugging purposes, e.g. the class or source file where this module is defined. The interface_classes tells the client which capabilities the module supports. In this case, it is a Readable which is a module with a value and a status that can both be read. Additional capabilities like custom commands or parameters are not excluded, this is a minimum set of things the Module has. For a full definition, have a look at the specification. The features field is similar to the interface classes, but Features are small additions in functionality, that can be plugged into any of the interface classes. The description here can again give supplemental information about the module.

"accessibles": {
  "value": {
    "description": "current value of the module",
    "datainfo": {
      "type": "double",
      "unit": "°C"
    },
    "description": "temperature outside",
    "readonly": true
  },
  "status": {
    "description": "current status of the module",
    "datainfo": {
      "type": "tuple",
      "members": [
        {
          "type": "enum",
          "members": {
            "IDLE": 100,
            "WARN": 200,
            "ERROR": 400
          }
        },
        { "type": "string" }
      ]
    },
    "readonly": true
  }
}

The accessibles field lists all parameters that are defined on the module and can be accessed over SECoP. In the block above, you can see value and status two parameters which almost all modules will have. The precise semantics of all such parameters are defined in the specification.

The value is the current value of the module, and the status is a two-element tuple of a status code and a message that can give more information about the module’s current state. Each parameter has a description and information about data format, whether they can be written to, and more.

For the heater module, most things parallel the one before it, but there are some differences:

It is a Drivable which comes with additional things:

  • an additional status code BUSY

  • a target which is a writable parameter

  • two commands (see below)

  • a custom parameter _maxheaterpower

Every parameter or command which is not defined by the interface class or a feature has to be prefixed with an underscore. This marks it as a custom name to prevent future name clashes with the standard but otherwise, it follows the same rules as a predefined parameter/command.

"_examplecommand": {
  "description": "Do some calculation.",
  "datainfo": {
    "type": "command",
    "argument": {
      "type": "struct",
      "members": {
        "a": {
          "min": 0.0,
          "max": 10.0,
          "type": "double"
        },
        "b": {
          "type": "double"
        }
      }
    },
    "result": {
      "type": "double"
    }
  }
}

Commands are like functions that you can call on a module, they can have arguments and results. Here, we will only look at the _examplecommand command, since the predefined stop has no arguments and no result. All the information is included in the datainfo field. Every command in SECoP can only have a single argument. To make multi-argument functions, one has to use either a tuple or a struct, as shown above, where there are two named arguments a and b. These follow the same rules as the parameter datatype definitions.

Interaction#

We now know the advertised capabilities of the SEC node, and armed with that knowledge, we can interact with specific parts of it.

Reading values#

The most basic command to access a module is the read message, where we can retrieve the value of a parameter:

> read outside:value
< reply outside:value [23.2, {"t": 1212121.1212121}]

We have to specify which module and parameter we want to access, and get back an answer containing the value and so-called qualifiers which contain additional information. Here, the only qualifier is t - the timestamp of the read.

Writing values#

If we want to set a value, for example the _maxheaterpower of the heater we can use the change message:

> change heater:_maxheaterpower 40.0
< changed heater:_maxheaterpower [40.0, {"t": 1212121.1212121}]

As a reply, we get the feedback that the parameter was set. If we try to set an invalid value, we get back an error instead:

> change heater:_maxheaterpower 200
< error_change heater:_maxheaterpower ["RangeError", "200.0 must be between 0 and 100", {}]

As you can see, errors use a different message name (error_<originalmessage>) and include more information in the data part: an error class (which is defined by the specification), an error string giving more information, and some qualifiers (in this case, none).

Running commands#

Running a command is done with the do message:

> do heater:stop
< done heater:stop

As feedback that the command was run, we get back a done acknowledgment. Again, we might get an error_do message instead that indicates that something did not go as requested (or the request itself is bad).

Actions that take longer#

In SECoP, running commands or changing parameters does not block until the physical action is done. To explain, if you set the target parameter to 20K above the current value, depending on what the heater actually heats, it may take a while to heat up. You would immediately get the feedback that the target was changed, and you would then see the value going up as the hardware does its job. To know when the command or parameter change is completed, you have to have a look at the status. It will go BUSY until the change is done. When it returns to IDLE then the action is finished.

The other commands won’t be discussed here, but as a pointer have a look at activate which enables asynchronous mode. That gives you a stream of updates for all parameters of a SEC node without polling.

Letting the computer do it for you#

Of course, the point of this protocol is to automate the communication with your sample environment hardware. It would defeat the purpose if you sit at the instrument all day, typing commands. Clients that can consume the description can do the work for you. For some existing implementations, have a look at the implementations or their respective starting guides. Or if you want to write your own, please tell us, we’d love to hear about it!