Programming Guide

Device Manager Extensions

The base classes provided by the FCF already implement large part of the functionality needed to implement the control of devices. However it is always needed to implement specific functionality and this can be achieved by extending basic device classes. This section gives a short overview of the most important aspect for developing custom devices.

Having type safe interfaces, as defined by CII ICD XML, enables to improve runtime robustness of our applications. However it is also makes the extendability harder to achieve. To solve this problem and to provide more flexibility to the developers, custom devices shall use a JSON based encoding for defining the parameters of the Setup command. This payload shall be encapsulated in the Setup command such that standard and custom devices can be managed by the FCF Device Manager.

Device Classes

Every device has a class which is derived from a common device class (fcf::devmgr:common:Device). This common class uses a dedicated configuration object to handle all the device configuration. It also uses a LCS interface object to manage the communication with the device controller running on the PLC.

Applications can create new classes by extending the Device class or by extending existing devices. If no special configuration is needed by the device, then there is no need to use a custom configuration class and it is possible to reuse the common one. The usage of a dedicated LCS interface class is normally needed because every device has a different interface.

Methods

There are few methods to be implemented in the device class to create a new one. The description of the methods are listed in the following table. These methods shall override the parent implementation and they will be called automatically by the device facade in response of the actions triggered by the user.

Method

Description

CreateObjects

Create the instances of the config and LCS interface classes. It writes the initial device configuration into the OLDB.

Setup

Parse the setup parameters and trigger the actions on the PLC. The payload for a custom device is specific and it shall contain a JSON string that have to be parsed by the device. Here the LCS interface class instance is used to interact with the controller.

IsSetupActive

It monitors the action triggered by the Setup to decide when the action has been completed for the device. This information is used by the device facade to return the answer of the setup command to the originator. For instance, if you move a motor to a certain position, it checks if the target position has been achieved. This method is called regularly by the device facade when waiting for the result of the setup command.

UpdateStatus

Get the latest device status received by the device. It also updates the OLDB with the device status data.

Status

Implement specific actions for the GetStatus command. This method adds all the relevant information about the device into the buffer. The buffer delivered by the GetStatus is a composition of all the status information delivered by every device.

Status

Similar to the Status above but specific for the FITS header metadata information.

Device Config

There is no need to use a custom device configuration unless the device has some special parameters. Even if a new devices uses new parameters that need to be downloaded to the PLC, it is possible to do it with the common configuration class (fcf::devmgr:common:DeviceConfig). In the FCF, most of the devices do not implement a dedicated configuration class, e.g. Shutter or Lamp. The DeviceConfig class will automatically download any configuration under ctrl_config which uses built-in data types.

LCS Interface

The FCF provides a basic class that implements the common functionality (fcf::devmgr:common:DeviceLcsIf). Custom devices do not require to implement a large and complex LCS interface class . A typical need is the implementation of a new RPC. In such a case, you can just extend the existing class by adding a new method to handle the RPC. An example of an RPC method implementation is show below (from Shutter device).

void ShutterLcsIf::Open() {
    LOG4CPLUS_TRACE_METHOD(m_logger,__PRETTY_FUNCTION__);

    protocol::base::VectorVariant attr_list;
    std::string obj;
    std::string proc;
    // Get the RPC node id based on the device configuration
    proc = m_config->GetProcId(GetMapValue(fcs::CAT_RPC,
                                       RPC_OPEN));
    // Get the PLC object ID from the configuration
    obj = m_config->GetObjId();
    try {
        LOG4CPLUS_INFO(m_logger, "[" << m_config->GetName() << "] "
                     << "Openning shutter ...");
        // Executes the RPC in this case with an empty attribute vector
        ExecuteRpc(obj, proc, attr_list);
    } catch (std::exception& e) {
        const std::string msg = "[" + m_config->GetName()
            + "] Openning failure: " + e.what();
        LOG4CPLUS_ERROR(m_logger,  msg);
        throw std::runtime_error(msg);
    }
}

The DeviceLcsIf class monitors (via OPCUA subscriptions) some basic status parameters from the device controller. These parameters are: state,substate,local and error. These parameters can be extended by modifying the internal map node. The map is a pair including the name of the attribute (coming from the mapping file) and an identifier. See the example below for the lamp device. The class will automatically create a subscription for these attributes.

const std::vector <std::pair<std::string, unsigned int>> subscription_vector = {
  {CI_STAT_INTENSITY, STAT_INTENSITY},
  {CI_STAT_TIME_LEFT, STAT_TIME_LEFT},
  {CI_STAT_ANALOG_FEEDBACK, STAT_ANALOG_FEEDBACK},
  {CI_STAT_ON_ANALOG, STAT_ON_ANALOG},
  {CI_STAT_ON_DIGITAL, STAT_ON_DIGITAL},
};

// Build a map with the real OPCUA names.
this->StoreUaNames(subscription_vector);

In order to handle the subscription events for new attributes, developers need to implement a Listener method. This method receives the notifications in a vector containing the attributes (OPCUA NodeIds) and their values. They are used to update the internal device status and the OLDB. Here the identifier, e.g. STAT_INTENSITY is used to determine which attribute has been modified in the controller (PLC).

void  LampLcsIf::Listener(fcf::common::VectorVariant& params) {
    LOG4CPLUS_TRACE_METHOD(m_logger,__PRETTY_FUNCTION__);

    // Process basic attributes in base class
    fcf::devmgr::actuator::ActuatorLcsIf::Listener(params);

    try {
        for (auto parIt = params.begin(); parIt != params.end(); parIt++)  {
            LOG4CPLUS_DEBUG(m_logger, "[" << m_config->GetName()
                            << "] Node ID: " << parIt->first);
            LOG4CPLUS_DEBUG(m_logger, "[" << m_config->GetName()
                          << "] Node Value: " << parIt->second);

            // NodeId has a defined format. We need to extract just the address space
            // path. For that we use boost:split function.
            // NodeId format: ns=4;s=MAIN.Lamp1.stat.nSubstate
            // We split by '=' char and we take the latest element of the vector.

            std::vector<std::string> tokens;
            boost::split(tokens, parIt->first, boost::is_any_of("="));
            std::string attribute = tokens[tokens.size()-1];

            auto it = m_ua_status_map.find(attribute);
            if (it != m_ua_status_map.end())  {
                LOG4CPLUS_DEBUG(m_logger, "Identifier: " << it->second);
                // Only in case variable is registered in the status map
                switch (it->second) {
                    case STAT_INTENSITY: {
                            // here we obtain the value from the notification.
                            m_intensity = boost::get<double>(parIt->second);
                            // here we prepare the DB attribute name.
                            const std::string attr = m_lcs_prefix +
                            std::string(utils::bat::CONFIG_DB_DELIMITER) + CI_STAT_INTENSITY;
                            // here we update the DB.
                            m_data_ctx.Set(attr,m_intensity);
                        }
                        break;
                    case STAT_TIME_LEFT: {
                            // here we obtain the value
                            m_time_left = boost::get<unsigned int>(parIt->second);
                            // here we prepare the DB attribute name.
                            const std::string attr = m_lcs_prefix +
                            std::string(utils::bat::CONFIG_DB_DELIMITER) + CI_STAT_TIME_LEFT;
                            // here we update the DB.
                            m_data_ctx.Set(attr,m_time_left);
                        }
                        break;
                    ...
                }
           } else {
                LOG4CPLUS_ERROR(m_logger, "Variable not handled, notification will be skipped: "
                                  << parIt->first);
            }
        }
     } catch (std::exception& e) {
            LOG4CPLUS_ERROR(m_logger, "[" << m_config->GetName()
                          << "] Problem processing event notification: "
                            << e.what());
            m_failure.broadcast();
    } catch (...) {
            LOG4CPLUS_ERROR(m_logger, "[" << m_config->GetName()
                          << "] Unknow error processing notification");
            m_failure.broadcast();
    }

}

To simplify adding instrument specific extensions to the Device Manager, The FCF provides a template that can be used as starting point to implement special devices along with some companion modules. Probably the best way to understand how to develop a device is having a look to the existing FCF devices. The simplest one is the Shutter device which is implemented in few lines of code.

Template

You should create your software based on the provided project template by following the procedure in the Getting Started guide here

Top Directory Structure

<root>                # Generated project directory
├── resource          # directory containing the resources
└── <prefix>-ics      # WAF project
    ├── <component>   # Generated FCS
    ├── seq
    ├── <prefix>stoo
    └── wscript

Component Directory Structure

Here it will be reported the specific parts about FCS in the generated project. The generated structure resembles the structure of the FCF component.

<component>
├── devices             # Custom Device
├── devsim              # Simulator for custom device
├── clib                # Custom Python client library
├── cli                 # Custom FCS Command Line Interface (CLI)
├── gui                 # Custom GUI
├── LCS                 # Example PLC Project (VS)
├── server              # Custom server
└── wscript

Note

All custom devices shall use the CUSTOM device type of the existing FCF XML interface for the Setup event payload.

Custom Device

The generated code includes the implementation of a dummy custom device that can be used as starting point to develop instrument specific ones. This device contains the intelligence to deserialize the custom setup command payload in method Setup. Users will have to modify this method and method IsSetupActive to adapt to their specific needs.

Warning

The serialization of the setup command for custom devices is done in JSON format. Devices shall parse the JSON serialization to obtain the device parameters. The template provides an example for this.

void Mirror::Setup(const std::any& payload) {
    LOG4CPLUS_TRACE_METHOD(m_logger,__PRETTY_FUNCTION__);

    LOG4CPLUS_INFO(m_logger, "Processing Setup command ...");
    if (!m_config->GetIgnored()) {
    try {
      auto fcf_vector = std::any_cast<std::vector<std::shared_ptr<fcfif::SetupElem>>>(&payload);
      for (auto it = fcf_vector->begin(); it != fcf_vector->end(); it++) {
           auto setup_elem = *it;
           LOG4CPLUS_INFO(m_logger, "ID: " << setup_elem->getId());
           // ignore other shutters
           if (IsMsgForMe(setup_elem->getId()) != true) {
               continue;
             }
           auto lcs_if = std::static_pointer_cast<{{cookiecutter.device_name|capitalize()}}LcsIf>(m_lcs_if);
           auto fcs_union = setup_elem->getDevice();
           if (fcs_union->getDiscriminator() != ::fcfif::DeviceType::CUSTOM) {
               LOG4CPLUS_ERROR(m_logger, "[" << m_config->GetName() << "] "
                               << "Setup is for me but device type is not correct?");
               continue;
             }

           auto custom = fcs_union->getCustom();
          LOG4CPLUS_DEBUG(m_logger, "[" << m_config->GetName() << "] "
                          << "Setup ID: " << setup_elem->getId());

          std::string params = boost::replace_all_copy(custom->getParameters(), "'", "\"");
          LOG4CPLUS_INFO(m_logger, "Setup buffer: " << params);
          //@TODO: Add handling of setup parameters

          // Parsing of JSON payload
          EncDec data = nlohmann::json::parse(params);
          if (data.has_piston()) {
              LOG4CPLUS_INFO(m_logger, "Piston: " << data.get_piston());
            }
          LOG4CPLUS_INFO(m_logger, "Tip: " << data.get_tip());
          LOG4CPLUS_INFO(m_logger, "Tilt: " << data.get_tilt());
          if (data.get_action() == "MOVE") {
              lcs_if->Ping();
            }
        }
    } catch(const std::bad_any_cast& e) {
        LOG4CPLUS_ERROR(m_logger, "CII payload is not recognized !");
        throw;
      }
    }
}

Custom Simulation

In oder to allow the testing of the dummy device, a device simulator is generated and can be used for testing purposes. The simulator implements the RPC_Ping dummy method used by the custom device as an action of the Setup command.

Extending JSON schema

In order to validate the setup payload before sending it to the server, applications shall extend the FCF schema for custom devices. We provide an example for this in the template for the Mirror device.

For more information about FCF schema for standard devices see JSON Schema.

Note

The schema presented in the template is just an example to illustrate how to achieve this. Instrument developers shall define the scheme that better fits the needs of the custom devices. In this case and for simplicity, we are using a flat structure.

{
      "type": "object",
      "properties": {
        "action": {
          "type": "string",
          "enum": ["MOVE" ],
          "description": "Mirror action."
        },
        "tip": {
          "type": "number",
          "description": "tip."
        },
        "tilt": {
          "type": "number",
          "description": "tilt."
        },
        "piston": {
          "type": "number",
          "description": "piston."
        }
      },
      "required": ["action","tip","tilt"]
}

Applications are required to define their own python library where to compose the FCS schema. This can be achieved by adding the custom device schemas. There are probably different ways to achieve this, we provide an example below where the composition in done in python.

""" Load basic schema for all standard devices """
self._schema = json.loads(json_obj.SETUP_SCHEMA)

""" Change the schema to add new device """
""" add the new definition """
self._schema['definitions']['mirror'] = json_obj.load_json_string(SetupBuffer.MIRROR_SCHEMA)
""" add new element the array """
self._schema['definitions']['param']['oneOf'].append(json_obj.load_json_string('{"required": [ "mirror"]}'))
self._schema['definitions']['param']['properties']['mirror'] = json_obj.load_json_string('{"$ref": "#/definitions/mirror"}')