Robox Library for Cpp: BCC/31 module 1.0.0
RLibCpp BCC/31 protocol implementation
The Robox BCC/31 communication protocol library for Cpp: BCC/31 module

Introduction

Description

This is the Robox's BCC/31 communication protocol library implementation for Cpp.
This library allows custom client software developped with Cpp to connect to Robox's remote controls.
You can get remote module device informations, handle all kinds of Variable running on that, and perform all the common allowed operations (read , write, force, release) according to the variable type.

How to use the library

In order to use the library, your client has to be build upon C++11 or successive.
Make sure to include all the requested include files distributed with the library and link the library modules to your project.
Before digging into the library specific usage, let's expose some general tips:

  • As general rules, all the library's API traps most common exception, returning a Globals::Results error code as accurate as possibile.
  • The error codes may be decoded into strings with specific helper functions ( Globals::DecodeResult ).
  • It's best practise that the methods and properties exposed by a specific instance of a Client object are invoked from a single thread, so for multithread operations it’s better to use a different Client for each single thread.
    Concurrent invokation of methods of the same Client instance from different threads is protected and will fail, but invoking methods (even one a time) by alternating the calling thread may have unexpected behaviors .
    Concurrent access of properties from different threads may have unexpected behaviors in the Client itself (they aren't thread safe).
    If you need to use the same Client object from different thread, you do it at your own risk and you must ensure with your logic (for example locking the thread) that just a thread at time invokes methods and properties of the Client object.
  • The library makes use of namespaces, so it's advice to declared the used one in your files, e.g.
    using namespace RLibCpp;
    using namespace RLibCpp::Bcc;
    Definition basemonitor.h:27
    Definition basemonitor.h:26

That said, let's see che basic step to start using the library

Getting a Connection

The basic object requested to communicate with a device is the Connection object.
The Connection object needs just few settings to be ready for use. For convenience it's advice to connect to some Connection signal to get rid of the most significative Connection status changes notifications.
Here is an example for the TcpIpConnection object, probably the most used one.

// Create the object
// Set the Vtx path
String vtxPath = "../etc/vtx";
con.setVtxPath(vtxPath);
// Set the address with default device port
con.setServerIp("192.168.0.180");
// Set the schedule type
con.setScheduleType(ThreadHelper::scheduleTypeFast);
// Connect to most significative signals
con.started.connect(onConnStarted);
con.stopped.connect(onConnStopped);
// or if event handler are non static member function of class (e.g. MyClass *obj)
// con.started.connect(obj, &MyClass::onConnStarted);
// con.stopped.connect(obj, &MyClass::onConnStopped);
// Start the connection
{
cout << "!!!!!! Failed to start connection\n";
return;
}
Signal1< uint64_t > stopped
Event fired when the connection has been stopped.
Definition connection.h:534
virtual Globals::Results start()
Start and activate the connection.
Signal1< uint64_t > started
Event fired when the connection has been started.
Definition connection.h:528
void setVtxPath(const String &value)
Set the VtxManager path.
@ resSuccess
The operation completed successfully.
Definition globals.h:39
The connection class for the BCC protocol with the standard single socket TCP/IP transport.
Definition tcpipconnection.h:70
void setScheduleType(Tool::ThreadHelper::ScheduleTypes value)
Set the schedule type for the connection.
void setServerIp(const String &value)
Set the server IP destination address to which you want to connect.

The Connection object per se doesn't expose any usefull method , so we need a Client object to perform the most significative operations.

Getting a Client

To get a Client for a Connection just invoke the proper method ( Connection::getClient )

Client *client;
if (con.getClient(client) != Globals::resSuccess)
{
cout << "!!!!!! Failed to get the client\n";
return;
}
Communication client declaration.
Definition client.h:4639
virtual Globals::Results getClient(Client *&client)
Obtain a new (default configured) communication client for the connection.

Now you can use the Client object from just a single thread a time.
When done with the object, it's best practice to expliciatlly release it (Connection::releaseClient) elsewhere it will survive for all the the Connection object lifespan.

{
cout << "!!!!!! Failed to release the client\n";
return;
}
virtual Globals::Results releaseClient(Client *client)
Release a communication client for the connection.


Note that you cannot neither create nor delete a Client object directly, everything has to be handled by the associated Connection object.

Getting a Variabile

Once we have a valid Client object (see Getting a Client) we can use it to read a Variable (Client.readVariable) The Variable object may be created in different ways:

  1. You can get standard variable using the method specific for that variable type
    e.g.
    The variable specification data.
    Definition variable.h:35
    static Variable fromVR32(uint32_t index, uint32_t count=1)
    Factory method to create and initialize a Message::varVR32 variable.
  2. If you need to access other types of variable (e.g. local task variable, predefined variables etc) or simply you prefer to specify the variable definition, you can get specify the variable name with proper syntax
    e.g.
    Variable var1= Variable::fromName("R 1000, 10");
    static Variable fromName(const String &name)
    Factory method to create and initialize a Message::varNamed variable.

The Variable may be further customized to use Safe modes (ClientStruct::ReadVariableData::setSafe) or coherent values (ClientStruct::ReadVariableData::setCoherentValues) : see section Using safe operations and Using coherent values for further details.

Reading a Variable

To read a Variable you'll have to get a ClientStruct::ReadVariableData object, assign the ClientStruct::ReadVariableData::var() property as seen in Getting a Variabile and call the Client::readVariable(ClientStruct::ReadVariableData&) method

if (client->readVariable(data) != Globals::resSuccess)
{
cout << "!!!!!! Failed to read variable\n";
return;
}
Globals::Results readVariable(ClientStruct::ReadVariableData &data)
Read variable values from the connected device.
The data for the readVariable method.
Definition client.h:4411

Now you can use the values read:

for (size_t valIdx = 0; valIdx < data.values().size(); valIdx++)
{
switch (data.values()[valIdx].type())
{
case Variable::Data::dtDouble:
case Variable::Data::dtSingle:
cout << data.values()[valIdx].getDouble() << " ";
break;
case Variable::Data::dtString:
cout << data.values()[valIdx].getString() << " ";
break;
default:
cout << data.values()[valIdx].getU32() << " ";
break;
}
}

Writing a Variable

To write a Variable you'll have to get a ClientStruct::WriteVariableData object, assign the ClientStruct::WriteVariableData::var() property as seen in Getting a Variabile and add the values to write

for (int32_t idx = 0; idx < 10; idx++)
{
data.values().pushBack(Variable::Data::fromU32(idx));
}
The data for the writeVariable method.
Definition client.h:4544
static Data fromU32(uint32_t value)
Factory method to get an object from a Types::dtU32.

... and write the values to the device with the proper method ( see Client::writeVariable(ClientStruct::WriteVariableData&) ) ...

if (client->writeVariable(data) != Globals::resSuccess)
{
cout << "!!!!!! Failed to write variable\n";
}
Globals::Results writeVariable(ClientStruct::WriteVariableData &data)
Write variable values to the connected device.

Getting a Monitor

To get a Monitor for a Connection just invoke the proper method ( Connection::getMonitor )

Monitor *mon;
{
cout << "!!!!!! Failed to get the monitor\n";
return;
}
virtual Globals::Results getMonitor(Monitor *&monitor)
Obtain a new (default configured) monitor for the connection.
The class to handle BCC protocol monitors.
Definition monitor.h:199

Now you can use the Monitor object from just a single thread a time.
When done with the object, it's best practice to expliciatlly release it (Connection::releaseMonitor) elsewhere it will survive for all the the Connection object lifespan.

{
cout << "!!!!!! Failed to release the monitor\n";
return;
}
virtual Globals::Results releaseMonitor(Monitor *monitor)
Release a monitor for the connection.

Note that you cannot neither create nor delete a Monitor object directly, everything has to be handled by the associated Connection object.

Using the Monitor

Once we got a Monitor object (see Getting a Monitor) we need some basic settings before to use it, such as the requested data frequency , the autorestart flag:

// Setup the monitor
mon->setAutoRestart(true);
mon->setDataFrequency(100);
void setAutoRestart(bool value)
Set the flag to restart the monitor automatically when the device variable set has changed.
void setDataFrequency(uint16_t value)
Set the desired frequency of the data returned from the device (Hz)

It's best practice to use the emitted signals to get rid of the Monitor status and to read data

mon.dataChanged.connect(onMonDataChanged);
mon.restarted.connect(onMonRestarted);
mon.started.connect(onMonRonMonStartedestarted);
mon.stopped.connect(onMonStopped);
mon.error.connect(onMonError);
// or if event handler are non static member function of class (e.g. MyClass *obj)
// mon.dataChanged.connect(obj, &MyClass::onMonDataChanged);
// mon.restarted.connect(obj, &MyClass::onMonRestarted);
// mon.started.connect(obj, &MyClass::onMonStarted);
// mon.stopped.connect(obj, &MyClass::onMonStopped);
// mon.error.connect(obj, &MyClass::onMonError);
Signal1< uint64_t > stopped
Event fired when the monitor is successfully stopped.
Definition monitor.h:646
Signal2< uint64_t, RLibCpp::Bcc::MonitorStruct::ErrorData > error
Event fired when an error occurs during monitor operations.
Definition monitor.h:670
Signal2< uint64_t, uint32_t > restarted
Event fired when the monitor is successfully restarted.
Definition monitor.h:656
Signal2< uint64_t, uint32_t > started
Event fired when the monitor is successfully started.
Definition monitor.h:640
Signal2< uint64_t, uint32_t > dataChanged
Event fired when the monitor data has changed i.e. some new monitor data has been received.
Definition monitor.h:663

The Monitor may be further customized to use coherent values (Monitor::setCoherentValues) : see section Using coherent values for further details.
Once the Monitor was properly setupped , we have to add it the variables be read ( see Getting a Variabile).
Unless you need to use Monitor::coherentValues, you can add as many variables as needed (with arbitrary repetition count) to the single Monitor. Instead, if you setted the Monitor::coherentValues flag, then you have to distribute the variable into as many distinct monitors as needed to accomplish the BCC 31 monitor specification data size.
The limit to the number of variables and their repetition, is the maximum number of bcc monitor that the device can allows to be created.

mon->add(Variable::fromVRR(2000, 10));
mon->add(Variable::fromVR32(1000, 10));
Globals::Results add(const Variable &var) override
Adds a variable to the monitor.
static Variable fromVRR(uint32_t index, uint32_t count=1)
Factory method to create and initialize a Message::varVRR variable.

When done, you have to Monitor::start the Monitor ...

if (mon->start() != Globals::resSuccess)
{
cout << "!!!!!! Failed to start the monitor\n";
return;
}
Globals::Results start()
Starts the monitor.

... and wait for Monitor::dataChanged events

void onMonDataChanged(uint64_t senderId, uint32_t changeId)
{
if( mon->read(readData)!= Globals::resSuccess)
{
cout << "!!!!!! Failed to read the monitor\n";
return;
}
cout << "Monitor data changed: " << readData.changeId() << "\n";
for (size_t valIdx = 0; valIdx < readData.values().size(); valIdx++)
{
Variable::Data::PtrVector *list = readData.values()[valIdx];
for (size_t itemIdx = 0; itemIdx < list->size(); itemIdx++)
{
Variable::Data *data= (*list)[itemIdx];
switch (data->type())
{
case Variable::Data::dtDouble:
case Variable::Data::dtSingle:
cout << data->getDouble() << " ";
break;
case Variable::Data::dtString:
cout << data->getString() << " ";
break;
default:
cout << data->getU32() << " ";
break;
}
}
}
}
Variable::Data::PtrVector::PtrPtrVector & values()
Get the list of pointers to list of pointers variable data values.
Globals::Results read(MonitorStruct::ReadData &data)
Read the last monitor data from the device.
The data for the Monitor::read(MonitorStruct::ReadData &) method.
Definition monitor.h:42
uint32_t changeId() const
Get the progressive Id of receive data.
Class to handle with lists of Data pointers.
Definition variable.h:192
size_t size() const
Get the number of elements in the container.
Class to handle variable data values.
Definition variable.h:52
double getDouble()
Get the value as Types::dtUDouble.
uint32_t getU32()
Get the value as Types::dtU32.
Message::DataTypes type() const
Get the data type.
String getString()
Get the value as Types::dtString.

Make sure to process data as fast as possible, according to the setted data frequency and the capabilities of the device running your application.
Alternatively, if you don't want to use signals, you can pool the Monitor::changeId for variations and read data directly

uint32_t lastChangeId= 0;
void readMonData(Monitor *mon)
{
uint32_t changeId= mon->changeid();
if( lastChangeId == changeId)
return;
lastChangeId= changeId;
if( mon->read(readData)!= Globals::resSuccess)
{
cout << "!!!!!! Failed to read the monitor\n";
return;
}
cout << "Monitor data changed: " << readData.changeId() << "\n";
for (size_t valIdx = 0; valIdx < readData.values().size(); valIdx++)
{
Variable::Data::PtrVector *list = readData.values()[valIdx];
for (size_t itemIdx = 0; itemIdx < list->size(); itemIdx++)
{
Variable::Data *data= (*list)[itemIdx];
switch (data->type())
{
case Variable::Data::dtDouble:
case Variable::Data::dtSingle:
cout << data->getDouble() << " ";
break;
case Variable::Data::dtString:
cout << data->getString() << " ";
break;
default:
cout << data->getU32() << " ";
break;
}
}
}
}

Getting an Oscilloscope

To get an Oscilloscope for a Connection just invoke the proper method ( Connection::getOscilloscope )

{
cout << "!!!!!! Failed to get the oscilloscope\n";
return;
}
virtual Globals::Results getOscilloscope(Oscilloscope *&oscilloscope)
Obtain a new (default configured) oscilloscope for the connection.
The class to handle BCC protocol oscilloscopes.
Definition oscilloscope.h:853

Now you can use the Oscilloscope object from just a single thread a time.
When done with the object, it's best practice to expliciatlly release it (Connection::releaseOscilloscope) elsewhere it will survive for all the the Connection object lifespan.

{
cout << "!!!!!! Failed to release the oscilloscope\n";
return;
}
virtual Globals::Results releaseOscilloscope(Oscilloscope *oscilloscope)
Release a oscilloscope for the connection.

Note that you cannot neither create nor delete an Oscilloscope object directly, everything has to be handled by the associated Connection object.

Using the Oscilloscope

Once we got an Oscilloscope object (see Getting an Oscilloscope) we need some basic settings before to use it, such as the requested data and event frequency , the autorestart flag:

// Setup the oscilloscope
osc->setAutoRestart(true);
osc->setDataFrequency(500);
void setEventFrequency(uint32_t value)
Set the desired event frequency (Hz)
void setAutoRestart(bool value)
Set the flag to restart the oscilloscope automatically when the device variable set has changed.
void setDataFrequency(uint16_t value)
Set the desired frequency of the data returned from the device (Hz)

It's best practice to use the emitted signals to get rid of the Oscilloscope status and to read data

osc->dataChanged.connect(onOscDataChanged);
osc->restarted.connect(onOscRestarted);
osc->started.connect(onOscStarted);
osc->stopped.connect(onOscStopped);
osc->error.connect(onOscError);
osc->dataOverflowed.connect(onOscDataOverflowed);
// or if event handler are non static member function of class (e.g. MyClass *obj)
// osc->dataChanged.connect(obj, &MyClass::onOscDataChanged);
// osc->restarted.connect(obj, &MyClass::onOscRestarted);
// osc->started.connect(obj, &MyClass::onOscStarted);
// osc->stopped.connect(obj, &MyClass::onOscStopped);
// osc->error.connect(obj, &MyClass::onOscError);
// osc->dataOverflowed.connect(obj, &MyClass::onOscDataOverflowed);
Signal1< uint64_t > stopped
Event fired when the oscilloscope is successfully stopped.
Definition oscilloscope.h:1220
Signal2< uint64_t, RLibCpp::Bcc::OscilloscopeStruct::ErrorData > error
Event fired when an error occurs during oscilloscope operations.
Definition oscilloscope.h:1244
Signal2< uint64_t, uint32_t > restarted
Event fired when the oscilloscope is successfully restarted.
Definition oscilloscope.h:1230
Signal1< uint64_t > dataOverflowed
Event fired when the number of cached samples exceeds maxDataSamples() . You have to invoke the read(...
Definition oscilloscope.h:1255
Signal2< uint64_t, uint32_t > started
Event fired when the oscilloscope is successfully started.
Definition oscilloscope.h:1214
Signal2< uint64_t, uint32_t > dataChanged
Event fired when the oscilloscope data has changed i.e. some new oscilloscope data has been received.
Definition oscilloscope.h:1237

Once the Oscilloscope was properly setupped , we have to add it the variables be read ( see Getting a Variabile).
You have to distribute the variable into as many distinct oscilloscopes as needed to accomplish the BCC 31 oscilloscope specification data size

osc->add(Variable::fromName("RR 2000"));
osc->add(Variable::fromVR32(var, 1000));
Globals::Results add(const Variable &var)
Adds a variable (i.e. a track) to the oscilloscope.


When done, you have to Oscilloscope::start the Oscilloscope ...

if (osc->start() != Globals::resSuccess)
{
cout << "!!!!!! Failed to start the oscilloscope\n";
return;
}
Globals::Results start()
Starts the oscilloscope.

... and wait for Oscilloscope::dataChanged events

void onOscDataChanged(uint64_t, uint32_t)
{
if (osc->read(oscData) != Globals::resSuccess)
{
cout << "!!!!!! Failed to read the oscilloscope\n";
return;
}
// Show the last returned value
RLibCpp::Bcc::OscilloscopeStruct::DataSample *sample = oscData.samples()[oscData.samples().size() - 1];
if (oscData.samples().size() == 0)
return;
RLibCpp::Bcc::OscilloscopeStruct::DataSample *sample = oscData.samples()[oscData.samples().size() - 1];
FloatVector &values = sample->values();
for (size_t valIdx = 0; valIdx < values.size(); valIdx++)
{
cout << StringHelper::fromSingle(values[valIdx]) << " ";
}
cout << " @ " << StringHelper::fromDouble(sample->time()) << endl;
}
Globals::Results read(OscilloscopeStruct::ReadData &data)
Reads the samples cached from the device since last invocation of thie method.
size_t size() const
Get the number of elements in the container.
Single data sample read from the oscilloscope: it holds all the track ( values() ) returned at a spec...
Definition oscilloscope.h:516
double time() const
Get the sample reference time (uSec)
The data for the Oscilloscope::read(OscilloscopeStruct::ReadData &) method.
Definition oscilloscope.h:705
DataSample::PtrVector & samples()
Get the list of variable values.

Make sure to process data as fast as possible, according to the setted data frequency and the capabilities of the device running your application.
Alternatively, if you don't want to use signals, you can pool the Oscilloscope::changeId for variations and read data directly

uint32_t lastChangeId= 0;
void readOscData(Oscilloscope *osc)
{
uint32_t changeId = osc->changeId();
if (lastChangeId == changeId)
return;
lastChangeId = changeId;
if (osc->read(oscData) != Globals::resSuccess)
{
cout << "!!!!!! Failed to read the oscilloscope\n";
return;
}
cout << "Oscilloscope data changed: " << oscData.changeId() << "\n";
// Show the last returned value
RLibCpp::Bcc::OscilloscopeStruct::DataSample *sample = oscData.samples()[oscData.samples().size() - 1];
if (oscData.samples().size() == 0)
return;
RLibCpp::Bcc::OscilloscopeStruct::DataSample *sample = oscData.samples()[oscData.samples().size() - 1];
FloatVector &values = sample->values();
for (size_t valIdx = 0; valIdx < values.size(); valIdx++)
{
cout << StringHelper::fromSingle(values[valIdx]) << " ";
}
cout << " @ " << StringHelper::fromDouble(sample->time()) << endl;
}
uint32_t changeId()
The id of the data read from the device.
uint32_t changeId() const
Get the progressive Id of receive data.

If this is the case, make sure to read data at proper rate to avoid the Oscilloscope data overflow.

Using safe operations

The use of safe operation (see RLibCpp::Bcc::ClientStruct::SafeModes) grants you that the variable operation will be synched with the remote device, i.e. ensures that the device's varset identifier matches the library stored varset identifier. If the the stored varset identifier doesn't match the device one, then the library will transparently try to synch and performs the operation again.
Not all the variables operations supports safe operations. Here is the list:

When setting the property you have some choices to choose :

The suggested and preferred mode is RLibCpp::Bcc::ClientStruct::safeModeIfSupported unless you are concerned with perfomance and want to skip any extra comunication with the device or are accessing variables that don't rely on the device varset identifier (e.g. accessing standard register such as V32, NvR32). If that's the case then you may want to set the RLibCpp::Bcc::ClientStruct::safeModeNever mode

Using coherent values

The use of coherent values in operations grants you that the data packet handled is atomic i.e. performed with same operation.
If coherent value flag is resetted than the library may transparently split the operation to the device in multiple one , and variable modification may occur during successive device operations.
If coherent value flag is resetted than you are free to add as many variable as needed to the operation data, or few variable but with large repetition count (e.g. 300 Nv32 register). The library will take care of splitting (if necessary) the single user operation into as many device operations as needed to accomlish with the BCC 31 protocol specification
Otherwise if coherent value flag is setted than the responsability to fit the BCC 31 protocol specification is up to you.
Not all the variables operations have coherent values. Here is the list:

Using the cache

The library provides a Connection's Variable cache storage.
You can save Variable definitions into the cache and the retrive them when needed.

QString varName= "RR 1000, 10";
uint32_t cacheId = con.findInVarsCache(varName);
if( cacheId == 0)
{
// Variable not present into the cache: add it
cacheId = con.addToVarsCache(varName);
if (cacheId == 0)
{
cout << "!!!!!! Failed to add variable to cache\n";
return;
}
}
Variable cachedVar= Variable::fromCache(cacheId);
uint32_t addToVarsCache(const String &name, const String &extraTag="")
Adds a variable to the cache.
uint32_t findInVarsCache(const String &name, const String &extraTag="")
Check if the variable of specified in name with extraTag is cached.
static Variable fromCache(uint32_t cacheId)
Factory method to create and initialize a Message::varNamed variable.


If you need the same Variable definition to be used for multiple purpose and you need to keep them in cache as different entries , then you can add an extra tag to Variable cache definition

QString varName= "RR 1000, 10";
uint32_t cacheId = con.findInVarsCache(varName, "MON");
if( cacheId == 0)
{
// Variable not present into the cache: add it
cacheId = con.addToVarsCache(varName, "MON");
if (cacheId == 0)
{
cout << "!!!!!! Failed to add variable to cache\n";
return;
}
}
Variable cachedVar= Variable::fromCache(cacheId);