Embedded C++ SDK

ABB Ability™ History Embedded SDK helps you to connect your hardware to the VtrinLib data abstraction interface. It comes with features to seamlessly and securely connect between your devices and servers. Embedded SDK is written in C++, and the SDK offers various basic data types, access to the EqM API, and part of the VtrinLib API.

By using Embedded SDK, developers can create software to collect real-time sensor values and other data with minimal knowledge of the receiving system. Building on top of secure websocket, the SDK handles data collection securely, depending on the metadata configured in the server. Embedded SDK is available in C++ for Linux platforms, while the websocket API is available in both C++ and C, and compatible with multiple platforms.

Minimum requirements

Above you can see simplified ABB Ability™ History architecture picture. To build a minimal setup, you will need

  1. ABB Ability™ History installed on the same or separate servers.
  2. Clients with firmware or software implemented with the Embedded SDK. Clients collect values and data from attached sensors or local OS, and push it to the ABB Ability™ History server.

There are a few major components in the SDK:

  • SDK for embedded devices: The framework which will use your code to collect real-time data.
  • Websocket API for embedded devices: The API for accessing resources hosted by ABB Ability™ History data abstraction server.
  • OpenSSL, JSON Parser: Auxiliary libraries used by the Websocket API.

Equipment interface

An ABB Ability™ History embedded plugin is a bundle of code that the SDK will use to collect the data. The plugin implements an interface and supplies to the SDK when the equipment is instantiated. The SDK will require the following parameters to identify the equipment.

  • Equipment name: The name of the equipment. It does not have to be unique.
  • Equipment Id: The ID of the equipment, which will be used to identify the equipment. It has to be unique.
  • Equipment Type: The equipment type will be used to identify the equipment model. Several equipment models can be targeted to an equipment, reflecting different data perspectives.

The interface requires updating the device with device properties before being subscribed. Additionally, the SDK needs to know how frequently the device is available for publishing data. Because of the nature of the SDK, the plugin calls are implemented so that the methods are as fast as possible in order to prevent delays in data production. The best practice is to cache in the memory instead of querying the device on the SDK’s demand.

Installation

Please refer to ABB Ability™ History] installation guides (Windows, Linux, and Docker) for steps to set up the software.

This section explains the steps to set up and build your first application using the SDK.

  • Retrieve a development environment running POSIX-type Operating Systems. Namely Ubuntu, Derbian, etc.

  • Install the necessary software for development: eclipse, build-essential, ssl libraries and its auxiliaries. In case of cross-platform complication, i.e. compiling for ARM-devices on x86-64, the tool-chains must be installed with the aforementioned libraries.

  • Create a workspace folder. You can use the one suggested by eclipse

    /home/<username>/workspace
  • Download the SDK to the created directory. From eclipse, you can already import the projects to your workspace.

  • Check if the imported projects are already compilable.

Getting started

Main article: Embedded C++ SDK onboarding

Developing with the SDK

Authentication

The SDK supports 3 authentication mechanisms:

  • Basic authentication using username/password
  • OpenSSL certificates with private/public keys. To generate keys for this authentication, please follow the Vtrin Certificate Instructions to create your own CA-signed keys
  • TPM based certificates
static std::string g_Cert = "client.pem";
static std::string g_Private = "client.key";

ABB::Mia::C_Authentication auth = ABB::Mia::C_Authentication::M_S_UsingCerts(g_Cert, g_Private); // Using certificates

try
{
	std::cout << "[" << ABB::Mia::C_DateTime::M_S_Now() << "] [ INFO  ]: Connecting to " << g_Host << std::endl;
	driver.M_Connect(auth);
}
catch (ABB::Mia::C_Exception &ex)
{
	std::cout << "[" << ABB::Mia::C_DateTime::M_S_Now() << "] [ ERROR ]: Failed to connect!! Reason: " << ex.M_ToString().c_str() << std::endl;
	return -1;
}

With certificate-based authentication, it is possible to verify the server certificate. To enable the verification, create the authentication object with a certificate from the trusted certificate authority (CA) who has signed the server certificate. If the server certificate has not been signed by the CA, the TLS handshake terminates.

static std::string g_Cert = "client.pem";
static std::string g_Private = "client.key";
static std::string g_TrustedCA = "trustedRootCA.pem";

ABB::Mia::C_Authentication auth = ABB::Mia::C_Authentication::M_S_UsingCerts(g_Cert, g_Private, g_TrustedCA); // Using certificates

Using the Vtrin class interface

Using the VtrinLib interface is quite similar to the C# VtrinLib style.

  • Step 1: Establish a connection to the server
C_MiaDriver driver(host);
try
{
	driver.M_Connect(username, password);
} catch (C_Exception &e)
{
	std::cout<<"[ERROR]: Failed to connect to the host because of: " << e.M_ToString() << std::endl;
	assert(0); // Fail here
}
  • Step 2: Retrieve the handle associated with a class
C_DbClass variableclass = driver["Variable"];
  • Step 3: Perform operations as described in the document
    • ​ Add
try
{
	C_DbClassInstance i = variableclass->M_Add();
	i->M_SetValue("Name", "DemoVariable");
	i->M_SetRawValue("Type", "1"); // Type Uint
	i->M_SetValue("ValueMin", 0);
	i->M_SetValue("ValueMax", C_Variant(1000));
	i->M_CommitChanges();
} catch (C_Exception &e)
{
	std::cout<<"[ERROR]: Unable to add new instance because of: " << e.M_ToString() << std::endl;
	assert(0); // Fail here
}
  • Remove
try
{
	C_DbClassInstances instances;
	if (variableclass->M_GetInstanceSet(&instances, 1, "Name = ?", "DemoVariable"))
	{
		// Remember to check instances size
		C_DbClassInstance ii = instances.front();
		if (ii->M_Remove())
		{
			std::cout << "Variable DemoVariable is removed ";
		}
	}
} catch (C_Exception &e)
{
	std::cout<<"[ERROR]: Unable to delete the instance because of: " << e.M_ToString() << std::endl;
	assert(0); // Fail here
}

Using the Device API

The Device API is an implementation of the EqM API. It exposes the C_Device abstract class that the developer must implement in order to interact with the API. On the other hand, the developer may choose to develop his own implementation via the C_MiaDriver::M_SubscribeEquipmentSession.

C_Device abstract class exposes three pure virtual functions to define its fundamental required parameters as described in the EqM API document:

class C_SimulatedDrive : public C_Device
{
	// ...
	public: virtual C_Guid 			M_GetId() const
	{
		C_Guid guid;
		guid.M_SetData1(0x442211AB);
		guid.M_SetData2(0x1133);
		guid.M_SetData3(0x1133);
		guid.M_SetData4((const byte*) m_DeviceId.c_str()); // Device id used to built the forth part of the GUID.
		return guid;
	}
 
	public: virtual std::string 	M_GetEquipmentName() { return m_DeviceName; }
	public: virtual std::string 	M_GetEquipmentType()  const { return m_DeviceType;};
  	// ...
};

Properties can be added to the device by calling M_AddProperty methods. After that, the device can be initialized and subscribed to the driver:

C_SimulatedDrive simdrive(name, deviceid, equipment);
driver.M_SubscribeEquipmentSession(&simdrive);
simdrive.M_StartProduction();

Data production is then done by calling M_ValueChanged on the property

property->M_ValueChanged(newvalue);

Using the Equipment API

The C_MiaDriver exposes three methods to retrieve equipment:

public:	C_Equipment M_GetEquipment(const std::string &equipmentName);

public:	bool M_GetEquipment(C_Equipments *equipments, const std::string &filter, const std::string &filterData,  int fetchMax = -1);
			
public:	bool M_GetEquipment(C_Equipments *equipments, const std::string &filter, const C_ArgumentList &filterData,  int fetchMax = -1);

Developing for the SDK usually requires two parallel processes: building the model and writing the plugin code. As it isn’t easy to upgrade the plugin code to change the model, the code should be well engineered to support future changes in the model.

  • Step 1: Application identifies local devices and equipment. The information should at least include the equipment name, type, hardware and software version that are necessary to find the equipment model.
  • Step 2: Establish connection from the driver to a server. This connection should be encrypted and authenticated.
  • Step 3: Query for the equipment model using the driver.
  • Step 4: Query for the equipment instance from the equipment model received in Step 3.
  • Step 5: If there is no equipment instance retrieved, create an instance, otherwise continue to value production.

Using Pusher

Pusher is an easy way to update values of Variable and Equipment Properties

C_CurrentValuePusher pusher = driver.M_CreateValuePusher();
pusher->M_Push("Demo1", "234"); //Automatically resolve the variable id
pusher->M_Push("Demo2", 243.0);
pusher->M_Push("Demo3", 243);
C_Thread::M_S_Sleep(10);

SDK data types

Size of the data type is determined by the following table:

t_DataTypeNameData sizeC_Variant
g_CHARchar1 byte8 byte
g_UCHARunsigned char1 byte8 byte
g_SHORTshort2 byte8 byte
g_USHORTunsigned short2 byte8 byte
g_INTinteger4 byte8 byte
g_UINTunsigned integer4 byte8 byte
g_LONGLONGlong integer8 byte8 byte
g_ULONGLONGunsigned long integer8 byte8 byte
g_FLOATfloat4 byte8 byte
g_DOUBLEdouble8 byte8 byte
g_STRINGstringat least 8 byte(Pointer to string object)8 byte
g_GUIDGUID16 byte16 byte
g_DATETIMEdatetime8 byte8 byte

Additional datatypes are from t_PropertyType

SDK API documentation

Core modules

  • C_DateTime provides date and time handling functions.
  • C_Variant provides a generic wrapper for this SDK data types.
  • C_Guid provides GUID handling functions.
  • C_Event provides even synchronization within same process.
  • C_ExceptionBase
  • C_Thread
  • C_ThreadLock
  • C_Locker

Driver modules

  • C_MiaDriver

Vtrin Class interface

  • C_DbClass
  • C_DbClassInstance
  • C_DbPropertyInfo

Equipment interface

  • C_Equipment
  • C_EquipmentInstance
  • C_EquipmentPropertyInfo
  • C_EquipmentProperty

Device interface

Device interfaces contain a list of methods that developers must implement to publish their devices to CPM Architecture.

  • C_Device defines the device class.
  • C_DeviceProperty defines the device property.
  • C_DeviceEvent defines the device event.

Examples

Driver connection

C_MiaDriver driver(host);
try
{
	driver.M_Connect(username, password);
} catch (C_Exception &e)
{
	std::cout<<"[ERRO]: Failed to connect to the host " << e.M_ToString() << std::endl;
}

Using the Vtrin class interface

#include <Mia_Driver.h>
#include <Mia_Exception.h>
#include <Mia_Base.h>

//...

C_DbClass cl = driver["Variable"];
C_DbPropertyInfos properties;
cl->M_GetProperties(&properties);
for (C_DbPropertyInfos::iterator piter = properties.begin(); piter != properties.end(); piter++)
{
	C_DbPropertyInfo pi = *piter;
	std::cout << " Property [" <<  pi->M_GetName() << "] "  
                  << " ["pi->M_GetIndex()<<"]: " << pi -> M_GetCategory() 
                  << ", " << pi->M_GetDisplayName()<<", " 
                  << pi->M_GetDescription()<< std::endl;
}

// Adding new instances
try
{
	C_DbClassInstance i = cl->M_Add();
	i->M_SetValue("Name", "DemoVariable");
	i->M_SetRawValue("Type", "1"); // Type Uint
	i->M_SetValue("ValueMin", 0);
	i->M_SetValue("ValueMax", C_Variant(1000));
	i->M_CommitChanges();
} catch (C_Exception &e)
{
	assert(0); // Fail here
}

// Checking for the instance and remove it
try
{
	C_DbClassInstances instances;
	if (cl->M_GetInstanceSet(&instances, 1, "Name = ?", "DemoVariable"))
	{
		C_DbClassInstance ii = instances.front();
		if (ii->M_Remove())
		{
			std::cout << "Variable DemoVariable is removed ";
		}
	}
} catch (C_Exception &e)
{
	assert(0); // Fail here
}

Using the equipment interface

Source code for NETA-21 device:

Step 1: Get the local devices

C_Devices devices;
C_NetaReader::M_S_ReadDevices(&devices);
C_SharedPointer<C_Device> device = *(devices.begin());

Step 2: Establish connection

MiaDriver* m_Driver = new C_MiaDriver(m_Host);
m_Driver->M_Connect(m_Username, m_Password);

Step 3: Get the corresponding equipment model from server

std::string devicename = device->M_GetConfigValue("category");
C_Equipments equipments;
m_Driver->M_GetEquipment(&equipments, "Name=?", devicename);
C_Equipment equipment = *(equipments->begin());

Step 4: Check the instances

std::string deviceid = m_Device->M_GetConfigValue("target_id");
std::string path = m_Equipment->M_GetName() + "-" + deviceid; 
C_EquipmentInstances instances;
equipment->M_GetInstanceSet(&instances, 1, "Name=?", path);

Step 5: Create or fetch equipment instance

C_EquipmentInstance m_EquipmentInstance;
if (!instances.size()) 
{
 	m_EquipmentInstance = m_Equipment->M_Add(path);
} else 
{			
	m_EquipmentInstance = *(instances.begin());
}

Using the device interface

This example describe the case of NETA-21

Step 1: Implement the C_Device and C_DeviceProperty

class C_Drive : protected C_Device
{
	// ...
	public: C_Drive(const Json::Value &value);
	public: virtual ~C_Drive() {};
	/* Interface implementation */
	public: virtual C_Variant 		M_GetEquipmentId() const { return C_Variant(m_DeviceId);}
	public: virtual C_Guid 			M_GetId() const
	{
		C_Guid guid;
		guid.M_SetData1(0x442211AA);
		guid.M_SetData2(0x1123);
		guid.M_SetData3(0x1122);
		guid.M_SetData4((const byte*) m_DeviceId.c_str());
		return guid;
	}

	public: virtual std::string   M_GetEquipmentName(void);
	public: virtual std::string 	M_GetEquipmentManufacturer() const { return "ABB"; };
	public: virtual std::string 	M_GetEquipmentType() const { return m_DeviceType;};
	public: virtual std::string 	M_GetEquipmentRevision() const { return m_DeviceRevision;}

	// ...
}

Drive property JSON object:

{
	"text_id": "tid_drv_type", 
	"default_value": "-", 
	"name": "Drive type", 
	"static_text_id": "stid_drv_type", 
	"sort_n": 9, 
	"param_type": "static_text", 
	"param_id": "drive_type", 
	"cont_changing": false, 
 	"static_text": "ACS580"
}

Drive JSON object:

{
	"device_n": 1, 
	"_user_writable": false, 
	"adapter_id": "pnl", 
	"_id": "/pnl/pnl_2_01/device", 
	"channel_n": 2, 
	"type": "device", 
	"device_id": "pnl_2_01"
}

Step 2: Publish the device through the driver

C_Drives drives = C_Drive::M_S_LoadDrives();
for (C_Drives::iterator iter = drives.begin(); iter != drives.end(); iter++)
{
	C_SharedPointer<C_Drive> drive = *iter;
	try
	{
		m_Driver->M_SubscribeDeviceSession(drive.get());
	} catch (ABB::C_Exception &ex)
	{
		std::cout << ex.M_ToString() << std::endl;
	}
}

Step 3: Start production

drive->M_StartProduction();

Step 4: Update the device property values by calling M_ValueChanged

drive->M_ValueChanged(property, value);