diff --git a/audioconverter.cpp b/audioconverter.cpp
index 4180741..50723db 100644
--- a/audioconverter.cpp
+++ b/audioconverter.cpp
@@ -83,8 +83,8 @@ audioConverter::~audioConverter()
bool audioConverter::convert(audioPacket audio)
{
- // If inFormat and outFormat are identical, just emit the data back.
- if (audio.data.size() > 0 && inFormat != outFormat)
+ // If inFormat and outFormat are identical, just emit the data back (removed as it doesn't then process amplitude)
+ if (audio.data.size() > 0)
{
if (inFormat.codec() == "audio/opus")
diff --git a/audiohandler.cpp b/audiohandler.cpp
index 52326dd..51d015d 100644
--- a/audiohandler.cpp
+++ b/audiohandler.cpp
@@ -35,9 +35,7 @@ audioHandler::~audioHandler()
converterThread->quit();
converterThread->wait();
}
-}
-
-bool audioHandler::init(audioSetup setup)
+}bool audioHandler::init(audioSetup setup)
{
if (isInitialized) {
return false;
@@ -80,14 +78,37 @@ bool audioHandler::init(audioSetup setup)
return false;
}
- /* if (outFormat.channelCount() == 1 && inFormat.channelCount() == 2) {
+ if (outFormat.channelCount() == 1 && inFormat.channelCount() == 2) {
outFormat.setChannelCount(2);
if (!setup.port.isFormatSupported(outFormat)) {
- qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request stereo input!";
+ qInfo(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request stereo reverting to mono";
outFormat.setChannelCount(1);
}
}
+ if (outFormat.sampleRate() < 48000) {
+ int tempRate=outFormat.sampleRate();
+ outFormat.setSampleRate(48000);
+ if (!setup.port.isFormatSupported(outFormat)) {
+ qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request 48K, reverting to "<< tempRate;
+ outFormat.setSampleRate(tempRate);
+ }
+ }
+
+ if (outFormat.sampleType() == QAudioFormat::UnSignedInt && outFormat.sampleSize()==8) {
+ outFormat.setSampleType(QAudioFormat::SignedInt);
+ outFormat.setSampleSize(16);
+
+ if (!setup.port.isFormatSupported(outFormat)) {
+ qCritical(logAudio()) << (setup.isinput ? "Input" : "Output") << "Cannot request 16bit Signed samples, reverting to 8bit Unsigned";
+ outFormat.setSampleType(QAudioFormat::UnSignedInt);
+ outFormat.setSampleSize(8);
+ }
+ }
+
+
+ /*
+
if (outFormat.sampleType()==QAudioFormat::SignedInt) {
outFormat.setSampleType(QAudioFormat::Float);
outFormat.setSampleSize(32);
@@ -232,7 +253,12 @@ void audioHandler::convertedOutput(audioPacket packet) {
currentLatency = packet.time.msecsTo(QTime::currentTime()) + (outFormat.durationForBytes(audioOutput->bufferSize() - audioOutput->bytesFree()) / 1000);
if (audioDevice != Q_NULLPTR) {
- audioDevice->write(packet.data);
+ if (audioDevice->write(packet.data) < packet.data.size()) {
+ qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Buffer full!";
+ isOverrun=true;
+ } else {
+ isOverrun = false;
+ }
if (lastReceived.msecsTo(QTime::currentTime()) > 100) {
qDebug(logAudio()) << (setup.isinput ? "Input" : "Output") << "Time since last audio packet" << lastReceived.msecsTo(QTime::currentTime()) << "Expected around" << setup.blockSize;
}
@@ -245,7 +271,7 @@ void audioHandler::convertedOutput(audioPacket packet) {
}
*/
lastSentSeq = packet.seq;
- emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun);
+ emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun);
amplitude = packet.amplitude;
}
@@ -288,7 +314,7 @@ void audioHandler::convertedInput(audioPacket audio)
}
lastReceived = QTime::currentTime();
amplitude = audio.amplitude;
- emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun);
+ emit haveLevels(getAmplitude(), setup.latency, currentLatency, isUnderrun, isOverrun);
}
}
diff --git a/audiohandler.h b/audiohandler.h
index f9ec2a1..8bc8103 100644
--- a/audiohandler.h
+++ b/audiohandler.h
@@ -105,7 +105,7 @@ signals:
void audioMessage(QString message);
void sendLatency(quint16 newSize);
void haveAudioData(const audioPacket& data);
- void haveLevels(quint16 amplitude,quint16 latency,quint16 current,bool under);
+ void haveLevels(quint16 amplitude,quint16 latency,quint16 current,bool under,bool over);
void setupConverter(QAudioFormat in, QAudioFormat out, quint8 opus, quint8 resamp);
void sendToConverter(audioPacket audio);
@@ -117,6 +117,7 @@ private:
bool isUnderrun = false;
+ bool isOverrun = true;
bool isInitialized=false;
bool isReady = false;
bool audioBuffered = false;
diff --git a/udphandler.cpp b/udphandler.cpp
index 384c517..38646e2 100644
--- a/udphandler.cpp
+++ b/udphandler.cpp
@@ -143,18 +143,20 @@ void udpHandler::receiveDataFromUserToRig(QByteArray data)
}
}
-void udpHandler::getRxLevels(quint16 amplitude,quint16 latency,quint16 current, bool under) {
+void udpHandler::getRxLevels(quint16 amplitude,quint16 latency,quint16 current, bool under, bool over) {
status.rxAudioLevel = amplitude;
status.rxLatency = latency;
status.rxCurrentLatency = current;
status.rxUnderrun = under;
+ status.rxOverrun = over;
}
-void udpHandler::getTxLevels(quint16 amplitude,quint16 latency, quint16 current, bool under) {
+void udpHandler::getTxLevels(quint16 amplitude,quint16 latency, quint16 current, bool under, bool over) {
status.txAudioLevel = amplitude;
status.txLatency = latency;
status.txCurrentLatency = current;
status.txUnderrun = under;
+ status.txOverrun = over;
}
void udpHandler::dataReceived()
@@ -208,13 +210,19 @@ void udpHandler::dataReceived()
}
QString tempLatency;
- if (status.rxCurrentLatency < status.rxLatency*1.2 && !status.rxUnderrun)
+ if (status.rxCurrentLatency <= status.rxLatency && !status.rxUnderrun && !status.rxOverrun)
{
tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3);
}
- else {
+ else if (status.rxUnderrun){
tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3);
}
+ else if (status.rxOverrun){
+ tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3);
+ } else
+ {
+ tempLatency = QString("%1 ms").arg(status.rxCurrentLatency,3);
+ }
QString txString="";
if (txSetup.codec == 0) {
txString = "(no tx)";
@@ -306,8 +314,8 @@ void udpHandler::dataReceived()
QObject::connect(audio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket)));
QObject::connect(this, SIGNAL(haveChangeLatency(quint16)), audio, SLOT(changeLatency(quint16)));
QObject::connect(this, SIGNAL(haveSetVolume(unsigned char)), audio, SLOT(setVolume(unsigned char)));
- QObject::connect(audio, SIGNAL(haveRxLevels(quint16, quint16, quint16,bool)), this, SLOT(getRxLevels(quint16, quint16,quint16,bool)));
- QObject::connect(audio, SIGNAL(haveTxLevels(quint16, quint16,quint16,bool)), this, SLOT(getTxLevels(quint16, quint16,quint16,bool)));
+ QObject::connect(audio, SIGNAL(haveRxLevels(quint16, quint16, quint16,bool,bool)), this, SLOT(getRxLevels(quint16, quint16,quint16,bool,bool)));
+ QObject::connect(audio, SIGNAL(haveTxLevels(quint16, quint16,quint16,bool,bool)), this, SLOT(getTxLevels(quint16, quint16,quint16,bool,bool)));
streamOpened = true;
}
@@ -637,3 +645,1112 @@ void udpHandler::sendToken(uint8_t magic)
return;
}
+
+// Class that manages all Civ Data to/from the rig
+udpCivData::udpCivData(QHostAddress local, QHostAddress ip, quint16 civPort, bool splitWf, quint16 localPort=0 )
+{
+ qInfo(logUdp()) << "Starting udpCivData";
+ localIP = local;
+ port = civPort;
+ radioIP = ip;
+ splitWaterfall = splitWf;
+
+ udpBase::init(localPort); // Perform connection
+
+ QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpCivData::dataReceived);
+
+ sendControl(false, 0x03, 0x00); // First connect packet
+
+ /*
+ Connect various timers
+ */
+ pingTimer = new QTimer();
+ idleTimer = new QTimer();
+ areYouThereTimer = new QTimer();
+ startCivDataTimer = new QTimer();
+ watchdogTimer = new QTimer();
+
+ connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing);
+ connect(watchdogTimer, &QTimer::timeout, this, &udpCivData::watchdog);
+ connect(idleTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, true, 0, 0));
+ connect(startCivDataTimer, &QTimer::timeout, this, std::bind(&udpCivData::sendOpenClose, this, false));
+ connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0));
+ watchdogTimer->start(WATCHDOG_PERIOD);
+ // Start sending are you there packets - will be stopped once "I am here" received
+ // send ping packets every 100 ms (maybe change to less frequent?)
+ pingTimer->start(PING_PERIOD);
+ // Send idle packets every 100ms, this timer will be reset every time a non-idle packet is sent.
+ idleTimer->start(IDLE_PERIOD);
+ areYouThereTimer->start(AREYOUTHERE_PERIOD);
+}
+
+udpCivData::~udpCivData()
+{
+ sendOpenClose(true);
+ if (startCivDataTimer != Q_NULLPTR)
+ {
+ startCivDataTimer->stop();
+ delete startCivDataTimer;
+ startCivDataTimer = Q_NULLPTR;
+ }
+ if (pingTimer != Q_NULLPTR)
+ {
+ pingTimer->stop();
+ delete pingTimer;
+ pingTimer = Q_NULLPTR;
+ }
+ if (idleTimer != Q_NULLPTR)
+ {
+ idleTimer->stop();
+ delete idleTimer;
+ idleTimer = Q_NULLPTR;
+ }
+ if (watchdogTimer != Q_NULLPTR)
+ {
+ watchdogTimer->stop();
+ delete watchdogTimer;
+ watchdogTimer = Q_NULLPTR;
+ }
+}
+
+void udpCivData::watchdog()
+{
+ static bool alerted = false;
+ if (lastReceived.msecsTo(QTime::currentTime()) > 2000)
+ {
+ if (!alerted) {
+ qInfo(logUdp()) << " CIV Watchdog: no CIV data received for 2s, requesting data start.";
+ if (startCivDataTimer != Q_NULLPTR)
+ {
+ startCivDataTimer->start(100);
+ }
+ alerted = true;
+ }
+ }
+ else
+ {
+ alerted = false;
+ }
+}
+
+void udpCivData::send(QByteArray d)
+{
+ //qInfo(logUdp()) << "Sending: (" << d.length() << ") " << d;
+ data_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p)+d.length();
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ p.reply = (char)0xc1;
+ p.datalen = d.length();
+ p.sendseq = qToBigEndian(sendSeqB); // THIS IS BIG ENDIAN!
+
+ QByteArray t = QByteArray::fromRawData((const char*)p.packet, sizeof(p));
+ t.append(d);
+ sendTrackedPacket(t);
+ sendSeqB++;
+ return;
+}
+
+
+void udpCivData::sendOpenClose(bool close)
+{
+ uint8_t magic = 0x04;
+
+ if (close)
+ {
+ magic = 0x00;
+ }
+
+ openclose_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p);
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ p.data = 0x01c0; // Not sure what other values are available:
+ p.sendseq = qToBigEndian(sendSeqB);
+ p.magic = magic;
+
+ sendSeqB++;
+
+ sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p)));
+ return;
+}
+
+
+
+void udpCivData::dataReceived()
+{
+ while (udp->hasPendingDatagrams())
+ {
+ QNetworkDatagram datagram = udp->receiveDatagram();
+ //qInfo(logUdp()) << "Received: " << datagram.data();
+ QByteArray r = datagram.data();
+
+
+ switch (r.length())
+ {
+ case (CONTROL_SIZE): // Control packet
+ {
+ control_packet_t in = (control_packet_t)r.constData();
+ if (in->type == 0x04)
+ {
+ areYouThereTimer->stop();
+ }
+ else if (in->type == 0x06)
+ {
+ // Update remoteId
+ remoteId = in->sentid;
+ // Manually send a CIV start request and start the timer if it isn't received.
+ // The timer will be stopped as soon as valid CIV data is received.
+ sendOpenClose(false);
+ if (startCivDataTimer != Q_NULLPTR) {
+ startCivDataTimer->start(100);
+ }
+ }
+ break;
+ }
+ default:
+ {
+ if (r.length() > 21) {
+ data_packet_t in = (data_packet_t)r.constData();
+ if (in->type != 0x01) {
+ // Process this packet, any re-transmit requests will happen later.
+ //uint16_t gotSeq = qFromLittleEndian(r.mid(6, 2));
+ // We have received some Civ data so stop sending Start packets!
+ if (startCivDataTimer != Q_NULLPTR) {
+ startCivDataTimer->stop();
+ }
+ lastReceived = QTime::currentTime();
+ if (quint16(in->datalen + 0x15) == (quint16)in->len)
+ {
+ //if (r.mid(0x15).length() != 157)
+ // Find data length
+ int pos = r.indexOf(QByteArrayLiteral("\x27\x00\x00"))+2;
+ int len = r.mid(pos).indexOf(QByteArrayLiteral("\xfd"));
+ //splitWaterfall = false;
+ if (splitWaterfall && pos > 1 && len > 100) {
+ // We need to split waterfall data into its component parts
+ // There are only 2 types that we are currently aware of
+ int numDivisions = 0;
+ if (len == 490) // IC705, IC9700, IC7300(LAN)
+ {
+ numDivisions = 11;
+ }
+ else if (len == 704) // IC7610, IC7851, ICR8600
+ {
+ numDivisions = 15;
+ }
+ else {
+ qInfo(logUdp()) << "Unknown spectrum size" << len;
+ break;
+ }
+ // (sequence #1) includes center/fixed mode at [05]. No pixels.
+ // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 "
+ // "DATA: 27 00 00 01 11 01 00 00 00 14 00 00 00 35 14 00 00 fd "
+ // (sequences 2-10, 50 pixels)
+ // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 "
+ // "DATA: 27 00 00 07 11 27 13 15 01 00 22 21 09 08 06 19 0e 20 23 25 2c 2d 17 27 29 16 14 1b 1b 21 27 1a 18 17 1e 21 1b 24 21 22 23 13 19 23 2f 2d 25 25 0a 0e 1e 20 1f 1a 0c fd "
+ // ^--^--(seq 7/11)
+ // ^-- start waveform data 0x00 to 0xA0, index 05 to 54
+ // (sequence #11)
+ // "INDEX: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 "
+ // "DATA: 27 00 00 11 11 0b 13 21 23 1a 1b 22 1e 1a 1d 13 21 1d 26 28 1f 19 1a 18 09 2c 2c 2c 1a 1b fd "
+
+ int divSize = (len / numDivisions)+6;
+ QByteArray wfPacket;
+ for (int i = 0; i < numDivisions; i++) {
+
+ wfPacket = r.mid(pos - 6, 9); // First part of packet
+
+ wfPacket = r.mid(pos - 6, 9); // First part of packet
+ char tens = ((i + 1) / 10);
+ char units = ((i + 1) - (10 * tens));
+ wfPacket[7] = units | (tens << 4);
+
+ tens = (numDivisions / 10);
+ units = (numDivisions - (10 * tens));
+ wfPacket[8] = units | (tens << 4);
+
+ if (i == 0) {
+ //Just send initial data, first BCD encode the max number:
+ wfPacket.append(r.mid(pos + 3, 12));
+ }
+ else
+ {
+ wfPacket.append(r.mid((pos + 15) + ((i-1) * divSize),divSize));
+ }
+ if (i < numDivisions-1) {
+ wfPacket.append('\xfd');
+ }
+ //printHex(wfPacket, false, true);
+
+ emit receive(wfPacket);
+ wfPacket.clear();
+
+ }
+ //qDebug(logUdp()) << "Waterfall packet len" << len << "Num Divisions" << numDivisions << "Division Size" << divSize;
+ }
+ else {
+ // Not waterfall data or split not enabled.
+ emit receive(r.mid(0x15));
+ }
+ //qDebug(logUdp()) << "Got incoming CIV datagram" << r.mid(0x15).length();
+
+ }
+
+ }
+ }
+ break;
+ }
+ }
+ udpBase::dataReceived(r); // Call parent function to process the rest.
+
+ r.clear();
+ datagram.clear();
+
+ }
+}
+
+
+// Audio stream
+udpAudio::udpAudio(QHostAddress local, QHostAddress ip, quint16 audioPort, quint16 lport, audioSetup rxSetup, audioSetup txSetup)
+{
+ qInfo(logUdp()) << "Starting udpAudio";
+ this->localIP = local;
+ this->port = audioPort;
+ this->radioIP = ip;
+
+ if (txSetup.sampleRate == 0) {
+ enableTx = false;
+ }
+
+ init(lport); // Perform connection
+
+ QUdpSocket::connect(udp, &QUdpSocket::readyRead, this, &udpAudio::dataReceived);
+
+ rxaudio = new audioHandler();
+ rxAudioThread = new QThread(this);
+ rxAudioThread->setObjectName("rxAudio()");
+
+ rxaudio->moveToThread(rxAudioThread);
+
+ rxAudioThread->start(QThread::TimeCriticalPriority);
+
+ connect(this, SIGNAL(setupRxAudio(audioSetup)), rxaudio, SLOT(init(audioSetup)));
+
+ // signal/slot not currently used.
+ connect(this, SIGNAL(haveAudioData(audioPacket)), rxaudio, SLOT(incomingAudio(audioPacket)));
+ connect(this, SIGNAL(haveChangeLatency(quint16)), rxaudio, SLOT(changeLatency(quint16)));
+ connect(this, SIGNAL(haveSetVolume(unsigned char)), rxaudio, SLOT(setVolume(unsigned char)));
+ connect(rxaudio, SIGNAL(haveLevels(quint16, quint16, quint16,bool,bool)), this, SLOT(getRxLevels(quint16, quint16, quint16,bool,bool)));
+ connect(rxAudioThread, SIGNAL(finished()), rxaudio, SLOT(deleteLater()));
+
+
+ sendControl(false, 0x03, 0x00); // First connect packet
+
+ pingTimer = new QTimer();
+ connect(pingTimer, &QTimer::timeout, this, &udpBase::sendPing);
+ pingTimer->start(PING_PERIOD); // send ping packets every 100ms
+
+ if (enableTx) {
+ txaudio = new audioHandler();
+ txAudioThread = new QThread(this);
+ rxAudioThread->setObjectName("txAudio()");
+
+ txaudio->moveToThread(txAudioThread);
+
+ txAudioThread->start(QThread::TimeCriticalPriority);
+
+ connect(this, SIGNAL(setupTxAudio(audioSetup)), txaudio, SLOT(init(audioSetup)));
+ connect(txaudio, SIGNAL(haveAudioData(audioPacket)), this, SLOT(receiveAudioData(audioPacket)));
+ connect(txaudio, SIGNAL(haveLevels(quint16, quint16, quint16, bool,bool)), this, SLOT(getTxLevels(quint16, quint16, quint16, bool,bool)));
+
+ connect(txAudioThread, SIGNAL(finished()), txaudio, SLOT(deleteLater()));
+ emit setupTxAudio(txSetup);
+ }
+
+ emit setupRxAudio(rxSetup);
+
+ watchdogTimer = new QTimer();
+ connect(watchdogTimer, &QTimer::timeout, this, &udpAudio::watchdog);
+ watchdogTimer->start(WATCHDOG_PERIOD);
+
+ areYouThereTimer = new QTimer();
+ connect(areYouThereTimer, &QTimer::timeout, this, std::bind(&udpBase::sendControl, this, false, 0x03, 0));
+ areYouThereTimer->start(AREYOUTHERE_PERIOD);
+}
+
+udpAudio::~udpAudio()
+{
+ if (pingTimer != Q_NULLPTR)
+ {
+ qDebug(logUdp()) << "Stopping pingTimer";
+ pingTimer->stop();
+ delete pingTimer;
+ pingTimer = Q_NULLPTR;
+ }
+
+ if (idleTimer != Q_NULLPTR)
+ {
+ qDebug(logUdp()) << "Stopping idleTimer";
+ idleTimer->stop();
+ delete idleTimer;
+ idleTimer = Q_NULLPTR;
+ }
+
+ if (watchdogTimer != Q_NULLPTR)
+ {
+ qDebug(logUdp()) << "Stopping watchdogTimer";
+ watchdogTimer->stop();
+ delete watchdogTimer;
+ watchdogTimer = Q_NULLPTR;
+ }
+
+ if (rxAudioThread != Q_NULLPTR) {
+ qDebug(logUdp()) << "Stopping rxaudio thread";
+ rxAudioThread->quit();
+ rxAudioThread->wait();
+ }
+
+ if (txAudioThread != Q_NULLPTR) {
+ qDebug(logUdp()) << "Stopping txaudio thread";
+ txAudioThread->quit();
+ txAudioThread->wait();
+ }
+ qDebug(logUdp()) << "udpHandler successfully closed";
+}
+
+void udpAudio::watchdog()
+{
+ static bool alerted = false;
+ if (lastReceived.msecsTo(QTime::currentTime()) > 2000)
+ {
+ if (!alerted) {
+ /* Just log it at the moment, maybe try signalling the control channel that it needs to
+ try requesting civ/audio again? */
+
+ qInfo(logUdp()) << " Audio Watchdog: no audio data received for 2s, restart required?";
+ alerted = true;
+ }
+ }
+ else
+ {
+ alerted = false;
+ }
+}
+
+
+void udpAudio::sendTxAudio()
+{
+ if (txaudio == Q_NULLPTR) {
+ return;
+ }
+
+}
+
+void udpAudio::receiveAudioData(audioPacket audio) {
+ // I really can't see how this could be possible but a quick sanity check!
+ if (txaudio == Q_NULLPTR) {
+ return;
+ }
+ if (audio.data.length() > 0) {
+ int counter = 1;
+ int len = 0;
+
+ while (len < audio.data.length()) {
+ QByteArray partial = audio.data.mid(len, 1364);
+ audio_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p) + partial.length();
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ if (partial.length() == 0xa0) {
+ p.ident = 0x9781;
+ }
+ else {
+ p.ident = 0x0080; // TX audio is always this?
+ }
+ p.datalen = (quint16)qToBigEndian((quint16)partial.length());
+ p.sendseq = (quint16)qToBigEndian((quint16)sendAudioSeq); // THIS IS BIG ENDIAN!
+ QByteArray tx = QByteArray::fromRawData((const char*)p.packet, sizeof(p));
+ tx.append(partial);
+ len = len + partial.length();
+ //qInfo(logUdp()) << "Sending audio packet length: " << tx.length();
+ sendTrackedPacket(tx);
+ sendAudioSeq++;
+ counter++;
+ }
+ }
+}
+
+void udpAudio::changeLatency(quint16 value)
+{
+ emit haveChangeLatency(value);
+}
+
+void udpAudio::setVolume(unsigned char value)
+{
+ emit haveSetVolume(value);
+}
+
+void udpAudio::getRxLevels(quint16 amplitude,quint16 latency, quint16 current, bool under, bool over) {
+
+ emit haveRxLevels(amplitude,latency, current, under, over);
+}
+
+void udpAudio::getTxLevels(quint16 amplitude,quint16 latency, quint16 current, bool under, bool over) {
+ emit haveTxLevels(amplitude,latency, current, under, over);
+}
+
+void udpAudio::dataReceived()
+{
+ while (udp->hasPendingDatagrams()) {
+ QNetworkDatagram datagram = udp->receiveDatagram();
+ //qInfo(logUdp()) << "Received: " << datagram.data().mid(0,10);
+ QByteArray r = datagram.data();
+
+ switch (r.length())
+ {
+ case (16): // Response to control packet handled in udpBase
+ {
+ //control_packet_t in = (control_packet_t)r.constData();
+ break;
+ }
+ default:
+ {
+ /* Audio packets start as follows:
+ PCM 16bit and PCM8/uLAW stereo: 0x44,0x02 for first packet and 0x6c,0x05 for second.
+ uLAW 8bit/PCM 8bit 0xd8,0x03 for all packets
+ PCM 16bit stereo 0x6c,0x05 first & second 0x70,0x04 third
+
+
+ */
+ control_packet_t in = (control_packet_t)r.constData();
+
+ if (in->type != 0x01 && in->len >= 0x20) {
+ if (in->seq == 0)
+ {
+ // Seq number has rolled over.
+ seqPrefix++;
+ }
+
+ // 0xac is the smallest possible audio packet.
+ lastReceived = QTime::currentTime();
+ audioPacket tempAudio;
+ tempAudio.seq = (quint32)seqPrefix << 16 | in->seq;
+ tempAudio.time = lastReceived;
+ tempAudio.sent = 0;
+ tempAudio.data = r.mid(0x18);
+ // Prefer signal/slot to forward audio as it is thread/safe
+ // Need to do more testing but latency appears fine.
+ //rxaudio->incomingAudio(tempAudio);
+ emit haveAudioData(tempAudio);
+ }
+ break;
+ }
+ }
+
+ udpBase::dataReceived(r); // Call parent function to process the rest.
+ r.clear();
+ datagram.clear();
+ }
+}
+
+
+
+void udpBase::init(quint16 lport)
+{
+ //timeStarted.start();
+ udp = new QUdpSocket(this);
+ udp->bind(lport); // Bind to random port.
+ localPort = udp->localPort();
+ qInfo(logUdp()) << "UDP Stream bound to local port:" << localPort << " remote port:" << port;
+ uint32_t addr = localIP.toIPv4Address();
+ myId = (addr >> 8 & 0xff) << 24 | (addr & 0xff) << 16 | (localPort & 0xffff);
+
+ retransmitTimer = new QTimer();
+ connect(retransmitTimer, &QTimer::timeout, this, &udpBase::sendRetransmitRequest);
+ retransmitTimer->start(RETRANSMIT_PERIOD);
+}
+
+udpBase::~udpBase()
+{
+ qInfo(logUdp()) << "Closing UDP stream :" << radioIP.toString() << ":" << port;
+ if (udp != Q_NULLPTR) {
+ sendControl(false, 0x05, 0x00); // Send disconnect
+ udp->close();
+ delete udp;
+ }
+ if (areYouThereTimer != Q_NULLPTR)
+ {
+ areYouThereTimer->stop();
+ delete areYouThereTimer;
+ }
+
+ if (pingTimer != Q_NULLPTR)
+ {
+ pingTimer->stop();
+ delete pingTimer;
+ }
+ if (idleTimer != Q_NULLPTR)
+ {
+ idleTimer->stop();
+ delete idleTimer;
+ }
+ if (retransmitTimer != Q_NULLPTR)
+ {
+ retransmitTimer->stop();
+ delete retransmitTimer;
+ }
+
+ pingTimer = Q_NULLPTR;
+ idleTimer = Q_NULLPTR;
+ areYouThereTimer = Q_NULLPTR;
+ retransmitTimer = Q_NULLPTR;
+
+}
+
+// Base class!
+
+void udpBase::dataReceived(QByteArray r)
+{
+ if (r.length() < 0x10)
+ {
+ return; // Packet too small do to anything with?
+ }
+
+ switch (r.length())
+ {
+ case (CONTROL_SIZE): // Empty response used for simple comms and retransmit requests.
+ {
+ control_packet_t in = (control_packet_t)r.constData();
+ if (in->type == 0x01 && in->len == 0x10)
+ {
+ // Single packet request
+ packetsLost++;
+ congestion = static_cast(packetsSent) / packetsLost * 100;
+ txBufferMutex.lock();
+ auto match = txSeqBuf.find(in->seq);
+ if (match != txSeqBuf.end()) {
+ // Found matching entry?
+ // Send "untracked" as it has already been sent once.
+ // Don't constantly retransmit the same packet, give-up eventually
+ qDebug(logUdp()) << this->metaObject()->className() << ": Sending (single packet) retransmit of " << QString("0x%1").arg(match->seqNum, 0, 16);
+ match->retransmitCount++;
+ udpMutex.lock();
+ udp->writeDatagram(match->data, radioIP, port);
+ udpMutex.unlock();
+ }
+ else {
+ qDebug(logUdp()) << this->metaObject()->className() << ": Remote requested packet"
+ << QString("0x%1").arg(in->seq, 0, 16) <<
+ "not found, have " << QString("0x%1").arg(txSeqBuf.firstKey(), 0, 16) <<
+ "to" << QString("0x%1").arg(txSeqBuf.lastKey(), 0, 16);
+ }
+ txBufferMutex.unlock();
+ }
+ if (in->type == 0x04) {
+ qInfo(logUdp()) << this->metaObject()->className() << ": Received I am here ";
+ areYouThereCounter = 0;
+ // I don't think that we will ever receive an "I am here" other than in response to "Are you there?"
+ remoteId = in->sentid;
+ if (areYouThereTimer != Q_NULLPTR && areYouThereTimer->isActive()) {
+ // send ping packets every second
+ areYouThereTimer->stop();
+ }
+ sendControl(false, 0x06, 0x01); // Send Are you ready - untracked.
+ }
+ else if (in->type == 0x06)
+ {
+ // Just get the seqnum and ignore the rest.
+ }
+ break;
+ }
+ case (PING_SIZE): // ping packet
+ {
+ ping_packet_t in = (ping_packet_t)r.constData();
+ if (in->type == 0x07)
+ {
+ // It is a ping request/response
+ if (in->reply == 0x00)
+ {
+ ping_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p);
+ p.type = 0x07;
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ p.reply = 0x01;
+ p.seq = in->seq;
+ p.time = in->time;
+ udpMutex.lock();
+ udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port);
+ udpMutex.unlock();
+ }
+ else if (in->reply == 0x01) {
+ if (in->seq == pingSendSeq)
+ {
+ // This is response to OUR request so increment counter
+ pingSendSeq++;
+ }
+ }
+ else {
+ qInfo(logUdp()) << this->metaObject()->className() << "Unhandled response to ping. I have never seen this! 0x10=" << r[16];
+ }
+
+ }
+ break;
+ }
+ default:
+ {
+
+ // All packets "should" be added to the incoming buffer.
+ // First check that we haven't already received it.
+
+
+ }
+ break;
+
+ }
+
+
+ // All packets except ping and retransmit requests should trigger this.
+ control_packet_t in = (control_packet_t)r.constData();
+
+ // This is a variable length retransmit request!
+ if (in->type == 0x01 && in->len != 0x10)
+ {
+
+ for (quint16 i = 0x10; i < r.length(); i = i + 2)
+ {
+ quint16 seq = (quint8)r[i] | (quint8)r[i + 1] << 8;
+ auto match = txSeqBuf.find(seq);
+ if (match == txSeqBuf.end()) {
+ qDebug(logUdp()) << this->metaObject()->className() << ": Remote requested packet"
+ << QString("0x%1").arg(seq, 0, 16) <<
+ "not found, have " << QString("0x%1").arg(txSeqBuf.firstKey(), 0, 16) <<
+ "to" << QString("0x%1").arg(txSeqBuf.lastKey(), 0, 16);
+ // Just send idle packet.
+ sendControl(false, 0, seq);
+ }
+ else {
+ // Found matching entry?
+ // Send "untracked" as it has already been sent once.
+ qDebug(logUdp()) << this->metaObject()->className() << ": Sending (multiple packet) retransmit of " << QString("0x%1").arg(match->seqNum,0,16);
+ match->retransmitCount++;
+ udpMutex.lock();
+ udp->writeDatagram(match->data, radioIP, port);
+ udpMutex.unlock();
+ packetsLost++;
+ congestion = static_cast(packetsSent) / packetsLost * 100;
+ }
+ }
+ }
+ else if (in->len != PING_SIZE && in->type == 0x00 && in->seq != 0x00)
+ {
+ rxBufferMutex.lock();
+ if (rxSeqBuf.isEmpty()) {
+ rxSeqBuf.insert(in->seq, QTime::currentTime());
+ }
+ else
+ {
+ if (in->seq < rxSeqBuf.firstKey() || in->seq - rxSeqBuf.lastKey() > MAX_MISSING)
+ {
+ qInfo(logUdp()) << this->metaObject()->className() << "Large seq number gap detected, previous highest: " <<
+ QString("0x%1").arg(rxSeqBuf.lastKey(),0,16) << " current: " << QString("0x%1").arg(in->seq,0,16);
+ //seqPrefix++;
+ // Looks like it has rolled over so clear buffer and start again.
+ rxSeqBuf.clear();
+ // Add this packet to the incoming buffer
+ rxSeqBuf.insert(in->seq, QTime::currentTime());
+ rxBufferMutex.unlock();
+ missingMutex.lock();
+ rxMissing.clear();
+ missingMutex.unlock();
+ return;
+ }
+
+ if (!rxSeqBuf.contains(in->seq))
+ {
+ // Add incoming packet to the received buffer and if it is in the missing buffer, remove it.
+
+ if (in->seq > rxSeqBuf.lastKey() + 1) {
+ qInfo(logUdp()) << this->metaObject()->className() << "1 or more missing packets detected, previous: " <<
+ QString("0x%1").arg(rxSeqBuf.lastKey(),0,16) << " current: " << QString("0x%1").arg(in->seq,0,16);
+ // We are likely missing packets then!
+ missingMutex.lock();
+ //int missCounter = 0;
+ // Sanity check!
+ for (quint16 f = rxSeqBuf.lastKey() + 1; f <= in->seq; f++)
+ {
+ if (rxSeqBuf.size() > BUFSIZE)
+ {
+ rxSeqBuf.erase(rxSeqBuf.begin());
+ }
+ rxSeqBuf.insert(f, QTime::currentTime());
+ if (f != in->seq && !rxMissing.contains(f))
+ {
+ rxMissing.insert(f, 0);
+ }
+ }
+ missingMutex.unlock();
+ }
+ else {
+ if (rxSeqBuf.size() > BUFSIZE)
+ {
+ rxSeqBuf.erase(rxSeqBuf.begin());
+ }
+ rxSeqBuf.insert(in->seq, QTime::currentTime());
+
+ }
+ }
+ else {
+ // This is probably one of our missing packets!
+ missingMutex.lock();
+ auto s = rxMissing.find(in->seq);
+ if (s != rxMissing.end())
+ {
+ qInfo(logUdp()) << this->metaObject()->className() << ": Missing SEQ has been received! " << QString("0x%1").arg(in->seq,0,16);
+
+ s = rxMissing.erase(s);
+ }
+ missingMutex.unlock();
+
+ }
+
+ }
+ rxBufferMutex.unlock();
+
+ }
+}
+
+bool missing(quint16 i, quint16 j) {
+ return (i + 1 != j);
+}
+
+void udpBase::sendRetransmitRequest()
+{
+ // Find all gaps in received packets and then send requests for them.
+ // This will run every 100ms so out-of-sequence packets will not trigger a retransmit request.
+ if (rxMissing.isEmpty()) {
+ return;
+ }
+ else if (rxMissing.size() > MAX_MISSING) {
+ qInfo(logUdp()) << "Too many missing packets," << rxMissing.size() << "flushing all buffers";
+ missingMutex.lock();
+ rxMissing.clear();
+ missingMutex.unlock();
+
+ rxBufferMutex.lock();
+ rxSeqBuf.clear();
+ rxBufferMutex.unlock();
+ return;
+ }
+
+ QByteArray missingSeqs;
+
+ missingMutex.lock();
+ auto it = rxMissing.begin();
+ while (it != rxMissing.end())
+ {
+ if (it.key() != 0x0)
+ {
+ if (it.value() < 4)
+ {
+ missingSeqs.append(it.key() & 0xff);
+ missingSeqs.append(it.key() >> 8 & 0xff);
+ missingSeqs.append(it.key() & 0xff);
+ missingSeqs.append(it.key() >> 8 & 0xff);
+ it.value()++;
+ it++;
+ }
+ else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": No response for missing packet" << QString("0x%1").arg(it.key(), 0, 16) << "deleting";
+ it = rxMissing.erase(it);
+ }
+ } else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": found empty key in missing buffer";
+ it++;
+ }
+ }
+ missingMutex.unlock();
+
+ if (missingSeqs.length() != 0)
+ {
+ control_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p);
+ p.type = 0x01;
+ p.seq = 0x0000;
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ if (missingSeqs.length() == 4) // This is just a single missing packet so send using a control.
+ {
+ p.seq = (missingSeqs[0] & 0xff) | (quint16)(missingSeqs[1] << 8);
+ qInfo(logUdp()) << this->metaObject()->className() << ": sending request for missing packet : " << QString("0x%1").arg(p.seq,0,16);
+ udpMutex.lock();
+ udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port);
+ udpMutex.unlock();
+ }
+ else
+ {
+ qInfo(logUdp()) << this->metaObject()->className() << ": sending request for multiple missing packets : " << missingSeqs.toHex(':');
+ missingMutex.lock();
+ p.len = sizeof(p)+missingSeqs.size();
+ missingSeqs.insert(0, p.packet, sizeof(p));
+ missingMutex.unlock();
+
+ udpMutex.lock();
+ udp->writeDatagram(missingSeqs, radioIP, port);
+ udpMutex.unlock();
+ }
+ }
+
+}
+
+
+
+// Used to send idle and other "control" style messages
+void udpBase::sendControl(bool tracked = true, quint8 type = 0, quint16 seq = 0)
+{
+ control_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p);
+ p.type = type;
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+
+ if (!tracked) {
+ p.seq = seq;
+ udpMutex.lock();
+ udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port);
+ udpMutex.unlock();
+ }
+ else {
+ sendTrackedPacket(QByteArray::fromRawData((const char*)p.packet, sizeof(p)));
+ }
+ return;
+}
+
+// Send periodic ping packets
+void udpBase::sendPing()
+{
+ ping_packet p;
+ memset(p.packet, 0x0, sizeof(p)); // We can't be sure it is initialized with 0x00!
+ p.len = sizeof(p);
+ p.type = 0x07;
+ p.sentid = myId;
+ p.rcvdid = remoteId;
+ p.seq = pingSendSeq;
+ QTime now=QTime::currentTime();
+ p.time = (quint32)now.msecsSinceStartOfDay();
+ lastPingSentTime = QDateTime::currentDateTime();
+ udpMutex.lock();
+ udp->writeDatagram(QByteArray::fromRawData((const char*)p.packet, sizeof(p)), radioIP, port);
+ udpMutex.unlock();
+ return;
+}
+
+
+void udpBase::sendTrackedPacket(QByteArray d)
+{
+ // As the radio can request retransmission of these packets, store them in a buffer
+ d[6] = sendSeq & 0xff;
+ d[7] = (sendSeq >> 8) & 0xff;
+ SEQBUFENTRY s;
+ s.seqNum = sendSeq;
+ s.timeSent = QTime::currentTime();
+ s.retransmitCount = 0;
+ s.data = d;
+ if (txBufferMutex.tryLock(100))
+ {
+
+ if (sendSeq == 0) {
+ // We are either the first ever sent packet or have rolled-over so clear the buffer.
+ txSeqBuf.clear();
+ congestion = 0;
+ }
+ if (txSeqBuf.size() > BUFSIZE)
+ {
+ txSeqBuf.erase(txSeqBuf.begin());
+ }
+ txSeqBuf.insert(sendSeq, s);
+
+ txBufferMutex.unlock();
+ } else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked";
+ }
+ // Stop using purgeOldEntries() as it is likely slower than just removing the earliest packet.
+ //qInfo(logUdp()) << this->metaObject()->className() << "RX:" << rxSeqBuf.size() << "TX:" <writeDatagram(d, radioIP, port);
+ if (congestion>10) { // Poor quality connection?
+ udp->writeDatagram(d, radioIP, port);
+ if (congestion>20) // Even worse so send again.
+ udp->writeDatagram(d, radioIP, port);
+ }
+ if (idleTimer != Q_NULLPTR && idleTimer->isActive()) {
+ idleTimer->start(IDLE_PERIOD); // Reset idle counter if it's running
+ }
+ udpMutex.unlock();
+ packetsSent++;
+ return;
+}
+
+
+///
+/// Once a packet has reached PURGE_SECONDS old (currently 10) then it is not likely to be any use.
+///
+void udpBase::purgeOldEntries()
+{
+ // Erase old entries from the tx packet buffer
+ if (txBufferMutex.tryLock(100))
+ {
+ if (!txSeqBuf.isEmpty())
+ {
+ // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS
+ for (auto it = txSeqBuf.begin(); it != txSeqBuf.end();) {
+ if (it.value().timeSent.secsTo(QTime::currentTime()) > PURGE_SECONDS) {
+ txSeqBuf.erase(it++);
+ }
+ else {
+ break;
+ }
+ }
+ }
+ txBufferMutex.unlock();
+
+ } else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": txBuffer mutex is locked";
+ }
+
+
+
+ if (rxBufferMutex.tryLock(100))
+ {
+ if (!rxSeqBuf.isEmpty()) {
+ // Loop through the earliest items in the buffer and delete if older than PURGE_SECONDS
+ for (auto it = rxSeqBuf.begin(); it != rxSeqBuf.end();) {
+ if (it.value().secsTo(QTime::currentTime()) > PURGE_SECONDS) {
+ rxSeqBuf.erase(it++);
+ }
+ else {
+ break;
+ }
+ }
+ }
+ rxBufferMutex.unlock();
+ } else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": rxBuffer mutex is locked";
+ }
+
+ if (missingMutex.tryLock(100))
+ {
+ // Erase old entries from the missing packets buffer
+ if (!rxMissing.isEmpty() && rxMissing.size() > 50) {
+ for (size_t i = 0; i < 25; ++i) {
+ rxMissing.erase(rxMissing.begin());
+ }
+ }
+ missingMutex.unlock();
+ } else {
+ qInfo(logUdp()) << this->metaObject()->className() << ": missingBuffer mutex is locked";
+ }
+}
+
+void udpBase::printHex(const QByteArray& pdata)
+{
+ printHex(pdata, false, true);
+}
+
+void udpBase::printHex(const QByteArray& pdata, bool printVert, bool printHoriz)
+{
+ qDebug(logUdp()) << "---- Begin hex dump -----:";
+ QString sdata("DATA: ");
+ QString index("INDEX: ");
+ QStringList strings;
+
+ for (int i = 0; i < pdata.length(); i++)
+ {
+ strings << QString("[%1]: %2").arg(i, 8, 10, QChar('0')).arg((unsigned char)pdata[i], 2, 16, QChar('0'));
+ sdata.append(QString("%1 ").arg((unsigned char)pdata[i], 2, 16, QChar('0')));
+ index.append(QString("%1 ").arg(i, 2, 10, QChar('0')));
+ }
+
+ if (printVert)
+ {
+ for (int i = 0; i < strings.length(); i++)
+ {
+ //sdata = QString(strings.at(i));
+ qDebug(logUdp()) << strings.at(i);
+ }
+ }
+
+ if (printHoriz)
+ {
+ qDebug(logUdp()) << index;
+ qDebug(logUdp()) << sdata;
+ }
+ qDebug(logUdp()) << "----- End hex dump -----";
+}
+
+///
+/// passcode function used to generate secure (ish) code
+///
+///
+/// pointer to encoded username or password
+void passcode(QString in, QByteArray& out)
+{
+ const quint8 sequence[] =
+ {
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0x47,0x5d,0x4c,0x42,0x66,0x20,0x23,0x46,0x4e,0x57,0x45,0x3d,0x67,0x76,0x60,0x41,0x62,0x39,0x59,0x2d,0x68,0x7e,
+ 0x7c,0x65,0x7d,0x49,0x29,0x72,0x73,0x78,0x21,0x6e,0x5a,0x5e,0x4a,0x3e,0x71,0x2c,0x2a,0x54,0x3c,0x3a,0x63,0x4f,
+ 0x43,0x75,0x27,0x79,0x5b,0x35,0x70,0x48,0x6b,0x56,0x6f,0x34,0x32,0x6c,0x30,0x61,0x6d,0x7b,0x2f,0x4b,0x64,0x38,
+ 0x2b,0x2e,0x50,0x40,0x3f,0x55,0x33,0x37,0x25,0x77,0x24,0x26,0x74,0x6a,0x28,0x53,0x4d,0x69,0x22,0x5c,0x44,0x31,
+ 0x36,0x58,0x3b,0x7a,0x51,0x5f,0x52,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
+
+ };
+
+ QByteArray ba = in.toLocal8Bit();
+ uchar* ascii = (uchar*)ba.constData();
+ for (int i = 0; i < in.length() && i < 16; i++)
+ {
+ int p = ascii[i] + i;
+ if (p > 126)
+ {
+ p = 32 + p % 127;
+ }
+ out.append(sequence[p]);
+ }
+ return;
+}
+
+///
+/// returns a QByteArray of a null terminated string
+///
+///
+///
+///
+QByteArray parseNullTerminatedString(QByteArray c, int s)
+{
+ //QString res = "";
+ QByteArray res;
+ for (int i = s; i < c.length(); i++)
+ {
+ if (c[i] != '\0')
+ {
+ res.append(c[i]);
+ }
+ else
+ {
+ break;
+ }
+ }
+ return res;
+}
+
diff --git a/udphandler.h b/udphandler.h
index 0faf2eb..8300805 100644
--- a/udphandler.h
+++ b/udphandler.h
@@ -56,8 +56,8 @@ public slots:
void setVolume(unsigned char value);
void init();
void setCurrentRadio(quint8 radio);
- void getRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under);
- void getTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under);
+ void getRxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over);
+ void getTxLevels(quint16 amplitude, quint16 latency, quint16 current, bool under, bool over);
signals: