Publishing Multiple Message Types (Adv.) Tutorial

This advanced tutorial covers how to populate and publish data from a single node into a PolySync message on the bus, enable multiple nodes to subscribe to messages to access data asynchronously, and visualize all data being published to the PolySync bus.

Overview

The example program polysync-data-generator-cpp is included in the PolySync installation. It is designed to populate PolySync messages with sample data and publish those messages to the global, shared bus.

The sample data comes in multiple types, each with its own associated high-level PolySync message:

  • Radar Targets: ps_radar_targets_msg
  • Classified Objects: ps_objects_msg
  • LiDAR Points: ps_lidar_points_msg

PolySync messages are defined in multiple data model modules:

1. Running the data generator

Begin by starting the PolySync manager. Open a new terminal and run:

$ sudo service polysync-core-manager start

1.2 Generate data

You can find the C++ Data Generation code as part of our public C++ examples repo here.

To build the C++ application you will use the tools cmake and make. The runtime dependencies are detailed in the local README.md file.

$ git clone git@github.com:PolySync/PolySync-Core-CPP-Examples.git
$ cd PolySync-Core-CPP-Examples/DataGenerator
$ sudo apt-get install libglib2.0-dev  # this is required for this example to build, documented in the examples README.md file
$ mkdir build && cd build
$ cmake ..
$ make

Now the polysync-data-generator-cpp node is built, and can be started with the command:

$ ./polysync-data-generator-cpp

This is an overview of the source files for the example:

PolySync-Core-CPP-Examples/DataGenerator/
├── CMakeLists.txt
├── DataGenerator.cpp
├── LidarPointGenerator.cpp
├── LidarPointGenerator.hpp
├── ObjectGenerator.cpp
├── ObjectGenerator.hpp
├── RadarTargetGenerator.cpp
├── README.md
└── RadarTargetGenerator.hpp

1.2.1 DataGenerator.cpp

This is where the PolySync node is defined, and where you can access the main entry point for this node/application.

#include < iostream >

#include < PolySyncNode.hpp >
#include < PolySyncDataModel.hpp >

#include "LidarPointGenerator.hpp"
#include "RadarTargetGenerator.hpp"
#include "ObjectGenerator.hpp"

using namespace polysync::datamodel;

class DataGenerator : public polysync::Node
{
protected:
    virtual void okStateEvent()
    {
        _lidarPointGenerator->updatePoints();
        _lidarPointGenerator->publishPoints();

        _radarTargetGenerator->updateTargets();
        _radarTargetGenerator->publishTargets();

        _objectGenerator->updateObjects();
        _objectGenerator->publishObjects();

        polysync::sleepMicro( _updateInterval );
    }
    virtual void initStateEvent()
    {
        _lidarPointGenerator =
                std::unique_ptr< LidarPointGenerator >{
                    new LidarPointGenerator( *this ) };

        _radarTargetGenerator =
                std::unique_ptr< RadarTargetGenerator >{
                    new RadarTargetGenerator( *this ) };

        _objectGenerator =
                std::unique_ptr< ObjectGenerator >{
                    new ObjectGenerator( *this ) };
    }

private:
    ps_timestamp _updateInterval{ 50000 };
    std::unique_ptr< LidarPointGenerator > _lidarPointGenerator;
    std::unique_ptr< RadarTargetGenerator > _radarTargetGenerator;
    std::unique_ptr< ObjectGenerator > _objectGenerator;
};
int main()
{
    DataGenerator dataGenerator;

    dataGenerator.connectPolySync();

    return 0;
}

1.2.2 DataGenerator.cpp explained

Now let’s break down the DataGenerator.cpp code.

The line below defines the PolySync node and each of the protected event methods in the node state machine. Each of the states do minimal or no operation unless they are overridden.

#include < PolySyncNode.hpp >

The C++ data model defines all types and messages in the PolySync runtime. This must be included in all C++ node applications.

#include < PolySyncDataModel.hpp >

The following headers are specific to this application and define the data “generators.” They are responsible for creating, populating, and updating the respective message types with data.

#include "LidarPointGenerator.hpp"
#include "RadarTargetGenerator.hpp"
#include "ObjectGenerator.hpp"

All of our PolySync C++ applications must then subclass the PolySync node object in order to connect to the PolySync bus and publish/subscribe messages.

class DataGenerator : public polysync::Node

The following is called continuously while in the node’s OK state, and is where almost all of the action happens for the node.

virtual void okStateEvent()

For each execution loop, each of the three primitive data types will be updated in a new message instance. These are then published to the bus.

_lidarPointGenerator->updatePoints();
_lidarPointGenerator->publishPoints();

_radarTargetGenerator->updateTargets();
_radarTargetGenerator->publishTargets();

_objectGenerator->updateObjects();
_objectGenerator->publishObjects();

This is called once after the node transitions to the INIT state. The initStateEvent is typically where a node creates messages and initializes any other resources that require a PolySync node reference.

virtual void initStateEvent()

This creates a reference to the new LiDAR point generator instance.

_lidarPointGenerator = std::unique_ptr< LidarPointGenerator >{ new LidarPointGenerator( *this ) };

The following is how you can control the execution speed of the state machine. The _updateInterval is called at the end of each event, whether it has been overridden or not.

ps_timestamp _updateInterval{ 50000 };

This creates a reference for each of your data generator objects defined in their unique header and source files.

std::unique_ptr< LidarPointGenerator > _lidarPointGenerator;
std::unique_ptr< RadarTargetGenerator > _radarTargetGenerator;
std::unique_ptr< ObjectGenerator > _objectGenerator;

This creates an instance of your DataGenerator object. The second command is a blocking operation, and places the node in the node state machine. If there is a valid license, the node leaves the AUTH state and enters the INIT state, calling the initStateEvent.

DataGenerator dataGenerator;
dataGenerator.connectPolySync();

1.3 Generate LiDAR points

Th LiDAR point generator defines the class that is used to create, initialize, populate, and publish LiDAR message on PolySync. This is reimplemented in RadarTargetGenerator.cpp and ObjectGenerator.cpp for a RADAR generator class and an object’s generator class.

#include < PolySyncNode.hpp >
#include < PolySyncDataModel.hpp >


class LidarPointGenerator
{
public:
    LidarPointGenerator( polysync::Node & );


    void updatePoints();


    void publishPoints();


    void initializeMessage();

private:

    polysync::datamodel::LidarPointsMessage _message;

    float _relativeTime{ 0.0 };
    const float _gridScale{ 10.0 };
    const ulong _gridSideLength{ 100 };
    const ulong _sensorID{ 11 };
    const ulong _numberOfPoints{ 10000 };
    const float _sineFrequency{ 4.0 };
};

1.3.1 LidarPointsGenerator.hpp explained

Now let’s break down the LidarPointsGenerator.hpp code.

#include < PolySyncNode.hpp >
#include < PolySyncDataModel.hpp >

The three primitive data types (LiDAR, RADAR, objects) each create a class with a basic PolySync node constructor and three functions to initialize, update, and publish data to the bus. The LiDAR point generator class is then called.

class LidarPointGenerator
{ ... };

This class must be constructed with a valid node reference, which happens in the initStateEvent defined in the DataGenerator.cpp file.

public:
    LidarPointGenerator( polysync::Node & );
};

This will be used to populate the LidarPointsMessage with a valid sensor descriptor─describing this node as a publisher on the bus─as well as set the initial header, and message start/end timestamps.

Once the message is initialized, you will use function below to call updatePoints.

public:
    ...
    void initializeMessage();
};

For each message published to the bus, the node needs to update each of the valid fields within the message. updatePoints iterates over a vector of PolySync LidarPoint and updates each LiDAR points:

  • Intensity
  • x, y and z position

Each point in the vector is updated to represent a sine wave.

public:
    ...
    void updatePoints();
};

After you’re done setting the header timestamp, the following will publish the LidarPointsMessage with an updated vector of points to the PolySync bus.

public:
    ...
    void publishPoints();
};

Next, you will call this high-level message used to store LiDAR points.

private:

    polysync::datamodel::LidarPointsMessage _message;
};

The variables are then used to calculate the sine wave and determine the number of LiDAR points required to represent it.

private:
    ...
    float _relativeTime{ 0.0 };
    const float _gridScale{ 10.0 };
    const ulong _gridSideLength{ 100 };
    const ulong _numberOfPoints{ 10000 };
    const float _sineFrequency{ 4.0 };
};

1.3.2 LidarPointsGenerator.cpp

#include "LidarPointGenerator.hpp"

using namespace std;
using namespace polysync::datamodel;

LidarPointGenerator::LidarPointGenerator( polysync::Node & node )
    :
    _message( node ),
    _numberOfPoints( _gridSideLength * _gridSideLength )
{
    initializeMessage();
}

void LidarPointGenerator::initializeMessage()
{
    polysync::datamodel::SensorDescriptor descriptor;

    descriptor.setTransformParentId( PSYNC_COORDINATE_FRAME_LOCAL );
    descriptor.setType( PSYNC_SENSOR_KIND_NOT_AVAILABLE );
    _message.setSensorDescriptor( descriptor );

    auto time = polysync::getTimestamp();
    _message.setHeaderTimestamp( time );
    _message.setStartTimestamp( time );
    _message.setEndTimestamp( time );

    updatePoints();
}

void LidarPointGenerator::updatePoints()
{
    auto time = polysync::getTimestamp();
    auto timeDelta = time - _message.getStartTimestamp();
    auto timeDeltaSeconds = static_cast< float >( timeDelta ) / 1000000.0;

    _relativeTime += timeDeltaSeconds;

    _message.setStartTimestamp( time );
    _message.setEndTimestamp( time );

    std::vector< LidarPoint > outputPoints;
    outputPoints.reserve( _numberOfPoints );

    for( auto pointNum = 0U; pointNum < _numberOfPoints; ++pointNum )
    {
        polysync::datamodel::LidarPoint point;
        point.setIntensity( 255 );

        auto x = pointNum % 100;
        auto y = pointNum / 100;

        float u = static_cast< float >( x )/ 100.0;
        float v = static_cast< float >( y ) / 100.0;

        // center u/v at origin
        u = ( u * 2.0 ) - 1.0;
        v = ( v * 2.0 ) - 1.0;

        float w = sin( ( u * _sineFrequency ) + _relativeTime )
                * cos( ( v * _sineFrequency ) + _relativeTime )
                * 0.5;

        point.setPosition( { u * 10, v * 10, w * 10 } );
        outputPoints.emplace_back( point );
    }

    _message.setPoints( outputPoints );
}

void LidarPointGenerator::publishPoints()
{
    _message.setHeaderTimestamp( polysync::getTimestamp() );
    _message.publish();
}

1.3.3 LidarPointsGenerator.cpp explained

Now let’s break down the LidarPointsGenerator.cpp code.

During the object’s construction, the LiDAR message is initialized with the node reference provided from the DataGenerator.cpp initStateEvent function call. Once you initialize the message here, the message fields can be set to the desired values.

LidarPointGenerator::LidarPointGenerator( polysync::Node & node )
    :
    _message( node ),
    _numberOfPoints( _gridSideLength * _gridSideLength )
{
    initializeMessage();
}

The sensor descriptor is used by the subscribing node to determine which node the message originated from. It is not mandatory, but we highly recommend it as a means to populate the sensor descriptor for each message instance.

The TransformParentId represents the active coordinate frame identifier. In this case, you are setting the active coordinate frame to the local frame, which means no transform corrections are needed for outgoing data.

Messages should be initialized with the current UTC timestamp, multiple timestamps fields exist.

void LidarPointGenerator::initializeMessage()
{
    polysync::datamodel::SensorDescriptor descriptor;

    descriptor.setTransformParentId( PSYNC_COORDINATE_FRAME_LOCAL );
    descriptor.setType( PSYNC_SENSOR_KIND_NOT_AVAILABLE );
    _message.setSensorDescriptor( descriptor );

    auto time = polysync::getTimestamp();
    _message.setHeaderTimestamp( time );
    _message.setStartTimestamp( time );
    _message.setEndTimestamp( time );


    updatePoints();
}

This function is responsible for setting each point in the LiDAR points message, stored as a vector of PolySync LidarPoint. You should be aware that the namespace is polysync::data model.

Timestamp fields are updated for each iteration that the message is updated and subsequently published by publishPoints(). PolySync timestamps can be referenced globally across the PolySync bus, and they are used here both to calculate the sine wave, as well as in the message timestamp fields.

void LidarPointGenerator::updatePoints()
{
    auto time = polysync::getTimestamp();

    auto timeDelta = time - _message.getStartTimestamp();

    auto timeDeltaSeconds = static_cast< float >( timeDelta ) / 1000000.0;

    _relativeTime += timeDeltaSeconds;

    _message.setStartTimestamp( time );
    _message.setEndTimestamp( time );
}

Each point is iterated over to set the intensity, and x/y/z position values before it’s emplaced within the vector. The following for loop will be used to calculate the position of each point to represent a sine wave.

Once the vector is filled the setPoints member function is used to copy the local vector into the message.

    std::vector< LidarPoint > outputPoints;
    outputPoints.reserve( _numberOfPoints );

    for( auto pointNum = 0U; pointNum < _numberOfPoints; ++pointNum )
    {
        polysync::datamodel::LidarPoint point;
        point.setIntensity( 255 );

        auto x = pointNum % 100;
        auto y = pointNum / 100;

        float u = static_cast< float >( x )/ 100.0;
        float v = static_cast< float >( y ) / 100.0;

        // center u/v at origin
        u = ( u * 2.0 ) - 1.0;
        v = ( v * 2.0 ) - 1.0;

        float w = sin( ( u * _sineFrequency ) + _relativeTime )
                * cos( ( v * _sineFrequency ) + _relativeTime )
                * 0.5;

        point.setPosition( { u * 10, v * 10, w * 10 } );
        outputPoints.emplace_back( point );
    }

    _message.setPoints( outputPoints );
}

Now the header timestamp will be set─immediately before publishing─to represent when the UTC timestamp of this message will be published to the PolySync bus.

void LidarPointGenerator::publishPoints()
{
    _message.setHeaderTimestamp( polysync::getTimestamp() );
    _message.publish();
}

2. Visualize data

Once a node─in this case polysync-data-generator-cpp─is publishing data to the PolySync bus, any other application can access the data by subscribing to the PolySync message type(s).

PolySync provides two applications that are already set up to subscribe to the message types published by this node, and visualize the data in multiple ways:

  • PolySync Studio
    • A Qt based application that has multiple plugins providing different ways to view the data on the bus.
  • Viewer Lite
    • An OpenGL 2D visualizer application that allows you to see primitive data types being transmitted using the PolySync bus.

2.1 Viewer Lite

Viewer lite is an example written in C.

In another terminal download, build, and start the viewer lite application.

$ git clone git@github.com:PolySync/PolySync-Core-C-Examples.git
$ cd PolySync-Core-C-Examples/viewer_lite
$ sudo apt-get install libglib2.0-dev freeglut3-dev  # this is required for this example to build, documented in the examples README.md file
$ make
$ ./bin/polysync-viewer-lite

DataGen

Your sample node should display the following:

  • A LiDAR “point cloud” as the the block in the center
  • Two “identified objects” as the rectangles on the upper right side of the point cloud. These are similar to those that you might get from an object identification sensor
  • Two circles representing RADAR targets on the upper left side of the point cloud

PolySync Viewer Lite does not allow for much distinction in the point cloud, but updates to the object data and RADAR pings that are immediately obvious.

2.2 Studio

In this instance, the 3D View plugin is the easiest way to view the provided data.

Leave the PolySync Viewer Lite application running, and either double-click the PolySync Studio icon on your desktop or execute the following command in a new terminal:

$ polysync-core-studio

When Studio opens, you will be able to visualize the data that the PolySync Data Generator is creating, select the “3D View” button on the right. With that item selected, PolySync Studio will display something similar to the image:

DataGen

Conclusion

Congratulations! You have now successfully populated and published data from a single node into a PolySync message on the bus, enabled multiple nodes to subscribe to messages to access data asynchronously, and visualized all data being published to the PolySync bus.