Custom Transports: POSIX Ethernet 802.1Q Tutorial

This section serves as a comprehensive guide to assist in creating custom transports for Safe DDS, using the Ethernet 802.1Q transport as a concrete example. Custom transports allow Safe DDS to communicate over different network interfaces and protocols, extending its flexibility and applicability in various environments.

Note

It is recommended to first read the Getting Started section to gain a better understanding of the basic concepts and terminology used in this section.

Introduction to Safe DDS Custom Transports

Safe DDS provides a transport abstraction layer that allows communication over various network interfaces and protocols. The library ships with built-in transports like POSIX UDPv4, but also allows the creation of custom transports to adapt to specific requirements or hardware configurations.

In this tutorial, the implementation of the POSIX Ethernet transport will be explored. This approach provides direct Ethernet frame communication capabilities with Time-Sensitive Networking (TSN) support through 802.1Q VLAN tagging. This transport is particularly useful for applications requiring deterministic communication with tight latency requirements.

Understanding the Transport Architecture

In Safe DDS, a transport is responsible for the following key functions:

  1. Sending messages to specific destinations

  2. Receiving messages

  3. Managing locator information

Extended explanation about this functionality can be found in Transport module section.

To implement a custom transport, it is required to create a class that inherits from the ITransport interface and implement all of its required methods. The Ethernet transport example illustrates how to properly implement these methods for raw Ethernet communication.

The Ethernet Transport Example

The Ethernet transport example demonstrates a transport implementation that operates directly at the Ethernet frame level, bypassing the IP stack. It includes 802.1Q VLAN tagging for Quality of Service (QoS) and TSN capabilities.

TSN Configuration

The Ethernet transport is compatible with Time-Sensitive Networking through a flow configuration based on transport identifiers (source/destination MAC addresses, PCP, VLAN ID).

Safe DDS allows to specify these values through the DataWriterWireProtocolQosPolicy::sending_locator on the sending side. If multiple DataWriters need to share the same MAC address, a virtual sending logical port can be defined, which provides a lightweight way of distinguishing between them.

    uint16_t vlanid = 10;
    uint8_t pcp = 5;
    uint16_t sending_logical_port = 9999;
    transport::Locator::MAC datawriter_mac_address = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    datawriter_qos.wire_protocol_qos().sending_locator =
            transport::Locator::from_mac(datawriter_mac_address, sending_logical_port, vlanid, pcp);

On the receiving side, the destination MAC address is specified via the DataReaderWireProtocolQosPolicy::unicast_endpoint_locator. Just like with DataWriters, Safe DDS also supports defining a virtual reception logical port. This allows multiple DataReaders to share the same MAC address but still keep their traffic logically separated.

    uint16_t reception_logical_port = 8888;
    transport::Locator::MAC datareader_mac_address = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    datareader_qos.wire_protocol_qos().unicast_endpoint_locator =
            transport::Locator::from_mac(datareader_mac_address, reception_logical_port, 0, 0);

For the participant associated discovery traffic, the DomainParticipantWireProtocolQosPolicy::announced_locator must be set with a MAC address and a logical port as well.

Any DataWriter or DataReader that does not have a specific sending or reception configuration will use the participant’s announced locator configuration by default.

    DomainParticipantQos participant_qos{};
    transport::Locator::MAC participant_mac_address = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    participant_qos.wire_protocol_qos().announced_locator =
            transport::Locator::from_mac(participant_mac_address, participant_logical_port, 0, 0);

Transport Class Structure

The Ethernet transport is defined in the transport::posix::Ethernet class. It inherits from the ITransport interface and implements all required methods.

class Ethernet :
    public ITransport

Message Sending

The Ethernet transport must implement methods for sending messages. The key method implementations are:

  1. commit_message - For sending a message to a specific receiver

  2. commit_message_to_group - For sending a message to a group of receivers

Both of them rely on the send_message method to handle the actual sending process.

In the send_message method, the transport:

  1. Checks if the sending origin GUID has a specific sending configuration
    • Custom configurations are set when the transport is called to set_origin_configuration, when a DataWriter is associated with a specific sending configuration.

      The transport stores the network interface index (from the given locator’s MAC address) and the sending configuration (source MAC address, VLAN ID, PCP, and source logical port) which is mapped to the DataWriter’s GUID.

    • If not, it falls back to a default configuration (participant’s announced locator one).

  2. Craft the remaining content of the Ethernet frame

  3. Sends the frame using the sendmsg system call, using the network interface index which corresponds to the sending locator’s MAC address

The Ethernet frame complete crafting includes:

  1. Setting the EthernetPacketHeader:

        struct EthernetPacketHeader
        {
            transport::Locator::MAC h_dest{};       ///< Destination MAC address
            transport::Locator::MAC h_source{};     ///< Source MAC address
            uint16_t h_proto = htons(static_cast<uint16_t>(ETH_P_8021Q));  ///< Ethertype / VLAN tag
        };
    
  2. Setting the EthernetPacketPrefix:

        struct EthernetPacketPrefix
        {
            static constexpr uint32_t ETH_MIN_SIZE = 46;    ///< Minimum payload size for Ethernet frames
            static constexpr uint32_t ETH_MTU = 1500;       ///< Maximum Transmission Unit for Ethernet
            static constexpr uint16_t ETH_P_RTPS = 0xEDD5;  ///< Ethertype for RTPS
    
            uint16_t pcp_dei_vid = 0;                                   ///< Priority Code Point (PCP), Drop Eligible Indicator (DEI), and VLAN ID
            uint16_t proto = htons(EthernetPacketPrefix::ETH_P_RTPS);   ///< Ethertype field
            uint16_t source_port = 0;                                   ///< RTPS source logical port number
            uint16_t dest_port = 0;                                     ///< RTPS destination logical port number
        };
    
  3. Appending the actual payload data

Message Reception

The first step to enable message reception is to configure the listening sockets. This is done in the listen_on_locator method, which given a locator:

  1. Creates a raw socket using the socket system call
    • In case it is a multicast MAC address, it also sets the SO_REUSEADDR socket option

  2. Binds the socket to the network interface associated with the locator’s MAC address
    • In case it is a multicast MAC address, it also joins the corresponding multicast group

There is a variation of the listen_on_locator method that is called when no specific locator is provided, the listen_on_first_available_locator. This method binds to the localhost’s default network interface.

Message reception is handled by the listen_message method. This method:

  1. Configures a timeout for blocking or non-blocking operations

  2. Waits for incoming packets using the select system call

  3. Receives raw Ethernet frames using the recv function

  4. Discard the frames that are invalid or that do not match the expected Safe DDS EtherType or the reception logical port

  5. Extract the payload and reception locator information

Discovery Integration

For proper discovery, the transport implements methods that handle locator information. Those methods shall take into account the locator specific type for Ethernet as per defined in OMG DDS-TSN Specification

This locator information is passed to the locator database.

TSN DomainParticipantFactory

A TSN-compatible implementation requires extend the QoS policies checks. This can be done by inheriting from the DomainParticipantFactory and overriding the on_participant_pre_enable method to check if the entities meet the QoS policies defined in OMG DDS-TSN Specification.

/**
 * @class TSNDomainParticipantFactory
 *
 * @brief Custom DomainParticipantFactory to enforce TSN QoS rules.
 */
class TSNDomainParticipantFactory :
    public dds::DomainParticipantFactory
{
public:

    TSNDomainParticipantFactory()
        : DomainParticipantFactory()
    {
        // Nothing to do
    }

    dds::ReturnCode on_participant_pre_enable(
            dds::BaseDomainParticipant& participant) noexcept override
    {
        dds::ReturnCode ret = check_qos(participant);

        if (dds::ReturnCode::OK == ret)
        {
            ret = dds::DomainParticipantFactory::on_participant_pre_enable(participant);
        }

        return ret;
    }

protected:

    dds::ReturnCode check_qos(
            dds::BaseDomainParticipant& participant) const noexcept
    {
        dds::ReturnCode ret = dds::ReturnCode::OK;

        // Check QoS of each DataWriter
        memory::IReferenceList<dds::BaseDataWriter>& datawriters = participant.get_datawriters();

        for (uint32_t i = 0; (dds::ReturnCode::OK == ret) && (i < datawriters.size()); ++i)
        {
            dds::DataWriterQos datawriter_qos = datawriters.at(i)->get_qos();

            if (datawriter_qos.reliability().kind != dds::ReliabilityQosPolicyKind::BEST_EFFORT_RELIABILITY_QOS)
            {
                ret = dds::ReturnCode::INCONSISTENT_POLICY;
            }

            if (datawriter_qos.durability().kind != dds::DurabilityQosPolicyKind::VOLATILE_DURABILITY_QOS)
            {
                ret = dds::ReturnCode::INCONSISTENT_POLICY;
            }

            if (datawriter_qos.history().kind != dds::HistoryQosPolicyKind::KEEP_LAST_HISTORY_QOS)
            {
                ret = dds::ReturnCode::INCONSISTENT_POLICY;
            }

            if (datawriter_qos.history().depth != 1)
            {
                ret = dds::ReturnCode::INCONSISTENT_POLICY;
            }
        }

        return ret;
    }

};

Using a Custom Transport

Safe DDS can be built with any custom transport support using the SAFEDDS_TRANSPORT_DIR CMake option (see CMake options). The transport directory must have the following structure:

<transport_dir>
├── include
│   └── ...
└── src
    └── ...

Having that, the DomainParticipantFactory, with no construct arguments, will directly create the given custom transport.

In this case the TSNDomainParticipantFactory must be used to ensure the TSN QoS policies are checked.

// Create the transport and factory
TSNDomainParticipantFactory factory;

Run the Example

The complete example can be found in the tutorial_custom_transport folder. This example shall be executed using sudo to allow the application to access the network interface directly.

Note

Remember to build the Safe DDS library with the custom transport enabled (see Using a Custom Transport for more details).