2021-04-07 15:25:12 -04:00
///////////////////////////////////////////////////////////////////////////////////
2023-11-18 07:12:18 -05:00
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Copyright (C) 2022 Jiří Pinkava <jiri.pinkava@rossum.ai> //
2021-04-07 15:25:12 -04:00
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation as version 3 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License V3 for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
# include <algorithm>
# include <random>
# include <QDebug>
# include <QTcpServer>
# include <QTcpSocket>
# include <QNetworkDatagram>
# include <QEventLoop>
# include <QTimer>
2024-10-21 12:08:41 -04:00
# include <QRegularExpression>
2021-04-07 15:25:12 -04:00
# include "util/ax25.h"
# include "pertester.h"
# include "pertesterworker.h"
# include "pertesterreport.h"
MESSAGE_CLASS_DEFINITION ( PERTesterWorker : : MsgConfigurePERTesterWorker , Message )
MESSAGE_CLASS_DEFINITION ( PERTesterReport : : MsgReportStats , Message )
PERTesterWorker : : PERTesterWorker ( ) :
m_msgQueueToFeature ( nullptr ) ,
m_msgQueueToGUI ( nullptr ) ,
m_rxUDPSocket ( nullptr ) ,
2021-11-25 04:28:59 -05:00
m_txUDPSocket ( this ) ,
m_txTimer ( this ) ,
2021-04-07 15:25:12 -04:00
m_tx ( 0 ) ,
m_rxMatched ( 0 ) ,
m_rxUnmatched ( 0 )
{
connect ( & m_inputMessageQueue , SIGNAL ( messageEnqueued ( ) ) , this , SLOT ( handleInputMessages ( ) ) ) ;
}
PERTesterWorker : : ~ PERTesterWorker ( )
{
2022-09-20 16:13:12 -04:00
stopWork ( ) ;
2021-04-07 15:25:12 -04:00
closeUDP ( ) ;
disconnect ( & m_inputMessageQueue , SIGNAL ( messageEnqueued ( ) ) , this , SLOT ( handleInputMessages ( ) ) ) ;
m_inputMessageQueue . clear ( ) ;
}
2022-09-20 06:48:25 -04:00
void PERTesterWorker : : startWork ( )
2021-04-07 15:25:12 -04:00
{
qDebug ( ) < < " PERTesterWorker::startWork " ;
QMutexLocker mutexLocker ( & m_mutex ) ;
openUDP ( m_settings ) ;
// Automatically restart if previous run had finished, otherwise continue
if ( m_tx > = m_settings . m_packetCount )
resetStats ( ) ;
connect ( & m_txTimer , SIGNAL ( timeout ( ) ) , this , SLOT ( tx ( ) ) ) ;
m_txTimer . start ( m_settings . m_interval * 1000.0 ) ;
2022-09-20 06:48:25 -04:00
// Handle any messages already on the queue
handleInputMessages ( ) ;
2021-04-07 15:25:12 -04:00
}
void PERTesterWorker : : stopWork ( )
{
2021-11-25 04:28:59 -05:00
qDebug ( ) < < " PERTesterWorker::stopWork " ;
2021-04-07 15:25:12 -04:00
m_txTimer . stop ( ) ;
closeUDP ( ) ;
disconnect ( & m_txTimer , SIGNAL ( timeout ( ) ) , this , SLOT ( tx ( ) ) ) ;
}
void PERTesterWorker : : handleInputMessages ( )
{
Message * message ;
while ( ( message = m_inputMessageQueue . pop ( ) ) ! = nullptr )
{
if ( handleMessage ( * message ) ) {
delete message ;
}
}
}
void PERTesterWorker : : resetStats ( )
{
m_tx = 0 ;
m_rxMatched = 0 ;
m_rxUnmatched = 0 ;
if ( getMessageQueueToGUI ( ) )
getMessageQueueToGUI ( ) - > push ( PERTesterReport : : MsgReportStats : : create ( m_tx , m_rxMatched , m_rxUnmatched ) ) ;
}
bool PERTesterWorker : : handleMessage ( const Message & cmd )
{
if ( MsgConfigurePERTesterWorker : : match ( cmd ) )
{
QMutexLocker mutexLocker ( & m_mutex ) ;
MsgConfigurePERTesterWorker & cfg = ( MsgConfigurePERTesterWorker & ) cmd ;
2022-11-26 06:35:05 -05:00
applySettings ( cfg . getSettings ( ) , cfg . getSettingsKeys ( ) , cfg . getForce ( ) ) ;
2021-04-07 15:25:12 -04:00
return true ;
}
else if ( PERTester : : MsgResetStats : : match ( cmd ) )
{
resetStats ( ) ;
return true ;
}
else
{
return false ;
}
}
2022-11-26 06:35:05 -05:00
void PERTesterWorker : : applySettings ( const PERTesterSettings & settings , const QList < QString > & settingsKeys , bool force )
2021-04-07 15:25:12 -04:00
{
2022-11-26 06:35:05 -05:00
qDebug ( ) < < " PERTesterWorker::applySettings: " < < settings . getDebugString ( settingsKeys , force )
2021-04-07 15:25:12 -04:00
< < " force: " < < force ;
2022-11-26 06:35:05 -05:00
// if ( (settings.m_rxUDPAddress != m_settings.m_rxUDPAddress)
// || (settings.m_rxUDPPort != m_settings.m_rxUDPPort)
// || force)
if ( settingsKeys . contains ( " rxUDPAddress " )
| | settingsKeys . contains ( " rxUDPPort " )
| | force )
2021-04-07 15:25:12 -04:00
{
openUDP ( settings ) ;
}
2022-11-26 06:35:05 -05:00
if ( settingsKeys . contains ( " interval " ) | | force ) {
2021-04-07 15:25:12 -04:00
m_txTimer . setInterval ( settings . m_interval * 1000.0 ) ;
2022-11-26 06:35:05 -05:00
}
2021-04-07 15:25:12 -04:00
2022-11-26 06:35:05 -05:00
if ( force ) {
m_settings = settings ;
} else {
m_settings . applySettings ( settingsKeys , settings ) ;
}
2021-04-07 15:25:12 -04:00
}
void PERTesterWorker : : openUDP ( const PERTesterSettings & settings )
{
closeUDP ( ) ;
m_rxUDPSocket = new QUdpSocket ( ) ;
if ( ! m_rxUDPSocket - > bind ( QHostAddress ( settings . m_rxUDPAddress ) , settings . m_rxUDPPort ) )
{
qCritical ( ) < < " PERTesterWorker::openUDP: Failed to bind to port " < < settings . m_rxUDPAddress < < " : " < < settings . m_rxUDPPort < < " . Error: " < < m_rxUDPSocket - > error ( ) ;
if ( m_msgQueueToFeature )
m_msgQueueToFeature - > push ( PERTester : : MsgReportWorker : : create ( QString ( " Failed to bind to port %1:%2 - %3 " ) . arg ( settings . m_rxUDPAddress ) . arg ( settings . m_rxUDPPort ) . arg ( m_rxUDPSocket - > error ( ) ) ) ) ;
}
else
qDebug ( ) < < " PERTesterWorker::openUDP: Listening on port " < < settings . m_rxUDPAddress < < " : " < < settings . m_rxUDPPort < < " . " ;
connect ( m_rxUDPSocket , & QUdpSocket : : readyRead , this , & PERTesterWorker : : rx ) ;
}
void PERTesterWorker : : closeUDP ( )
{
if ( m_rxUDPSocket ! = nullptr )
{
qDebug ( ) < < " PERTesterWorker::closeUDP: Closing port " < < m_settings . m_rxUDPAddress < < " : " < < m_settings . m_rxUDPPort < < " . " ;
disconnect ( m_rxUDPSocket , & QUdpSocket : : readyRead , this , & PERTesterWorker : : rx ) ;
delete m_rxUDPSocket ;
m_rxUDPSocket = nullptr ;
}
}
void PERTesterWorker : : rx ( )
{
while ( m_rxUDPSocket - > hasPendingDatagrams ( ) )
{
QNetworkDatagram datagram = m_rxUDPSocket - > receiveDatagram ( ) ;
QByteArray packet = datagram . data ( ) ;
// Ignore header and CRC, if requested
packet = packet . mid ( m_settings . m_ignoreLeadingBytes , packet . size ( ) - m_settings . m_ignoreLeadingBytes - m_settings . m_ignoreTrailingBytes ) ;
// Remove from list of transmitted packets
int i ;
for ( i = 0 ; i < m_txPackets . size ( ) ; i + + )
{
if ( packet = = m_txPackets [ i ] )
{
m_rxMatched + + ;
m_txPackets . removeAt ( i ) ;
break ;
}
}
if ( i = = m_txPackets . size ( ) )
{
qDebug ( ) < < " PERTesterWorker::rx: Received packet that was not transmitted: " < < packet . toHex ( ) ;
m_rxUnmatched + + ;
}
}
if ( getMessageQueueToGUI ( ) )
getMessageQueueToGUI ( ) - > push ( PERTesterReport : : MsgReportStats : : create ( m_tx , m_rxMatched , m_rxUnmatched ) ) ;
}
void PERTesterWorker : : tx ( )
{
2024-10-21 12:08:41 -04:00
QRegularExpression ax25Dst ( " ^% \\ {ax25 \\ .dst=([A-Za-z0-9-]+) \ \ } " ) ;
QRegularExpression ax25Src ( " ^% \\ {ax25 \\ .src=([A-Za-z0-9-]+) \ \ } " ) ;
QRegularExpression num ( " ^% \\ {num \\ } " ) ;
QRegularExpression data ( " ^% \\ {data=([0-9]+) , ( [ 0 - 9 ] + ) \ \ } " ) ;
QRegularExpression hex ( " ^(0x) ? ( [ 0 - 9 a - fA - F ] ? [ 0 - 9 a - fA - F ] ) " ) ;
QRegularExpressionMatch match ;
2021-04-07 15:25:12 -04:00
QByteArray bytes ;
int pos = 0 ;
while ( pos < m_settings . m_packet . size ( ) )
{
if ( m_settings . m_packet [ pos ] = = ' % ' )
{
2024-10-21 12:08:41 -04:00
if ( ( match = ax25Dst . match ( m_settings . m_packet , pos , QRegularExpression : : NormalMatch , QRegularExpression : : AnchoredMatchOption ) ) . hasMatch ( ) )
2021-04-07 15:25:12 -04:00
{
// AX.25 destination callsign & SSID
2024-10-21 12:08:41 -04:00
QString address = match . captured ( 1 ) ;
2021-04-07 15:25:12 -04:00
bytes . append ( AX25Packet : : encodeAddress ( address ) ) ;
2024-10-21 12:08:41 -04:00
pos + = match . capturedLength ( ) ;
2021-04-07 15:25:12 -04:00
}
2024-10-21 12:08:41 -04:00
else if ( ( match = ax25Src . match ( m_settings . m_packet , pos , QRegularExpression : : NormalMatch , QRegularExpression : : AnchoredMatchOption ) ) . hasMatch ( ) )
2021-04-07 15:25:12 -04:00
{
// AX.25 source callsign & SSID
2024-10-21 12:08:41 -04:00
QString address = match . captured ( 1 ) ;
2021-04-07 15:25:12 -04:00
bytes . append ( AX25Packet : : encodeAddress ( address , 1 ) ) ;
2024-10-21 12:08:41 -04:00
pos + = match . capturedLength ( ) ;
2021-04-07 15:25:12 -04:00
}
2024-10-21 12:08:41 -04:00
else if ( ( match = num . match ( m_settings . m_packet , pos , QRegularExpression : : NormalMatch , QRegularExpression : : AnchoredMatchOption ) ) . hasMatch ( ) )
2021-04-07 15:25:12 -04:00
{
// Big endian packet number
bytes . append ( ( m_tx > > 24 ) & 0xff ) ;
bytes . append ( ( m_tx > > 16 ) & 0xff ) ;
bytes . append ( ( m_tx > > 8 ) & 0xff ) ;
bytes . append ( m_tx & 0xff ) ;
2024-10-21 12:08:41 -04:00
pos + = match . capturedLength ( ) ;
2021-04-07 15:25:12 -04:00
}
2024-10-21 12:08:41 -04:00
else if ( ( match = data . match ( m_settings . m_packet , pos , QRegularExpression : : NormalMatch , QRegularExpression : : AnchoredMatchOption ) ) . hasMatch ( ) )
2021-04-07 15:25:12 -04:00
{
// Constrained random number of random bytes
2024-10-21 12:08:41 -04:00
int minBytes = match . captured ( 1 ) . toInt ( ) ;
int maxBytes = match . captured ( 2 ) . toInt ( ) ;
2021-04-07 15:25:12 -04:00
std : : random_device rd ;
std : : mt19937 gen ( rd ( ) ) ;
std : : uniform_int_distribution < > distr0 ( minBytes , maxBytes ) ;
std : : uniform_int_distribution < > distr1 ( 0 , 255 ) ;
int count = distr0 ( gen ) ;
for ( int i = 0 ; i < count ; i + + )
bytes . append ( distr1 ( gen ) ) ;
2024-10-21 12:08:41 -04:00
pos + = match . capturedLength ( ) ;
2021-04-07 15:25:12 -04:00
}
else
{
qWarning ( ) < < " PERTester: Unsupported substitution in packet: pos= " < < pos < < " in " < < m_settings . m_packet ;
break ;
}
}
else if ( m_settings . m_packet [ pos ] = = ' \" ' )
{
// ASCII string in double quotes
int startIdx = pos + 1 ;
int endQuoteIdx = m_settings . m_packet . indexOf ( ' \" ' , startIdx ) ;
if ( endQuoteIdx ! = - 1 )
{
int len = endQuoteIdx - startIdx ;
QString string = m_settings . m_packet . mid ( startIdx , len ) ;
bytes . append ( string . toLocal8Bit ( ) ) ;
pos = endQuoteIdx + 1 ;
}
else
{
qWarning ( ) < < " PERTester: Unterminated string: pos= " < < pos < < " in " < < m_settings . m_packet ;
break ;
}
}
2024-10-21 12:08:41 -04:00
else if ( ( match = hex . match ( m_settings . m_packet , pos , QRegularExpression : : NormalMatch , QRegularExpression : : AnchoredMatchOption ) ) . hasMatch ( ) )
2021-04-07 15:25:12 -04:00
{
// Hex byte
2024-10-21 12:08:41 -04:00
int value = match . captured ( 2 ) . toInt ( nullptr , 16 ) ;
2021-04-07 15:25:12 -04:00
bytes . append ( value ) ;
2024-10-21 12:08:41 -04:00
pos + = match . capturedLength ( ) ;
2021-04-07 15:25:12 -04:00
}
else if ( ( m_settings . m_packet [ pos ] = = ' ' ) | | ( m_settings . m_packet [ pos ] = = ' , ' ) | | ( m_settings . m_packet [ pos ] = = ' : ' ) )
{
pos + + ;
}
else
{
qWarning ( ) < < " PERTester: Unexpected character in packet: pos= " < < pos < < " in " < < m_settings . m_packet ;
break ;
}
}
if ( ( pos > = m_settings . m_packet . size ( ) ) & & ( m_tx < m_settings . m_packetCount ) )
{
// Send packet via UDP
m_txUDPSocket . writeDatagram ( bytes . data ( ) , bytes . size ( ) , QHostAddress ( m_settings . m_txUDPAddress ) , m_settings . m_txUDPPort ) ;
m_tx + + ;
m_txPackets . append ( bytes ) ;
if ( getMessageQueueToGUI ( ) )
getMessageQueueToGUI ( ) - > push ( PERTesterReport : : MsgReportStats : : create ( m_tx , m_rxMatched , m_rxUnmatched ) ) ;
}
if ( m_tx > = m_settings . m_packetCount )
{
// Test complete
m_txTimer . stop ( ) ;
// Wait for a couple of seconds for the last packet to be received
QTimer : : singleShot ( 2000 , this , & PERTesterWorker : : testComplete ) ;
}
}
void PERTesterWorker : : testComplete ( )
{
if ( m_msgQueueToFeature ! = nullptr )
m_msgQueueToFeature - > push ( PERTester : : MsgReportWorker : : create ( " Complete " ) ) ;
qDebug ( ) < < " PERTesterWorker::tx: Test complete " ;
}