South Plugins¶
South plugins are used to communicate with sensors and actuators, there are two modes of plugin operation; asyncio and polled.
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 configuration. The South service determines the type of the plugin by examining at 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 initialization call, the return of the method should be the JSON payload of the readings to return.
The JSON payload returned, as a Python dictionary, should contain the properties; asset, timestamp, key and readings.
Property |
Description |
---|---|
asset |
The asset key of the sensor device that is being read |
timestamp |
A timestamp for the reading data |
key |
A UUID which is the unique key of this reading |
readings |
The reading data itself as a JSON object |
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:
def plugin_poll(handle):
""" Extracts data from the sensor and returns it in a JSON document as a Python dict.
Available for poll mode only.
Args:
handle: handle returned by the plugin initialisation call
Returns:
returns a sensor reading in a JSON document, as a Python dict, if it is available
None - If no reading is available
Raises:
DataRetrievalError
"""
try:
humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, handle)
if humidity is not None and temperature is not None:
time_stamp = str(datetime.now(tz=timezone.utc))
readings = { 'temperature': temperature , 'humidity' : humidity }
wrapper = {
'asset': 'dht11',
'timestamp': time_stamp,
'key': str(uuid.uuid4()),
'readings': readings
}
return wrapper
else:
return None
except Exception as ex:
raise exceptions.DataRetrievalError(ex)
return None
Async IO Mode¶
In asyncio mode the plugin inserts itself into the event processing loop of the South Service itself. This is a more complex mechanism and is intended for plugins that need to block or listen for incoming data via a network.
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 install the plugin code into the python event handling system for asyncIO. Assuming an example whereby the interface to a sensor is via HTTP and the sensor will make HTTP POST calls to our plugin in order to send data into FogLAMP, a plugin_start for this scenario would create a web application endpoint for reception of the POST command.
loop = asyncio.get_event_loop()
app = web.Application(middlewares=[middleware.error_middleware])
app.router.add_route('POST', '/', SensorPhoneIngest.render_post)
handler = app.make_handler()
coro = loop.create_server(handler, host, port)
server = asyncio.ensure_future(coro)
This code first gets the event loop for this Python execution, it then creates the web application and adds a route for the POST request. In this case it is calling the render_post method of the object SensorPhone. It then goes on to create the handler and install the web server instance into the event system.
Async Data Callback¶
The async data callback is used for incoming sensor data and passing that reading data into the FogLAMP ingest process. Unlike the poll mechanism, this is done from within the callback rather than by passing the data back to the South service itself. A plugin entry point, plugin_register_ingest is called by the south service before the plugin is started to register the callback with the plugin. The plugin would usually save the callback function and the reference data for later use.
def plugin_register_ingest(handle, callback, ingest_ref):
"""Required plugin interface component to communicate to South C server
Args:
handle: handle returned by the plugin initialisation call
callback: C opaque object required to passed back to C->ingest method
ingest_ref: C opaque object required to passed back to C->ingest method
"""
global c_callback, c_ingest_ref
c_callback = callback
c_ingest_ref = ingest_ref
The plugin then uses these saved references when it has data to be ingested. A new reading is constructed and passed to the callback function using async_ingest object that should be imported by the plugin.
import async_ingest
Then for each reading to be ingested the data is sent to the ingest thread of the south plugin using the following construct.
data = {
'asset': self.asset_name,
'timestamp': utils.local_timestamp(),
'readings': reads
}
async_ingest.ingest_callback(c_callback, c_ingest_ref, data)
message['status'] = code
return web.json_response(message)
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 mode field in the plugin information dict which is returned by the plugin_info entry point of the plugin. The flag value control should be added to the mode field of the plugin. Multiple flag values are separated by the pipe symbol ‘|’.
# plugin information dict
{
'name': 'Sinusoid Poll plugin',
'version': '1.9.2',
'mode': 'poll|control',
'type': 'south',
'interface': '1.0',
'config': _DEFAULT_CONFIG
}
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
def plugin_write(handle, name, 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 to the item
The return value defines if the write was successful or not. True is returned for a successful write.
def plugin_write(handle, name, value):
""" Setpoint write operation
Args:
handle: handle returned by the plugin initialisation call
name: Name of parameter to write
value: Value to be written to that parameter
Returns:
bool: Result of the write operation
"""
_LOGGER.info("plugin_write(): name={}, value={}".format(name, value))
return True
In this case we are merely printing the parameter name and the value to be set for this parameter. Normally control would be used for making a change with the connected device itself, such as changing a PLC register value. This is simply an example to demonstrate the API.
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
def plugin_operation(handle, operation, params)
Where the parameters are;
handle the handle of the plugin instance
operation the name of the operation to be executed
params a list of name/value tuples that are passed to the operation
The operation parameter should be used by the plugin to determine which operation is to be performed. The actual parameters are passed in a list of key/value tuples 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.
def plugin_operation(handle, operation, params):
""" Setpoint control operation
Args:
handle: handle returned by the plugin initialisation call
operation: Name of operation
params: Parameter list
Returns:
bool: Result of the operation
"""
_LOGGER.info("plugin_operation(): operation={}, params={}".format(operation, params))
return True
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. Above example is just a demonstration of the API.
A South Plugin Example In Python: the DHT11 Sensor¶
Let’s try to put all the information together and write a plugin. We can continue to use the example of an inexpensive sensor, the DHT11, used to measure temperature and humidity, directly wired to a Raspberry PI. This plugin is available on github, FogLAMP DHT11 South Plugin.
First, here is a set of links where you can find more information regarding this sensor:
The Hardware¶
The DHT sensor is directly connected to a Raspberry PI 2 or 3. You may decide to buy a sensor and a resistor and solder them yourself, or you can buy a ready-made circuit that provides the correct output to wire to the Raspberry PI. This picture shows a DHT11 with resistor that you can buy online.
The sensor can be directly connected to the Raspberry PI GPIO (General Purpose Input/Output). An introduction to the GPIO and the pinset is available here. In our case, you must connect the sensor on these pins:
VCC is connected to PIN #2 (5v Power)
GND is connected to PIN #6 (Ground)
DATA is connected to PIN #7 (BCM 4 - GPCLK0)
This picture shows the sensor wired to the Raspberry PI and this is a zoom into the wires used.
The Software¶
For this plugin we use the ADAFruit Python Library (links to the GitHub repository are above). First, you must install the library (in future versions the library will be provided in a ready-made package):
$ git clone https://github.com/adafruit/Adafruit_Python_DHT.git
Cloning into 'Adafruit_Python_DHT'...
remote: Counting objects: 249, done.
remote: Total 249 (delta 0), reused 0 (delta 0), pack-reused 249
Receiving objects: 100% (249/249), 77.00 KiB | 0 bytes/s, done.
Resolving deltas: 100% (142/142), done.
$ cd Adafruit_Python_DHT
$ sudo apt-get install build-essential python-dev
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following NEW packages will be installed:
build-essential python-dev
...
$ sudo python3 setup.py install
running install
running bdist_egg
running egg_info
creating Adafruit_DHT.egg-info
...
$
The Plugin¶
This is the code for the plugin:
# -*- coding: utf-8 -*-
# FOGLAMP_BEGIN
# See: http://foglamp.readthedocs.io/
# FOGLAMP_END
""" Plugin for a DHT11 temperature and humidity sensor attached directly
to the GPIO pins of a Raspberry Pi
This plugin uses the Adafruit DHT library, to install this perform
the following steps:
git clone https://github.com/adafruit/Adafruit_Python_DHT.git
cd Adafruit_Python_DHT
sudo apt-get install build-essential python-dev
sudo python setup.py install
To access the GPIO pins foglamp must be able to access /dev/gpiomem,
the default access for this is owner and group read/write. Either
FogLAMP must be added to the group or the permissions altered to
allow FogLAMP access to the device.
"""
from datetime import datetime, timezone
import uuid
from foglamp.common import logger
from foglamp.services.south import exceptions
__author__ = "Mark Riddoch"
__copyright__ = "Copyright (c) 2017 OSIsoft, LLC"
__license__ = "Apache 2.0"
__version__ = "${VERSION}"
_DEFAULT_CONFIG = {
'plugin': {
'description': 'Python module name of the plugin to load',
'type': 'string',
'default': 'dht11'
},
'pollInterval': {
'description': 'The interval between poll calls to the device poll routine expressed in milliseconds.',
'type': 'integer',
'default': '1000'
},
'gpiopin': {
'description': 'The GPIO pin into which the DHT11 data pin is connected',
'type': 'integer',
'default': '4'
}
}
_LOGGER = logger.setup(__name__)
""" Setup the access to the logging system of FogLAMP """
def plugin_info():
""" Returns information about the plugin.
Args:
Returns:
dict: plugin information
Raises:
"""
return {
'name': 'DHT11 GPIO',
'version': '1.0',
'mode': 'poll',
'type': 'south',
'interface': '1.0',
'config': _DEFAULT_CONFIG
}
def plugin_init(config):
""" Initialise the plugin.
Args:
config: JSON configuration document for the device configuration category
Returns:
handle: JSON object to be used in future calls to the plugin
Raises:
"""
handle = config['gpiopin']['value']
return handle
def plugin_poll(handle):
""" Extracts data from the sensor and returns it in a JSON document as a Python dict.
Available for poll mode only.
Args:
handle: handle returned by the plugin initialisation call
Returns:
returns a sensor reading in a JSON document, as a Python dict, if it is available
None - If no reading is available
Raises:
DataRetrievalError
"""
try:
humidity, temperature = Adafruit_DHT.read_retry(Adafruit_DHT.DHT11, handle)
if humidity is not None and temperature is not None:
time_stamp = str(datetime.now(tz=timezone.utc))
readings = {'temperature': temperature, 'humidity': humidity}
wrapper = {
'asset': 'dht11',
'timestamp': time_stamp,
'key': str(uuid.uuid4()),
'readings': readings
}
return wrapper
else:
return None
except Exception as ex:
raise exceptions.DataRetrievalError(ex)
return None
def plugin_reconfigure(handle, new_config):
""" Reconfigures the plugin, it should be called when the configuration of the plugin is changed during the
operation of the device service.
The new configuration category should be passed.
Args:
handle: handle returned by the plugin initialisation call
new_config: JSON object representing the new configuration category for the category
Returns:
new_handle: new handle to be used in the future calls
Raises:
"""
new_handle = new_config['gpiopin']['value']
return new_handle
def plugin_shutdown(handle):
""" Shutdowns the plugin doing required cleanup, to be called prior to the device service being shut down.
Args:
handle: handle returned by the plugin initialisation call
Returns:
Raises:
"""
pass
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.
If you have started FogLAMP from the build directory, copy the structure of the foglamp-south-dht11/python/ directory into the python directory:
$ cd ~/FogLAMP
$ cp -R ~/foglamp-south-dht11/python/foglamp/plugins/south/dht11 python/foglamp/plugins/south/
$
If you have installed FogLAMP by executing
sudo make install
, copy the structure of the foglamp-south-dht11/python/ directory into the installed python directory:
$ sudo cp -R ~/foglamp-south-dht11/python/foglamp/plugins/south/dht11 /usr/local/foglamp/python/foglamp/plugins/south/
$
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": "dht11", "type": "south", "plugin": "dht11", "enabled": true}'
Note
Each plugin repo has its own debian packaging script and documentation, And that is the recommended way to go! As above method(s) may need explicit action for linux and/or python dependencies installation.
Using the Plugin¶
Once south plugin is added as an enabled service, You are ready to use the DHT11 plugin.
$ curl -X GET http://localhost:8081/foglamp/service | jq
Let’s see what we have collected so far:
$ curl -s http://localhost:8081/foglamp/asset | jq
[
{
"count": 158,
"asset_code": "dht11"
}
]
$
Finally, let’s extract some values:
$ curl -s http://localhost:8081/foglamp/asset/dht11?limit=5 | jq
[
{
"timestamp": "2017-12-30 14:41:39.672",
"reading": {
"temperature": 19,
"humidity": 62
}
},
{
"timestamp": "2017-12-30 14:41:35.615",
"reading": {
"temperature": 19,
"humidity": 63
}
},
{
"timestamp": "2017-12-30 14:41:34.087",
"reading": {
"temperature": 19,
"humidity": 62
}
},
{
"timestamp": "2017-12-30 14:41:32.557",
"reading": {
"temperature": 19,
"humidity": 63
}
},
{
"timestamp": "2017-12-30 14:41:31.028",
"reading": {
"temperature": 19,
"humidity": 63
}
}
]
$
Clearly we will not see many changes in temperature or humidity, unless we place our thumb on the sensor or we blow warm breathe on it :-)
$ curl -s http://localhost:8081/foglamp/asset/dht11?limit=5 | jq
[
{
"timestamp": "2017-12-30 14:43:16.787",
"reading": {
"temperature": 25,
"humidity": 95
}
},
{
"timestamp": "2017-12-30 14:43:15.258",
"reading": {
"temperature": 25,
"humidity": 95
}
},
{
"timestamp": "2017-12-30 14:43:13.729",
"reading": {
"temperature": 24,
"humidity": 95
}
},
{
"timestamp": "2017-12-30 14:43:12.201",
"reading": {
"temperature": 24,
"humidity": 95
}
},
{
"timestamp": "2017-12-30 14:43:05.616",
"reading": {
"temperature": 22,
"humidity": 95
}
}
]
$
Needless to say, the North plugin will send the buffered data to the PI system using the OMF plugin or any other north system using the appropriate north plugin.