mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-12-23 10:05:46 -05:00
424 lines
14 KiB
C++
424 lines
14 KiB
C++
///////////////////////////////////////////////////////////////////////////////////
|
|
// Copyright (C) 2023 Daniele Forsi <iu5hkx@gmail.com> //
|
|
// Copyright (C) 2023 Frank Werner <mail@hb9fxq.ch> //
|
|
// Copyright (C) 2023 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
|
|
// //
|
|
// 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 <boost/endian/conversion.hpp>
|
|
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QJsonValue>
|
|
|
|
#include "util/messagequeue.h"
|
|
|
|
#include "aaroniartsainputsettings.h"
|
|
#include "aaroniartsainputworker.h"
|
|
|
|
MESSAGE_CLASS_DEFINITION(AaroniaRTSAInputWorker::MsgReportSampleRateAndFrequency, Message)
|
|
|
|
AaroniaRTSAInputWorker::AaroniaRTSAInputWorker(SampleSinkFifo* sampleFifo) :
|
|
QObject(),
|
|
m_timer(this),
|
|
m_samplesBuf(),
|
|
m_sampleFifo(sampleFifo),
|
|
m_centerFrequency(0),
|
|
m_sampleRate(1),
|
|
m_inputMessageQueue(nullptr),
|
|
m_status(AaroniaRTSAInputSettings::ConnectionIdle),
|
|
mReply(nullptr),
|
|
m_convertBuffer(64e6)
|
|
{
|
|
// Initialize network managers
|
|
m_networkAccessManager = new QNetworkAccessManager(this);
|
|
m_networkAccessManagerConfig = new QNetworkAccessManager(this);
|
|
QObject::connect(
|
|
m_networkAccessManagerConfig,
|
|
&QNetworkAccessManager::finished,
|
|
this,
|
|
&AaroniaRTSAInputWorker::handleConfigReply
|
|
);
|
|
|
|
// Request 16bit raw samples
|
|
// m_serverAddress = "localhost:55123";
|
|
// QUrl url(tr("http://%1/stream?format=float32").arg(m_serverAddress));
|
|
|
|
// QNetworkRequest req(url);
|
|
// mReply = m_networkAccessManager->get(req);
|
|
|
|
// // Connect Qt slots to network events
|
|
// connect(mReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
|
|
// connect(mReply, SIGNAL(finished()), this, SLOT(onFinished()));
|
|
// connect(mReply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
|
|
|
mPrevTime = 0;
|
|
mPacketSamples = 0;
|
|
}
|
|
|
|
AaroniaRTSAInputWorker::~AaroniaRTSAInputWorker()
|
|
{
|
|
if (mReply)
|
|
{
|
|
// disconnect previous sugnals
|
|
disconnect(mReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
|
|
disconnect(mReply, SIGNAL(finished()), this, SLOT(onFinished()));
|
|
disconnect(mReply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
|
|
|
mReply->abort();
|
|
mReply->deleteLater();
|
|
}
|
|
|
|
m_networkAccessManager->deleteLater();
|
|
|
|
QObject::disconnect(
|
|
m_networkAccessManagerConfig,
|
|
&QNetworkAccessManager::finished,
|
|
this,
|
|
&AaroniaRTSAInputWorker::handleConfigReply
|
|
);
|
|
m_networkAccessManagerConfig->deleteLater();
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::onSocketError(QAbstractSocket::SocketError error)
|
|
{
|
|
(void) error;
|
|
m_status = AaroniaRTSAInputSettings::ConnectionError;
|
|
emit updateStatus(m_status);
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::sendCenterFrequencyAndSampleRate()
|
|
{
|
|
if (m_iqDemodName.size() == 0) {
|
|
return;
|
|
}
|
|
|
|
qDebug("AaroniaRTSAInputWorker::sendCenterFrequencyAndSampleRate: %llu samplerate: %d", m_centerFrequency, m_sampleRate);
|
|
|
|
QJsonObject object {
|
|
{"receiverName", m_iqDemodName},
|
|
{"simpleconfig", QJsonObject({
|
|
{"main", QJsonObject({
|
|
{"centerfreq", QJsonValue((qint64) m_centerFrequency)},
|
|
{"samplerate", QJsonValue(m_sampleRate)},
|
|
{"spanfreq", QJsonValue(m_sampleRate)},
|
|
})}
|
|
})}
|
|
};
|
|
|
|
QJsonDocument document;
|
|
document.setObject(object);
|
|
QUrl url(tr("http://%1/remoteconfig").arg(m_serverAddress));
|
|
QNetworkRequest request(url);
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
|
m_networkAccessManagerConfig->put(request, document.toJson());
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::getConfig()
|
|
{
|
|
QUrl url(tr("http://%1/remoteconfig").arg(m_serverAddress));
|
|
QNetworkRequest request(url);
|
|
m_networkAccessManagerConfig->get(request);
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::onCenterFrequencyChanged(quint64 centerFrequency)
|
|
{
|
|
if (m_centerFrequency == centerFrequency) {
|
|
return;
|
|
}
|
|
|
|
m_centerFrequency = centerFrequency;
|
|
sendCenterFrequencyAndSampleRate();
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::onSampleRateChanged(int sampleRate)
|
|
{
|
|
if (m_sampleRate == sampleRate) {
|
|
return;
|
|
}
|
|
|
|
m_sampleRate = sampleRate;
|
|
sendCenterFrequencyAndSampleRate();
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::onServerAddressChanged(QString serverAddress)
|
|
{
|
|
m_status = AaroniaRTSAInputSettings::ConnectionDisconnected;
|
|
updateStatus(m_status);
|
|
|
|
if (mReply)
|
|
{
|
|
// disconnect previous sugnals
|
|
disconnect(mReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
|
|
disconnect(mReply, SIGNAL(finished()), this, SLOT(onFinished()));
|
|
disconnect(mReply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
|
|
|
mReply->abort();
|
|
mReply->deleteLater();
|
|
}
|
|
|
|
QUrl url(tr("http://%1/stream?format=float32").arg(serverAddress));
|
|
QNetworkRequest req(url);
|
|
mReply = m_networkAccessManager->get(req);
|
|
|
|
// Connect Qt slots to network events
|
|
connect(mReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
|
|
connect(mReply, SIGNAL(finished()), this, SLOT(onFinished()));
|
|
connect(mReply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
|
|
|
|
mPrevTime = 0;
|
|
mPacketSamples = 0;
|
|
m_serverAddress = serverAddress;
|
|
|
|
getConfig();
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::tick()
|
|
{
|
|
}
|
|
|
|
/**************************CPY ********************************* */
|
|
|
|
void AaroniaRTSAInputWorker::onError(QNetworkReply::NetworkError code)
|
|
{
|
|
(void) code;
|
|
qWarning() << "AaroniaRTSAInputWorker::onError: network Error: " << mReply->errorString();
|
|
m_status = 3;
|
|
emit updateStatus(3);
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::onFinished()
|
|
{
|
|
qDebug() << "AaroniaRTSAInputWorker::onFinished(: finished: " << mReply->errorString();
|
|
mBuffer.append(mReply->readAll());
|
|
mReply->deleteLater();
|
|
mReply = nullptr;
|
|
}
|
|
|
|
// bytes received from the socket
|
|
void AaroniaRTSAInputWorker::onReadyRead()
|
|
{
|
|
if (m_status != AaroniaRTSAInputSettings::ConnectionOK)
|
|
{
|
|
m_status = AaroniaRTSAInputSettings::ConnectionOK;
|
|
emit updateStatus(m_status);
|
|
}
|
|
|
|
// read as many bytes as possible into input buffer
|
|
qint64 n = mReply->bytesAvailable();
|
|
qint64 bs = mBuffer.size();
|
|
mBuffer.resize(bs + n);
|
|
qint64 done = mReply->read(mBuffer.data() + bs, n);
|
|
mBuffer.resize(bs + done);
|
|
|
|
// initialize parsing
|
|
int offset = 0;
|
|
int avail = mBuffer.size();
|
|
|
|
// consume all input data if possible
|
|
while (offset < avail)
|
|
{
|
|
// any samples so far (not looking for meta data)
|
|
if (mPacketSamples)
|
|
{
|
|
// enough samples
|
|
if (offset + mPacketSamples * 2 * sizeof(float) <= (unsigned long) avail)
|
|
{
|
|
// do something with the IQ data
|
|
const float *sp = (const float *)(mBuffer.constData() + offset);
|
|
|
|
SampleVector::iterator it = m_convertBuffer.begin();
|
|
|
|
m_decimatorsFloatIQ.decimate1(&it, sp, 2*mPacketSamples);
|
|
|
|
/*m_samplesBuf.clear();
|
|
for (int i = 0; i < mPacketSamples*2; i+=2)
|
|
{
|
|
m_samplesBuf.push_back(Sample(
|
|
sp[i] << (SDR_RX_SAMP_SZ - 8),
|
|
sp[i+1] << (SDR_RX_SAMP_SZ - 8)
|
|
));
|
|
}*/
|
|
|
|
//m_sampleFifo->write(m_samplesBuf.begin(), m_samplesBuf.end());
|
|
m_sampleFifo->write(m_convertBuffer.begin(), it);
|
|
|
|
//qDebug() << "IQ " << sp[0] << ", " << sp[1];
|
|
//m_sampleFifo->write()
|
|
|
|
// consume all samples from the input buffer
|
|
offset += mPacketSamples * 2 * sizeof(float);
|
|
mPacketSamples = 0;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// is there a complete JSON metadata object in the buffer
|
|
int split = mBuffer.indexOf('\x1e', offset);
|
|
|
|
if (split != -1)
|
|
{
|
|
// Extract it
|
|
QByteArray data = mBuffer.mid(offset, split - offset);
|
|
offset = split + 1;
|
|
|
|
// Parse the JSON data
|
|
|
|
QJsonParseError error;
|
|
QJsonDocument jdoc = QJsonDocument::fromJson(data, &error);
|
|
|
|
if (error.error == QJsonParseError::NoError)
|
|
{
|
|
// Extract fields of interest
|
|
// double startTime = jdoc["startTime"].toDouble(), endTime = jdoc["endTime"].toDouble();
|
|
int samples = jdoc["samples"].toInt();
|
|
|
|
// Dump packet loss
|
|
// if (startTime != mPrevTime)
|
|
// {
|
|
// qDebug() << "AaroniaRTSAInputWorker::onReadyRead: packet loss: "
|
|
// << QDateTime::fromMSecsSinceEpoch(startTime * 1000).toString()
|
|
// << " D " << endTime - startTime
|
|
// << " O " << startTime - mPrevTime
|
|
// << " S " << samples
|
|
// << " L " << QDateTime::currentMSecsSinceEpoch() / 1000.0 - startTime;
|
|
|
|
// if (m_status != AaroniaRTSAInputSettings::ConnectionUnstable)
|
|
// {
|
|
// m_status = AaroniaRTSAInputSettings::ConnectionUnstable;
|
|
// emit updateStatus(m_status);
|
|
// }
|
|
// }
|
|
|
|
// Switch to data phase
|
|
// mPrevTime = endTime;
|
|
mPacketSamples = samples;
|
|
// qDebug() << jdoc.toJson();
|
|
quint64 endFreq = jdoc["endFrequency"].toDouble();
|
|
quint64 startFreq = jdoc["startFrequency"].toDouble();
|
|
int bw = jdoc["sampleFrequency"].toInt();
|
|
quint64 midFreq = (endFreq + startFreq) / 2;
|
|
|
|
if ((bw != m_sampleRate) || (midFreq != m_centerFrequency))
|
|
{
|
|
if (m_inputMessageQueue)
|
|
{
|
|
MsgReportSampleRateAndFrequency *msg = MsgReportSampleRateAndFrequency::create(bw, midFreq);
|
|
m_inputMessageQueue->push(msg);
|
|
}
|
|
|
|
}
|
|
|
|
m_sampleRate = bw;
|
|
m_centerFrequency = midFreq;
|
|
}
|
|
else
|
|
{
|
|
QTextStream qerr(stderr);
|
|
qerr << "Json Parse Error: " + error.errorString();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove consumed data from the buffer
|
|
mBuffer.remove(0, offset);
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::handleConfigReply(QNetworkReply* reply)
|
|
{
|
|
if (reply->operation() == QNetworkAccessManager::GetOperation) // return from GET to /remoteconfig
|
|
{
|
|
parseConfig(reply->readAll());
|
|
}
|
|
else if (reply->operation() == QNetworkAccessManager::PutOperation) // return from PUT to /remoteconfig
|
|
{
|
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
if ((httpStatusCode / 100) == 2) {
|
|
qDebug("AaroniaRTSAInputWorker::handleConfigReply: remoteconfig OK (%d)", httpStatusCode);
|
|
} else {
|
|
qWarning("AaroniaRTSAInputWorker::handleConfigReply: remoteconfig ended with error (%d)", httpStatusCode);
|
|
}
|
|
}
|
|
|
|
reply->deleteLater();
|
|
}
|
|
|
|
void AaroniaRTSAInputWorker::parseConfig(QByteArray bytes)
|
|
{
|
|
QJsonDocument document = QJsonDocument::fromJson(bytes);
|
|
m_iqDemodName = "";
|
|
|
|
if (document.isObject())
|
|
{
|
|
QJsonObject documentObject = document.object();
|
|
|
|
if (documentObject.contains(QStringLiteral("config")))
|
|
{
|
|
QJsonObject config = documentObject.value(QStringLiteral("config")).toObject();
|
|
|
|
if (config.contains(QStringLiteral("items")))
|
|
{
|
|
QJsonArray configItems = config.value(QStringLiteral("items")).toArray();
|
|
|
|
for (const auto& configIem : configItems)
|
|
{
|
|
QJsonObject configIemObject = configIem.toObject();
|
|
|
|
if (configIemObject.contains(QStringLiteral("name")))
|
|
{
|
|
QString nameItem = configIemObject.value(QStringLiteral("name")).toString();
|
|
|
|
if (nameItem.startsWith("Block_IQDemodulator"))
|
|
{
|
|
m_iqDemodName = nameItem;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "AaroniaRTSAInputWorker::parseConfig: config has no items: " << config;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "AaroniaRTSAInputWorker::parseConfig: document has no config object: " << documentObject;
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "AaroniaRTSAInputWorker::parseConfig: document is not an object: " << document;
|
|
}
|
|
|
|
if (m_iqDemodName == "") {
|
|
qWarning("AaroniaRTSAInputWorker.parseConfig: could not find IQ demdulator");
|
|
} else {
|
|
qDebug("AaroniaRTSAInputWorker::parseConfig: IQ demdulator name: %s", qPrintable(m_iqDemodName));
|
|
}
|
|
}
|