net/timestamp_inject/timestamp_inject_example.cpp

Reference Documentation

product_line_custom
IntelĀ® PAC
Napatech SmartNIC
category
Reference Information
Napatech Software Suite: net/timestamp_inject/timestamp_inject_example.cpp
net/timestamp_inject/timestamp_inject_example.cpp

Description

INFO: This is an 4GA TX timestamp inject and FCS generation example.

This source file is an example of how to control TX time stamp inject and FCS generation per packet using dynamic descriptor 3.

One bit in the TX packet descriptor is used to control TX time stamp inject, i.e. if the packet descriptor bit is 1 a TX time stamp will be inserted in the packet.

Two bits in the TX packet descriptor is used to control FCS generation, i.e. 0: Insert good FCS, 1: Insert bad FCS, 2: Reserved 3: Don't change FCS

This example requires the following setting in ntservive.ini

TimestampFormat = UNIX_NS

TimestampInjectStaticOffset = 80

TimestampInjectDynamicOffset = TSI_DYN_SOF

PacketDescriptor = NT

This program consists of a TX and RX part.

The TX part will transmit 6 packets in a loop. The number of loops is a command line option. To start the TX part use the -t <txport> option.

The six packets in a loop are based on the same packet template. Time stamp inject and FCS generation are then controlled per packet to be:

txPacket(hNetTx, NO_TS_INJECT, FCS_UNCHANGED);

txPacket(hNetTx, NO_TS_INJECT, FCS_GOOD);

txPacket(hNetTx, NO_TS_INJECT, FCS_BAD);

txPacket(hNetTx, TS_INJECT,    FCS_UNCHANGED);

txPacket(hNetTx, TS_INJECT,    FCS_GOOD);

txPacket(hNetTx, TS_INJECT,    FCS_BAD);

The RX part will receive packets from one port. It will check for the a TX time stamp in received packets and - if present - calculate and print the TX-to-RX latency. The RX part also dumps the received packet. To start the RX part use the -r <rxport> option.

Example:

timestamp_inject_example -t 0 -n 5 -r 1

will transmit on port 0 and receive on port 1. The transmitter will execute the loop 5 times, i.e. 30 packets will be transmitted.

/*
*
* Copyright 2020 Napatech A/S. All Rights Reserved.
*
* 1. Copying, modification, and distribution of this file, or executable
* versions of this file, is governed by the terms of the Napatech Software
* license agreement under which this file was made available. If you do not
* agree to the terms of the license do not install, copy, access or
* otherwise use this file.
*
* 2. Under the Napatech Software license agreement you are granted a
* limited, non-exclusive, non-assignable, copyright license to copy, modify
* and distribute this file in conjunction with Napatech SmartNIC's and
* similar hardware manufactured or supplied by Napatech A/S.
*
* 3. The full Napatech Software license agreement is included in this
* distribution, please see "NP-0405 Napatech Software license
* agreement.pdf"
*
* 4. Redistributions of source code must retain this copyright notice,
* list of conditions and the following disclaimer.
*
* THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES, EXPRESS OR
* IMPLIED, AND NAPATECH DISCLAIMS ALL IMPLIED WARRANTIES INCLUDING ANY
* IMPLIED WARRANTY OF TITLE, MERCHANTABILITY, NONINFRINGEMENT, OR OF
* FITNESS FOR A PARTICULAR PURPOSE. TO THE EXTENT NOT PROHIBITED BY
* APPLICABLE LAW, IN NO EVENT SHALL NAPATECH BE LIABLE FOR PERSONAL INJURY,
* OR ANY INCIDENTAL, SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES WHATSOEVER,
* INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF PROFITS, CORRUPTION OR
* LOSS OF DATA, FAILURE TO TRANSMIT OR RECEIVE ANY DATA OR INFORMATION,
* BUSINESS INTERRUPTION OR ANY OTHER COMMERCIAL DAMAGES OR LOSSES, ARISING
* OUT OF OR RELATED TO YOUR USE OR INABILITY TO USE NAPATECH SOFTWARE OR
* SERVICES OR ANY THIRD PARTY SOFTWARE OR APPLICATIONS IN CONJUNCTION WITH
* THE NAPATECH SOFTWARE OR SERVICES, HOWEVER CAUSED, REGARDLESS OF THE THEORY
* OF LIABILITY (CONTRACT, TORT OR OTHERWISE) AND EVEN IF NAPATECH HAS BEEN
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. SOME JURISDICTIONS DO NOT ALLOW
* THE EXCLUSION OR LIMITATION OF LIABILITY FOR PERSONAL INJURY, OR OF
* INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT APPLY TO YOU.
*
*
*/
/**
* @example net/timestamp_inject/timestamp_inject_example.cpp
* @section timestamp_inject_example_description Description
*
* INFO: This is an 4GA TX timestamp inject and FCS generation example.
*
* This source file is an example of how to control TX time stamp inject
* and FCS generation per packet using dynamic descriptor 3.
*
* One bit in the TX packet descriptor is used to control
* TX time stamp inject, i.e. if the packet descriptor bit is
* 1 a TX time stamp will be inserted in the packet.
*
* Two bits in the TX packet descriptor is used to control
* FCS generation, i.e.
* 0: Insert good FCS,
* 1: Insert bad FCS,
* 2: Reserved
* 3: Don't change FCS
*
* This example requires the following setting in ntservive.ini
*
* TimestampFormat = UNIX_NS
*
* TimestampInjectStaticOffset = 80
*
* TimestampInjectDynamicOffset = TSI_DYN_SOF
*
* PacketDescriptor = NT
*
* This program consists of a TX and RX part.
*
* The TX part will transmit 6 packets in a loop.
* The number of loops is a command line option.
* To start the TX part use the -t <txport> option.
*
* The six packets in a loop are based on the same
* packet template. Time stamp inject and FCS generation
* are then controlled per packet to be:
*
* txPacket(hNetTx, NO_TS_INJECT, FCS_UNCHANGED);
*
* txPacket(hNetTx, NO_TS_INJECT, FCS_GOOD);
*
* txPacket(hNetTx, NO_TS_INJECT, FCS_BAD);
*
* txPacket(hNetTx, TS_INJECT, FCS_UNCHANGED);
*
* txPacket(hNetTx, TS_INJECT, FCS_GOOD);
*
* txPacket(hNetTx, TS_INJECT, FCS_BAD);
*
* The RX part will receive packets from one port.
* It will check for the a TX time stamp in received
* packets and - if present - calculate and print
* the TX-to-RX latency.
* The RX part also dumps the received packet.
* To start the RX part use the -r <rxport> option.
*
* Example:
*
* timestamp_inject_example -t 0 -n 5 -r 1
*
* will transmit on port 0 and receive on port 1.
* The transmitter will execute the loop 5 times,
* i.e. 30 packets will be transmitted.
*
*/
#include "nt.h"
#include <atomic>
#include <iostream>
#include <thread>
#include <unistd.h>
#include <signal.h>
// Intentional global
std::atomic<bool> running {true};
// The struct below represents the TX dynamic descriptor
// used by this application
//
// The struct is based on a copy of Dynamic descriptor 3
// It is modified with the location of the command bits
// for controlling L3/L4 checksum handling, time stamp
// inject handling and FCS handling.
//
// This example uses 12 bits of the color_hi field
// for these command bits. In other words - the
// normal color_hi:28 has been replaced by the
// 12 command bits and a color_hi_unused:16
//
// note: This example doesn't use the L3/L4 checksum
// command bits. See examples/net/checksum for an
// example of L3/L4 checksum control.
uint64_t capLength:14;
uint64_t wireLength:14;
uint64_t color_lo:14;
uint64_t rxPort:6;
uint64_t descrFormat:8;
uint64_t descrLength:6;
uint64_t tsColor:1;
uint64_t ntDynDescr:1;
uint64_t timestamp;
// Location of control bits in the packet descriptor:
uint64_t frame_type:4; // Frame type: 4 bits starting at bit offset 128
uint64_t checksum_cmd:5; // L3/L4 check sum command, 5 bits starting at bit offset 132
uint64_t tsiCmd:1; // Timestamp inject command, 1 bits starting at bit offset 137
uint64_t fcsCmd:2; // FCS command, 1 bits starting at bit offset 138
uint64_t color_hi_unused:16; // The remaing 16 unused bits of color_hi
uint64_t offset0:10;
uint64_t offset1:10;
};
{
switch(descrType)
{
printf("Descriptor type is PCAP.\n");
break;
printf("Descriptor type is NT.\n");
break;
printf("Descriptor type is NT extended.\n");
break;
printf("Descriptor type is Dynamic\n");
break;
default:
printf("Unknown descriptor type.\n");
break;
}
}
/*
The packet reader thread is responsible for
reading packets from a single RX stream
*/
{
public:
uint64_t rxPort;
uint32_t streamId;
std::thread thr;
PacketReaderThread(uint64_t port) : rxPort(port) {
streamId = (uint32_t) port;
printf("Created PacketReaderThread, port=%lu, stream=%u\n", rxPort, streamId);
}
void processPackets(void) {
NtNetStreamRx_t hNetRx = NULL; // Handle to the RX stream
NtNetBuf_t hNetBuf; // Net buffer container.
int status;
status = NT_NetRxOpen(&hNetRx, "PacketReader", NT_NET_INTERFACE_PACKET, streamId, -1);
if (status != 0) {
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
printf("NT_NetRxOpen() failed: %s\n", errorBuffer);
running = false;
return;
}
printf("Receiving packets on stream %u\n", streamId);
while(running)
{
if ((status = NT_NetRxGetNextPacket(hNetRx, &hNetBuf, 100)) == NT_SUCCESS)
{
printf("-------------------------------------------------\n");
printf("Packet received:\n");
NtPacketDescriptorType_e descrType = NT_NET_GET_PKT_DESCRIPTOR_TYPE(hNetBuf);
printDescriptor(descrType);
if (descrType != NT_PACKET_DESCRIPTOR_TYPE_NT)
{
printf("Error: RX descriptor expected to be NT_PACKET_DESCRIPTOR_TYPE_NT\n");
printf(" Shutting down...\n");
running = false;
break;
}
uint64_t captureLength = NT_NET_GET_PKT_CAP_LENGTH(hNetBuf);
if (captureLength < (16 + 80 + 2 + 8)) // Descriptor + Timestamp Inject Static Offset + 2 byte compensation + 8 byte Time stamp
{
printf("Packet is too small. Can't extract time stamp\n");
}
else
{
unsigned char * data = (unsigned char *) NT_NET_GET_PKT_L2_PTR(hNetBuf);
uint64_t txTime = *((uint64_t*)(data + 80 + 2));
if (txTime == 0)
{
printf("No TX time stamp in packet.\n");
}
else
{
uint64_t rxTime = NT_NET_GET_PKT_TIMESTAMP(hNetBuf);
printf("txTime=%lu, rxTime=%lu, latency=%lu\n", txTime, rxTime, rxTime - txTime);
}
// Dump packet
uint64_t wireLength = NT_NET_GET_PKT_WIRE_LENGTH(hNetBuf);
for (uint64_t g = 0; g < wireLength; ++g)
{
if ((g % 16) == 0) printf("\n");
printf("%02X ", *(data + g));
}
printf("\n");
}
}
else if ((status != NT_STATUS_TIMEOUT) && (status != NT_STATUS_TRYAGAIN))
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
printf("NT_NetRxGet() failed: %s\n", errorBuffer);
running = false;
break;
}
}
NT_NetRxClose(hNetRx);
}
void run(void) {
thr = std::thread(&PacketReaderThread::processPackets, this);
}
void join(void) {
if (thr.joinable())
{
thr.join();
}
}
};
// Packet template: Ethernet, IPv4, TCP
const unsigned char examplePacket[] =
{
0x94, 0xC6, 0x91, 0x1C, 0x68, 0x1D, 0x94, 0xC6, 0x91, 0x1C, 0x68, 0xC3, 0x08, 0x00, 0x45, 0x00,
0x00, 0x78, 0x00, 0x01, 0x00, 0x00, 0x40, 0x06, 0xB5, 0xCF, 0xC0, 0xA8, 0x00, 0x02, 0x01, 0x02,
0x03, 0x04, 0x00, 0x14, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x02,
0x20, 0x00, 0x42, 0xF7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
/*
The packet transmitter thread is responsible for
transmitting on a single port.
The packets will have different combinations
of time stamp inject and FCS calculation
via command bits in the TX packet decriptor
*/
{
public:
uint64_t transmittedPackets {0};
uint32_t txPort;
int64_t txLoops;
std::thread thr;
// Time stamp inject command bit values
const unsigned char NO_TS_INJECT = 0;
const unsigned char TS_INJECT = 1;
// FCS command bits values
const unsigned char FCS_GOOD = 0;
const unsigned char FCS_BAD = 1;
const unsigned char FCS_RESERVED = 2;
const unsigned char FCS_UNCHANGED = 3;
PacketTransmitterThread(uint64_t port, int64_t loops) : txPort((uint32_t)port), txLoops(loops) {
printf("Created PacketTransmitterThread port=%u\n", txPort);
}
void run(void) {
thr = std::thread(&PacketTransmitterThread::transmitPackets, this);
}
void join(void) {
if (thr.joinable())
{
thr.join();
}
}
private:
void txPacket(NtNetStreamTx_t hNetTx, unsigned char tsi, unsigned char fcs) {
uint32_t sleep_time = 1000000;
NtNetBuf_t hNetBufTx; // Net buffer container. Used when getting a transmit buffer
int status;
const unsigned char * pData = examplePacket;
size_t pktSize = sizeof examplePacket;
if ((status = NT_NetTxGet(hNetTx, &hNetBufTx, txPort, pktSize, NT_NETTX_PACKET_OPTION_DYN, 100)) != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxGet() failed: %s\n", errorBuffer);
running = false;
return;
}
//
memcpy(NT_NET_GET_PKT_L2_PTR(hNetBufTx), pData, pktSize);
auto packet_ptr = NT_NET_GET_PKT_DESCR_PTR_DYN3(hNetBufTx);
auto overlay_ptr = reinterpret_cast<Dyn3_tx_descriptor*>(packet_ptr);
packet_ptr->offset0 = 14; // Point to start of Layer 3, i.e. set to size of Ethernet header
packet_ptr->offset1 = 34; // Point to start of Layer 4, i.e. set to size of Ethernet header + IPv4 header with no options
overlay_ptr->frame_type = 0x4; // IPv4 packet carrying TCP
overlay_ptr->checksum_cmd = 0; // L3/L4 checksum_command, set to "do nothing"
overlay_ptr->tsiCmd = tsi & 1; // Set the time stamp inject command bits
overlay_ptr->fcsCmd = fcs & 3; // Set the FCS command bits
// Release the TX buffer and the packet will be transmitted
if((status = NT_NetTxRelease(hNetTx, hNetBufTx)) != NT_SUCCESS)
{
// Get the status code as text
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxRelease() failed: %s\n", errorBuffer);
running = false;
return;
}
++transmittedPackets;
if (sleep_time)
{
usleep(sleep_time);
}
}
void transmitPackets(void) {
int status;
NtNetStreamTx_t hNetTx; // Handle to the TX stream
printf("============================================\n");
printf("Transmitter configuration\n");
printf("============================================\n");
printf("TX port : %u\n", txPort);
printf("============================================\n");
NtNetTxAttr_t txAttr;
NT_NetTxOpenAttrSetName(&txAttr, "Time stamp Inject and FCS example");
NT_NetTxOpenAttrSetPortMask(&txAttr, (uint64_t)(0x1 << txPort));
// Set the position of the various descriptor command
// in accordance with struct Dyn3_tx_descriptor
status = NT_NetTxOpenAttrSetDescriptorPosFrameType(&txAttr, true, 128);
if (status != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxOpenAttrSetDescriptorPosFrameType failed: %s\n", errorBuffer);
exit(1);
}
status = NT_NetTxOpenAttrSetDescriptorPosChecksumCmd(&txAttr, true, 132);
if (status != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxOpenAttrSetDescriptorPosChecksumCmd failed: %s\n", errorBuffer);
exit(1);
}
if (status != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxOpenAttrSetTxtDescriptorPosTimestampInjectCmd failed: %s\n", errorBuffer);
exit(1);
}
status = NT_NetTxOpenAttrSetTxtDescriptorPosFcs(&txAttr, true, 138);
if (status != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxOpenAttrSetTxtDescriptorPosFcs failed: %s\n", errorBuffer);
exit(1);
}
// Open the TX network stream
status = NT_NetTxOpen_Attr(&hNetTx, &txAttr);
if (status != NT_SUCCESS) {
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxOpen() failed: %s\n", errorBuffer);
return;
}
// Main TX loop
while(running && txLoops != 0)
{
if (txLoops > 0) --txLoops;
// Transmit 6 packets
txPacket(hNetTx, NO_TS_INJECT, FCS_UNCHANGED);
txPacket(hNetTx, NO_TS_INJECT, FCS_GOOD);
txPacket(hNetTx, NO_TS_INJECT, FCS_BAD);
txPacket(hNetTx, TS_INJECT, FCS_UNCHANGED);
txPacket(hNetTx, TS_INJECT, FCS_GOOD);
txPacket(hNetTx, TS_INJECT, FCS_BAD);
}
printf("Packets transmitted %lu\n ", transmittedPackets);
printf("Shutting down TX\n");
if((status = NT_NetTxClose(hNetTx)) != NT_SUCCESS)
{
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_NetTxClose() failed: %s\n", errorBuffer);
return;
}
running = false;
}
};
static void sighandler(int /*sig*/)
{
running = false;
}
void printError(int status, const char* formatstr)
{
// Retrieve the textual description of the status code
NT_ExplainError(status, errorBuffer, (uint32_t)(sizeof(errorBuffer)-1));
// Log to stderr
fprintf(stderr, formatstr, errorBuffer);
}
// Setup RX filter
void applyNtpl(uint64_t rxPort)
{
NtConfigStream_t hCfgStream;
char ntplStr[400];
int status;
snprintf(ntplStr, 400, "assign[streamid=%lu] = Port==%lu", rxPort, rxPort);
// Open a config stream to assign a filter to a stream ID.
if ((status = NT_ConfigOpen(&hCfgStream, "flowtest")) != NT_SUCCESS) {
printError(status, "NT_ConfigOpen() failed: %s\n");
exit(1);
}
printf("\nDoing NTPL:\n");
NtNtplInfo_t ntplInfo;
printf("%s\n\n", ntplStr);
if ((status = NT_NTPL(hCfgStream, ntplStr, &ntplInfo, NT_NTPL_PARSER_VALIDATE_NORMAL)) != NT_SUCCESS) {
printError(status, "NT_NTPL() failed: %s\n");
exit(1);
}
NT_ConfigClose(hCfgStream);
}
bool featuresAvailable(uint64_t txPort)
{
int status;
NtInfo_t infoRead;
// Open the infostream.
if((status = NT_InfoOpen(&hInfo, "tsi")) != NT_SUCCESS) {
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_InfoOpen() failed: %s\n", errorBuffer);
return false;
}
// Check whether or not Time stamp inject is supported on the defined port
infoRead.u.port_v9.portNo = 0xFF & txPort;
if((status = NT_InfoRead(hInfo, &infoRead)) != NT_SUCCESS) {
NT_ExplainError(status, errorBuffer, sizeof(errorBuffer));
fprintf(stderr, "NT_InfoRead() failed: %s\n", errorBuffer);
NT_InfoClose(hInfo);
return false;
}
NT_InfoClose(hInfo);
if (
)
{
return false;
}
return true;
}
static void usage(const char *argv0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [options]\n", argv0);
fprintf(stderr, "\n");
fprintf(stderr, "This program is an example of how to control TX time stamp inject\n");
fprintf(stderr, "and FCS generation per packet using dynamic descriptor 3.\n");
fprintf(stderr, "\n");
fprintf(stderr, " Options:\n"
" -r <rx port> : RX port.\n"
" -t <tx port> : TX port.\n"
" -n <tx loops> : Number of TX loops\n"
" -h : Print help message\n");
fprintf(stderr, "\n");
}
int main(int argc, char **argv)
{
int opt;
int64_t txLoops = -1;
uint64_t txPort = 0;
uint64_t rxPort = 0;
uint64_t txActive = false;
uint64_t rxActive = false;
PacketReaderThread* rxThread = nullptr;
PacketTransmitterThread* txThread = nullptr;
char *endptr;
while ((opt = getopt(argc, argv, "n:t:r:")) != -1) {
switch (opt) {
case 'r':
rxPort = strtoul(optarg, &endptr, 10);
rxActive = true;
if (*optarg == '\0' || *endptr != '\0') {
fprintf(stderr, "Could not parse RX port, i.e. -r option\n");
exit(1);
}
break;
case 't':
txPort = strtoul(optarg, &endptr, 10);
txActive = true;
if (*optarg == '\0' || *endptr != '\0') {
fprintf(stderr, "Could not parse TX port, i.e. -t option\n");
exit(1);
}
break;
case 'n':
txLoops = strtol(optarg, &endptr, 10);
if (*optarg == '\0' || *endptr != '\0') {
fprintf(stderr, "Could not parse number of TX loops, , i.e. -n option\n");
exit(1);
}
break;
case 'h':
usage(argv[0]);
exit(0);
break;
default:
usage(argv[0]);
exit(1);
}
}
signal(SIGINT, sighandler);
if (!featuresAvailable(txPort))
{
printf("\nRequired Test&Measurement features not supported on port %lu\n", txPort);
return 0;
}
if (txActive)
{
txThread = new PacketTransmitterThread(txPort, txLoops);
}
if (rxActive)
{
applyNtpl(rxPort);
rxThread = new PacketReaderThread(rxPort);
}
if (rxThread == nullptr && txThread == nullptr)
{
printf("\nUse at least one of the options -t, -r\n");
return 0;
}
if (rxThread != nullptr)
{
rxThread->run();
}
usleep(1000000);
if (txThread != nullptr)
{
txThread->run();
}
printf("Threads started. Press CTRL-C to terminate program.\n");
while(running)
{
usleep(1000);
};
if (txThread != nullptr)
{
txThread->join();
if (rxThread != nullptr && running)
{
printf("TX thread stopped. Use CTRL-C to terminate RX thread\n");
}
}
if (rxThread != nullptr)
{
rxThread->join();
}
return 0;
}