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 FogLAMP 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 FogLAMP. 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 FogLAMP 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 in the south service
*
* @param reading The reading we have ingested into the plugin
*/
void MyPluginClass::ingest(Reading& reading)
{
(*m_ingest)(m_data, reading);
}
In version 2.0.0 onward of the south plugin interface, the callback method has been updated to pass multiple readings.
The plugin_register_ingest call in versions 2.0.0 and later of the interface would be rewritten as follow
/**
* Register ingest callback
*
* @param handle The plugin handle
* @param cb The callback function to be called with the readings
* @param data Data to be passed to the callback function
*/
void plugin_register_ingest(PLUGIN_HANDLE *handle, INGEST_CB2 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 in the same way as before. The following code snippets show how a plugin class might store the callback and data and then use it to send readings into FogLAMP 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_CB2 cb)
{
m_ingest = cb;
m_data = data;
}
/**
* Called when a data is available to send in the south service
*
* @param readings A vector of readings to be processed
*/
void MyPluginClass::ingest(std::vector<Reading *>& readings)
{
(*m_ingest)(m_data, readings);
}
The ownership of the readings in the vector is transferred to the south plugin by the call to the ingest callback and should not be deleted by the south plugin.
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
*
* @param handle The plugin instance handle
*/
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:
/*
* FogLAMP 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 FogLAMP and Adding the Plugin¶
If you have not built FogLAMP yet, follow the steps described here. After the build, you can optionally install FogLAMP following these steps.
Clone the foglamp-south-dht repository
$ git clone https://github.com/fledge-iot/fledge-south-dht.git
...
$
Set the environment variable FOGLAMP_ROOT to the directory in which you built FogLAMP
$ export FOGLAMP_ROOT=~/foglamp
$
Go to the location in which you cloned the foglamp-south-dht repository and create a build directory and run cmake in that directory
$ cd ~/foglamp-south-dht
$ mkdir build
$ cd build
$ cmake ..
...
$
Now make the plugin
$ make
$
If you have started FogLAMP from the build directory, copy the plugin into the destination directory
$ mkdir -p $FOGLAMP_ROOT/plugins/south/dht
$ cp libdht.so $FOGLAMP_ROOT/plugins/south/dht
$
If you have installed FogLAMP by executing
sudo make install
, copy the plugin into the destination directory
$ sudo mkdir -p /usr/local/foglamp/plugins/south/dht
$ sudo cp libdht.so /usr/local/foglamp/plugins/south/dht
$
Note
If you have installed FogLAMP 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/foglamp/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.