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: