South Plugins in C

South plugins written in C/C++ are no different in use to those written in Python, it is merely a case that they are implemented in a different language. The same options of polled or asynchronous methods still exist and the enduser of Flir is not aware in which language the plugin has been written.

Polled Mode

Polled mode is the simplest form of South plugin that can be written, a poll routine is called at an interval defined in the plugin advanced configuration. The South service determines the type of the plugin by examining the mode property in the information the plugin returns from the plugin_info call.

Plugin Poll

The plugin poll method is called periodically to collect the readings from a poll mode sensor. As with all other calls the argument passed to the method is the handle returned by the plugin_init call, the return of the method should be a Reading instance that contains the data read.

The Reading class consists of

Property

Description

assetName

The asset key of the sensor device that is being read

userTimestamp

A timestamp for the reading data

datapoints

The reading data itself as a set if datapoint instances

More detail regarding the Reading class can be found in the section C++ Support Classes.

It is important that the poll method does not block as this will prevent the proper operation of the South microservice. Using the example of our simple DHT11 device attached to a GPIO pin, the poll routine could be:

/**
 * Poll for a plugin reading
 */
Reading plugin_poll(PLUGIN_HANDLE *handle)
{
        DHT11 *dht11 = (DHT11*)handle;
        return dht11->takeReading();
}

Where our DHT11 class has a method takeReading as follows

/**
 * Take reading from sensor
 *
 * @param firstReading   This flag indicates whether this is the first reading to be taken from sensor,
 *                       if so get it reliably even if takes multiple retries. Subsequently (firstReading=false),
 *                       if reading from sensor fails, last good reading is returned.
 */
Reading DHT11::takeReading(bool firstReading)
{
        static uint8_t sensorData[4] = {0,0,0,0};

        bool valid = false;
        unsigned int count=0;
        do {
                valid = readSensorData(sensorData);
                count++;
        } while(!valid && firstReading && count < MAX_SENSOR_READ_RETRIES);

        if (firstReading && count >= MAX_SENSOR_READ_RETRIES)
                Logger::getLogger()->error("Unable to get initial valid reading from DHT11 sensor connected to pin %d even after %d tries", m_pin, MAX_SENSOR_READ_RETRIES);

        vector<Datapoint *> vec;

        ostringstream tmp;
        tmp << ((unsigned int)sensorData[0]) << "." << ((unsigned int)sensorData[1]);
        DatapointValue dpv1(stod(tmp.str()));
        vec.push_back(new Datapoint("Humidity", dpv1));

        ostringstream tmp2;
        tmp2 << ((unsigned int)sensorData[2]) << "." <<  ((unsigned int)sensorData[3]);
        DatapointValue dpv2(stod(tmp2.str()));
        vec.push_back(new Datapoint ("Temperature", dpv2));

        return Reading(m_assetName, vec);
}

We are creating two DatapointValues for the Humidity and Temperature values returned by reading the DHT11 sensor.

Plugin Poll Returning Multiple Values

It is possible in a C/C++ plugin to have a plugin that returns multiple readings in a single call to a poll routine. This is done by setting the interface version of 2.0.0 rather than 1.0.0. In this interface version the plugin_poll call returns a vector of Reading rather than a single Reading.

/**
 * Poll for a plugin reading
 */
std::vector<Reading *> *plugin_poll(PLUGIN_HANDLE *handle)
{
Modbus *modbus = (Modbus *)handle;

        if (!handle)
                throw runtime_error("Bad plugin handle");
        return modbus->takeReading();
}

Async IO Mode

In asyncio mode the plugin runs either a separate thread or uses some incoming event from a device or callback mechanism to trigger sending data to Flir. The asynchronous mode uses two additional entry points to the plugin, one to register a callback on which the plugin sends data, plugin_register_ingest and another to start the asynchronous behavior plugin_start.

Plugin Register Ingest

The plugin_register_ingest call is used to allow the south service to pass a callback function to the plugin that the plugin uses to send data to the service every time the plugin has some new data.

/**
 * Register ingest callback
 */
void plugin_register_ingest(PLUGIN_HANDLE *handle, INGEST_CB cb, void *data)
{
MyPluginClass *plugin = (MyPluginClass *)handle;

        if (!handle)
                throw new exception();
        plugin->registerIngest(data, cb);
}

The plugin should store the callback function pointer and the data associated with the callback such that it can use that information to pass a reading to the south service. The following code snippets show how a plugin class might store the callback and data and then use it to send readings into Flir at a later stage.

/**
 * Record the ingest callback function and data in member variables
 *
 * @param data  The Ingest function data
 * @param cb    The callback function to call
 */
void MyPluginClass::registerIngest(void *data, INGEST_CB cb)
{
        m_ingest = cb;
        m_data = data;
}

/**
 * Called when a data is available to send to the south service
 *
 * @param points        The points in the reading we must create
 */
void MyPluginClass::ingest(Reading& reading)
{

        (*m_ingest)(m_data, reading);
}

Plugin Start

The plugin_start method, as with other plugin calls, is called with the plugin handle data that was returned from the plugin_init call. The plugin_start call will only be called once for a plugin, it is the responsibility of plugin_start to take whatever action is required in the plugin in order to start the asynchronous actions of the plugin. This might be to start a thread, register an endpoint for a remote connection or call an entry point in a third party library to start asynchronous processing.

/**
 * Start the Async handling for the plugin
 */
void plugin_start(PLUGIN_HANDLE *handle)
{
MyPluginClass *plugin = (MyPluginClass *)handle;


        if (!handle)
                return;
        plugin->start();
}

/**
 * Start the asynchronous processing thread
 */
void MyPluginClass::start()
{
        m_running = true;
        m_thread = new thread(threadWrapper, this);
}

Set Point Control

South plugins can also be used to exert control on the underlying device to which they are connected. This is not intended for use as a substitute for real time control systems, but rather as a mechanism to make non-time critical changes to a device or to trigger an operation on the device.

To make a south plugin support control features there are two steps that need to be taken

  • Tag the plugin as supporting control

  • Add the entry points for control

Enable Control

A plugin enables control features by means of the flags in the plugin information data structure which is returned by the plugin_info entry point of the plugin. The flag value SP_CONTROL should be added to the flags of the plugin.

/**
 * The plugin information structure
 */
static PLUGIN_INFORMATION info = {
        PLUGIN_NAME,              // Name
        VERSION,                  // Version
        SP_CONTROL,           // Flags - add control
        PLUGIN_TYPE_SOUTH,        // Type
        "1.0.0",                  // Interface version
        CONFIG                    // Default configuration
};

Adding this flag will cause the south service to do a number of things when it loads the plugin;

  • The south service will attempt to resolve the two control entry points.

  • A toggle will be added to the advanced configuration category of the service that will permit the disabling of control services.

  • A security category will be added to the south service that contains the access control lists and permissions associated with the service.

Control Entry Points

Two entry points are supported for control operations in the south plugin

  • plugin_write: which is used to set the value of a parameter within the plugin or device

  • plugin_operation: which is used to perform an operation on the plugin or device

The south plugin can support one or both of these entry points as appropriate for the plugin.

Write Entry Point

The write entry point is used to set data in the plugin or write data into the device.

The plugin write entry point is defined as follows

bool plugin_write(PLUGIN_HANDLE *handle, string name, string value)

Where the parameters are;

  • handle the handle of the plugin instance

  • name the name of the item to be changed

  • value a string presentation of the new value to assign top the item

The return value defines if the write was successful or not. True is returned for a successful write.

bool plugin_write(PLUGIN_HANDLE *handle, string& name, string& value)
{
Random *random = (Random *)handle;

      return random->write(operation, name, value);
}

In this case the main logic of the write operation is implemented in a class that contains all the plugin logic. Note that the assumption here, and a design pattern often used by plugin writers, is that the PLUGIN_HANDLE is actually a pointer to a C++ class instance.

In this case the implementation in the plugin class is as follows:

bool Random::write(string& name, string& value)
{
      if (name.compare("mode") == 0)
      {
              if (value.compare("relative") == 0)
              {
                      m_mode = RELATIVE_MODE;
              }
              else if (value.compare("absolute") == 0)
              {
                      m_mode = ABSOLUTE_MODE;
              }
              Logger::getLogger()->error("Unknown mode requested '%s' ignored.", value.c_str());
              return false;
      }
      else
      {
              Logger::getLogger()->error("Unknown control item '%s' ignored.", name.c_str());
              return false;
      }
      return true;
}

In this case the code is relatively simple as we assume there is a single control parameter that can be written, the mode of operation. We look for the known name and if a different name is passed an error is logged and false is returned. If the correct name is passed in we then check the value and take the appropriate action. If the value is not a recognized value then an error is logged and we again return false.

In this case we are merely setting a value within the plugin, this could equally well be done via configuration and would in that case be persisted between restarted. Normally control would not be used for this, but rather for making a change with the connected device itself, such as changing a PLC register value. This is simply an example to demonstrate the mechanism.

Operation Entry Point

The plugin will support an operation entry point. This will execute the given operation synchronously, it is expected that this operation entry point will be called using a separate thread, therefore the plugin should implement operations in a thread safe environment.

The plugin write operation entry point is defined as follows

bool plugin_operation(PLUGIN_HANDLE *handle, string& operation, int count, PLUGIN_PARAMETER **params)

Where the parameters are;

  • handle the handle of the plugin instance

  • operation the name of the operation to be executed

  • count the number of parameters

  • params a set of name/value pairs that are passed to the operation

The operation parameter should be used by the plugin to determine which operation is to be performed, that operation may also be passed a number of parameters. The count of these parameters are passed to the plugin in the count argument and the actual parameters are passed in an array of key/value pairs as strings.

The return from the call is a boolean result of the operation, a failure of the operation or a call to an unrecognized operation should be indicated by returning a false value. If the operation succeeds a value of true should be returned.

The following example shows the implementation of the plugin operation entry point.

bool plugin_operation(PLUGIN_HANDLE *handle, string& operation, int count, PLUGIN_PARAMETER **params)
{
Random *random = (Random *)handle;

      return random->operation(operation, count, params);
}

In this case the main logic of the operation is implemented in a class that contains all the plugin logic. Note that the assumption here, and a design pattern often used by plugin writers, is that the PLUGIN_HANDLE is actually a pointer to a C++ class instance.

In this case the implementation in the plugin class is as follows:

/**
 * SetPoint operation. We support reseeding the random number generator
 */
bool Random::operation(const std::string& operation, int count, PLUGIN_PARAMETER **params)
{
        if (operation.compare("seed") == 0)
        {
                if (count)
                {
                        if (params[0]->name.compare("seed"))
                        {
                                long seed = strtol(params[0]->value.c_str(), NULL, 10);
                                srand(seed);
                        }
                        else
                        {
                                return false;
                        }
                }
                else
                {
                        srand(time(0));
                }
                Logger::getLogger()->info("Reseeded random number generator");
                return true;
        }
        Logger::getLogger()->error("Unrecognised operation %s", operation.c_str());
        return false;
}

In this example, the operation method checks the name of the operation to perform, only a single operation is supported by this plugin. If this operation name differs the method will log an error and return false. If the operation is recognized it will check for any arguments passed in, retrieve and use it. In this case an optional seed argument may be passed.

There is no actual machine connected here, therefore the operation occurs within the plugin. In the case of a real machine the operation would most likely cause an action on a machine, for example a request to the machine to re-calibrate itself.

A South Plugin Example In C/C++: the DHT11 Sensor

Using the same example as before, the DHT11 temperature and humidity sensor, let’s look at how to create the plugin in C/C++.

The Software

For this plugin we use the wiringpi C library to connect to the hardware of the Raspberry Pi

$ sudo apt-get install wiringpi
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
wiringpi
...
$

The Plugin

This is the code for the plugin.cpp file that provides the plugin API:

/*
 * Flir south plugin.
 *
 * Copyright (c) 2018 OSisoft, LLC
 *
 * Released under the Apache 2.0 Licence
 *
 * Author: Amandeep Singh Arora
 */
#include <dht11.h>
#include <plugin_api.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string>
#include <logger.h>
#include <plugin_exception.h>
#include <config_category.h>
#include <rapidjson/document.h>
#include <version.h>

using namespace std;
#define PLUGIN_NAME "dht11_V2"

/**
 * Default configuration
 */
const static char *default_config = QUOTE({
                "plugin" : {
                        "description" : "DHT11 C south plugin",
                        "type" : "string",
                        "default" : PLUGIN_NAME,
                        "readonly": "true"
                        },
                "asset" : {
                        "description" : "Asset name",
                        "type" : "string",
                        "default" : "dht11",
                        "order": "1",
                        "displayName": "Asset Name",
                        "mandatory" : "true"
                        },
                "pin" : {
                        "description" : "Rpi pin to which DHT11 is attached",
                        "type" : "integer",
                        "default" : "7",
                        "displayName": "Rpi Pin"
                        }
                });


/**
 * The DHT11 plugin interface
 */
extern "C" {

/**
 * The plugin information structure
 */
static PLUGIN_INFORMATION info = {
        PLUGIN_NAME,              // Name
        VERSION,                  // Version
        0,                        // Flags
        PLUGIN_TYPE_SOUTH,        // Type
        "1.0.0",                  // Interface version
        default_config            // Default configuration
};

/**
 * Return the information about this plugin
 */
PLUGIN_INFORMATION *plugin_info()
{
        return &info;
}

/**
 * Initialise the plugin, called to get the plugin handle
 */
PLUGIN_HANDLE plugin_init(ConfigCategory *config)
{
        unsigned int pin;

        if (config->itemExists("pin"))
        {
                pin = stoul(config->getValue("pin"), nullptr, 0);
        }

        DHT11 *dht11= new DHT11(pin);

        if (config->itemExists("asset"))
                dht11->setAssetName(config->getValue("asset"));
        else
                dht11->setAssetName("dht11");

        Logger::getLogger()->info("m_assetName set to %s", dht11->getAssetName());

        return (PLUGIN_HANDLE)dht11;
}

/**
 * Poll for a plugin reading
 */
Reading plugin_poll(PLUGIN_HANDLE *handle)
{
        DHT11 *dht11 = (DHT11*)handle;
        return dht11->takeReading();
}

/**
 * Reconfigure the plugin
 */
void plugin_reconfigure(PLUGIN_HANDLE *handle, string& newConfig)
{
ConfigCategory        conf("dht", newConfig);
DHT11 *dht11 = (DHT11*)*handle;

        if (conf.itemExists("asset"))
                dht11->setAssetName(conf.getValue("asset"));
        if (conf.itemExists("pin"))
        {
                unsigned int pin = stoul(conf.getValue("pin"), nullptr, 0);
                dht11->setPin(pin);
        }
}

/**
 * Shutdown the plugin
 */
void plugin_shutdown(PLUGIN_HANDLE *handle)
{
        DHT11 *dht11 = (DHT11*)handle;
        delete dht11;
}
};

The full source code, including the DHT11 class can be found in GitHub https://github.com/fledge-iot/fledge-south-dht

Building Flir and Adding the Plugin

If you have not built Flir yet, follow the steps described here. After the build, you can optionally install Flir following these steps.

  • Clone the flir-south-dht repository

$ git clone https://github.com/fledge-iot/fledge-south-dht.git
...
$
  • Set the environment variable FLIR_ROOT to the directory in which you built Flir

$ export FLIR_ROOT=~/flir
$
  • Go to the location in which you cloned the flir-south-dht repository and create a build directory and run cmake in that directory

$ cd ~/flir-south-dht
$ mkdir build
$ cd build
$ cmake ..
...
$
  • Now make the plugin

$ make
$
  • If you have started Flir from the build directory, copy the plugin into the destination directory

$ mkdir -p $FLIR_ROOT/plugins/south/dht
$ cp libdht.so $FLIR_ROOT/plugins/south/dht
$
  • If you have installed Flir by executing sudo make install, copy the plugin into the destination directory

$ sudo mkdir -p /usr/local/flir/plugins/south/dht
$ sudo cp libdht.so /usr/local/flir/plugins/south/dht
$

Note

If you have installed Flir using an alternative DESTDIR, remember to add the path to the destination directory to the cp command.

  • Add service

$ curl -sX POST http://localhost:8081/flir/service -d '{"name": "dht", "type": "south", "plugin": "dht", "enabled": true}'

You may now use the C/C++ plugin in exactly the same way as you used a Python plugin earlier.