Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 7 Next »

The SDK C++ API provides an object-based interface for controlling/configuring the radar and for processing incoming data. See information describing the core features here, in the earlier C++11 implementation.

This C++17 implementation additionally includes the source code of a peak finding function to operate on a single azimuth of raw radar data: the same functionality that is implemented within the radar firmware to enable Navigation Mode .

It also contains a connection test project, which can be used to stress test the connection to a radar.

Building the SDK

The SDK is written in C++ and must be compiled prior to use.  The SDK is designed to be compiled by a standard C++ compiler without needing to build external libraries (although a header-only library is required. See below for more details)

The SDK may be built using either:

  • Command line

  • Visual Studio Code

Prerequisites

This requisite libraries can be installed on Linux using the following command line

sudo apt install build-essential clang g++ protobuf-compiler libprotobuf-dev cmake

The SDK also needs the Boost ASIO library.  The latest version of ASIO can be downloaded from:

 https://sourceforge.net/projects/asio/files/asio/

 

The ASIO library is header-only.  It is recommended that ASIO is installed where your compiler toolchain system path is located; for example /usr/include

Building using CMAKE

These steps assumes that the SDK has been cloned into ~/iasdk.

To simplify the (arcane) CMake syntax, a simple batch script has been written to automate the build process.  The script creates a folder, `output`, which holds the build.  The script will create a subfolder - `debug`/`release` - depending on the option chosen at the commend line.

./bootstrap_cpp17.sh [-v | --verbose] [ reset | clean | debug | release ]

Unless otherwise specified, the default build type is debug

The output executables (test_client, navigation_client and connection_tester) are placed in the root of the appropriate folder.

Building from VS Code

The project is configured to work with the Microsoft CMake Tools

(https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools).

If the CMake Tools are installed, opening the project in VSCode should detect the make files and configure the project accordingly.  The first time a build is selected you will need to select a compiler _kit_.  This will present a list of possible compiler configurations.

Using the VS Code build task

The .vscode folder contains a build task configuration for invoking CMake.

To build:

  • hit ctrl-shift-b

  • select Build [debug]

 

(Note: you can also select Clean [debug ] as one of the build options).

The build process will (again) generate three executables by default:

/iasdk/output/debug/test_client

/iasdk/output/debug/navigation_client

/iasdk/output/debug/connection_tester

Example projects

The SDK comes with three example projects.

 test_client

This project gives an example of how to create a radar client and connect up free-function handlers for processing various message types.

In this case, the radar client requests, and receives, FFT data from the radar.  The FFT data itself is not processed, but statistics about the data are presented - packet rate, packet size and packet time.

Note, when a client connects to a radar a configuration message is always sent.  The radar client must process this message, before requesting any other types.

The `test_client` program has two command-line options, as follows:

-i - The IP address of the server   [default: 127.0.0.1]
-p - The server port                [default: 6317]

If an option is not provided, its default value will be used.

 navigation_client

The navigation_client project is a sample application that will peak search and report back up to ten targets per azimuth.

The class Peak_finder can be used to process FFT data and search for peaks.

The algorithm will sub-resolve within radar bins and return a power at a distance in metres on the azmith being checked.

The algorithm implemented here will slide a window over the FFT data moving forwards by the size of the window, when the FFT has risen and then fallen, the peak resolving algorithm is run to sub-resolve the distance.

See Peak_finder.h for the data structure that is generated per azimuth

  • threshold - Threshold in dB

  • bins_to_operate_on - Radar bins window size to search for peaks in

  • start_bin - Start Bin

  • buffer_mode - Buffer mode should only be used with a staring radar

  • buffer_length - Buffer length

  • max_peaks_per_azimuth - Maximum number of peaks to find in a single azimuth

 

The navigation_client executable has two command-line options, as follows:

-i - The IP address of the server   [default: 127.0.0.1]
-p - The server port                [default: 6317]

If an option is not provided, its default value will be used.

connection_tester

The Connection tester project is a utility to stress-test the stability of the server.  The program creates a radar client that randomly connects and disconnects from a server.  On each connection, the radar client requests FFT data.

 The Connection_tester object runs in its own thread-of-control and can therefore be run in parallel.  Up to three Connection_tester objects can be run, all connecting/disconnecting to the same radar.  More than three Connection_tester objects may be running, but a server will only allow a maximum of three connections at any one time.

The `connection_tester` executable has three command-line options, as follows:

-i - The IP address of the server             [default: 127.0.0.1]
-p - The server port                          [default: 6317]
-c - Number of connections to run in parallel [default: 1]

If an option is not provided, its default value will be used.

NOTE:

The connection tester can be terminated with ctrl-c.  This will send an asynchronous message to each Connection_tester object.  The Connection_tester will only act upon this signal once it has completed its current connect/disconnect sequence.  This means it may take several seconds for the program to end, and you may see additional connect/disconnects after the ctrl-c.

Colossus messaging protocol

The C++17 SDK contains a minimum viable set of Colossus network messages for communicating to/from a radar.

The protocol is designed to be extensible.  Please see the /network/protocol/README.md file for a detailed overview of how the Colossus protocol code works, how it used, and how it can be extended.

Utilities

The SDK comes with a library of utilities classes and functions, designed to make programming easier.

The utilities are used throughout the code.  Notable examples include:

Time utilities (Time_utils.h)

The time utilities library provides an alternative to the C++ std::chrono library.  The Time Utilities support both real-time and monotonic clocks.  The main classes are:

  • Durations. A Duration represents a period of time.  Durations have nanosecond resolution

  • Observations. An Observation represents a point in time; computed as a Duration from their clock's epoch.

Durations and Observations are designed to be compared manipulated in intuitive ways - for example, subtracting two Observations will yield the Duration between them.  Durations and Observations also support expressive streaming options, meaning outputting/streaming time objects requires little additional code from the programmer to achieve readable results.

Logging (Log.h)

The logging class extends C++ console output, whilst maintaining a similar interface (all the IO manipulators available to std::ostream are also available to use, for example).

By default each log entry contains

  • A date/time; whose format can be configured

  • A logging level

The Log class provides methods for filtering log output on logging level.

Log outputs can be streamed together, as with std::ostream types. When the Log receives an endl manipulator, it terminates the current log entry and the next log entry will have a new date/time, logging level, etc.

The logging library presents a single global object, Navtech::stdout_log, which outputs to the console.

IP Address (IP_address.h)

The IP_address class holds IPv4 address in a low-memory footprint format (32-bits).  The class provides IP address validation on creation, IP address manipulation methods, as well as outputting/streaming facilities.

Active classes (Active.h)

The Active class provides a high-level abstraction from OS threading and support for asynchronous messaging. Using the Active class minimises the need for programmers to deal with common multi-threaded issues like race conditions, thread synchronisation, and lifetime management.

See the Active.h header for more information on how to use the Active class.

 Object lifetime management (pointer_types.h)

The code uses aliasing directives to make object lifetime management - ownership - as explicit as possible.

Raw pointers for dynamically-allocated objects is discouraged. In their place, specific lifetime-management types are provided. There are two basic models.

  • Single ownership (owner_of)- a dynamic resource is only ever 'owned' by one owner. Ownership may be transferred, but there can never be two modules managing the same resource.

  • Multiple ownership (shared_owner) - a resource is 'shared' between multiple modules. The lifetime of the resource persists until the last owner relinquishes it.

There is a distinct separation between object-to-object association (the "uses-a" relationship) and lifetime management of an object. owner_of and shared_owner types must never be used for association. Instead, prefer to explicitly state association with the association_to type.

Under the hood:

  • owner_of is an alias of std::unique_ptr

  • shared_owner is an alias of std::shared_ptr

  • association_to wraps 'raw' pointers

 Please see Utility/pointer_types.h for a more detailed explanation.

Option parsing (Option_parser.h)

The Option_parser class provides a simple interface for parsing command line options. It takes the standard C++ parameters for main() - argc and argv - as initialisation and provides a simple interface for querying and extracting options from the input stream.

Please note, this class is very limited in its functionality. It requires all command line arguments to have two elements

  • A option flag (for example -i)

  • A value (for example 127.0.0.1)

Values are always returned as std::string objects.

Examples of the Option_parser's use can be seen in the example projects.

Programming with the SDK

Connection to a radar is handled through a Radar_client object.  A Radar_client provides two basic interfaces:

  • Setting up callbacks for incoming Colossus messages

  • Sending Colossus messages (to the radar)

Constructing the Radar client

The Radar_client must be constructed with the IP address and port number of the server:

#include "Radar_client.h"
#include "IP_address.h"

using namespace Navtech::Networking;
using namespace Navtech::Utility;

int main()
{
    Radar_client radar_client { "198.168.0.1"_ipv4, "6317" };

    // more...
}

In the above code we are using the user-defined literal _ipv4 to construct an IP_address object from a string literal.

Creating a callback

Incoming messages are dispatched to an appropriate callback, which must be provided by the client.  The callback can be any C++ Callable type - that is, a function, a member function or a lambda expression - that satisfies the function signature:

void (*callback)(Navtech::Networking::Radar_client&, Navtech::Networking::Colossus_protocol::Message&);

Note, the callback has two parameters - the (incoming) message, and a reference to the (calling) Radar_client.  This reference allows the callback to access the API of the Radar_client without resorting to static (global) Radar_client objects.  An example of how this may be exploited is shown later.

In this simple example, we'll use a free function to process the incoming configuration.

#include "Radar_client.h"
#include "IP_address.h"
#include "Colossus_protocol.h"

using namespace Navtech::Networking;
using namespace Navtech::Utility;

// Define a callback to process incoming configuration messages
//
void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    // See later...
}

 
int main()
{
    Radar_client radar_client { "198.168.0.1"_ipv4, "6317" };

    // more...
}

We must register the handler with the Radar_client and associate it with a particular message type.

#include "Radar_client.h"
#include "IP_address.h"
#include "Colossus_protocol.h"

using namespace Navtech::Networking;
using namespace Navtech::Utility;

void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    // See later...
}

 
int main()
{
    Radar_client radar_client { "198.168.0.1"_ipv4, "6317" };

    // Register the callback
    //
    radar_client.set_handler(Colossus_protocol::Message::Type::configuration, process_config);

    // more...
}

 

Next, we must set the Radar_client running, so that it can connect to the radar and begin processing messages.  The Radar_client runs in its own thread.  Callbacks are executed in the context of the Radar_client (more specifically, in the context of the Radar_client's Dispatcher thread). It is your responsibility to protect against race conditions if your callback functions interact with other threads-of-control.

For this example, we will simply let the Radar_client run for a period of time, before stopping.

#include "Radar_client.h"
#include "IP_address.h"
#include "Colossus_protocol.h"

using namespace Navtech::Networking;
using namespace Navtech::Utility;
using namespace Navtech::Time;
using namespace Navtech::Time::Monotonic;

void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    // See later...
}

 
int main()
{
    Radar_client radar_client { "198.168.0.1"_ipv4, "6317" };

    radar_client.set_handler(Colossus_protocol::Message::Type::configuration, process_config);
 
    // Start the Radar_client and let it run for a period of time.
    // Note the use of a user-defined literal for the sleep duration.
    //
    radar_client.start();
    sleep_for(30_sec);
    radar_client.stop();
}

Handling incoming messages

The Colossus_protocol::Message type encapsulates a buffer with a basic interface for accessing data. For more details on the `Message` type, please read /network/protocol/README.md, which explains the concept behind the Colossus messaging classes.

To access the radar data in a message, the simplest way (and our recommended way) is to perform a memory overlay onto the message data. The Message class provides an interface for this.  Once the overlay has been done, the specific message's API can be accessed to read from the message buffer.  The API has been constructed to hide any endianness issues, scaling or conversion that may be required to get from 'raw' message bytes to usable radar information.

#include "Radar_client.h"
#include "IP_address.h"
#include "Colossus_protocol.h"
#include "Log.h"

using namespace Navtech::Networking;
using namespace Navtech::Utility;
using namespace Navtech::Time;
using namespace Navtech::Time::Monotonic;

void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    // Interpret the message data as a Configuration message
    //
    Colossus_protocol::Configuration* config = msg.view_as<Colossus_protocol::Configuration>();

    // Access the data via the pointer
    //
    stdout_log << "Azimuth samples [" << config->azimuth_samples() << "]" << endl;
    stdout_log << "Bin size        [" << config->bin_size()<< "]" << endl;
    stdout_log << "Range in bins   [" << config->range_in_bins()<< "]" << endl;
    stdout_log << "Encoder size    [" << config->encoder_size()<< "]" << endl;
    stdout_log << "Rotation rate   [" << config->rotation_speed()<< "]" << endl;
    stdout_log << "Range gain      [" << config->range_gain()<< "]" << endl;
    stdout_log << "Range offset    [" << config->range_offset()<< "]" << endl;

    // more...
}

In the example above we are using the logging utility, stdout_log for output.

Handling a protocol buffer from a message

If the incoming message contains a protocol buffer, this data can be extracted into a protobuf object and then accessed through the normal protobuf API.

#include "Radar_client.h"
#include "IP_address.h"
#include "Colossus_protocol.h"
#include "Log.h"
#include "configurationdata.pb.h"
#include "Protobuf_helpers.h"

// As previously...

void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    using Navtech::Protobuf::from_vector_into;
    using namespace Colossus;
    using namespace Navtech::Networking;

    auto config = msg.view_as<Colossus_protocol::Configuration>();

    stdout_log << "Azimuth samples [" << config->azimuth_samples() << "]" << endl;
    stdout_log << "Bin size        [" << config->bin_size()<< "]" << endl;
    stdout_log << "Range in bins   [" << config->range_in_bins()<< "]" << endl;
    stdout_log << "Encoder size    [" << config->encoder_size()<< "]" << endl;
    stdout_log << "Rotation rate   [" << config->rotation_speed()<< "]" << endl;
    stdout_log << "Range gain      [" << config->range_gain()<< "]" << endl;
    stdout_log << "Range offset    [" << config->range_offset()<< "]" << endl;

    // Overlay onto the message memory, then extract the raw message payload
    // data into std::optional protobuf object using the helper function
    // Protobuf::from_vector_into.  If the conversion fails, the function
    // will return std::nullopt
    //
    std::optional<Protobuf::ConfigurationData> protobuf = from_vector_into<Protobuf::ConfigurationData>(config->to_vector());

    if (protobuf.has_value()) {
        stdout_log << "Radar ID   [" << protobuf->model().id()   << "]" << endl;
        stdout_log << "Radar name [" << protobuf->model().name() << "]" << endl;
        // etc...
    }
}

 

Sending a message

The full details of sending Colossus messages is beyond the scope of this 'getting started' overview.  For full details of how to construct and send Colossus messages, please read /network/protocol/README.md.

In the case of a radar client, typically only simple messages are sent to the radar, to enable/disable features such as FFT data, or radar health.

Remember - if you enable data transmission from the radar you *must* have a handler for it, otherwise you will receive an error each time a message arrives!

void process_FFT(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    // Process the incoming FFT data...
}

 
int main()
{
    Radar_client radar_client { "192.168.0.1"_ipv4, "6317" };

    radar_client.set_handler(Colossus_protocol::Message::Type::configuration, process_config);
    radar_client.set_handler(Colossus_protocol::Message::Type::fft_data, process_FFT);

    radar_client.start();
    sleep_for(5_sec);

    // Construct a message to start FFT data and send it to the radar
    //
    Colossus_protocol::Message msg { };
    msg.type(Colossus_protocol::Message::Type::start_fft_data);

    radar_client.send(std::move(msg));

    sleep_for(30_sec);
    radar_client.stop();
}

Sending a message within a callback

Commonly, you will want to send a message from within a callback. Since the callback has the Radar_client as an argument, you can directly send a message.

Note, in the case of simple messages (with no header or payload), the send() method can construct the Colossus_protocol::Message object implicitly as part of the call.

void process_config(Radar_client& radar_client, Colossus_protocol::Message& msg)
{
    using Navtech::Protobuf::from_vector_into;
    using namespace Colossus;
    using namespace Navtech::Networking;

    auto config = msg.view_as<Colossus_protocol::Configuration>();

    stdout_log << "Azimuth samples [" << config->azimuth_samples() << "]" << endl;
    stdout_log << "Bin size        [" << config->bin_size()<< "]" << endl;
    stdout_log << "Range in bins   [" << config->range_in_bins()<< "]" << endl;
    stdout_log << "Encoder size    [" << config->encoder_size()<< "]" << endl;
    stdout_log << "Rotation rate   [" << config->rotation_speed()<< "]" << endl;
    stdout_log << "Range gain      [" << config->range_gain()<< "]" << endl;
    stdout_log << "Range offset    [" << config->range_offset()<< "]" << endl;

    auto protobuf = from_vector_into<Protobuf::ConfigurationData>(config->to_vector());

    if (protobuf.has_value()) {
        stdout_log << "Radar ID   [" << protobuf->model().id()   << "]" << endl;
        stdout_log << "Radar name [" << protobuf->model().name() << "]" << endl;
        // etc...
    }

    // Send a simple message to the radar
    //
    radar_client.send(Colossus_protocol::Message::Type::start_fft_data);
}

Next steps

You should now be in a position to read and understand the example projects. We recommend reviewing the test_client project first (testclient_main.cpp), which expands on the above code.

  • No labels