diff --git a/CMakeLists.txt b/CMakeLists.txt index 88619d1bc..1836dc7e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ option(ENABLE_CHANNELRX_DEMODFT8 "Enable channelrx demodft8 plugin" ON) option(ENABLE_CHANNELRX_DEMODNAVTEX "Enable channelrx demodnavtex plugin" ON) option(ENABLE_CHANNELRX_DEMODRTTY "Enable channelrx demodrtty plugin" ON) option(ENABLE_CHANNELRX_DEMODILS "Enable channelrx demodils plugin" ON) +option(ENABLE_CHANNELRX_DEMODDSC "Enable channelrx demoddsc plugin" ON) # Channel Tx enablers option(ENABLE_CHANNELTX "Enable channeltx plugins" ON) diff --git a/doc/img/AISDemod_plugin_slotmap.png b/doc/img/AISDemod_plugin_slotmap.png new file mode 100644 index 000000000..92e4d599b Binary files /dev/null and b/doc/img/AISDemod_plugin_slotmap.png differ diff --git a/doc/img/DSCDemod_plugin.png b/doc/img/DSCDemod_plugin.png new file mode 100644 index 000000000..e79449ba6 Binary files /dev/null and b/doc/img/DSCDemod_plugin.png differ diff --git a/doc/img/DSCDemod_plugin_geocall.png b/doc/img/DSCDemod_plugin_geocall.png new file mode 100644 index 000000000..a7f3745bf Binary files /dev/null and b/doc/img/DSCDemod_plugin_geocall.png differ diff --git a/doc/img/DSCDemod_plugin_messages.png b/doc/img/DSCDemod_plugin_messages.png new file mode 100644 index 000000000..80081e3e7 Binary files /dev/null and b/doc/img/DSCDemod_plugin_messages.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 47a6f077b..2c156b81b 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -125,6 +125,10 @@ if (ENABLE_CHANNELRX_DEMODILS) add_subdirectory(demodils) endif() +if (ENABLE_CHANNELRX_DEMODDSC) + add_subdirectory(demoddsc) +endif() + if(NOT SERVER_MODE) add_subdirectory(heatmap) diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index ae65c0394..fd898e6d0 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -951,10 +951,7 @@ Aircraft *ADSBDemodGUI::getAircraft(int icao, bool &newAircraft) QIcon *icon = nullptr; if (aircraft->m_aircraftInfo->m_operatorICAO.size() > 0) { - aircraft->m_airlineIconURL = AircraftInformation::getAirlineIconPath(aircraft->m_aircraftInfo->m_operatorICAO); - if (aircraft->m_airlineIconURL.startsWith(':')) { - aircraft->m_airlineIconURL = "qrc://" + aircraft->m_airlineIconURL.mid(1); - } + aircraft->m_airlineIconURL = AircraftInformation::getFlagIconURL(aircraft->m_aircraftInfo->m_operatorICAO); icon = AircraftInformation::getAirlineIcon(aircraft->m_aircraftInfo->m_operatorICAO); if (icon != nullptr) { diff --git a/plugins/channelrx/demodais/aisdemod.cpp b/plugins/channelrx/demodais/aisdemod.cpp index a165428fe..0f639d196 100644 --- a/plugins/channelrx/demodais/aisdemod.cpp +++ b/plugins/channelrx/demodais/aisdemod.cpp @@ -223,7 +223,8 @@ bool AISDemod::handleMessage(const Message& cmd) << ais->getType() << "," << "\"" << ais->toString() << "\"" << "," << "\"" << ais->toNMEA() << "\"" << "," - << report.getSlot() << "\n"; + << report.getSlot() << "," + << report.getSlots() << "\n"; delete ais; } @@ -355,7 +356,7 @@ void AISDemod::applySettings(const AISDemodSettings& settings, bool force) if (newFile) { // Write header - m_logStream << "Date,Time,Data,MMSI,Type,Message,NMEA,Slot\n"; + m_logStream << "Date,Time,Data,MMSI,Type,Message,NMEA,Slot,Slots\n"; } } else diff --git a/plugins/channelrx/demodais/aisdemod.h b/plugins/channelrx/demodais/aisdemod.h index ba7bd76bf..d4c523b26 100644 --- a/plugins/channelrx/demodais/aisdemod.h +++ b/plugins/channelrx/demodais/aisdemod.h @@ -73,22 +73,25 @@ public: QByteArray getMessage() const { return m_message; } QDateTime getDateTime() const { return m_dateTime; } int getSlot() const { return m_slot; } + int getSlots() const { return m_slots; } - static MsgMessage* create(QByteArray message, QDateTime dateTime, int slot) + static MsgMessage* create(QByteArray message, QDateTime dateTime, int slot, int totalSlots) { - return new MsgMessage(message, dateTime, slot); + return new MsgMessage(message, dateTime, slot, totalSlots); } private: QByteArray m_message; QDateTime m_dateTime; int m_slot; + int m_slots; - MsgMessage(QByteArray message, QDateTime dateTime, int slot) : + MsgMessage(QByteArray message, QDateTime dateTime, int slot, int totalSlots) : Message(), m_message(message), m_dateTime(dateTime), - m_slot(slot) + m_slot(slot), + m_slots(totalSlots) { } }; diff --git a/plugins/channelrx/demodais/aisdemodgui.cpp b/plugins/channelrx/demodais/aisdemodgui.cpp index 7b60ad873..01c732105 100644 --- a/plugins/channelrx/demodais/aisdemodgui.cpp +++ b/plugins/channelrx/demodais/aisdemodgui.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "aisdemodgui.h" @@ -40,6 +41,7 @@ #include "util/ais.h" #include "util/csv.h" #include "util/db.h" +#include "util/mmsi.h" #include "gui/basicchannelsettingsdialog.h" #include "gui/devicestreamselectiondialog.h" #include "gui/dialpopup.h" @@ -61,10 +63,12 @@ void AISDemodGUI::resizeTable() // Trailing spaces are for sort arrow int row = ui->messages->rowCount(); ui->messages->setRowCount(row + 1); - ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016-")); + ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Frid Apr 15 2016-")); ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00")); ui->messages->setItem(row, MESSAGE_COL_MMSI, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_COUNTRY, new QTableWidgetItem("flag")); ui->messages->setItem(row, MESSAGE_COL_TYPE, new QTableWidgetItem("Position report")); + ui->messages->setItem(row, MESSAGE_COL_ID, new QTableWidgetItem("25")); ui->messages->setItem(row, MESSAGE_COL_DATA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); ui->messages->setItem(row, MESSAGE_COL_NMEA, new QTableWidgetItem("!AIVDM,1,1,,A,AAAAAAAAAAAAAAAAAAAAAAAAAAAA,0*00")); ui->messages->setItem(row, MESSAGE_COL_HEX, new QTableWidgetItem("04058804002000069a0760728d9e00000040000000")); @@ -154,8 +158,270 @@ bool AISDemodGUI::deserialize(const QByteArray& data) } } +// Distint palette generator +// https://mokole.com/palette.html +QList AISDemodGUI::m_colors = { + 0xffffff, + 0xff0000, + 0x00ff00, + 0x0000ff, + 0x00ffff, + 0xff00ff, + 0x7fff00, + 0x000080, + 0xa9a9a9, + 0x2f4f4f, + 0x556b2f, + 0x8b4513, + 0x6b8e23, + 0x191970, + 0x006400, + 0x708090, + 0x8b0000, + 0x3cb371, + 0xbc8f8f, + 0x663399, + 0xb8860b, + 0xbdb76b, + 0x008b8b, + 0x4682b4, + 0xd2691e, + 0x9acd32, + 0xcd5c5c, + 0x32cd32, + 0x8fbc8f, + 0x8b008b, + 0xb03060, + 0x66cdaa, + 0x9932cc, + 0x00ced1, + 0xff8c00, + 0xffd700, + 0xc71585, + 0x0000cd, + 0xdeb887, + 0x00ff7f, + 0x4169e1, + 0xe9967a, + 0xdc143c, + 0x00bfff, + 0xf4a460, + 0x9370db, + 0xa020f0, + 0xff6347, + 0xd8bfd8, + 0xdb7093, + 0xf0e68c, + 0xffff54, + 0x6495ed, + 0xdda0dd, + 0x87ceeb, + 0xff1493, + 0xafeeee, + 0xee82ee, + 0xfaf0e6, + 0x98fb98, + 0x7fffd4, + 0xff69b4, + 0xfffacd, + 0xffb6c1, +}; + +QHash m_categoryColors = { + {"Class A Vessel", 0xff0000}, + {"Class B Vessel", 0x0000ff}, + {"Coast", 0x00ff00}, + {"Physical AtoN", 0xffff00}, + {"Virtual AtoN", 0xc0c000}, + {"Mobile AtoN", 0xa0a000}, + {"AtoN", 0x808000}, + {"SAR", 0x00ffff}, + {"SAR Aircraft", 0x00c0c0}, + {"SAR Helicopter", 0x00a0a0}, + {"Group", 0xff00ff}, + {"Man overboard", 0xc000c0}, + {"EPIRB", 0xa000a0}, + {"AMRD", 0x800080}, + {"Craft with parent ship", 0x600060} +}; + +QMutex AISDemodGUI::m_colorMutex; +QHash AISDemodGUI::m_usedInFrame; +QHash AISDemodGUI::m_slotMapColors; +QDateTime AISDemodGUI::m_lastColorUpdate; +QHash AISDemodGUI::m_category; + +QColor AISDemodGUI::getColor(const QString& mmsi) +{ + if (true) + { + if (m_category.contains(mmsi)) + { + QString category = m_category.value(mmsi); + if (m_categoryColors.contains(category)) { + return QColor(m_categoryColors.value(category)); + } + qDebug() << "No color for " << category; + } + else + { + // Use white for no category + return Qt::white; + } + } + else + { + QMutexLocker locker(&m_colorMutex); + + QColor color; + if (m_slotMapColors.contains(mmsi)) + { + m_usedInFrame.insert(mmsi, true); + color = m_slotMapColors.value(mmsi); + } + else + { + if (m_colors.size() > 0) + { + color = m_colors.takeFirst(); + qDebug() << "Taking colour from list " << color << "for" << mmsi << " - remaining " << m_colors.size(); + } + else + { + qDebug() << "Out of colors - looking to reuse"; + // Look for recently unused color + QMutableHashIterator it(m_usedInFrame); + color = Qt::black; + while (it.hasNext()) + { + it.next(); + if (!it.value()) + { + color = m_slotMapColors.value(it.key()); + if (color != Qt::black) + { + qDebug() << "Reusing " << color << " from " << it.key(); + m_slotMapColors.remove(it.key()); + m_usedInFrame.remove(it.key()); + break; + } + } + } + } + if (color != Qt::black) + { + m_slotMapColors.insert(mmsi, color); + m_usedInFrame.insert(mmsi, true); + } + else + { + qDebug() << "No free colours"; + } + } + + // Don't actually draw with black, as it's the background colour + if (color == Qt::black) { + return Qt::white; + } else { + return color; + } + } +} + +void AISDemodGUI::updateColors() +{ + QMutexLocker locker(&m_colorMutex); + + QDateTime currentDateTime = QDateTime::currentDateTime(); + if (!m_lastColorUpdate.isValid() || (m_lastColorUpdate.time().minute() != currentDateTime.time().minute())) + { + QHashIterator it(m_usedInFrame); + while (it.hasNext()) + { + it.next(); + m_usedInFrame.insert(it.key(), false); + } + } + m_lastColorUpdate = currentDateTime; +} + +void AISDemodGUI::updateSlotMap() +{ + QDateTime currentDateTime = QDateTime::currentDateTime(); + + if (!m_lastSlotMapUpdate.isValid() || (m_lastSlotMapUpdate.time().minute() != currentDateTime.time().minute())) + { + // Update slot utilisation stats for previous frame + ui->slotsFree->setText(QString::number(2250 - m_slotsUsed)); + ui->slotsUsed->setText(QString::number(m_slotsUsed)); + ui->slotUtilization->setValue(std::round(m_slotsUsed * 100.0 / 2250.0)); + m_slotsUsed = 0; + // Draw empty grid + m_image.fill(Qt::transparent); + //m_image.fill(Qt::); + m_painter.setPen(Qt::black); + for (int x = 0; x < m_image.width(); x += 5) { + m_painter.drawLine(x, 0, x, m_image.height() - 1); + } + for (int y = 0; y < m_image.height(); y += 5) { + m_painter.drawLine(0, y, m_image.width() - 1, y); + } + updateColors(); + } + ui->slotMap->setPixmap(m_image); + + m_lastSlotMapUpdate = currentDateTime; +} + +void AISDemodGUI::updateCategory(const QString& mmsi, const AISMessage *message) +{ + QMutexLocker locker(&m_colorMutex); + + if (!m_category.contains(mmsi)) + { + // Categorise by MMSI + QString category = MMSI::getCategory(mmsi); + if (category != "Ship") + { + m_category.insert(mmsi, category); + return; + } + + // Handle Search and Rescue Aircraft Report, where MMSI doesn't indicate SAR + if (message->m_id == 9) + { + m_category.insert(mmsi, "SAR"); + return; + } + + // If ship, determine Class A or B by message type + // See table 42 in ITU-R M.1371-5 + if ( (message->m_id <= 12) + || ((message->m_id >= 15) && (message->m_id <= 17)) + || ((message->m_id >= 20) && (message->m_id <= 23)) + || (message->m_id >= 25) + ) + { + m_category.insert(mmsi, "Class A Vessel"); + return; + } + + // Only Class B should transmit Part B static data reports + const AISStaticDataReport *staticDataReport = dynamic_cast(message); + if ( (message->m_id == 18) + || (message->m_id == 19) + || (staticDataReport && (staticDataReport->m_partNumber == 1)) + ) + { + m_category.insert(mmsi, "Class B Vessel"); + return; + } + // Other messages (such as safety) could be broadcast from either Class A or B + } +} + // Add row to table -void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot) +void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot, int totalSlots) { AISMessage *ais; @@ -174,7 +440,9 @@ void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& da QTableWidgetItem *dateItem = new QTableWidgetItem(); QTableWidgetItem *timeItem = new QTableWidgetItem(); QTableWidgetItem *mmsiItem = new QTableWidgetItem(); + QTableWidgetItem *countryItem = new QTableWidgetItem(); QTableWidgetItem *typeItem = new QTableWidgetItem(); + QTableWidgetItem *idItem = new QTableWidgetItem(); QTableWidgetItem *dataItem = new QTableWidgetItem(); QTableWidgetItem *nmeaItem = new QTableWidgetItem(); QTableWidgetItem *hexItem = new QTableWidgetItem(); @@ -182,24 +450,51 @@ void AISDemodGUI::messageReceived(const QByteArray& message, const QDateTime& da ui->messages->setItem(row, MESSAGE_COL_DATE, dateItem); ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); ui->messages->setItem(row, MESSAGE_COL_MMSI, mmsiItem); + ui->messages->setItem(row, MESSAGE_COL_COUNTRY, countryItem); ui->messages->setItem(row, MESSAGE_COL_TYPE, typeItem); + ui->messages->setItem(row, MESSAGE_COL_ID, idItem); ui->messages->setItem(row, MESSAGE_COL_DATA, dataItem); ui->messages->setItem(row, MESSAGE_COL_NMEA, nmeaItem); ui->messages->setItem(row, MESSAGE_COL_HEX, hexItem); ui->messages->setItem(row, MESSAGE_COL_SLOT, slotItem); dateItem->setText(dateTime.date().toString()); timeItem->setText(dateTime.time().toString()); - mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + QString mmsi = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); + mmsiItem->setText(mmsi); + QIcon *flag = MMSI::getFlagIcon(mmsi); + if (flag) + { + countryItem->setSizeHint(QSize(40, 20)); + countryItem->setIcon(*flag); + } typeItem->setText(ais->getType()); + idItem->setData(Qt::DisplayRole, ais->m_id); dataItem->setText(ais->toString()); nmeaItem->setText(ais->toNMEA()); hexItem->setText(ais->toHex()); slotItem->setData(Qt::DisplayRole, slot); - ui->messages->setSortingEnabled(true); - if (scrollToBottom) { - ui->messages->scrollToBottom(); + if (!m_loadingData) + { + filterRow(row); + ui->messages->setSortingEnabled(true); + if (scrollToBottom) { + ui->messages->scrollToBottom(); + } } - filterRow(row); + + updateCategory(mmsi, ais); + + // Update slot map + updateSlotMap(); + QColor color = getColor(mmsi); + m_painter.setPen(color); + for (int i = 0; i < totalSlots; i++) + { + int y = (slot + i) / m_slotMapWidth; + int x = (slot + i) % m_slotMapWidth; + m_painter.fillRect(x * 5 + 1, y * 5 + 1, 4, 4, color); + } + m_slotsUsed += totalSlots; delete ais; } @@ -221,7 +516,7 @@ bool AISDemodGUI::handleMessage(const Message& message) else if (AISDemod::MsgMessage::match(message)) { AISDemod::MsgMessage& report = (AISDemod::MsgMessage&) message; - messageReceived(report.getMessage(), report.getDateTime(), report.getSlot()); + messageReceived(report.getMessage(), report.getDateTime(), report.getSlot(), report.getSlots()); return true; } else if (DSPSignalNotification::match(message)) @@ -330,18 +625,6 @@ void AISDemodGUI::on_udpFormat_currentIndexChanged(int value) applySettings(); } -void AISDemodGUI::on_channel1_currentIndexChanged(int index) -{ - m_settings.m_scopeCh1 = index; - applySettings(); -} - -void AISDemodGUI::on_channel2_currentIndexChanged(int index) -{ - m_settings.m_scopeCh2 = index; - applySettings(); -} - void AISDemodGUI::on_messages_cellDoubleClicked(int row, int column) { // Get MMSI of message in row double clicked @@ -440,7 +723,9 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban m_deviceCenterFrequency(0), m_basebandSampleRate(1), m_doApplySettings(true), - m_tickCount(0) + m_tickCount(0), + m_loadingData(false), + m_slotsUsed(0) { setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/channelrx/demodais/readme.md"; @@ -458,8 +743,10 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban m_scopeVis = m_aisDemod->getScopeSink(); m_scopeVis->setGLScope(ui->glScope); + m_scopeVis->setNbStreams(AISDemodSettings::m_scopeStreams); ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + ui->scopeGUI->setStreams(QStringList({"IQ", "MagSq", "FM demod", "Gaussian", "RX buf", "Correlation", "Threshold met", "DC offset", "CRC"})); // Scope settings to display the IQ waveforms ui->scopeGUI->setPreTrigger(1); @@ -534,6 +821,16 @@ AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, Baseban ui->scopeContainer->setVisible(false); + // Create slot map image + m_image = QPixmap(m_slotMapWidth*5+1, m_slotMapHeight*5+1); + m_image.fill(Qt::transparent); + m_image.fill(Qt::black); + m_painter.begin(&m_image); + m_pen.setColor(Qt::white); + m_painter.setPen(m_pen); + ui->slotMap->setPixmap(m_image); + updateSlotMap(); + displaySettings(); makeUIConnections(); applySettings(true); @@ -612,12 +909,12 @@ void AISDemodGUI::displaySettings() ui->udpPort->setText(QString::number(m_settings.m_udpPort)); ui->udpFormat->setCurrentIndex((int)m_settings.m_udpFormat); - ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); - ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); - ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); ui->logEnable->setChecked(m_settings.m_logEnabled); + ui->showSlotMap->setChecked(m_settings.m_showSlotMap); + ui->slotMapWidget->setVisible(m_settings.m_showSlotMap); + // Order and size columns QHeaderView *header = ui->messages->horizontalHeader(); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) @@ -662,13 +959,22 @@ void AISDemodGUI::tick() (100.0f + powDbPeak) / 100.0f, nbMagsqSamples); - if (m_tickCount % 4 == 0) { + if (m_tickCount % 4 == 0) + { ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + updateSlotMap(); } m_tickCount++; } +void AISDemodGUI::on_showSlotMap_clicked(bool checked) +{ + ui->slotMapWidget->setVisible(checked); + m_settings.m_showSlotMap = checked; + applySettings(); +} + void AISDemodGUI::on_logEnable_clicked(bool checked) { m_settings.m_logEnabled = checked; @@ -704,6 +1010,8 @@ void AISDemodGUI::on_logOpen_clicked() QFile file(fileNames[0]); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QDateTime startTime = QDateTime::currentDateTime(); + m_loadingData = true; QTextStream in(&file); QString error; QHash colIndexes = CSV::readHeader(in, {"Date", "Time", "Data", "Slot"}, error); @@ -713,7 +1021,8 @@ void AISDemodGUI::on_logOpen_clicked() int timeCol = colIndexes.value("Time"); int dataCol = colIndexes.value("Data"); int slotCol = colIndexes.value("Slot"); - int maxCol = std::max({dateCol, timeCol, dataCol, slotCol}); + int slotsCol = colIndexes.contains("Slots") ? colIndexes.value("Slots") : -1; + int maxCol = std::max({dateCol, timeCol, dataCol, slotCol, slotsCol}); QMessageBox dialog(this); dialog.setText("Reading messages"); @@ -725,7 +1034,7 @@ void AISDemodGUI::on_logOpen_clicked() QStringList cols; QList aisPipes; - MainCore::instance()->getMessagePipes().getMessagePipes(this, "ais", aisPipes); + MainCore::instance()->getMessagePipes().getMessagePipes(m_aisDemod, "ais", aisPipes); while (!cancelled && CSV::readRow(in, &cols)) { @@ -736,9 +1045,10 @@ void AISDemodGUI::on_logOpen_clicked() QDateTime dateTime(date, time); QByteArray bytes = QByteArray::fromHex(cols[dataCol].toLatin1()); int slot = cols[slotCol].toInt(); + int totalSlots = slotsCol == -1 ? 1 : cols[slotsCol].toInt(); // Add to table - messageReceived(bytes, dateTime, slot); + messageReceived(bytes, dateTime, slot, totalSlots); // Forward to AIS feature for (const auto& pipe : aisPipes) @@ -764,6 +1074,10 @@ void AISDemodGUI::on_logOpen_clicked() { QMessageBox::critical(this, "AIS Demod", error); } + m_loadingData = false; + ui->messages->setSortingEnabled(true); + QDateTime finishTime = QDateTime::currentDateTime(); + qDebug() << "Read CSV in " << startTime.secsTo(finishTime); } else { @@ -789,8 +1103,7 @@ void AISDemodGUI::makeUIConnections() QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &AISDemodGUI::on_logEnable_clicked); QObject::connect(ui->logFilename, &QToolButton::clicked, this, &AISDemodGUI::on_logFilename_clicked); QObject::connect(ui->logOpen, &QToolButton::clicked, this, &AISDemodGUI::on_logOpen_clicked); - QObject::connect(ui->channel1, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel1_currentIndexChanged); - QObject::connect(ui->channel2, QOverload::of(&QComboBox::currentIndexChanged), this, &AISDemodGUI::on_channel2_currentIndexChanged); + QObject::connect(ui->showSlotMap, &ButtonSwitch::clicked, this, &AISDemodGUI::on_showSlotMap_clicked); } void AISDemodGUI::updateAbsoluteCenterFrequency() diff --git a/plugins/channelrx/demodais/aisdemodgui.h b/plugins/channelrx/demodais/aisdemodgui.h index 252fbcf98..61f18fa00 100644 --- a/plugins/channelrx/demodais/aisdemodgui.h +++ b/plugins/channelrx/demodais/aisdemodgui.h @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "channel/channelgui.h" #include "dsp/channelmarker.h" @@ -88,20 +90,39 @@ private: AISDemod* m_aisDemod; uint32_t m_tickCount; MessageQueue m_inputMessageQueue; + bool m_loadingData; QMenu *messagesMenu; // Column select context menu QMenu *copyMenu; + QPixmap m_image; + QPainter m_painter; + QPen m_pen; + QDateTime m_lastSlotMapUpdate; + int m_slotsUsed; + static QMutex m_colorMutex; + static QHash m_usedInFrame; // Indicates if MMSI used in current frame + static QHash m_slotMapColors; // MMSI to color + static QHash m_category; // MMSI to category + static QList m_colors; + static QDateTime m_lastColorUpdate; + static const int m_slotMapWidth = 50; // 2250 slots per minute + static const int m_slotMapHeight = 45; + explicit AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); virtual ~AISDemodGUI(); void blockApplySettings(bool block); void applySettings(bool force = false); void displaySettings(); - void messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot); + void messageReceived(const QByteArray& message, const QDateTime& dateTime, int slot, int slots); bool handleMessage(const Message& message); void makeUIConnections(); void updateAbsoluteCenterFrequency(); + void updateSlotMap(); + static void updateColors(); + static QColor getColor(const QString& mmsi); + static void updateCategory(const QString& mmsi, const AISMessage *message); void leaveEvent(QEvent*); void enterEvent(EnterEventType*); @@ -113,7 +134,9 @@ private: MESSAGE_COL_DATE, MESSAGE_COL_TIME, MESSAGE_COL_MMSI, + MESSAGE_COL_COUNTRY, MESSAGE_COL_TYPE, + MESSAGE_COL_ID, MESSAGE_COL_DATA, MESSAGE_COL_NMEA, MESSAGE_COL_HEX, @@ -131,12 +154,11 @@ private slots: void on_udpAddress_editingFinished(); void on_udpPort_editingFinished(); void on_udpFormat_currentIndexChanged(int value); - void on_channel1_currentIndexChanged(int index); - void on_channel2_currentIndexChanged(int index); void on_messages_cellDoubleClicked(int row, int column); void on_logEnable_clicked(bool checked=false); void on_logFilename_clicked(); void on_logOpen_clicked(); + void on_showSlotMap_clicked(bool checked=false); void filterRow(int row); void filter(); void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); diff --git a/plugins/channelrx/demodais/aisdemodgui.ui b/plugins/channelrx/demodais/aisdemodgui.ui index b2eced5b8..0453b5d38 100644 --- a/plugins/channelrx/demodais/aisdemodgui.ui +++ b/plugins/channelrx/demodais/aisdemodgui.ui @@ -7,7 +7,7 @@ 0 0 388 - 446 + 985 @@ -557,6 +557,32 @@ + + + + + 24 + 16777215 + + + + Show/hide slot map + + + + + + + :/constellation.png:/constellation.png + + + true + + + true + + + @@ -632,10 +658,10 @@ - 0 - 210 - 391 - 171 + 10 + 150 + 361 + 351 @@ -647,79 +673,225 @@ Received Messages - - - 2 - - - 3 - - - 3 - - - 3 - - - 3 - + - - - Received packets + + + Qt::Vertical - - QAbstractItemView::NoEditTriggers - - - - Date - - - - - Time - - - - - MMSI - - - - - Type - - - - - Data + + + + 0 + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 64 + 35 + + + + Slot map + + + QFrame::Panel + + + QFrame::Sunken + + + + + + true + + + Qt::AlignCenter + + + + + + + + + Used + + + + + + + Number of used slots in previous frame + + + true + + + + + + + Qt::Vertical + + + + + + + Free + + + + + + + Number of free slots in previous frame + + + true + + + + + + + Qt::Vertical + + + + + + + Utilisation + + + + + + + Slot utilisation in % for previous frame + + + 24 + + + + + + + + - Packet data as ASCII + Received packets - - - - NMEA + + QAbstractItemView::NoEditTriggers - - - - Hex - - - Packet data as hex - - - - - Slot - - - Time slot - - + + + Date + + + Date message was received + + + + + Time + + + Time message was received + + + + + MMSI + + + Maritime Mobile Service Identity + + + + + Country + + + Country with jurisdiction over station/vessel + + + + + Type + + + Message type + + + + + Id + + + Message type identifier + + + + + Data + + + Decoded message data + + + + + NMEA + + + Message data in NMEA format + + + + + Hex + + + Message data as hex + + + + + Slot + + + Time slot + + + @@ -728,7 +900,7 @@ 20 - 400 + 510 716 341 @@ -758,150 +930,6 @@ 3 - - - - - - Real - - - - - - - - 0 - 0 - - - - - I - - - - - Q - - - - - Mag Sq - - - - - FM demod - - - - - Gaussian - - - - - RX buf - - - - - Correlation - - - - - Threshold met - - - - - DC offset - - - - - CRC - - - - - - - - - 0 - 0 - - - - Imag - - - - - - - - 0 - 0 - - - - - I - - - - - Q - - - - - Mag Sq - - - - - FM demod - - - - - Gaussian - - - - - RX buf - - - - - Correlation - - - - - Threshold met - - - - - DC offset - - - - - CRC - - - - - - diff --git a/plugins/channelrx/demodais/aisdemodsettings.cpp b/plugins/channelrx/demodais/aisdemodsettings.cpp index b719dfb0e..3757aa862 100644 --- a/plugins/channelrx/demodais/aisdemodsettings.cpp +++ b/plugins/channelrx/demodais/aisdemodsettings.cpp @@ -43,10 +43,9 @@ void AISDemodSettings::resetToDefaults() m_udpAddress = "127.0.0.1"; m_udpPort = 9999; m_udpFormat = Binary; - m_scopeCh1 = 5; - m_scopeCh2 = 6; m_logFilename = "ais_log.csv"; m_logEnabled = false; + m_showSlotMap = false; m_rgbColor = QColor(102, 0, 0).rgb(); m_title = "AIS Demodulator"; m_streamIndex = 0; @@ -78,8 +77,6 @@ QByteArray AISDemodSettings::serialize() const s.writeString(7, m_udpAddress); s.writeU32(8, m_udpPort); s.writeS32(9, (int)m_udpFormat); - s.writeS32(10, m_scopeCh1); - s.writeS32(11, m_scopeCh2); s.writeU32(12, m_rgbColor); s.writeString(13, m_title); @@ -105,6 +102,7 @@ QByteArray AISDemodSettings::serialize() const s.writeS32(26, m_workspaceIndex); s.writeBlob(27, m_geometryBytes); s.writeBool(28, m_hidden); + s.writeBool(29, m_showSlotMap); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) s.writeS32(100 + i, m_messageColumnIndexes[i]); @@ -146,8 +144,6 @@ bool AISDemodSettings::deserialize(const QByteArray& data) } d.readS32(9, (int *)&m_udpFormat, (int)Binary); - d.readS32(10, &m_scopeCh1, 0); - d.readS32(11, &m_scopeCh2, 0); d.readU32(12, &m_rgbColor, QColor(102, 0, 0).rgb()); d.readString(13, &m_title, "AIS Demodulator"); @@ -192,6 +188,7 @@ bool AISDemodSettings::deserialize(const QByteArray& data) d.readS32(26, &m_workspaceIndex, 0); d.readBlob(27, &m_geometryBytes); d.readBool(28, &m_hidden, false); + d.readBool(29, &m_showSlotMap, false); for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) { d.readS32(100 + i, &m_messageColumnIndexes[i], i); diff --git a/plugins/channelrx/demodais/aisdemodsettings.h b/plugins/channelrx/demodais/aisdemodsettings.h index 9e0ca9b86..eba2418b0 100644 --- a/plugins/channelrx/demodais/aisdemodsettings.h +++ b/plugins/channelrx/demodais/aisdemodsettings.h @@ -27,7 +27,7 @@ class Serializable; // Number of columns in the tables -#define AISDEMOD_MESSAGE_COLUMNS 8 +#define AISDEMOD_MESSAGE_COLUMNS 10 struct AISDemodSettings { @@ -44,11 +44,10 @@ struct AISDemodSettings Binary, NMEA } m_udpFormat; - int m_scopeCh1; - int m_scopeCh2; QString m_logFilename; bool m_logEnabled; + bool m_showSlotMap; quint32 m_rgbColor; QString m_title; @@ -69,6 +68,7 @@ struct AISDemodSettings int m_messageColumnSizes[AISDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table static const int AISDEMOD_CHANNEL_SAMPLE_RATE = 57600; //!< 6x 9600 baud rate (use even multiple so Gausian filter has odd number of taps) + static const int m_scopeStreams = 9; AISDemodSettings(); void resetToDefaults(); diff --git a/plugins/channelrx/demodais/aisdemodsink.cpp b/plugins/channelrx/demodais/aisdemodsink.cpp index bd567290e..ca1c74323 100644 --- a/plugins/channelrx/demodais/aisdemodsink.cpp +++ b/plugins/channelrx/demodais/aisdemodsink.cpp @@ -47,7 +47,9 @@ AISDemodSink::AISDemodSink(AISDemod *aisDemod) : m_demodBuffer.resize(1<<12); m_demodBufferFill = 0; - m_sampleBuffer.resize(m_sampleBufferSize); + for (int i = 0; i < AISDemodSettings::m_scopeStreams; i++) { + m_sampleBuffer[i].resize(m_sampleBufferSize); + } applySettings(m_settings, true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); @@ -59,18 +61,28 @@ AISDemodSink::~AISDemodSink() delete[] m_train; } -void AISDemodSink::sampleToScope(Complex sample) +void AISDemodSink::sampleToScope(Complex sample, Real magsq, Real fmDemod, Real filt, Real rxBuf, Real corr, Real thresholdMet, Real dcOffset, Real crcValid) { if (m_scopeSink) { - Real r = std::real(sample) * SDR_RX_SCALEF; - Real i = std::imag(sample) * SDR_RX_SCALEF; - m_sampleBuffer[m_sampleBufferIndex++] = Sample(r, i); - + m_sampleBuffer[0][m_sampleBufferIndex] = sample; + m_sampleBuffer[1][m_sampleBufferIndex] = Complex(m_magsq, 0.0f); + m_sampleBuffer[2][m_sampleBufferIndex] = Complex(fmDemod, 0.0f); + m_sampleBuffer[3][m_sampleBufferIndex] = Complex(filt, 0.0f); + m_sampleBuffer[4][m_sampleBufferIndex] = Complex(rxBuf, 0.0f); + m_sampleBuffer[5][m_sampleBufferIndex] = Complex(corr, 0.0f); + m_sampleBuffer[6][m_sampleBufferIndex] = Complex(thresholdMet, 0.0f); + m_sampleBuffer[7][m_sampleBufferIndex] = Complex(dcOffset, 0.0f); + m_sampleBuffer[8][m_sampleBufferIndex] = Complex(crcValid, 0.0f); + m_sampleBufferIndex++; if (m_sampleBufferIndex == m_sampleBufferSize) { - std::vector vbegin; - vbegin.push_back(m_sampleBuffer.begin()); + std::vector vbegin; + + for (int i = 0; i < AISDemodSettings::m_scopeStreams; i++) { + vbegin.push_back(m_sampleBuffer[i].begin()); + } + m_scopeSink->feed(vbegin, m_sampleBufferSize); m_sampleBufferIndex = 0; } @@ -252,10 +264,13 @@ void AISDemodSink::processOneSample(Complex &ci) // This is unlikely to be accurate in absolute terms, given we don't know latency from SDR or buffering within SDRangel // But can be used to get an idea of congestion QDateTime currentTime = QDateTime::currentDateTime(); - QDateTime startDateTime = currentTime.addMSecs(-(totalBitCount + 8 + 24 + 8) * (1000.0 / m_settings.m_baud)); // Add ramp up, preamble and start-flag + int txTimeMs = (totalBitCount + 8 + 24 + 8) * (1000.0 / m_settings.m_baud); // Add ramp up, preamble and start-flag + QDateTime startDateTime = currentTime.addMSecs(-txTimeMs); int ms = startDateTime.time().second() * 1000 + startDateTime.time().msec(); - int slot = ms / 26.67; // 2250 slots per minute, 26ms per slot - AISDemod::MsgMessage *msg = AISDemod::MsgMessage::create(rxPacket, currentTime, slot); + float slotTime = 60.0f * 1000.0f / 2250.0f; // 2250 slots per minute, 26.6ms per slot + int slot = ms / slotTime; + int totalSlots = std::ceil(txTimeMs / slotTime); + AISDemod::MsgMessage *msg = AISDemod::MsgMessage::create(rxPacket, currentTime, slot, totalSlots); getMessageQueueToChannel()->push(msg); } @@ -318,74 +333,7 @@ void AISDemodSink::processOneSample(Complex &ci) } // Select signals to feed to scope - Complex scopeSample; - switch (m_settings.m_scopeCh1) - { - case 0: - scopeSample.real(ci.real() / SDR_RX_SCALEF); - break; - case 1: - scopeSample.real(ci.imag() / SDR_RX_SCALEF); - break; - case 2: - scopeSample.real(magsq); - break; - case 3: - scopeSample.real(fmDemod); - break; - case 4: - scopeSample.real(filt); - break; - case 5: - scopeSample.real(m_rxBuf[m_rxBufIdx]); - break; - case 6: - scopeSample.real(corr / 100.0); - break; - case 7: - scopeSample.real(thresholdMet); - break; - case 8: - scopeSample.real(dcOffset); - break; - case 9: - scopeSample.real(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); - break; - } - switch (m_settings.m_scopeCh2) - { - case 0: - scopeSample.imag(ci.real() / SDR_RX_SCALEF); - break; - case 1: - scopeSample.imag(ci.imag() / SDR_RX_SCALEF); - break; - case 2: - scopeSample.imag(magsq); - break; - case 3: - scopeSample.imag(fmDemod); - break; - case 4: - scopeSample.imag(filt); - break; - case 5: - scopeSample.imag(m_rxBuf[m_rxBufIdx]); - break; - case 6: - scopeSample.imag(corr / 100.0); - break; - case 7: - scopeSample.imag(thresholdMet); - break; - case 8: - scopeSample.imag(dcOffset); - break; - case 9: - scopeSample.imag(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); - break; - } - sampleToScope(scopeSample); + sampleToScope(ci / SDR_RX_SCALEF, magsq, fmDemod, filt, m_rxBuf[m_rxBufIdx], corr / 100.0, thresholdMet, dcOffset, scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); // Send demod signal to Demod Analzyer feature m_demodBuffer[m_demodBufferFill++] = fmDemod * std::numeric_limits::max(); diff --git a/plugins/channelrx/demodais/aisdemodsink.h b/plugins/channelrx/demodais/aisdemodsink.h index 2a93c9570..b6bbbe41b 100644 --- a/plugins/channelrx/demodais/aisdemodsink.h +++ b/plugins/channelrx/demodais/aisdemodsink.h @@ -128,13 +128,13 @@ private: QVector m_demodBuffer; int m_demodBufferFill; - SampleVector m_sampleBuffer; + ComplexVector m_sampleBuffer[AISDemodSettings::m_scopeStreams]; static const int m_sampleBufferSize = AISDemodSettings::AISDEMOD_CHANNEL_SAMPLE_RATE / 20; int m_sampleBufferIndex; void processOneSample(Complex &ci); MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } - void sampleToScope(Complex sample); + void sampleToScope(Complex sample, Real magsq, Real fmDemod, Real filt, Real rxBuf, Real corr, Real thresholdMet, Real dcOffset, Real crcValid); }; #endif // INCLUDE_AISDEMODSINK_H diff --git a/plugins/channelrx/demodais/readme.md b/plugins/channelrx/demodais/readme.md index 97d47e1ca..621e14811 100644 --- a/plugins/channelrx/demodais/readme.md +++ b/plugins/channelrx/demodais/readme.md @@ -82,19 +82,40 @@ Click to specify the name of the .csv file which received AIS messages are logge Click to specify a previously written AIS .csv log file, which is read and used to update the table. +

Slot Map

+ +AIS uses TMDA (Time Division Multiple Access), whereby each one minute frame is divided into 2,250 26.6ms slots. +The slot map shows which slots within a frame are used. The slot map is drawn as bitmap of 50x45 pixels. + +![AIS Slot Map](../../../doc/img/AISDemod_plugin_slotmap.png) + +Slots are by category: + +* Red: Class A Mobile +* Blue: Class B Mobile +* Green: Base Station +* Yellow: AtoN (Aid-to-Navigation) +* Cyan: Search and Rescue +* Magenta: Other (Man overboard / EPIRB / AMRD). + +Due to SDR to SDRangel latency being unknown, the slot map is likely to have some offset, as slot timing is calculated based on the time messages +are demodulated in SDRangel. +

Received Messages Table

The received messages table displays information about each AIS message received. Only messages with valid CRCs are displayed. -![AIS Demodulator plugin GUI](../../../doc/img/AISDemod_plugin_messages.png) +![AIS Received Messages Table](../../../doc/img/AISDemod_plugin_messages.png) * Date - The date the message was received. * Time - The time the message was received. * MMSI - The Maritime Mobile Service Identity number of the source of the message. Double clicking on this column will search for the MMSI on https://www.vesselfinder.com/ +* Country - The country with jurisdiction over station/vessel. * Type - The type of AIS message. E.g. Position report, Base station report or Ship static and voyage related data. +* Id - Message type numeric identifier. * Data - A textual decode of the message displaying the most interesting fields. * NMEA - The message in NMEA format. * Hex - The message in hex format. -* Slot - Time slot (0-2249). Due to SDR to SDRangel latency being unknown, this is likely to have some offset. +* Slot - Time slot (0-2249). Right clicking on the table header allows you to select which columns to show. The columns can be reordered by left clicking and dragging the column header. Right clicking on an item in the table allows you to copy the value to the clipboard. diff --git a/plugins/channelrx/demoddab/dabdemodgui.cpp b/plugins/channelrx/demoddab/dabdemodgui.cpp index efdc47ea4..15587e8ae 100644 --- a/plugins/channelrx/demoddab/dabdemodgui.cpp +++ b/plugins/channelrx/demoddab/dabdemodgui.cpp @@ -191,8 +191,8 @@ void DABDemodGUI::addProgramName(const DABDemod::MsgDABProgramName& program) frequencyItem->setData(Qt::UserRole, 0.0); } ensembleItem->setText(ui->ensemble->text()); - ui->programs->setSortingEnabled(true); filterRow(row); + ui->programs->setSortingEnabled(true); } // Tune to the selected program diff --git a/plugins/channelrx/demoddsc/CMakeLists.txt b/plugins/channelrx/demoddsc/CMakeLists.txt new file mode 100644 index 000000000..ed2164296 --- /dev/null +++ b/plugins/channelrx/demoddsc/CMakeLists.txt @@ -0,0 +1,63 @@ +project(demoddsc) + +set(demoddsc_SOURCES + dscdemod.cpp + dscdemodsettings.cpp + dscdemodbaseband.cpp + dscdemodsink.cpp + dscdemodplugin.cpp + dscdemodwebapiadapter.cpp +) + +set(demoddsc_HEADERS + dscdemod.h + dscdemodsettings.h + dscdemodbaseband.h + dscdemodsink.h + dscdemodplugin.h + dscdemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demoddsc_SOURCES + ${demoddsc_SOURCES} + dscdemodgui.cpp + dscdemodgui.ui + ) + set(demoddsc_HEADERS + ${demoddsc_HEADERS} + dscdemodgui.h + ) + + set(TARGET_NAME demoddsc) + set(TARGET_LIB "Qt::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demoddscsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demoddsc_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) + +# Install debug symbols +if (WIN32) + install(FILES $ CONFIGURATIONS Debug RelWithDebInfo DESTINATION ${INSTALL_FOLDER} ) +endif() diff --git a/plugins/channelrx/demoddsc/dscdemod.cpp b/plugins/channelrx/demoddsc/dscdemod.cpp new file mode 100644 index 000000000..7ba80a471 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemod.cpp @@ -0,0 +1,763 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "dscdemod.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGWorkspaceInfo.h" +#include "SWGDSCDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGMapItem.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "settings/serializable.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(DSCDemod::MsgConfigureDSCDemod, Message) +MESSAGE_CLASS_DEFINITION(DSCDemod::MsgMessage, Message) + +const char * const DSCDemod::m_channelIdURI = "sdrangel.channel.dscdemod"; +const char * const DSCDemod::m_channelId = "DSCDemod"; + +DSCDemod::DSCDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new DSCDemodBaseband(this); + m_basebandSink->setMessageQueueToChannel(getInputMessageQueue()); + m_basebandSink->setChannel(this); + m_basebandSink->moveToThread(&m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + QObject::connect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &DSCDemod::networkManagerFinished + ); + QObject::connect( + this, + &ChannelAPI::indexInDeviceSetChanged, + this, + &DSCDemod::handleIndexInDeviceSetChanged + ); +} + +DSCDemod::~DSCDemod() +{ + qDebug("DSCDemod::~DSCDemod"); + QObject::disconnect( + m_networkManager, + &QNetworkAccessManager::finished, + this, + &DSCDemod::networkManagerFinished + ); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +void DSCDemod::setDeviceAPI(DeviceAPI *deviceAPI) +{ + if (deviceAPI != m_deviceAPI) + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + m_deviceAPI = deviceAPI; + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + } +} + +uint32_t DSCDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void DSCDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void DSCDemod::start() +{ + qDebug("DSCDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + DSCDemodBaseband::MsgConfigureDSCDemodBaseband *msg = DSCDemodBaseband::MsgConfigureDSCDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void DSCDemod::stop() +{ + qDebug("DSCDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool DSCDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureDSCDemod::match(cmd)) + { + MsgConfigureDSCDemod& cfg = (MsgConfigureDSCDemod&) cmd; + qDebug() << "DSCDemod::handleMessage: MsgConfigureDSCDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "DSCDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + // Forward to GUI if any + if (m_guiMessageQueue) { + m_guiMessageQueue->push(new DSPSignalNotification(notif)); + } + + return true; + } + else if (DSCDemod::MsgMessage::match(cmd)) + { + // Forward to GUI + DSCDemod::MsgMessage& report = (DSCDemod::MsgMessage&)cmd; + if (getMessageQueueToGUI()) + { + DSCDemod::MsgMessage *msg = new DSCDemod::MsgMessage(report); + getMessageQueueToGUI()->push(msg); + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + //qDebug() << "Forwarding to " << m_settings.m_udpAddress << ":" << m_settings.m_udpPort; + QByteArray bytes = report.getMessage().m_data; + m_udpSocket.writeDatagram(bytes, bytes.size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + + // Forward valid messages to yaddnet.org + if (m_settings.m_feed) + { + const DSCMessage& message = report.getMessage(); + if (message.m_valid) + { + QString yaddnet = message.toYaddNetFormat(MainCore::instance()->getSettings().getStationName(), m_centerFrequency + m_settings.m_inputFrequencyOffset); + qDebug() << "Forwarding to yaddnet.org " << yaddnet; + QByteArray bytes = yaddnet.toLocal8Bit(); + QHostInfo info = QHostInfo::fromName("www.yaddnet.org"); + if (info.addresses().size() > 0) + { + qint64 sent = m_udpSocket.writeDatagram(bytes.data(), bytes.size(), info.addresses()[0], 50666); + if (bytes.size() != sent) { + qDebug() << "Failed to send datagram to www.yaddnet.org. Sent " << sent << " of " << bytes.size() << " Error " << m_udpSocket.error(); + } + } + else + { + qDebug() << "Can't get IP address for www.yaddnet.org"; + } + } + } + + // Write to log file + if (m_logFile.isOpen()) + { + const DSCMessage &dscMsg = report.getMessage(); + + if (dscMsg.m_valid) + { + m_logStream + << dscMsg.m_dateTime.date().toString() << "," + << dscMsg.m_dateTime.time().toString() << "," + << dscMsg.formatSpecifier() << "," + << dscMsg.m_selfId << "," + << dscMsg.m_address << "," + << dscMsg.m_data.toHex() << "," + << report.getErrors() << "," + << report.getRSSI() + << "\n"; + } + } + + return true; + } + else if (MainCore::MsgChannelDemodQuery::match(cmd)) + { + qDebug() << "DSCDemod::handleMessage: MsgChannelDemodQuery"; + sendSampleRateToDemodAnalyzer(); + + return true; + } + else + { + return false; + } +} + +ScopeVis *DSCDemod::getScopeSink() +{ + return m_basebandSink->getScopeSink(); +} + +void DSCDemod::setCenterFrequency(qint64 frequency) +{ + DSCDemodSettings settings = m_settings; + settings.m_inputFrequencyOffset = frequency; + applySettings(settings, false); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureDSCDemod *msgToGUI = MsgConfigureDSCDemod::create(settings, false); + m_guiMessageQueue->push(msgToGUI); + } +} + +void DSCDemod::applySettings(const DSCDemodSettings& settings, bool force) +{ + qDebug() << "DSCDemod::applySettings:" + << " m_logEnabled: " << settings.m_logEnabled + << " m_logFilename: " << settings.m_logFilename + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + if ((settings.m_filterInvalid != m_settings.m_filterInvalid) || force) { + reverseAPIKeys.append("filterInvalid"); + } + if ((settings.m_filterColumn != m_settings.m_filterColumn) || force) { + reverseAPIKeys.append("filterColumn"); + } + if ((settings.m_filter != m_settings.m_filter) || force) { + reverseAPIKeys.append("filter"); + } + if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) { + reverseAPIKeys.append("udpEnabled"); + } + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { + reverseAPIKeys.append("udpAddress"); + } + if ((settings.m_udpPort != m_settings.m_udpPort) || force) { + reverseAPIKeys.append("udpPort"); + } + if ((settings.m_logFilename != m_settings.m_logFilename) || force) { + reverseAPIKeys.append("logFilename"); + } + if ((settings.m_logEnabled != m_settings.m_logEnabled) || force) { + reverseAPIKeys.append("logEnabled"); + } + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSink(this, settings.m_streamIndex); + m_deviceAPI->addChannelSinkAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + DSCDemodBaseband::MsgConfigureDSCDemodBaseband *msg = DSCDemodBaseband::MsgConfigureDSCDemodBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + if ((settings.m_logEnabled != m_settings.m_logEnabled) + || (settings.m_logFilename != m_settings.m_logFilename) + || force) + { + if (m_logFile.isOpen()) + { + m_logStream.flush(); + m_logFile.close(); + } + if (settings.m_logEnabled && !settings.m_logFilename.isEmpty()) + { + m_logFile.setFileName(settings.m_logFilename); + if (m_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) + { + qDebug() << "DSCDemod::applySettings - Logging to: " << settings.m_logFilename; + bool newFile = m_logFile.size() == 0; + m_logStream.setDevice(&m_logFile); + if (newFile) + { + // Write header + m_logStream << "Date,Time,Format,From,To,Message,Errors,RSSI\n"; + } + } + else + { + qDebug() << "DSCDemod::applySettings - Unable to open log file: " << settings.m_logFilename; + } + } + } + + m_settings = settings; +} + +void DSCDemod::sendSampleRateToDemodAnalyzer() +{ + QList pipes; + MainCore::instance()->getMessagePipes().getMessagePipes(this, "reportdemod", pipes); + + if (pipes.size() > 0) + { + for (const auto& pipe : pipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create( + this, + DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE + ); + messageQueue->push(msg); + } + } +} + +QByteArray DSCDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool DSCDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int DSCDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings()); + response.getDscDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int DSCDemod::webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setIndex(m_settings.m_workspaceIndex); + return 200; +} + +int DSCDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + DSCDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureDSCDemod *msg = MsgConfigureDSCDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("DSCDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureDSCDemod *msgToGUI = MsgConfigureDSCDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +int DSCDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setDscDemodReport(new SWGSDRangel::SWGDSCDemodReport()); + response.getDscDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void DSCDemod::webapiUpdateChannelSettings( + DSCDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getDscDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getDscDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("filterInvalid")) { + settings.m_filterInvalid = response.getDscDemodSettings()->getFilterInvalid(); + } + if (channelSettingsKeys.contains("filterColumn")) { + settings.m_filterColumn = response.getDscDemodSettings()->getFilterColumn(); + } + if (channelSettingsKeys.contains("filter")) { + settings.m_filter = *response.getDscDemodSettings()->getFilter(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getDscDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getDscDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getDscDemodSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("logFilename")) { + settings.m_logFilename = *response.getAdsbDemodSettings()->getLogFilename(); + } + if (channelSettingsKeys.contains("logEnabled")) { + settings.m_logEnabled = response.getAdsbDemodSettings()->getLogEnabled(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getDscDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getDscDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getDscDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getDscDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getDscDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getDscDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getDscDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getDscDemodSettings()->getReverseApiChannelIndex(); + } + if (settings.m_scopeGUI && channelSettingsKeys.contains("scopeConfig")) { + settings.m_scopeGUI->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getScopeConfig()); + } + if (settings.m_channelMarker && channelSettingsKeys.contains("channelMarker")) { + settings.m_channelMarker->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getChannelMarker()); + } + if (settings.m_rollupState && channelSettingsKeys.contains("rollupState")) { + settings.m_rollupState->updateFrom(channelSettingsKeys, response.getDscDemodSettings()->getRollupState()); + } +} + +void DSCDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const DSCDemodSettings& settings) +{ + response.getDscDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getDscDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getDscDemodSettings()->setFilterInvalid(settings.m_filterInvalid); + response.getDscDemodSettings()->setFilterColumn(settings.m_filterColumn); + response.getDscDemodSettings()->setFilter(new QString(settings.m_filter)); + response.getDscDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getDscDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getDscDemodSettings()->setUdpPort(settings.m_udpPort); + response.getDscDemodSettings()->setLogFilename(new QString(settings.m_logFilename)); + response.getDscDemodSettings()->setLogEnabled(settings.m_logEnabled); + + response.getDscDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getDscDemodSettings()->getTitle()) { + *response.getDscDemodSettings()->getTitle() = settings.m_title; + } else { + response.getDscDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getDscDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getDscDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getDscDemodSettings()->getReverseApiAddress()) { + *response.getDscDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getDscDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getDscDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getDscDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getDscDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + if (settings.m_scopeGUI) + { + if (response.getDscDemodSettings()->getScopeConfig()) + { + settings.m_scopeGUI->formatTo(response.getDscDemodSettings()->getScopeConfig()); + } + else + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + response.getDscDemodSettings()->setScopeConfig(swgGLScope); + } + } + if (settings.m_channelMarker) + { + if (response.getDscDemodSettings()->getChannelMarker()) + { + settings.m_channelMarker->formatTo(response.getDscDemodSettings()->getChannelMarker()); + } + else + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + response.getDscDemodSettings()->setChannelMarker(swgChannelMarker); + } + } + + if (settings.m_rollupState) + { + if (response.getDscDemodSettings()->getRollupState()) + { + settings.m_rollupState->formatTo(response.getDscDemodSettings()->getRollupState()); + } + else + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + response.getDscDemodSettings()->setRollupState(swgRollupState); + } + } +} + +void DSCDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getDscDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getDscDemodReport()->setChannelSampleRate(m_basebandSink->getChannelSampleRate()); +} + +void DSCDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const DSCDemodSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void DSCDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const DSCDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("DSCDemod")); + swgChannelSettings->setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings()); + SWGSDRangel::SWGDSCDemodSettings *swgDSCDemodSettings = swgChannelSettings->getDscDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgDSCDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgDSCDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("filterInvalid") || force) { + swgDSCDemodSettings->setFilterInvalid(settings.m_filterInvalid); + } + if (channelSettingsKeys.contains("filterColumn") || force) { + swgDSCDemodSettings->setFilterColumn(settings.m_filterColumn); + } + if (channelSettingsKeys.contains("filter") || force) { + swgDSCDemodSettings->setFilter(new QString(settings.m_filter)); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgDSCDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgDSCDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgDSCDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("logFilename") || force) { + swgDSCDemodSettings->setLogFilename(new QString(settings.m_logFilename)); + } + if (channelSettingsKeys.contains("logEnabled") || force) { + swgDSCDemodSettings->setLogEnabled(settings.m_logEnabled); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgDSCDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgDSCDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgDSCDemodSettings->setStreamIndex(settings.m_streamIndex); + } + + if (settings.m_scopeGUI && (channelSettingsKeys.contains("scopeConfig") || force)) + { + SWGSDRangel::SWGGLScope *swgGLScope = new SWGSDRangel::SWGGLScope(); + settings.m_scopeGUI->formatTo(swgGLScope); + swgDSCDemodSettings->setScopeConfig(swgGLScope); + } + + if (settings.m_channelMarker && (channelSettingsKeys.contains("channelMarker") || force)) + { + SWGSDRangel::SWGChannelMarker *swgChannelMarker = new SWGSDRangel::SWGChannelMarker(); + settings.m_channelMarker->formatTo(swgChannelMarker); + swgDSCDemodSettings->setChannelMarker(swgChannelMarker); + } + + if (settings.m_rollupState && (channelSettingsKeys.contains("rollupState") || force)) + { + SWGSDRangel::SWGRollupState *swgRollupState = new SWGSDRangel::SWGRollupState(); + settings.m_rollupState->formatTo(swgRollupState); + swgDSCDemodSettings->setRollupState(swgRollupState); + } +} + +void DSCDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "DSCDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("DSCDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +void DSCDemod::handleIndexInDeviceSetChanged(int index) +{ + if (index < 0) { + return; + } + + QString fifoLabel = QString("%1 [%2:%3]") + .arg(m_channelId) + .arg(m_deviceAPI->getDeviceSetIndex()) + .arg(index); + m_basebandSink->setFifoLabel(fifoLabel); +} diff --git a/plugins/channelrx/demoddsc/dscdemod.h b/plugins/channelrx/demoddsc/dscdemod.h new file mode 100644 index 000000000..bca91b84f --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemod.h @@ -0,0 +1,205 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMOD_H +#define INCLUDE_DSCDEMOD_H + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" +#include "util/navtex.h" + +#include "dscdemodbaseband.h" +#include "dscdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; +class ScopeVis; + +class DSCDemod : public BasebandSampleSink, public ChannelAPI { +public: + class MsgConfigureDSCDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const DSCDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureDSCDemod* create(const DSCDemodSettings& settings, bool force) + { + return new MsgConfigureDSCDemod(settings, force); + } + + private: + DSCDemodSettings m_settings; + bool m_force; + + MsgConfigureDSCDemod(const DSCDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const DSCMessage& getMessage() const { return m_message; } + int getErrors() const { return m_errors; } + float getRSSI() const { return m_rssi; } + + static MsgMessage* create(const DSCMessage& message, int errors, float rssi) + { + return new MsgMessage(message, errors, rssi); + } + + private: + DSCMessage m_message; + int m_errors; + float m_rssi; + + MsgMessage(const DSCMessage& message, int errors, float rssi) : + m_message(message), + m_errors(errors), + m_rssi(rssi) + {} + }; + + DSCDemod(DeviceAPI *deviceAPI); + virtual ~DSCDemod(); + virtual void destroy() { delete this; } + virtual void setDeviceAPI(DeviceAPI *deviceAPI); + virtual DeviceAPI *getDeviceAPI() { return m_deviceAPI; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual void pushMessage(Message *msg) { m_inputMessageQueue.push(msg); } + virtual QString getSinkName() { return objectName(); } + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual QString getIdentifier() const { return objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + virtual void setCenterFrequency(qint64 frequency); + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return 0; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiWorkspaceGet( + SWGSDRangel::SWGWorkspaceInfo& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const DSCDemodSettings& settings); + + static void webapiUpdateChannelSettings( + DSCDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + ScopeVis *getScopeSink(); + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } +/* void setMessageQueueToGUI(MessageQueue* queue) override { + ChannelAPI::setMessageQueueToGUI(queue); + m_basebandSink->setMessageQueueToGUI(queue); + }*/ + + uint32_t getNumberOfDeviceStreams() const; + + static const char * const m_channelIdURI; + static const char * const m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + DSCDemodBaseband* m_basebandSink; + DSCDemodSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + QUdpSocket m_udpSocket; + QFile m_logFile; + QTextStream m_logStream; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + virtual bool handleMessage(const Message& cmd); + void applySettings(const DSCDemodSettings& settings, bool force = false); + void sendSampleRateToDemodAnalyzer(); + void webapiReverseSendSettings(QList& channelSettingsKeys, const DSCDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const DSCDemodSettings& settings, + bool force + ); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void handleIndexInDeviceSetChanged(int index); + +}; + +#endif // INCLUDE_DSCDEMOD_H + diff --git a/plugins/channelrx/demoddsc/dscdemodbaseband.cpp b/plugins/channelrx/demoddsc/dscdemodbaseband.cpp new file mode 100644 index 000000000..c154aa2a0 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodbaseband.cpp @@ -0,0 +1,182 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "dscdemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(DSCDemodBaseband::MsgConfigureDSCDemodBaseband, Message) + +DSCDemodBaseband::DSCDemodBaseband(DSCDemod *packetDemod) : + m_sink(packetDemod), + m_running(false) +{ + qDebug("DSCDemodBaseband::DSCDemodBaseband"); + + m_sink.setScopeSink(&m_scopeSink); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +DSCDemodBaseband::~DSCDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void DSCDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void DSCDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &DSCDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void DSCDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &DSCDemodBaseband::handleData + ); + m_running = false; +} + +void DSCDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void DSCDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void DSCDemodBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + SampleVector::iterator part1begin; + SampleVector::iterator part1end; + SampleVector::iterator part2begin; + SampleVector::iterator part2end; + + std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end); + + // first part of FIFO data + if (part1begin != part1end) { + m_channelizer->feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + m_channelizer->feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void DSCDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool DSCDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureDSCDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureDSCDemodBaseband& cfg = (MsgConfigureDSCDemodBaseband&) cmd; + qDebug() << "DSCDemodBaseband::handleMessage: MsgConfigureDSCDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "DSCDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + // We can run with very slow sample rate (E.g. 4k), but we don't want FIFO getting too small + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(std::max(notif.getSampleRate(), 48000))); + + return true; + } + else + { + return false; + } +} + +void DSCDemodBaseband::applySettings(const DSCDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +int DSCDemodBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} + +void DSCDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} + diff --git a/plugins/channelrx/demoddsc/dscdemodbaseband.h b/plugins/channelrx/demoddsc/dscdemodbaseband.h new file mode 100644 index 000000000..c1dc622d4 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodbaseband.h @@ -0,0 +1,103 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODBASEBAND_H +#define INCLUDE_DSCDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "dsp/scopevis.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "dscdemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class DSCDemod; +class ScopeVis; + +class DSCDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureDSCDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const DSCDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureDSCDemodBaseband* create(const DSCDemodSettings& settings, bool force) + { + return new MsgConfigureDSCDemodBaseband(settings, force); + } + + private: + DSCDemodSettings m_settings; + bool m_force; + + MsgConfigureDSCDemodBaseband(const DSCDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + DSCDemodBaseband(DSCDemod *packetDemod); + ~DSCDemodBaseband(); + void reset(); + void startWork(); + void stopWork(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_sink.getMagSqLevels(avg, peak, nbSamples); + } + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setBasebandSampleRate(int sampleRate); + int getChannelSampleRate() const; + ScopeVis *getScopeSink() { return &m_scopeSink; } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + void setFifoLabel(const QString& label) { m_sampleFifo.setLabel(label); } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + DSCDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + DSCDemodSettings m_settings; + ScopeVis m_scopeSink; + bool m_running; + QRecursiveMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(DSCDemodSink *sink); + void applySettings(const DSCDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_DSCDEMODBASEBAND_H + diff --git a/plugins/channelrx/demoddsc/dscdemodgui.cpp b/plugins/channelrx/demoddsc/dscdemodgui.cpp new file mode 100644 index 000000000..9f4e3b400 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.cpp @@ -0,0 +1,1235 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dscdemodgui.h" + +#include "device/deviceuiset.h" +#include "device/deviceset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/devicesamplesource.h" +#include "dsp/dspdevicesourceengine.h" +#include "ui_dscdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/csv.h" +#include "util/db.h" +#include "util/mmsi.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "gui/decimaldelegate.h" +#include "dsp/dspengine.h" +#include "dsp/glscopesettings.h" +#include "gui/crightclickenabler.h" +#include "gui/tabletapandhold.h" +#include "gui/dialogpositioner.h" +#include "gui/frequencydelegate.h" +#include "channel/channelwebapiutils.h" +#include "feature/featurewebapiutils.h" +#include "maincore.h" + +#include "dscdemod.h" +#include "dscdemodsink.h" + +#include "SWGMapItem.h" + +void DSCDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing spaces are for sort arrow + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + ui->messages->setItem(row, MESSAGE_COL_RX_DATE, new QTableWidgetItem("15/04/2016-")); + ui->messages->setItem(row, MESSAGE_COL_RX_TIME, new QTableWidgetItem("10:17")); + ui->messages->setItem(row, MESSAGE_COL_FORMAT, new QTableWidgetItem("Selective call")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_COUNTRY, new QTableWidgetItem("flag")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_TYPE, new QTableWidgetItem("Coast")); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_NAME, new QTableWidgetItem("A ships name")); + ui->messages->setItem(row, MESSAGE_COL_CATEGORY, new QTableWidgetItem("Distress")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_COUNTRY, new QTableWidgetItem("flag")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_TYPE, new QTableWidgetItem("Coast")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_NAME, new QTableWidgetItem("A ships name")); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_RANGE, new QTableWidgetItem("3000.0")); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_1, new QTableWidgetItem("No information")); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_2, new QTableWidgetItem("No information")); + ui->messages->setItem(row, MESSAGE_COL_RX, new QTableWidgetItem("30,000.0 kHz")); + ui->messages->setItem(row, MESSAGE_COL_TX, new QTableWidgetItem("30,000.0 kHz")); + ui->messages->setItem(row, MESSAGE_COL_POSITION, new QTableWidgetItem("-90d60N -180d60W")); + ui->messages->setItem(row, MESSAGE_COL_NUMBER, new QTableWidgetItem("0898123456")); + ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("12:00")); + ui->messages->setItem(row, MESSAGE_COL_COMMS, new QTableWidgetItem("FSK")); + ui->messages->setItem(row, MESSAGE_COL_EOS, new QTableWidgetItem("Req Ack")); + ui->messages->setItem(row, MESSAGE_COL_ECC, new QTableWidgetItem("Fail")); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, new QTableWidgetItem("9")); + ui->messages->setItem(row, MESSAGE_COL_VALID, new QTableWidgetItem("Invalid")); + ui->messages->setItem(row, MESSAGE_COL_RSSI, new QTableWidgetItem("-50")); + ui->messages->resizeColumnsToContents(); + ui->messages->removeRow(row); +} + +// Columns in table reordered +void DSCDemodGUI::messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_columnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void DSCDemodGUI::messages_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_columnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void DSCDemodGUI::columnSelectMenu(QPoint pos) +{ + m_menu->popup(ui->messages->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void DSCDemodGUI::columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->messages->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *DSCDemodGUI::createCheckableItem(QString &text, int idx, bool checked) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, SLOT(columnSelectMenuChecked())); + return action; +} + +DSCDemodGUI* DSCDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + DSCDemodGUI* gui = new DSCDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void DSCDemodGUI::destroy() +{ + delete this; +} + +void DSCDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray DSCDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool DSCDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +// Add row to table +void DSCDemodGUI::messageReceived(const DSCMessage& message, int errors, float rssi) +{ + // Is scroll bar at bottom + QScrollBar *sb = ui->messages->verticalScrollBar(); + bool scrollToBottom = sb->value() == sb->maximum(); + + ui->messages->setSortingEnabled(false); + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + + QTableWidgetItem *rxDateItem = new QTableWidgetItem(); + QTableWidgetItem *rxTimeItem = new QTableWidgetItem(); + QTableWidgetItem *formatItem = new QTableWidgetItem(); + QTableWidgetItem *addressItem = new QTableWidgetItem(); + QTableWidgetItem *addressCountryItem = new QTableWidgetItem(); + QTableWidgetItem *addressTypeItem = new QTableWidgetItem(); + QTableWidgetItem *addressNameItem = new QTableWidgetItem(); + QTableWidgetItem *categoryItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdCountryItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdTypeItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdNameItem = new QTableWidgetItem(); + QTableWidgetItem *selfIdRangeItem = new QTableWidgetItem(); + QTableWidgetItem *telecommand1Item = new QTableWidgetItem(); + QTableWidgetItem *telecommand2Item = new QTableWidgetItem(); + QTableWidgetItem *rxItem = new QTableWidgetItem(); + QTableWidgetItem *txItem = new QTableWidgetItem(); + QTableWidgetItem *positionItem = new QTableWidgetItem(); + QTableWidgetItem *distressIdItem = new QTableWidgetItem(); + QTableWidgetItem *distressItem = new QTableWidgetItem(); + QTableWidgetItem *numberItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *commsItem = new QTableWidgetItem(); + QTableWidgetItem *eosItem = new QTableWidgetItem(); + QTableWidgetItem *eccItem = new QTableWidgetItem(); + QTableWidgetItem *errorsItem = new QTableWidgetItem(); + QTableWidgetItem *validItem = new QTableWidgetItem(); + QTableWidgetItem *rssiItem = new QTableWidgetItem(); + ui->messages->setItem(row, MESSAGE_COL_RX_DATE, rxDateItem); + ui->messages->setItem(row, MESSAGE_COL_RX_TIME, rxTimeItem); + ui->messages->setItem(row, MESSAGE_COL_FORMAT, formatItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS, addressItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_COUNTRY, addressCountryItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_TYPE, addressTypeItem); + ui->messages->setItem(row, MESSAGE_COL_ADDRESS_NAME, addressNameItem); + ui->messages->setItem(row, MESSAGE_COL_CATEGORY, categoryItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID, selfIdItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_COUNTRY, selfIdCountryItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_TYPE, selfIdTypeItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_NAME, selfIdNameItem); + ui->messages->setItem(row, MESSAGE_COL_SELF_ID_RANGE, selfIdRangeItem); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_1, telecommand1Item); + ui->messages->setItem(row, MESSAGE_COL_TELECOMMAND_2, telecommand2Item); + ui->messages->setItem(row, MESSAGE_COL_RX, rxItem); + ui->messages->setItem(row, MESSAGE_COL_TX, txItem); + ui->messages->setItem(row, MESSAGE_COL_POSITION, positionItem); + ui->messages->setItem(row, MESSAGE_COL_DISTRESS_ID, distressIdItem); + ui->messages->setItem(row, MESSAGE_COL_DISTRESS, distressItem); + ui->messages->setItem(row, MESSAGE_COL_NUMBER, numberItem); + ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); + ui->messages->setItem(row, MESSAGE_COL_COMMS, commsItem); + ui->messages->setItem(row, MESSAGE_COL_EOS, eosItem); + ui->messages->setItem(row, MESSAGE_COL_ECC, eccItem); + ui->messages->setItem(row, MESSAGE_COL_ERRORS, errorsItem); + ui->messages->setItem(row, MESSAGE_COL_VALID, validItem); + ui->messages->setItem(row, MESSAGE_COL_RSSI, rssiItem); + + rxDateItem->setData(Qt::DisplayRole, message.m_dateTime.date()); + rxTimeItem->setData(Qt::DisplayRole, message.m_dateTime.time()); + + formatItem->setText(message.formatSpecifier()); + if (message.m_hasCategory) { + categoryItem->setText(message.category()); + } + if (message.m_hasAddress) + { + addressItem->setText(message.m_address); + if (message.m_formatSpecifier != DSCMessage::GEOGRAPHIC_CALL) + { + QIcon *addressFlag = MMSI::getFlagIcon(message.m_address); + if (addressFlag) + { + addressCountryItem->setSizeHint(QSize(40, 20)); + addressCountryItem->setIcon(*addressFlag); + } + addressTypeItem->setText(MMSI::getCategory(message.m_address)); + } + } + selfIdItem->setText(message.m_selfId); + QIcon *selfIdFlag = MMSI::getFlagIcon(message.m_selfId); + if (selfIdFlag) + { + selfIdCountryItem->setSizeHint(QSize(40, 20)); + selfIdCountryItem->setIcon(*selfIdFlag); + } + selfIdTypeItem->setText(MMSI::getCategory(message.m_selfId)); + if (message.m_hasTelecommand1) { + telecommand1Item->setText(DSCMessage::telecommand1(message.m_telecommand1)); + } + if (message.m_hasTelecommand2) { + telecommand2Item->setText(DSCMessage::telecommand2(message.m_telecommand2)); + } + if (message.m_hasFrequency1) { + rxItem->setData(Qt::DisplayRole, message.m_frequency1); + } else if (message.m_hasChannel1) { + rxItem->setText(message.m_channel1); + } + if (message.m_hasFrequency2) { + txItem->setData(Qt::DisplayRole, message.m_frequency2); + } else if (message.m_hasChannel2) { + txItem->setText(message.m_channel2); + } + if (message.m_hasPosition) { + positionItem->setText(message.m_position); + } + if (message.m_hasDistressId) { + distressIdItem->setText(message.m_distressId); + } + if (message.m_hasDistressNature) { + distressItem->setText(DSCMessage::distressNature(message.m_distressNature)); + } + if (message.m_hasNumber) { + numberItem->setText(message.m_number); + } + if (message.m_hasTime) { + timeItem->setData(Qt::DisplayRole, message.m_time); + } + if (message.m_hasSubsequenceComms) { + commsItem->setText(DSCMessage::telecommand1(message.m_subsequenceComms)); + } + eosItem->setText(DSCMessage::endOfSignal(message.m_eos)); + if (message.m_eccOk) { + eccItem->setText("OK"); + } else { + eccItem->setText(QString("Fail (%1 != %2)").arg(message.m_ecc).arg(message.m_calculatedECC)); + } + if (message.m_valid) { + validItem->setText("Valid"); + } else { + validItem->setText("Invalid"); + } + + errorsItem->setData(Qt::DisplayRole, errors); + rssiItem->setData(Qt::DisplayRole, rssi); + + filterRow(row); + ui->messages->setSortingEnabled(true); + ui->messages->resizeRowToContents(row); + if (scrollToBottom) { + ui->messages->scrollToBottom(); + } + + // Get latest APRS.fi data to calculate distance + if (m_aprsFi && message.m_valid) + { + QStringList addresses; + addresses.append(message.m_selfId); + if (message.m_hasAddress + && (message.m_formatSpecifier != DSCMessage::GEOGRAPHIC_CALL) + && (message.m_formatSpecifier != DSCMessage::GROUP_CALL) + && (message.m_formatSpecifier != DSCMessage::ALL_SHIPS) + && (message.m_selfId != message.m_address) + ) { + addresses.append(message.m_address); + } + m_aprsFi->getData(addresses); + } +} + +bool DSCDemodGUI::handleMessage(const Message& message) +{ + if (DSCDemod::MsgConfigureDSCDemod::match(message)) + { + qDebug("DSCDemodGUI::handleMessage: DSCDemod::MsgConfigureDSCDemod"); + const DSCDemod::MsgConfigureDSCDemod& cfg = (DSCDemod::MsgConfigureDSCDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + ui->scopeGUI->updateSettings(); + m_channelMarker.updateSettings(static_cast(m_settings.m_channelMarker)); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_deviceCenterFrequency = notif.getCenterFrequency(); + m_basebandSampleRate = notif.getSampleRate(); + ui->deltaFrequency->setValueRange(false, 7, -m_basebandSampleRate/2, m_basebandSampleRate/2); + ui->deltaFrequencyLabel->setToolTip(tr("Range %1 %L2 Hz").arg(QChar(0xB1)).arg(m_basebandSampleRate/2)); + updateAbsoluteCenterFrequency(); + return true; + } + else if (DSCDemod::MsgMessage::match(message)) + { + DSCDemod::MsgMessage& textMsg = (DSCDemod::MsgMessage&) message; + messageReceived(textMsg.getMessage(), textMsg.getErrors(), textMsg.getRSSI()); + return true; + } + + return false; +} + +void DSCDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void DSCDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void DSCDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void DSCDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + updateAbsoluteCenterFrequency(); + applySettings(); +} + +void DSCDemodGUI::on_filterInvalid_clicked(bool checked) +{ + m_settings.m_filterInvalid = checked; + filter(); + applySettings(); +} + +void DSCDemodGUI::on_filterColumn_currentIndexChanged(int index) +{ + m_settings.m_filterColumn = index; + filter(); + applySettings(); +} + +void DSCDemodGUI::on_filter_editingFinished() +{ + m_settings.m_filter = ui->filter->text(); + filter(); + applySettings(); +} + +void DSCDemodGUI::on_clearTable_clicked() +{ + ui->messages->setRowCount(0); +} + +void DSCDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void DSCDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void DSCDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void DSCDemodGUI::filterRow(int row) +{ + bool hidden = false; + if (m_settings.m_filterInvalid) + { + QTableWidgetItem *validItem = ui->messages->item(row, MESSAGE_COL_VALID); + if (validItem->text() != "Valid") { + hidden = true; + } + } + if (m_settings.m_filter != "") + { + QTableWidgetItem *item = ui->messages->item(row, m_settings.m_filterColumn); + QRegExp re(m_settings.m_filter); + if (!re.exactMatch(item->text())) { + hidden = true; + } + } + ui->messages->setRowHidden(row, hidden); +} + +void DSCDemodGUI::filter() +{ + for (int i = 0; i < ui->messages->rowCount(); i++) { + filterRow(i); + } +} + +void DSCDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; + + getRollupContents()->saveState(m_rollupState); + applySettings(); +} + +void DSCDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.setDefaultTitle(m_displayedName); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + dialog.setNumberOfStreams(m_dscDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + } + + dialog.move(p); + new DialogPositioner(&dialog, false); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitle(m_channelMarker.getTitle()); + setTitleColor(m_settings.m_rgbColor); + + if (m_deviceUISet->m_deviceMIMOEngine) + { + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + updateIndexLabel(); + } + + applySettings(); + } + + resetContextMenuType(); +} + +DSCDemodGUI::DSCDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::DSCDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_deviceCenterFrequency(0), + m_doApplySettings(true), + m_tickCount(0) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + m_helpURL = "plugins/channelrx/demoddsc/readme.md"; + RollupContents *rollupContents = getRollupContents(); + ui->setupUi(rollupContents); + setSizePolicy(rollupContents->sizePolicy()); + rollupContents->arrangeRollups(); + connect(rollupContents, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_dscDemod = reinterpret_cast(rxChannel); + m_dscDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + ui->messages->setItemDelegateForColumn(MESSAGE_COL_RSSI, new DecimalDelegate(1)); + + m_scopeVis = m_dscDemod->getScopeSink(); + m_scopeVis->setGLScope(ui->glScope); + m_scopeVis->setNbStreams(DSCDemodSettings::m_scopeStreams); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + ui->scopeGUI->setStreams(QStringList({"IQ", "MagSq", "abs1", "abs2", "Unbiased", "Biased", "Data", "Clock", "Bit", "GotSOP"})); + + // Scope settings to display the IQ waveforms + ui->scopeGUI->setPreTrigger(1); + GLScopeSettings::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionReal; + traceDataI.m_amp = 1.0; // for -1 to +1 + traceDataI.m_ofs = 0.0; // vertical offset + traceDataQ.m_projectionType = Projector::ProjectionImag; + traceDataQ.m_amp = 1.0; + traceDataQ.m_ofs = 0.0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->addTrace(traceDataQ); + ui->scopeGUI->setDisplayMode(GLScopeSettings::DisplayXYV); + ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI + + GLScopeSettings::TriggerData triggerData; + triggerData.m_triggerLevel = 0.1; + triggerData.m_triggerLevelCoarse = 10; + triggerData.m_triggerPositiveEdge = true; + ui->scopeGUI->changeTrigger(0, triggerData); + ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI + + m_scopeVis->setLiveRate(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE); + m_scopeVis->configure(500, 1, 0, 0, true); // not working! + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("DSC Demodulator"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + m_settings.setScopeGUI(ui->scopeGUI); + m_settings.setRollupState(&m_rollupState); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->messages->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->messages->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + m_menu = new QMenu(ui->messages); + for (int i = 0; i < ui->messages->horizontalHeader()->count(); i++) + { + QString text = ui->messages->horizontalHeaderItem(i)->text(); + m_menu->addAction(createCheckableItem(text, i, true)); + } + ui->messages->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->messages->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(messages_sectionMoved(int, int, int))); + connect(ui->messages->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(messages_sectionResized(int, int, int))); + + ui->messages->setItemDelegateForColumn(MESSAGE_COL_RX, new FrequencyDelegate()); + ui->messages->setItemDelegateForColumn(MESSAGE_COL_TX, new FrequencyDelegate()); + + ui->messages->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint))); + TableTapAndHold *tableTapAndHold = new TableTapAndHold(ui->messages); + connect(tableTapAndHold, &TableTapAndHold::tapAndHold, this, &DSCDemodGUI::customContextMenuRequested); + + ui->scopeContainer->setVisible(false); + + m_aprsFi = APRSFi::create(); + if (m_aprsFi) { + connect(m_aprsFi, &APRSFi::dataUpdated, this, &DSCDemodGUI::aprsFiDataUpdated); + } + + CRightClickEnabler *feedRightClickEnabler = new CRightClickEnabler(ui->feed); + connect(feedRightClickEnabler, &CRightClickEnabler::rightClick, this, &DSCDemodGUI::on_feed_rightClicked); + + displaySettings(); + makeUIConnections(); + applySettings(true); +} + +void DSCDemodGUI::createMenuOpenURLAction(QMenu* tableContextMenu, const QString& text, const QString& url, const QString& arg) +{ + QAction* action = new QAction(QString(text).arg(arg), tableContextMenu); + connect(action, &QAction::triggered, this, [url, arg]()->void { + QDesktopServices::openUrl(QUrl(QString(url).arg(arg))); + }); + tableContextMenu->addAction(action); +} + +void DSCDemodGUI::createMenuFindOnMapAction(QMenu* tableContextMenu, const QString& text, const QString& target) +{ + QAction* findOnMapAction = new QAction(QString(text).arg(target), tableContextMenu); + connect(findOnMapAction, &QAction::triggered, this, [target]()->void { + FeatureWebAPIUtils::mapFind(target); + }); tableContextMenu->addAction(findOnMapAction); + tableContextMenu->addAction(findOnMapAction); +} + +void DSCDemodGUI::customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->messages->itemAt(pos); + if (item) + { + int row = item->row(); + QString time = ui->messages->item(row, MESSAGE_COL_RX_TIME)->data(Qt::DisplayRole).toTime().toString("hh:mm:ss"); + QString selfId = ui->messages->item(row, MESSAGE_COL_SELF_ID)->text(); + QString address = ui->messages->item(row, MESSAGE_COL_ADDRESS)->text(); + QString position = ui->messages->item(row, MESSAGE_COL_POSITION)->text(); + QString format = ui->messages->item(row, MESSAGE_COL_FORMAT)->text(); + FrequencyDelegate fd; + QString rx = ui->messages->item(row, MESSAGE_COL_RX)->text(); + QString rxFormatted = fd.displayText(rx, QLocale::system()); + QString tx = ui->messages->item(row, MESSAGE_COL_TX)->text(); + QString txFormatted = fd.displayText(tx, QLocale::system()); + + QMenu* tableContextMenu = new QMenu(ui->messages); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + + // View MMSIs on various websites + + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aishub.net...", "https://www.aishub.net/vessels?Ship%5Bmmsi%5D=%1&mmsi=%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on vesselfinder.com...", "https://www.vesselfinder.com/vessels?name=%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aprs.fi...", "https://aprs.fi/#!mt=roadmap&z=11&call=i/%1", selfId); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on yaddnet.org...", "http://yaddnet.org/pages/php/band_today_messages.php?from_mmsi=%1", selfId); + + if (!address.isEmpty() + && (format != "Geographic call") + && (format != "Group call") + && (format != "All ships")) + { + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aishub.net...", "https://www.aishub.net/vessels?Ship%5Bmmsi%5D=%1&mmsi=%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on vesselfinder.com...", "https://www.vesselfinder.com/vessels?name=%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on aprs.fi...", "https://aprs.fi/#!mt=roadmap&z=11&call=i/%1", address); + createMenuOpenURLAction(tableContextMenu, "View MMSI %1 on yaddnet.fi...", "http://yaddnet.org/pages/php/band_today_messages.php?from_mmsi=%1", address); + } + + // Find on Map + if (!selfId.isEmpty() || !address.isEmpty() || !position.isEmpty()) + { + tableContextMenu->addSeparator(); + if (!selfId.isEmpty()) { + createMenuFindOnMapAction(tableContextMenu, "Find MMSI %1 on map", selfId); + } + if (!address.isEmpty() && (format != "Geographic call")) { + createMenuFindOnMapAction(tableContextMenu, "Find MMSI %1 on map", address); + } + if (!position.isEmpty()) { + createMenuFindOnMapAction(tableContextMenu, "Center map at %1", position); + } + if (!address.isEmpty() && (format == "Geographic call")) + { + QString name = QString("DSC Call %1 %2").arg(selfId).arg(time); + if (!m_mapItems.contains(name)) + { + QString flag = MMSI::getFlagIconURL(selfId); + QStringList s; + s.append("Geographic call"); + s.append(QString("From: %1 ").arg(selfId).arg(flag)); + s.append(QString("To: %1").arg(address)); + s.append(QString("Category: %1").arg(ui->messages->item(row, MESSAGE_COL_CATEGORY)->text())); + QString tc1 = ui->messages->item(row, MESSAGE_COL_TELECOMMAND_1)->text(); + if (!tc1.isEmpty() && (tc1 != "No information")) { + s.append(QString("Telecommand 1: %1").arg(tc1)); + } + FrequencyDelegate fd; + if (!rx.isEmpty()) { + s.append(QString("RX: %1").arg(rxFormatted)); + } + if (!tx.isEmpty()) { + s.append(QString("TX: %1").arg(txFormatted)); + } + QString info = s.join("
"); + + QAction* sendAreaToMapAction = new QAction(QString("Display %1 on map").arg(address), tableContextMenu); + connect(sendAreaToMapAction, &QAction::triggered, this, [this, name, address, info]()->void { + sendAreaToMapFeature(name, address, info); + QTimer::singleShot(500, [this, name] { + FeatureWebAPIUtils::mapFind(name); + }); + }); + tableContextMenu->addAction(sendAreaToMapAction); + } + else + { + QAction* findAreaOnMapAction = new QAction(QString("Center map on %1").arg(address), tableContextMenu); + connect(findAreaOnMapAction, &QAction::triggered, this, [this, name]()->void { + FeatureWebAPIUtils::mapFind(name); + }); + tableContextMenu->addAction(findAreaOnMapAction); + + QAction* clearAreaFromMapAction = new QAction(QString("Remove %1 from map").arg(address), tableContextMenu); + connect(clearAreaFromMapAction, &QAction::triggered, this, [this, name]()->void { + clearAreaFromMapFeature(name); + }); + tableContextMenu->addAction(clearAreaFromMapAction); + } + } + } + + // Menu to tune SSB demods + bool ok; + qint64 rxFreq = rx.toLongLong(&ok); + if (ok) + { + std::vector& deviceSets = MainCore::instance()->getDeviceSets(); + int deviceSetIndex = 0; + + for (const auto& deviceSet : deviceSets) + { + DSPDeviceSourceEngine *deviceSourceEngine = deviceSet->m_deviceSourceEngine; + + if (deviceSourceEngine) + { + for (int chi = 0; chi < deviceSet->getNumberOfChannels(); chi++) + { + ChannelAPI *channel = deviceSet->getChannelAt(chi); + + if (channel->getURI() == "sdrangel.channel.ssbdemod") + { + DeviceSampleSource *sampleSource = deviceSourceEngine->getSource(); + if (sampleSource) + { + QAction* tuneRxAction = new QAction(QString("Tune SSB Demod %1:%2 to %3").arg(deviceSetIndex).arg(chi).arg(rxFormatted), tableContextMenu); + connect(tuneRxAction, &QAction::triggered, this, [this, deviceSetIndex, chi, rxFreq, sampleSource]()->void { + + int bw = sampleSource->getSampleRate(); + quint64 cf = sampleSource->getCenterFrequency(); + qint64 low = (cf - bw/2 - 2000); + qint64 high = (cf + bw/2 + 2000); + + if ((rxFreq >= low) && (rxFreq <= high)) + { + int offset = rxFreq - cf; + ChannelWebAPIUtils::setFrequencyOffset(deviceSetIndex, chi, offset); + } + else + { + ChannelWebAPIUtils::setCenterFrequency(deviceSetIndex, rxFreq); + ChannelWebAPIUtils::setFrequencyOffset(deviceSetIndex, chi, 0); + } + }); + tableContextMenu->addAction(tuneRxAction); + } + } + } + } + deviceSetIndex++; + } + } + + tableContextMenu->popup(ui->messages->viewport()->mapToGlobal(pos)); + } +} + +void DSCDemodGUI::sendAreaToMapFeature(const QString& name, const QString& address, const QString& text) +{ + QRegularExpression re(QString("(\\d+)%1([NS]) (\\d+)%1([EW]) - (\\d+)%1([NS]) (\\d+)%1([EW])").arg(QChar(0xb0))); + QRegularExpressionMatch match = re.match(address); + if (match.hasMatch()) + { + int lat1 = match.captured(1).toInt(); + if (match.captured(2) == "S") { + lat1 = -lat1; + } + int lon1 = match.captured(3).toInt(); + if (match.captured(4) == "W") { + lon1 = -lon1; + } + int lat2 = match.captured(5).toInt(); + if (match.captured(6) == "S") { + lat2 = -lat2; + } + int lon2 = match.captured(7).toInt(); + if (match.captured(8) == "W") { + lon2 = -lon2; + } + + // Send to Map feature + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_dscDemod, "mapitems", mapPipes); + + if (mapPipes.size() > 0) + { + if (!m_mapItems.contains(name)) { + m_mapItems.append(name); + } + + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + + swgMapItem->setName(new QString(name)); + swgMapItem->setLatitude(lat1); + swgMapItem->setLongitude(lon1); + swgMapItem->setAltitude(0.0); + QString image = QString("none"); + swgMapItem->setImage(new QString(image)); + swgMapItem->setImageRotation(0); + swgMapItem->setText(new QString(text)); // Not used - label is used instead for now + swgMapItem->setLabel(new QString(text)); + swgMapItem->setAltitudeReference(0); + QList *coords = new QList(); + + SWGSDRangel::SWGMapCoordinate* c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon2); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat2); + c->setLongitude(lon2); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat2); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + c = new SWGSDRangel::SWGMapCoordinate(); + c->setLatitude(lat1); + c->setLongitude(lon1); + c->setAltitude(0.0); + coords->append(c); + + swgMapItem->setCoordinates(coords); + swgMapItem->setType(3); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_dscDemod, swgMapItem); + messageQueue->push(msg); + } + } + } + else + { + qDebug() << "DSCDemodGUI::sendAreaToMapFeature: Couldn't parse address " << address; + } +} + +void DSCDemodGUI::clearAreaFromMapFeature(const QString& name) +{ + QList mapPipes; + MainCore::instance()->getMessagePipes().getMessagePipes(m_dscDemod, "mapitems", mapPipes); + for (const auto& pipe : mapPipes) + { + MessageQueue *messageQueue = qobject_cast(pipe->m_element); + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(name)); + swgMapItem->setImage(new QString("")); + swgMapItem->setType(3); + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_dscDemod, swgMapItem); + messageQueue->push(msg); + } + m_mapItems.removeAll(name); +} + +DSCDemodGUI::~DSCDemodGUI() +{ + delete m_aprsFi; + delete ui; +} + +void DSCDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void DSCDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + DSCDemod::MsgConfigureDSCDemod* message = DSCDemod::MsgConfigureDSCDemod::create( m_settings, force); + m_dscDemod->getInputMessageQueue()->push(message); + } +} + +void DSCDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + setTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + updateIndexLabel(); + + ui->filterInvalid->setChecked(m_settings.m_filterInvalid); + ui->filterColumn->setCurrentIndex(m_settings.m_filterColumn); + ui->filter->setText(m_settings.m_filter); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + ui->logEnable->setChecked(m_settings.m_logEnabled); + + ui->feed->setChecked(m_settings.m_feed); + + // Order and size columns + QHeaderView *header = ui->messages->horizontalHeader(); + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) + { + bool hidden = m_settings.m_columnSizes[i] == 0; + header->setSectionHidden(i, hidden); + m_menu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_columnSizes[i] > 0) + ui->messages->setColumnWidth(i, m_settings.m_columnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]); + } + + filter(); + + getRollupContents()->restoreState(m_rollupState); + updateAbsoluteCenterFrequency(); + blockApplySettings(false); +} + +void DSCDemodGUI::leaveEvent(QEvent* event) +{ + m_channelMarker.setHighlighted(false); + ChannelGUI::leaveEvent(event); +} + +void DSCDemodGUI::enterEvent(EnterEventType* event) +{ + m_channelMarker.setHighlighted(true); + ChannelGUI::enterEvent(event); +} + +void DSCDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_dscDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + m_tickCount++; +} + +void DSCDemodGUI::on_feed_clicked(bool checked) +{ + m_settings.m_feed = checked; + applySettings(); +} + +void DSCDemodGUI::on_feed_rightClicked(const QPoint &point) +{ + (void) point; + + QString id = MainCore::instance()->getSettings().getStationName(); + QString url = QString("http://yaddnet.org/pages/php/live_rx.php?rxid=%1").arg(id); + QDesktopServices::openUrl(QUrl(url)); +} + +void DSCDemodGUI::on_logEnable_clicked(bool checked) +{ + m_settings.m_logEnabled = checked; + applySettings(); +} + +void DSCDemodGUI::on_logFilename_clicked() +{ + // Get filename to save to + QFileDialog fileDialog(nullptr, "Select file to log received messages to", "", "*.csv"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + m_settings.m_logFilename = fileNames[0]; + ui->logFilename->setToolTip(QString(".csv log filename: %1").arg(m_settings.m_logFilename)); + applySettings(); + } + } +} + +// Read .csv log and process as received messages +void DSCDemodGUI::on_logOpen_clicked() +{ + QFileDialog fileDialog(nullptr, "Select .csv log file to read", "", "*.csv"); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + QFile file(fileNames[0]); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QTextStream in(&file); + QString error; + QHash colIndexes = CSV::readHeader(in, {"Date", "Time", "Message", "Errors", "RSSI"}, error); + if (error.isEmpty()) + { + int dateCol = colIndexes.value("Date"); + int timeCol = colIndexes.value("Time"); + int messageCol = colIndexes.value("Message"); + int errorsCol = colIndexes.value("Errors"); + int rssiCol = colIndexes.value("RSSI"); + int maxCol = std::max({dateCol, timeCol, messageCol, errorsCol, rssiCol}); + + QMessageBox dialog(this); + dialog.setText("Reading message data"); + dialog.addButton(QMessageBox::Cancel); + dialog.show(); + QApplication::processEvents(); + int count = 0; + bool cancelled = false; + QStringList cols; + while (!cancelled && CSV::readRow(in, &cols)) + { + if (cols.size() > maxCol) + { + QDate date = QDate::fromString(cols[dateCol]); + QTime time = QTime::fromString(cols[timeCol]); + QDateTime dateTime(date, time); + QString messageHex = cols[messageCol]; + QByteArray bytes = QByteArray::fromHex(messageHex.toLatin1()); + DSCMessage message(bytes, dateTime); + int errors = cols[errorsCol].toInt(); + float rssi = cols[rssiCol].toFloat(); + messageReceived(message, errors, rssi); + if (count % 1000 == 0) + { + QApplication::processEvents(); + if (dialog.clickedButton()) { + cancelled = true; + } + } + count++; + } + } + dialog.close(); + } + else + { + QMessageBox::critical(this, "DSC Demod", error); + } + } + else + { + QMessageBox::critical(this, "DSC Demod", QString("Failed to open file %1").arg(fileNames[0])); + } + } + } +} + +void DSCDemodGUI::makeUIConnections() +{ + QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &DSCDemodGUI::on_deltaFrequency_changed); + QObject::connect(ui->filterInvalid, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_filterInvalid_clicked); + QObject::connect(ui->filterColumn, QOverload::of(&QComboBox::currentIndexChanged), this, &DSCDemodGUI::on_filterColumn_currentIndexChanged); + QObject::connect(ui->filter, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_filter_editingFinished); + QObject::connect(ui->clearTable, &QPushButton::clicked, this, &DSCDemodGUI::on_clearTable_clicked); + QObject::connect(ui->udpEnabled, &QCheckBox::clicked, this, &DSCDemodGUI::on_udpEnabled_clicked); + QObject::connect(ui->udpAddress, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_udpAddress_editingFinished); + QObject::connect(ui->udpPort, &QLineEdit::editingFinished, this, &DSCDemodGUI::on_udpPort_editingFinished); + QObject::connect(ui->logEnable, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_logEnable_clicked); + QObject::connect(ui->logFilename, &QToolButton::clicked, this, &DSCDemodGUI::on_logFilename_clicked); + QObject::connect(ui->logOpen, &QToolButton::clicked, this, &DSCDemodGUI::on_logOpen_clicked); + QObject::connect(ui->feed, &ButtonSwitch::clicked, this, &DSCDemodGUI::on_feed_clicked); +} + +void DSCDemodGUI::updateAbsoluteCenterFrequency() +{ + setStatusFrequency(m_deviceCenterFrequency + m_settings.m_inputFrequencyOffset); +} + +void DSCDemodGUI::aprsFiDataUpdated(const QList& data) +{ + for (int i = ui->messages->rowCount() - 1; i >= 0; i--) + { + bool match = false; + QString mmsi; + + for (const auto& item : data) + { + mmsi = ui->messages->item(i, MESSAGE_COL_ADDRESS)->text(); + if (mmsi == item.m_mmsi) + { + ui->messages->item(i, MESSAGE_COL_ADDRESS_NAME)->setText(item.m_name); + match = true; + } + mmsi = ui->messages->item(i, MESSAGE_COL_SELF_ID)->text(); + if (mmsi == item.m_mmsi) + { + ui->messages->item(i, MESSAGE_COL_SELF_ID_NAME)->setText(item.m_name); + + if ((item.m_latitude != 0.0) || (item.m_longitude != 0.0)) + { + // Calculate distance from My Position to position of source of message + Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); + Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); + Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); + QGeoCoordinate stationPosition(stationLatitude, stationLongitude, stationAltitude); + QGeoCoordinate shipPosition(item.m_latitude, item.m_longitude, 0.0); + + float distance = stationPosition.distanceTo(shipPosition); + + ui->messages->item(i, MESSAGE_COL_SELF_ID_RANGE)->setData(Qt::DisplayRole, (int)std::round(distance / 1000.0)); + } + + match = true; + } + } + if (match) { + break; + } + } +} diff --git a/plugins/channelrx/demoddsc/dscdemodgui.h b/plugins/channelrx/demoddsc/dscdemodgui.h new file mode 100644 index 000000000..c3297bcdc --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.h @@ -0,0 +1,171 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODGUI_H +#define INCLUDE_DSCDEMODGUI_H + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/aprsfi.h" +#include "util/messagequeue.h" +#include "settings/rollupstate.h" +#include "dscdemod.h" +#include "dscdemodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class DSCDemod; +class DSCDemodGUI; + +namespace Ui { + class DSCDemodGUI; +} +class DSCDemodGUI; + +class DSCDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static DSCDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + virtual void setWorkspaceIndex(int index) { m_settings.m_workspaceIndex = index; }; + virtual int getWorkspaceIndex() const { return m_settings.m_workspaceIndex; }; + virtual void setGeometryBytes(const QByteArray& blob) { m_settings.m_geometryBytes = blob; }; + virtual QByteArray getGeometryBytes() const { return m_settings.m_geometryBytes; }; + virtual QString getTitle() const { return m_settings.m_title; }; + virtual QColor getTitleColor() const { return m_settings.m_rgbColor; }; + virtual void zetHidden(bool hidden) { m_settings.m_hidden = hidden; } + virtual bool getHidden() const { return m_settings.m_hidden; } + virtual ChannelMarker& getChannelMarker() { return m_channelMarker; } + virtual int getStreamIndex() const { return m_settings.m_streamIndex; } + virtual void setStreamIndex(int streamIndex) { m_settings.m_streamIndex = streamIndex; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::DSCDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + RollupState m_rollupState; + DSCDemodSettings m_settings; + qint64 m_deviceCenterFrequency; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + DSCDemod* m_dscDemod; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + APRSFi *m_aprsFi; + QMenu *m_menu; // Column select context menu + QStringList m_mapItems; + + enum MessageCol { + MESSAGE_COL_RX_DATE, + MESSAGE_COL_RX_TIME, + MESSAGE_COL_FORMAT, + MESSAGE_COL_ADDRESS, + MESSAGE_COL_ADDRESS_COUNTRY, + MESSAGE_COL_ADDRESS_TYPE, + MESSAGE_COL_ADDRESS_NAME, + MESSAGE_COL_CATEGORY, + MESSAGE_COL_SELF_ID, + MESSAGE_COL_SELF_ID_COUNTRY, + MESSAGE_COL_SELF_ID_TYPE, + MESSAGE_COL_SELF_ID_NAME, + MESSAGE_COL_SELF_ID_RANGE, + MESSAGE_COL_TELECOMMAND_1, + MESSAGE_COL_TELECOMMAND_2, + MESSAGE_COL_RX, + MESSAGE_COL_TX, + MESSAGE_COL_POSITION, + MESSAGE_COL_DISTRESS_ID, + MESSAGE_COL_DISTRESS, + MESSAGE_COL_NUMBER, + MESSAGE_COL_TIME, + MESSAGE_COL_COMMS, + MESSAGE_COL_EOS, + MESSAGE_COL_ECC, + MESSAGE_COL_ERRORS, + MESSAGE_COL_VALID, + MESSAGE_COL_RSSI + }; + + explicit DSCDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~DSCDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void messageReceived(const DSCMessage& message, int errors, float rssi); + bool handleMessage(const Message& message); + void makeUIConnections(); + void updateAbsoluteCenterFrequency(); + + void leaveEvent(QEvent*); + void enterEvent(EnterEventType*); + + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked); + void createMenuOpenURLAction(QMenu* tableContextMenu, const QString& text, const QString& url, const QString& arg); + void createMenuFindOnMapAction(QMenu* tableContextMenu, const QString& text, const QString& target); + void sendAreaToMapFeature(const QString& name, const QString& address, const QString& text); + void clearAreaFromMapFeature(const QString& name); + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_filterInvalid_clicked(bool checked=false); + void on_filterColumn_currentIndexChanged(int index); + void on_filter_editingFinished(); + void on_clearTable_clicked(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_logEnable_clicked(bool checked=false); + void on_logFilename_clicked(); + void on_logOpen_clicked(); + void on_feed_clicked(bool checked=false); + void on_feed_rightClicked(const QPoint &point); + void filterRow(int row); + void filter(); + void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void messages_sectionResized(int logicalIndex, int oldSize, int newSize); + void columnSelectMenu(QPoint pos); + void columnSelectMenuChecked(bool checked = false); + void customContextMenuRequested(QPoint point); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); + void aprsFiDataUpdated(const QList& data); +}; + +#endif // INCLUDE_DSCDEMODGUI_H + diff --git a/plugins/channelrx/demoddsc/dscdemodgui.ui b/plugins/channelrx/demoddsc/dscdemodgui.ui new file mode 100644 index 000000000..cb1a0a848 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodgui.ui @@ -0,0 +1,982 @@ + + + DSCDemodGUI + + + + 0 + 0 + 411 + 751 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + Packet Demodulator + + + + + 0 + 0 + 390 + 121 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + UDP + + + + + + + Send messages via UDP + + + Qt::RightToLeft + + + + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 4530 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Filter + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + Display messages only from the specified station + + + + Date + + + + + Time + + + + + Format + + + + + To + + + + + To Country + + + + + To Type + + + + + To Name + + + + + Category + + + + + From + + + + + From Country + + + + + From Type + + + + + From Name + + + + + Range + + + + + Telecommand 1 + + + + + Telecommand 2 + + + + + RX + + + + + TX + + + + + Position + + + + + Distress Id + + + + + Distress + + + + + Number + + + + + Time + + + + + Comms + + + + + EOS + + + + + ECC + + + + + Errors + + + + + Valid + + + + + RSSI + + + + + + + + Filter regular expression + + + + + + + + 24 + 16777215 + + + + When checked, invalid messages are filtered from the table + + + + + + + :/funnel.png:/funnel.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 24 + 16777215 + + + + Feed messages to yaddnet.org + + + + + + + :/txon.png:/txon.png + + + + + + + + 24 + 16777215 + + + + Start/stop logging of received messages to .csv file + + + + + + + :/record_off.png:/record_off.png + + + + + + + Set log .csv filename + + + ... + + + + :/save.png:/save.png + + + false + + + + + + + Read messages from .csv log file + + + ... + + + + :/load.png:/load.png + + + false + + + + + + + Clear messages + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 10 + 140 + 381 + 241 + + + + + 0 + 0 + + + + Received Messages + + + + + + Received messages + + + QAbstractItemView::NoEditTriggers + + + + Date + + + Local date message was received + + + + + Time + + + Local time message was received + + + + + Format + + + Format specifier + + + + + To + + + Address (MMSI or coordinates) of who the message is to + + + + + Country + + + Country with jurisdiction of the destination of the message + + + + + Type + + + MMSI type of the destination of the message + + + + + Name + + + Name of the station the message is to + + + + + Category + + + Message category + + + + + From + + + MMSI of sender of message + + + + + Country + + + Country with jurisdiction of the sender of the message + + + + + Type + + + MMSI type of the sender of the message + + + + + Name + + + Name of the station the message is from + + + + + Range (km) + + + Distance in kilometers from My Position to ship's position obtained from aprs.fi + + + + + Telecommand 1 + + + Telecommand + + + + + Telecommand 2 + + + Telecommand + + + + + RX + + + RX frequency (Hz) or channel + + + + + TX + + + TX frequency (Hz) or channel + + + + + Position + + + Position of ship in degrees and minutes + + + + + Distress Id + + + MMSI of ship in distress + + + + + Distress + + + Nature of distress + + + + + Number + + + Telephone number + + + + + Time + + + UTC Time + + + + + Comms + + + Subsequent communications + + + + + EOS + + + End of Signal + + + + + ECC + + + Error checking code + + + + + Errors + + + Number of symbols received with errors (which may have been corrected if ECC OK) + + + + + Valid + + + Whether the message is determined to be valid (contains no detected errors) + + + + + RSSI + + + Received signal strenth indicator (Average power in dBFS) + + + + + + + + + + 10 + 390 + 716 + 341 + + + + + 714 + 0 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
+ + RollupContents + QWidget +
gui/rollupcontents.h
+ 1 +
+ + ValueDialZ + QWidget +
gui/valuedialz.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + GLScope + QWidget +
gui/glscope.h
+ 1 +
+ + GLScopeGUI + QWidget +
gui/glscopegui.h
+ 1 +
+
+ + deltaFrequency + udpEnabled + filterColumn + logEnable + logFilename + logOpen + clearTable + messages + + + + + +
diff --git a/plugins/channelrx/demoddsc/dscdemodplugin.cpp b/plugins/channelrx/demoddsc/dscdemodplugin.cpp new file mode 100644 index 000000000..48627aa54 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodplugin.cpp @@ -0,0 +1,93 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "dscdemodgui.h" +#endif +#include "dscdemod.h" +#include "dscdemodwebapiadapter.h" +#include "dscdemodplugin.h" + +const PluginDescriptor DSCDemodPlugin::m_pluginDescriptor = { + DSCDemod::m_channelId, + QStringLiteral("DSC Demodulator"), + QStringLiteral("7.14.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +DSCDemodPlugin::DSCDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& DSCDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void DSCDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(DSCDemod::m_channelIdURI, DSCDemod::m_channelId, this); +} + +void DSCDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + DSCDemod *instance = new DSCDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* DSCDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* DSCDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return DSCDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* DSCDemodPlugin::createChannelWebAPIAdapter() const +{ + return new DSCDemodWebAPIAdapter(); +} + diff --git a/plugins/channelrx/demoddsc/dscdemodplugin.h b/plugins/channelrx/demoddsc/dscdemodplugin.h new file mode 100644 index 000000000..18f2ab2bd --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodplugin.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODPLUGIN_H +#define INCLUDE_DSCDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class DSCDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.dscdemod") + +public: + explicit DSCDemodPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_DSCDEMODPLUGIN_H + diff --git a/plugins/channelrx/demoddsc/dscdemodsettings.cpp b/plugins/channelrx/demoddsc/dscdemodsettings.cpp new file mode 100644 index 000000000..2562ab281 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsettings.cpp @@ -0,0 +1,206 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "dscdemodsettings.h" + +DSCDemodSettings::DSCDemodSettings() : + m_channelMarker(nullptr), + m_scopeGUI(nullptr), + m_rollupState(nullptr) +{ + resetToDefaults(); +} + +void DSCDemodSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_rfBandwidth = 450.0f; // OBW for 2FSK = 2 * deviation + data rate. Then add a bit for carrier frequency offset + m_filterInvalid = true; + m_filterColumn = 4; + m_filter = ""; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_logFilename = "dsc_log.csv"; + m_logEnabled = false; + m_feed = true; + + m_rgbColor = QColor(181, 230, 29).rgb(); + m_title = "DSC Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + m_workspaceIndex = 0; + m_hidden = false; + + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) + { + m_columnIndexes[i] = i; + m_columnSizes[i] = -1; // Autosize + } +} + +QByteArray DSCDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_streamIndex); + s.writeBool(3, m_filterInvalid); + s.writeS32(4, m_filterColumn); + s.writeString(5, m_filter); + + if (m_channelMarker) { + s.writeBlob(6, m_channelMarker->serialize()); + } + s.writeFloat(7, m_rfBandwidth); + + s.writeBool(9, m_udpEnabled); + s.writeString(10, m_udpAddress); + s.writeU32(11, m_udpPort); + s.writeString(12, m_logFilename); + s.writeBool(13, m_logEnabled); + s.writeBool(14, m_feed); + + s.writeU32(20, m_rgbColor); + s.writeString(21, m_title); + s.writeBool(22, m_useReverseAPI); + s.writeString(23, m_reverseAPIAddress); + s.writeU32(24, m_reverseAPIPort); + s.writeU32(25, m_reverseAPIDeviceIndex); + s.writeU32(26, m_reverseAPIChannelIndex); + + if (m_rollupState) { + s.writeBlob(27, m_rollupState->serialize()); + } + + s.writeS32(28, m_workspaceIndex); + s.writeBlob(29, m_geometryBytes); + s.writeBool(30, m_hidden); + s.writeBlob(31, m_scopeGUI->serialize()); + + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) { + s.writeS32(100 + i, m_columnIndexes[i]); + } + + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) { + s.writeS32(200 + i, m_columnSizes[i]); + } + + return s.final(); +} + +bool DSCDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readS32(2, &m_streamIndex, 0); + d.readBool(3, &m_filterInvalid, true); + d.readS32(4, &m_filterColumn, 5); + d.readString(5, &m_filter, ""); + + if (m_channelMarker) + { + d.readBlob(6, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + d.readFloat(7, &m_rfBandwidth, 450.0f); + + d.readBool(9, &m_udpEnabled); + d.readString(10, &m_udpAddress); + d.readU32(11, &utmp); + + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + d.readString(12, &m_logFilename, "dsc_log.csv"); + d.readBool(13, &m_logEnabled, false); + d.readBool(14, &m_feed, true); + + d.readU32(20, &m_rgbColor, QColor(181, 230, 29).rgb()); + d.readString(21, &m_title, "DSC Demodulator"); + d.readBool(22, &m_useReverseAPI, false); + d.readString(23, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(24, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(25, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(26, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + if (m_rollupState) + { + d.readBlob(27, &bytetmp); + m_rollupState->deserialize(bytetmp); + } + + d.readS32(28, &m_workspaceIndex, 0); + d.readBlob(29, &m_geometryBytes); + d.readBool(30, &m_hidden, false); + + if (m_scopeGUI) + { + d.readBlob(31, &bytetmp); + m_scopeGUI->deserialize(bytetmp); + } + + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) { + d.readS32(100 + i, &m_columnIndexes[i], i); + } + + for (int i = 0; i < DSCDEMOD_COLUMNS; i++) { + d.readS32(200 + i, &m_columnSizes[i], -1); + } + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + diff --git a/plugins/channelrx/demoddsc/dscdemodsettings.h b/plugins/channelrx/demoddsc/dscdemodsettings.h new file mode 100644 index 000000000..9d793333a --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsettings.h @@ -0,0 +1,76 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODSETTINGS_H +#define INCLUDE_DSCDEMODSETTINGS_H + +#include + +class Serializable; + +// Number of columns in the table +#define DSCDEMOD_COLUMNS 28 + +struct DSCDemodSettings +{ + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; // Not currently in GUI as probably doesn't need to be adjusted + bool m_filterInvalid; + int m_filterColumn; + QString m_filter; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + bool m_feed; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + QString m_logFilename; + bool m_logEnabled; + Serializable *m_scopeGUI; + Serializable *m_rollupState; + int m_workspaceIndex; + QByteArray m_geometryBytes; + bool m_hidden; + + int m_columnIndexes[DSCDEMOD_COLUMNS];//!< How the columns are ordered in the table + int m_columnSizes[DSCDEMOD_COLUMNS]; //!< Size of the columns in the table + + static const int DSCDEMOD_CHANNEL_SAMPLE_RATE = 1000; // Must be integer multiple of baud rate (x10) + static const int DSCDEMOD_BAUD_RATE = 100; + static const int DSCDEMOD_FREQUENCY_SHIFT = 170; + static const int m_scopeStreams = 10; + + DSCDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + void setRollupState(Serializable *rollupState) { m_rollupState = rollupState; } + void setScopeGUI(Serializable *scopeGUI) { m_scopeGUI = scopeGUI; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_DSCDEMODSETTINGS_H */ diff --git a/plugins/channelrx/demoddsc/dscdemodsink.cpp b/plugins/channelrx/demoddsc/dscdemodsink.cpp new file mode 100644 index 000000000..695dcab4f --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsink.cpp @@ -0,0 +1,331 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "dsp/dspengine.h" +#include "dsp/scopevis.h" +#include "util/db.h" +#include "util/popcount.h" +#include "maincore.h" + +#include "dscdemod.h" +#include "dscdemodsink.h" + +DSCDemodSink::DSCDemodSink(DSCDemod *packetDemod) : + m_dscDemod(packetDemod), + m_channelSampleRate(DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_exp(nullptr), + m_sampleBufferIndex(0) +{ + m_magsq = 0.0; + + for (int i = 0; i < DSCDemodSettings::m_scopeStreams; i++) { + m_sampleBuffer[i].resize(m_sampleBufferSize); + } + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); + + m_lowpassComplex1.create(301, DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, DSCDemodSettings::DSCDEMOD_BAUD_RATE * 1.1); + m_lowpassComplex2.create(301, DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE, DSCDemodSettings::DSCDEMOD_BAUD_RATE * 1.1); +} + +DSCDemodSink::~DSCDemodSink() +{ + delete[] m_exp; +} + +void DSCDemodSink::sampleToScope(Complex sample, Real abs1Filt, Real abs2Filt, Real unbiasedData, Real biasedData) +{ + if (m_scopeSink) + { + m_sampleBuffer[0][m_sampleBufferIndex] = sample; + m_sampleBuffer[1][m_sampleBufferIndex] = Complex(m_magsq, 0.0f); + m_sampleBuffer[2][m_sampleBufferIndex] = Complex(abs1Filt, 0.0f); + m_sampleBuffer[3][m_sampleBufferIndex] = Complex(abs2Filt, 0.0f); + m_sampleBuffer[4][m_sampleBufferIndex] = Complex(unbiasedData, 0.0f); + m_sampleBuffer[5][m_sampleBufferIndex] = Complex(biasedData, 0.0f); + m_sampleBuffer[6][m_sampleBufferIndex] = Complex(m_data, 0.0f); + m_sampleBuffer[7][m_sampleBufferIndex] = Complex(m_clock, 0.0f); + m_sampleBuffer[8][m_sampleBufferIndex] = Complex(m_bit, 0.0f); + m_sampleBuffer[9][m_sampleBufferIndex] = Complex(m_gotSOP, 0.0f); + m_sampleBufferIndex++; + + if (m_sampleBufferIndex == m_sampleBufferSize) + { + std::vector vbegin; + + for (int i = 0; i < DSCDemodSettings::m_scopeStreams; i++) { + vbegin.push_back(m_sampleBuffer[i].begin()); + } + + m_scopeSink->feed(vbegin, m_sampleBufferSize); + m_sampleBufferIndex = 0; + } + } +} + +void DSCDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + + if (m_interpolatorDistance < 1.0f) // interpolate + { + while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + else // decimate + { + if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + } +} + +void DSCDemodSink::processOneSample(Complex &ci) +{ + // Calculate average and peak levels for level meter + double magsqRaw = ci.real()*ci.real() + ci.imag()*ci.imag();; + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; + + // Sum power while data is being received + if (m_gotSOP) + { + m_rssiMagSqSum += magsq; + m_rssiMagSqCount++; + } + + ci /= SDR_RX_SCALEF; + + // Correlate with expected frequencies + Complex exp = m_exp[m_expIdx]; + m_expIdx = (m_expIdx + 1) % m_expLength; + Complex corr1 = ci * exp; + Complex corr2 = ci * std::conj(exp); + + // Low pass filter + Real abs1Filt = std::abs(m_lowpassComplex1.filter(corr1)); + Real abs2Filt = std::abs(m_lowpassComplex2.filter(corr2)); + + // Envelope calculation + m_movMax1(abs1Filt); + m_movMax2(abs2Filt); + Real env1 = m_movMax1.getMaximum(); + Real env2 = m_movMax2.getMaximum(); + + // Automatic threshold correction to compensate for frequency selective fading + // http://www.w7ay.net/site/Technical/ATC/index.html + Real bias1 = abs1Filt - 0.5 * env1; + Real bias2 = abs2Filt - 0.5 * env2; + Real unbiasedData = abs1Filt - abs2Filt; + Real biasedData = bias1 - bias2; + + // Save current data for edge detection + m_dataPrev = m_data; + // Set data according to stongest correlation + m_data = biasedData > 0; + + // Calculate timing error (we expect clockCount to be 0 when data changes), and add a proportion of it + if (m_data && !m_dataPrev) { + m_clockCount -= m_clockCount * 0.25; + } + + m_clockCount += 1.0; + if (m_clockCount >= m_samplesPerBit/2.0-1.0) + { + // Sample in middle of symbol + receiveBit(m_data); + m_clock = 1; + // Wrap clock counter + m_clockCount -= m_samplesPerBit; + } + else + { + m_clock = 0; + } + + sampleToScope(ci, abs1Filt, abs2Filt, unbiasedData, biasedData); +} + +const QList DSCDemodSink::m_phasingPatterns = { + {0b1011111001'1111011001'1011111001, 9}, // 125 111 125 + {0b1111011001'1011111001'0111011010, 8}, // 111 125 110 + {0b1011111001'0111011010'1011111001, 7}, // 125 110 125 + {0b0111011010'1011111001'1011011010, 6}, // 110 125 109 + {0b1011111001'1011011010'1011111001, 5}, // 125 109 125 + {0b1011011010'1011111001'0011011011, 4}, // 109 125 108 + {0b1011111001'0011011011'1011111001, 3}, // 125 108 125 + {0b0011011011'1011111001'1101011010, 2}, // 108 125 107 + {0b1011111001'1101011010'1011111001, 1}, // 125 107 125 + {0b1101011010'1011111001'0101011011, 0}, // 107 125 106 +}; + +void DSCDemodSink::receiveBit(bool bit) +{ + m_bit = bit; + + // Store in shift reg + m_bits = (m_bits << 1) | m_bit; + m_bitCount++; + + if (!m_gotSOP) + { + // Dot pattern - 200 1/0s or 20 1/0s + // Phasing pattern - 6 DX=125 RX=111 110 109 108 107 106 105 104 + // Phasing is considered to be achieved when two DXs and one RX, or two RXs and one DX, or three RXs in the appropriate DX or RX positions, respectively, are successfully received. + if (m_bitCount == 10*3) + { + m_bitCount--; + + unsigned int pat = m_bits & 0x3fffffff; + for (int i = 0; i < m_phasingPatterns.size(); i++) + { + if (pat == m_phasingPatterns[i].m_pattern) + { + m_dscDecoder.init(m_phasingPatterns[i].m_offset); + m_gotSOP = true; + m_bitCount = 0; + break; + } + } + } + } + else + { + if (m_bitCount == 10) + { + if (m_dscDecoder.decodeBits(m_bits & 0x3ff)) + { + QByteArray bytes = m_dscDecoder.getMessage(); + DSCMessage message(bytes, QDateTime::currentDateTime()); + //qDebug() << "RX Bytes: " << bytes.toHex(); + //qDebug() << "DSC Message: " << message.toString(); + + if (getMessageQueueToChannel()) + { + float rssi = CalcDb::dbPower(m_rssiMagSqSum / m_rssiMagSqCount); + DSCDemod::MsgMessage *msg = DSCDemod::MsgMessage::create(message, m_dscDecoder.getErrors(), rssi); + getMessageQueueToChannel()->push(msg); + } + + // Reset demod + init(); + } + m_bitCount = 0; + } + } +} + +void DSCDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "DSCDemodSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((m_channelFrequencyOffset != channelFrequencyOffset) || + (m_channelSampleRate != channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) || force) + { + m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) channelSampleRate / (Real) DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void DSCDemodSink::init() +{ + m_expIdx = 0; + m_bit = 0; + m_bits = 0; + m_bitCount = 0; + m_gotSOP = false; + m_errorCount = 0; + m_clockCount = -m_samplesPerBit/2.0; + m_clock = 0; + m_int = 0.0; + m_rssiMagSqSum = 0.0; + m_rssiMagSqCount = 0; + m_consecutiveErrors = 0; + m_messageBuffer = ""; +} + +void DSCDemodSink::applySettings(const DSCDemodSettings& settings, bool force) +{ + qDebug() << "DSCDemodSink::applySettings:" + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " force: " << force; + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + if (force) + { + delete[] m_exp; + m_exp = new Complex[m_expLength]; + Real f0 = 0.0f; + for (int i = 0; i < m_expLength; i++) + { + m_exp[i] = Complex(cos(f0), sin(f0)); + f0 += 2.0f * (Real)M_PI * (DSCDemodSettings::DSCDEMOD_FREQUENCY_SHIFT/2.0f) / DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE; + } + init(); + + m_movMax1.setSize(m_samplesPerBit * 8); + m_movMax2.setSize(m_samplesPerBit * 8); + } + + m_settings = settings; +} diff --git a/plugins/channelrx/demoddsc/dscdemodsink.h b/plugins/channelrx/demoddsc/dscdemodsink.h new file mode 100644 index 000000000..3cce471c5 --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodsink.h @@ -0,0 +1,154 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMODSINK_H +#define INCLUDE_DSCDEMODSINK_H + +#include +#include +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "util/movingaverage.h" +#include "util/movingmaximum.h" +#include "util/messagequeue.h" +#include "util/dsc.h" + +#include "dscdemodsettings.h" + +class ChannelAPI; +class DSCDemod; +class ScopeVis; + + +class DSCDemodSink : public ChannelSampleSink { +public: + DSCDemodSink(DSCDemod *packetDemod); + ~DSCDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setScopeSink(ScopeVis* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const DSCDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + struct PhasingPattern { + unsigned int m_pattern; + unsigned int m_offset; + }; + + ScopeVis* m_scopeSink; // Scope GUI to display baseband waveform + DSCDemod *m_dscDemod; + DSCDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MessageQueue *m_messageQueueToChannel; + + MovingAverageUtil m_movingAverage; + + Lowpass m_lowpassComplex1; + Lowpass m_lowpassComplex2; + MovingMaximum m_movMax1; + MovingMaximum m_movMax2; + + static const int m_expLength = 600; + static const int m_samplesPerBit = DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE / DSCDemodSettings::DSCDEMOD_BAUD_RATE; + Complex *m_exp; + int m_expIdx; + int m_bit; + bool m_data; + bool m_dataPrev; + double m_clockCount; + double m_clock; + double m_int; + double m_rssiMagSqSum; + int m_rssiMagSqCount; + + unsigned int m_bits; + int m_bitCount; + bool m_gotSOP; + int m_errorCount; + int m_consecutiveErrors; + QString m_messageBuffer; + + DSCDecoder m_dscDecoder; + static const QList m_phasingPatterns; + + ComplexVector m_sampleBuffer[DSCDemodSettings::m_scopeStreams]; + static const int m_sampleBufferSize = DSCDemodSettings::DSCDEMOD_CHANNEL_SAMPLE_RATE / 20; + int m_sampleBufferIndex; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample, Real abs1Filt, Real abs2Filt, Real unbiasedData, Real biasedData); + void init(); + void receiveBit(bool bit); +}; + +#endif // INCLUDE_DSCDEMODSINK_H + diff --git a/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp new file mode 100644 index 000000000..91fb5f0ea --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "dscdemod.h" +#include "dscdemodwebapiadapter.h" + +DSCDemodWebAPIAdapter::DSCDemodWebAPIAdapter() +{} + +DSCDemodWebAPIAdapter::~DSCDemodWebAPIAdapter() +{} + +int DSCDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings()); + response.getDscDemodSettings()->init(); + DSCDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int DSCDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + DSCDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h new file mode 100644 index 000000000..cf75c968d --- /dev/null +++ b/plugins/channelrx/demoddsc/dscdemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_DSCDEMOD_WEBAPIADAPTER_H +#define INCLUDE_DSCDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "dscdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class DSCDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + DSCDemodWebAPIAdapter(); + virtual ~DSCDemodWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + DSCDemodSettings m_settings; +}; + +#endif // INCLUDE_DSCDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demoddsc/readme.md b/plugins/channelrx/demoddsc/readme.md new file mode 100644 index 000000000..71e61984d --- /dev/null +++ b/plugins/channelrx/demoddsc/readme.md @@ -0,0 +1,121 @@ +

DSC (Digital Selective Calling) Demodulator Plugin

+ +

Introduction

+ +This plugin can be used to demodulate DSC (Digital Selective Calling) transmissions, which are short, pre-defined digital messages transmitted by marine radios. + +DSC messages are transmitted using FSK with 170Hz separation at 100 baud, as specified by [ITU-R M.493](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.493-15-201901-I!!PDF-E.pdf]). + +DSC messages can be transmitted on a variety of frequencies, but are most commonly found on: 2,187.5kHz, 8,414.5kHz, 16,804.5kHz and 156.525 MHz (VHF Ch. 70). + +

Interface

+ +The top and bottom bars of the channel window are described [here](../../../sdrgui/channel/readme.md) + +![DSC Demodulator plugin GUI](../../../doc/img/DSCDemod_plugin.png) + +

1: Frequency shift from center frequency of reception

+ +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

2: Channel power

+ +Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. + +

3: Level meter in dB

+ + - top bar (green): average value + - bottom bar (blue green): instantaneous peak value + - tip vertical bar (bright green): peak hold value + +

4: UDP

+ +When checked, received messages are forwarded to the specified UDP address (5) and port (6). + +

5: UDP address

+ +IP address of the host to forward received messages to via UDP. + +

6: UDP port

+ +UDP port number to forward received messages to. + +

7: Filter

+ +This drop down displays a list of all columns which can be used for filtering (8). + +

8: Filter Reg Exp

+ +Specifes a [regular expression](https://regexr.com/) used to filter data in the table, using data in the column specified by (7). + +

9: Filter Invalid

+ +When checked, invalid messages will be filtered from the table. + +

10: Feed to YaDDNet

+ +When checked, valid messages will be forwarded to [YaDDNet](http://yaddnet.org/). +YaDDNet aggregates DSC messages from different users around the world storing them in a searchable database. +The messages are submitted with Preferences > My Position... > Station name used as the ID. + +Right click to open http://yaddnet.org/ in your browser, showing recent messages received from this ID. + +

11: Start/stop Logging Messages to .csv File

+ +When checked, writes all received messages to a .csv file, specified by (12). + +

12: .csv Log Filename

+ +Click to specify the name of the .csv file which received messasges are logged to. + +

13: Read Data from .csv File

+ +Click to specify a previously written .csv log file, which is read and used to update the table. + +

14: Received Messages Table

+ +![DSC Demodulator plugin GUI](../../../doc/img/DSCDemod_plugin_messages.png) + +The received messages table displays the contents of the messages that have been received. Most of the fields are decoded directly from the message, +however, a few, such as ship names, are found by querying [aprs.fi](http://aprs.fi) with the MMSI. + +* Date - Date the message was received. +* Time - Time the message was received. +* Format - The message format (Selective call, Geographic call, Group call, Distress alert, All ships, Automatic call). +* To - Who the message is to (The address field). This is typically an MMSI, but can also be a geographic area. +* Country - Country with jurisdiction of the destination of the message. +* Type - MMSI type of the destination of the message (Ship / Coast station). +* Name - The name of ship / station the message is for (From aprs.fi). +* Category - The message category (Safety, Routine, Urgency, Distress). +* From - MMSI of sender of message. +* Country - Country with jurisdiction of the sender of the message. +* Type - MMSI type of the sender of the message (Ship / Coast station). +* Name - The name of ship / station sending the message (From aprs.fi). +* Range (km) - The distance in kilometers from My Position (specified under Preferences > My Position) to the position of the sender of the message, as reported by aprs.fi (usually from AIS data). +* Telecommand 1 - First telecommand (Test / J3E (SSB) telephony and so on). +* Telecommand 2 - Second telecommand. +* RX - RX frequency (Hz) or channel. +* TX - TX frequency (Hz) or channel. +* Position - Position of ship in degrees and minutes. +* Distress Id - MMSI of ship in distress. +* Distress - Nature of distress (Sinking, Collision, Man overboard and so on). +* Number - Telephone number. +* Time - UTC Time. +* Comms - Subsequent communications. +* EOS - End of Signal (Req ACK, ACK, EOS). +* ECC - Indicates if calculated ECC (Error Checking Code) matches received ECC. +* Errors - Number of symbols received with errors (which may have been corrected if ECC is OK) +* Valid - Whether the message is determined to be valid (contains no detected errors). +* RSSI - Average channel power in dB, while receiving the message. + +Right clicking on the header will open a menu allowing you to select which columns are visible. +To reorder the columns, left click and drag left or right a column header. +Left click on a header to sort the table by the data in that column. + +Right clicking on a cell will open a pop-up menu that that allows: +* MMSIs to be looked up on some popular web sites, +* Ships to be located on the [Map](../../feature/map/readme.md) if also being tracked via AIS, +* Tune SSB Demods to the RX frequency or +* Geographical call areas to be drawn on the [Map](../../feature/map/readme.md): + +![DSC Demodulator plugin GUI](../../../doc/img/DSCDemod_plugin_geocall.png) diff --git a/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp b/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp index 464942421..19bc98476 100644 --- a/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp +++ b/plugins/channelrx/demodnavtex/navtexdemodbaseband.cpp @@ -145,7 +145,8 @@ bool NavtexDemodBaseband::handleMessage(const Message& cmd) DSPSignalNotification& notif = (DSPSignalNotification&) cmd; qDebug() << "NavtexDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); setBasebandSampleRate(notif.getSampleRate()); - m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + // We can run with very slow sample rate (E.g. 4k), but we don't want FIFO getting too small + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(std::max(notif.getSampleRate(), 48000))); return true; } diff --git a/plugins/channelrx/demodnavtex/navtexdemodgui.cpp b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp index de33b0ba2..8e208c2b7 100644 --- a/plugins/channelrx/demodnavtex/navtexdemodgui.cpp +++ b/plugins/channelrx/demodnavtex/navtexdemodgui.cpp @@ -222,12 +222,12 @@ void NavtexDemodGUI::messageReceived(const NavtexMessage& message, int errors, f rssiItem->setData(Qt::DisplayRole, rssi); } messageItem->setText(message.m_message); + filterRow(row); ui->messages->setSortingEnabled(true); ui->messages->resizeRowToContents(row); if (scrollToBottom) { ui->messages->scrollToBottom(); } - filterRow(row); } bool NavtexDemodGUI::handleMessage(const Message& message) diff --git a/plugins/channelrx/demodpacket/packetdemodgui.cpp b/plugins/channelrx/demodpacket/packetdemodgui.cpp index 23b162d52..d09e07a87 100644 --- a/plugins/channelrx/demodpacket/packetdemodgui.cpp +++ b/plugins/channelrx/demodpacket/packetdemodgui.cpp @@ -193,11 +193,11 @@ void PacketDemodGUI::packetReceived(QByteArray packet) pidItem->setText(ax25.m_pid); dataASCIIItem->setText(ax25.m_dataASCII); dataHexItem->setText(ax25.m_dataHex); + filterRow(row); ui->packets->setSortingEnabled(true); if (scrollToBottom) { ui->packets->scrollToBottom(); } - filterRow(row); } else qDebug() << "Unsupported AX.25 packet: " << packet; diff --git a/plugins/channelrx/demodpager/pagerdemodgui.cpp b/plugins/channelrx/demodpager/pagerdemodgui.cpp index 296c275f7..f2ae1d7e1 100644 --- a/plugins/channelrx/demodpager/pagerdemodgui.cpp +++ b/plugins/channelrx/demodpager/pagerdemodgui.cpp @@ -249,11 +249,11 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f numericItem->setText(numericMessage); evenPEItem->setText(QString("%1").arg(evenParityErrors)); bchPEItem->setText(QString("%1").arg(bchParityErrors)); + filterRow(row); ui->messages->setSortingEnabled(true); if (scrollToBottom) { ui->messages->scrollToBottom(); } - filterRow(row); } bool PagerDemodGUI::handleMessage(const Message& message) diff --git a/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp index 4a3034b22..ebbb5d0de 100644 --- a/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp +++ b/plugins/channelrx/demodradiosonde/radiosondedemodgui.cpp @@ -298,11 +298,11 @@ void RadiosondeDemodGUI::frameReceived(const QByteArray& frame, const QDateTime& eccItem->setData(Qt::DisplayRole, errorsCorrected); thItem->setData(Qt::DisplayRole, threshold); + filterRow(row); ui->frames->setSortingEnabled(true); if (scrollToBottom) { ui->frames->scrollToBottom(); } - filterRow(row); delete radiosonde; } diff --git a/plugins/channelrx/demodrtty/rttydemodbaseband.cpp b/plugins/channelrx/demodrtty/rttydemodbaseband.cpp index e54076203..3cd28f59b 100644 --- a/plugins/channelrx/demodrtty/rttydemodbaseband.cpp +++ b/plugins/channelrx/demodrtty/rttydemodbaseband.cpp @@ -145,7 +145,8 @@ bool RttyDemodBaseband::handleMessage(const Message& cmd) DSPSignalNotification& notif = (DSPSignalNotification&) cmd; qDebug() << "RttyDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); setBasebandSampleRate(notif.getSampleRate()); - m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + // We can run with very slow sample rate (E.g. 4k), but we don't want FIFO getting too small + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(std::max(notif.getSampleRate(), 48000))); return true; } diff --git a/plugins/channelrx/heatmap/heatmapsettings.cpp b/plugins/channelrx/heatmap/heatmapsettings.cpp index 100e75fb5..2fda3e9c3 100644 --- a/plugins/channelrx/heatmap/heatmapsettings.cpp +++ b/plugins/channelrx/heatmap/heatmapsettings.cpp @@ -107,9 +107,9 @@ QByteArray HeatMapSettings::serialize() const s.writeBlob(30, m_rollupState->serialize()); } - s.writeS32(32, m_workspaceIndex); - s.writeBlob(33, m_geometryBytes); - s.writeBool(34, m_hidden); + s.writeS32(31, m_workspaceIndex); + s.writeBlob(32, m_geometryBytes); + s.writeBool(33, m_hidden); return s.final(); } diff --git a/plugins/feature/ais/aisgui.cpp b/plugins/feature/ais/aisgui.cpp index 641a27150..481d021df 100644 --- a/plugins/feature/ais/aisgui.cpp +++ b/plugins/feature/ais/aisgui.cpp @@ -31,6 +31,7 @@ #include "gui/dialogpositioner.h" #include "mainwindow.h" #include "device/deviceuiset.h" +#include "util/mmsi.h" #include "ui_aisgui.h" #include "ais.h" @@ -377,6 +378,7 @@ void AISGUI::resizeTable() int row = ui->vessels->rowCount(); ui->vessels->setRowCount(row + 1); ui->vessels->setItem(row, VESSEL_COL_MMSI, new QTableWidgetItem("123456789")); + ui->vessels->setItem(row, VESSEL_COL_COUNTRY, new QTableWidgetItem("flag")); ui->vessels->setItem(row, VESSEL_COL_TYPE, new QTableWidgetItem("Base station")); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, new QTableWidgetItem("90.000000-")); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, new QTableWidgetItem("180.00000-")); @@ -503,6 +505,7 @@ void AISGUI::sendToMap(const QString &name, const QString &label, void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) { QTableWidgetItem *mmsiItem; + QTableWidgetItem *countryItem; QTableWidgetItem *typeItem; QTableWidgetItem *latitudeItem; QTableWidgetItem *longitudeItem; @@ -534,6 +537,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) { // Update existing item mmsiItem = ui->vessels->item(row, VESSEL_COL_MMSI); + countryItem = ui->vessels->item(row, VESSEL_COL_COUNTRY); typeItem = ui->vessels->item(row, VESSEL_COL_TYPE); latitudeItem = ui->vessels->item(row, VESSEL_COL_LATITUDE); longitudeItem = ui->vessels->item(row, VESSEL_COL_LONGITUDE); @@ -563,6 +567,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) ui->vessels->setRowCount(row + 1); mmsiItem = new QTableWidgetItem(); + countryItem = new QTableWidgetItem(); typeItem = new QTableWidgetItem(); latitudeItem = new QTableWidgetItem(); longitudeItem = new QTableWidgetItem(); @@ -580,6 +585,7 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) lastUpdateItem = new QTableWidgetItem(); messagesItem = new QTableWidgetItem(); ui->vessels->setItem(row, VESSEL_COL_MMSI, mmsiItem); + ui->vessels->setItem(row, VESSEL_COL_COUNTRY, countryItem); ui->vessels->setItem(row, VESSEL_COL_TYPE, typeItem); ui->vessels->setItem(row, VESSEL_COL_LATITUDE, latitudeItem); ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, longitudeItem); @@ -605,7 +611,15 @@ void AISGUI::updateVessels(AISMessage *ais, QDateTime dateTime) previousType = typeItem->text(); previousShipType = shipTypeItem->text(); - mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + QString mmsi = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); + mmsiItem->setText(mmsi); + QIcon *flag = MMSI::getFlagIcon(mmsi); + if (flag) + { + countryItem->setSizeHint(QSize(40, 20)); + countryItem->setIcon(*flag); + } + lastUpdateItem->setData(Qt::DisplayRole, dateTime); messagesItem->setData(Qt::DisplayRole, messagesItem->data(Qt::DisplayRole).toInt() + 1); @@ -991,24 +1005,24 @@ void AISGUI::vessels_customContextMenuRequested(QPoint pos) QAction* mmsiAction = new QAction(QString("View MMSI %1 on vesselfinder.com...").arg(mmsi), tableContextMenu); connect(mmsiAction, &QAction::triggered, this, [mmsi]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(mmsi))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(mmsi))); }); tableContextMenu->addAction(mmsiAction); if (!imo.isEmpty()) { - QAction* imoAction = new QAction(QString("View IMO %1 on vesselfinder.net...").arg(imo), tableContextMenu); + QAction* imoAction = new QAction(QString("View IMO %1 on vesselfinder.com...").arg(imo), tableContextMenu); connect(imoAction, &QAction::triggered, this, [imo]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(imo))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(imo))); }); tableContextMenu->addAction(imoAction); } if (!name.isEmpty()) { - QAction* nameAction = new QAction(QString("View %1 on vesselfinder.net...").arg(name), tableContextMenu); + QAction* nameAction = new QAction(QString("View %1 on vesselfinder.com...").arg(name), tableContextMenu); connect(nameAction, &QAction::triggered, this, [name]()->void { - QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.net/vessels?name=%1").arg(name))); + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(name))); }); tableContextMenu->addAction(nameAction); } diff --git a/plugins/feature/ais/aisgui.h b/plugins/feature/ais/aisgui.h index 893a59d7a..f09fe8ba7 100644 --- a/plugins/feature/ais/aisgui.h +++ b/plugins/feature/ais/aisgui.h @@ -106,6 +106,7 @@ private: enum VesselCol { VESSEL_COL_MMSI, + VESSEL_COL_COUNTRY, VESSEL_COL_TYPE, VESSEL_COL_LATITUDE, VESSEL_COL_LONGITUDE, diff --git a/plugins/feature/ais/aisgui.ui b/plugins/feature/ais/aisgui.ui index bfeaf9342..68918a104 100644 --- a/plugins/feature/ais/aisgui.ui +++ b/plugins/feature/ais/aisgui.ui @@ -89,10 +89,21 @@ Maritime Mobile Service Identity
+ + + Country + + + Country with jurisdiction over station/vessel + + Type + + Message type + diff --git a/plugins/feature/ais/aissettings.h b/plugins/feature/ais/aissettings.h index f1ff92e74..d2ac77812 100644 --- a/plugins/feature/ais/aissettings.h +++ b/plugins/feature/ais/aissettings.h @@ -27,7 +27,7 @@ class Serializable; // Number of columns in the tables -#define AIS_VESSEL_COLUMNS 16 +#define AIS_VESSEL_COLUMNS 18 struct AISSettings { diff --git a/plugins/feature/map/czml.cpp b/plugins/feature/map/czml.cpp index de7874222..bc0ecc019 100644 --- a/plugins/feature/map/czml.cpp +++ b/plugins/feature/map/czml.cpp @@ -77,6 +77,7 @@ QJsonObject CZML::update(PolygonMapItem *mapItem) if ( !mapItem->m_itemSettings->m_enabled || !mapItem->m_itemSettings->m_display3DTrack || filter(mapItem) + || mapItem->m_deleted ) { // Delete obj completely (including any history) @@ -147,6 +148,15 @@ QJsonObject CZML::update(PolygonMapItem *mapItem) polygon.insert("extrudedHeight", mapItem->m_extrudedHeight); } + // We need to have a position, otherwise viewer entity tracking doesn't seem to work + QJsonArray coords { + mapItem->m_longitude, mapItem->m_latitude, mapItem->m_altitude + }; + QJsonObject position { + {"cartographicDegrees", coords}, + }; + obj.insert("position", position); + obj.insert("polygon", polygon); obj.insert("description", mapItem->m_label); @@ -165,6 +175,7 @@ QJsonObject CZML::update(PolylineMapItem *mapItem) if ( !mapItem->m_itemSettings->m_enabled || !mapItem->m_itemSettings->m_display3DTrack || filter(mapItem) + || mapItem->m_deleted ) { // Delete obj completely (including any history) @@ -214,6 +225,15 @@ QJsonObject CZML::update(PolylineMapItem *mapItem) polyline.insert("altitudeReference", m_heightReferences[mapItem->m_altitudeReference]); // Custom code in map3d.html } + // We need to have a position, otherwise viewer entity tracking doesn't seem to work + QJsonArray coords { + mapItem->m_longitude, mapItem->m_latitude, mapItem->m_altitude + }; + QJsonObject position { + {"cartographicDegrees", coords}, + }; + obj.insert("position", position); + obj.insert("polyline", polyline); obj.insert("description", mapItem->m_label); diff --git a/plugins/feature/map/map/map.qml b/plugins/feature/map/map/map.qml index 193b2d95a..5d807c985 100644 --- a/plugins/feature/map/map/map.qml +++ b/plugins/feature/map/map/map.qml @@ -194,6 +194,7 @@ Item { Text { id: polygonText text: label + textFormat: TextEdit.RichText } } } @@ -226,6 +227,7 @@ Item { Text { id: polylineText text: label + textFormat: TextEdit.RichText } } } diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index 47285af89..d7e07ea68 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -309,6 +309,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur addAirspace(); addAirports(); addNavtex(); + addVLF(); displaySettings(); applySettings(true); @@ -419,6 +420,53 @@ void MapGUI::addIBPBeacons() } } +// https://sidstation.loudet.org/stations-list-en.xhtml +// https://core.ac.uk/download/pdf/224769021.pdf -- Table 1 +// GQD/GQZ callsigns: https://groups.io/g/VLF/message/19212?p=%2C%2C%2C20%2C0%2C0%2C0%3A%3Arecentpostdate%2Fsticky%2C%2C19.6%2C20%2C2%2C0%2C38924431 +const QList MapGUI::m_vlfTransmitters = { + // Other signals possibly seen: 13800, 19000 + {"VTX2", 17000, 8.387015, 77.752762, -1}, // South Vijayanarayanam, India + {"GQD", 19580, 54.911643, -3.278456, 100}, // Anthorn, UK, Often referred to as GBZ + {"NWC", 19800, -21.816325, 114.16546, 1000}, // Exmouth, Aus + {"ICV", 20270, 40.922946, 9.731881, 50}, // Isola di Tavolara, Italy (Can be distorted on 3D map if terrain used) + {"FTA", 20900, 48.544632, 2.579429, 50}, // Sainte-Assise, France (Satellite imagary obfuscated) + {"NPM", 21400, 21.420166, -158.151140, 600}, // Pearl Harbour, Lualuahei, USA (Not seen?) + {"HWU", 21750, 46.713129, 1.245248, 200}, // Rosnay, France + {"GQZ", 22100, 54.731799, -2.883033, 100}, // Skelton, UK (GVT in paper) + {"DHO38", 23400, 53.078900, 7.615000, 300}, // Rhauderfehn, Germany - Off air 7-8 UTC - Not seen on air! + {"NAA", 24000, 44.644506, -67.284565, 1000}, // Cutler, Maine, USA + {"TFK/NRK", 37500, 63.850365, -22.466773, 100}, // Grindavik, Iceland + {"SRC/SHR", 38000, 57.120328, 16.153083, -1}, // Ruda, Sweden +}; + +void MapGUI::addVLF() +{ + for (int i = 0; i < m_vlfTransmitters.size(); i++) + { + SWGSDRangel::SWGMapItem vlfMapItem; + // Need to suffix frequency, as there are multiple becaons with same callsign at different locations + QString name = QString("%1").arg(m_vlfTransmitters[i].m_callsign); + vlfMapItem.setName(new QString(name)); + vlfMapItem.setLatitude(m_vlfTransmitters[i].m_latitude); + vlfMapItem.setLongitude(m_vlfTransmitters[i].m_longitude); + vlfMapItem.setAltitude(0.0); + vlfMapItem.setImage(new QString("antenna.png")); + vlfMapItem.setImageRotation(0); + QString text = QString("VLF Transmitter\nCallsign: %1\nFrequency: %2 kHz") + .arg(m_vlfTransmitters[i].m_callsign) + .arg(m_vlfTransmitters[i].m_frequency/1000.0); + vlfMapItem.setText(new QString(text)); + vlfMapItem.setModel(new QString("antenna.glb")); + vlfMapItem.setFixedPosition(true); + vlfMapItem.setOrientation(0); + vlfMapItem.setLabel(new QString(name)); + vlfMapItem.setLabelAltitudeOffset(4.5); + vlfMapItem.setAltitudeReference(1); + update(m_map, &vlfMapItem, "VLF"); + } +} + + const QList MapGUI::m_radioTimeTransmitters = { {"MSF", 60000, 54.9075f, -3.27333f, 17}, // UK {"DCF77", 77500, 50.01611111f, 9.00805556f, 50}, // Germany @@ -1575,8 +1623,7 @@ void MapGUI::find(const QString& target) } else { - // FIXME: Support polygon/polyline - ObjectMapItem *mapItem = m_objectMapModel.findMapItem(target); + ObjectMapItem *mapItem = (ObjectMapItem *)m_objectMapModel.findMapItem(target); if (mapItem != nullptr) { map->setProperty("center", QVariant::fromValue(mapItem->getCoordinates())); @@ -1584,25 +1631,47 @@ void MapGUI::find(const QString& target) m_cesium->track(target); } m_objectMapModel.moveToFront(m_objectMapModel.findMapItemIndex(target).row()); + return; + } + + PolylineMapItem *polylineMapItem = (PolylineMapItem *)m_polylineMapModel.findMapItem(target); + if (polylineMapItem != nullptr) + { + map->setProperty("center", QVariant::fromValue(polylineMapItem->getCoordinates())); + if (m_cesium) { + m_cesium->track(target); + } + //m_polylineMapModel.moveToFront(m_polylineMapModel.findMapItemIndex(target).row()); + return; + } + + PolygonMapItem *polygonMapItem = (PolygonMapItem *)m_polylineMapModel.findMapItem(target); + if (polygonMapItem != nullptr) + { + map->setProperty("center", QVariant::fromValue(polygonMapItem->getCoordinates())); + if (m_cesium) { + m_cesium->track(target); + } + //m_polylineMapModel.moveToFront(m_polylineMapModel.findMapItemIndex(target).row()); + return; + } + + // Search as an address + QGeoServiceProvider* geoSrv = new QGeoServiceProvider("osm"); + if (geoSrv != nullptr) + { + QLocale qLocaleC(QLocale::C, QLocale::AnyCountry); + geoSrv->setLocale(qLocaleC); + QGeoCodeReply *pQGeoCode = geoSrv->geocodingManager()->geocode(target); + if (pQGeoCode) { + QObject::connect(pQGeoCode, &QGeoCodeReply::finished, this, &MapGUI::geoReply); + } else { + qDebug() << "MapGUI::find: GeoCoding failed"; + } } else { - QGeoServiceProvider* geoSrv = new QGeoServiceProvider("osm"); - if (geoSrv != nullptr) - { - QLocale qLocaleC(QLocale::C, QLocale::AnyCountry); - geoSrv->setLocale(qLocaleC); - QGeoCodeReply *pQGeoCode = geoSrv->geocodingManager()->geocode(target); - if (pQGeoCode) { - QObject::connect(pQGeoCode, &QGeoCodeReply::finished, this, &MapGUI::geoReply); - } else { - qDebug() << "MapGUI::find: GeoCoding failed"; - } - } - else - { - qDebug() << "MapGUI::find: osm not available"; - } + qDebug() << "MapGUI::find: osm not available"; } } } diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index c90a27b72..354388361 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -161,6 +161,7 @@ public: void addAirspace(); void addAirports(); void addNavtex(); + void addVLF(); void find(const QString& target); void track3D(const QString& target); Q_INVOKABLE void supportedMapsChanged(); @@ -228,6 +229,7 @@ private: static QString getDataDir(); static const QList m_radioTimeTransmitters; + static const QList m_vlfTransmitters; private slots: void init3DMap(); diff --git a/plugins/feature/map/mapitem.cpp b/plugins/feature/map/mapitem.cpp index 5a3c27af2..ba96454cb 100644 --- a/plugins/feature/map/mapitem.cpp +++ b/plugins/feature/map/mapitem.cpp @@ -39,6 +39,14 @@ void MapItem::update(SWGSDRangel::SWGMapItem *mapItem) m_altitude = mapItem->getAltitude(); } +QGeoCoordinate MapItem::getCoordinates() +{ + QGeoCoordinate coords; + coords.setLatitude(m_latitude); + coords.setLongitude(m_longitude); + return coords; +} + void ObjectMapItem::update(SWGSDRangel::SWGMapItem *mapItem) { MapItem::update(mapItem); @@ -116,6 +124,7 @@ void PolygonMapItem::update(SWGSDRangel::SWGMapItem *mapItem) m_colorValid = mapItem->getColorValid(); m_color = mapItem->getColor(); m_altitudeReference = mapItem->getAltitudeReference(); + m_deleted = *mapItem->getImage() == ""; qDeleteAll(m_points); m_points.clear(); @@ -151,6 +160,7 @@ void PolylineMapItem::update(SWGSDRangel::SWGMapItem *mapItem) m_colorValid = mapItem->getColorValid(); m_color = mapItem->getColor(); m_altitudeReference = mapItem->getAltitudeReference(); + m_deleted = *mapItem->getImage() == ""; qDeleteAll(m_points); m_points.clear(); @@ -180,14 +190,6 @@ void PolylineMapItem::update(SWGSDRangel::SWGMapItem *mapItem) m_bounds = QGeoRectangle(QGeoCoordinate(latMax, lonMin), QGeoCoordinate(latMin, lonMax)); } -QGeoCoordinate ObjectMapItem::getCoordinates() -{ - QGeoCoordinate coords; - coords.setLatitude(m_latitude); - coords.setLongitude(m_longitude); - return coords; -} - void ObjectMapItem::findFrequency() { // Look for a frequency in the text for this object diff --git a/plugins/feature/map/mapitem.h b/plugins/feature/map/mapitem.h index 02b47e55e..8a733bbfc 100644 --- a/plugins/feature/map/mapitem.h +++ b/plugins/feature/map/mapitem.h @@ -41,6 +41,7 @@ public: MapItem(const QObject *sourcePipe, const QString &group, MapSettings::MapItemSettings *itemSettings, SWGSDRangel::SWGMapItem *mapItem); virtual void update(SWGSDRangel::SWGMapItem *mapItem); + QGeoCoordinate getCoordinates(); protected: @@ -74,7 +75,6 @@ public: update(mapItem); } void update(SWGSDRangel::SWGMapItem *mapItem) override; - QGeoCoordinate getCoordinates(); protected: void findFrequency(); @@ -143,6 +143,7 @@ protected: bool m_colorValid; QRgb m_color; int m_altitudeReference; + bool m_deleted; }; class PolylineMapItem : public MapItem { @@ -165,6 +166,7 @@ protected: bool m_colorValid; QRgb m_color; int m_altitudeReference; + bool m_deleted; }; class ImageMapItem : public MapItem { diff --git a/plugins/feature/map/mapmodel.cpp b/plugins/feature/map/mapmodel.cpp index 7b110604d..0fd548074 100644 --- a/plugins/feature/map/mapmodel.cpp +++ b/plugins/feature/map/mapmodel.cpp @@ -87,12 +87,11 @@ void MapModel::update(const QObject *sourcePipe, SWGSDRangel::SWGMapItem *swgMap QString image = *swgMapItem->getImage(); if (image.isEmpty()) { - // Delete the item + // Delete the item from 2D map remove(item); - // Need to call update, for it to be removed in 3D map - // Item is set to not be available from this point in time - // It will still be available if time is set in the past + // Delete from 3D map item->update(swgMapItem); + update3D(item); } else { @@ -178,6 +177,36 @@ MapItem *MapModel::findMapItem(const QObject *source, const QString& name) return nullptr; } +// FIXME: This should potentially return a list, as we have have multiple items with the same name +// from different sources +MapItem *MapModel::findMapItem(const QString& name) +{ + QListIterator i(m_items); + while (i.hasNext()) + { + MapItem *item = i.next(); + if (!item->m_name.compare(name, Qt::CaseInsensitive)) { + return item; + } + } + return nullptr; +} + +QModelIndex MapModel::findMapItemIndex(const QString& name) +{ + int idx = 0; + QListIterator i(m_items); + while (i.hasNext()) + { + MapItem *item = i.next(); + if (item->m_name == name) { + return index(idx); + } + idx++; + } + return index(-1); +} + QHash MapModel::roleNames() const { QHash roles; @@ -589,36 +618,6 @@ Q_INVOKABLE void ObjectMapModel::moveToBack(int oldRow) } } -// FIXME: This should potentially return a list, as we have have multiple items with the same name -// from different sources -ObjectMapItem *ObjectMapModel::findMapItem(const QString& name) -{ - QListIterator i(m_items); - while (i.hasNext()) - { - MapItem *item = i.next(); - if (!item->m_name.compare(name, Qt::CaseInsensitive)) { - return (ObjectMapItem *)item; - } - } - return nullptr; -} - -QModelIndex ObjectMapModel::findMapItemIndex(const QString& name) -{ - int idx = 0; - QListIterator i(m_items); - while (i.hasNext()) - { - MapItem *item = i.next(); - if (item->m_name == name) { - return index(idx); - } - idx++; - } - return index(-1); -} - QVariant ObjectMapModel::data(const QModelIndex &index, int role) const { int row = index.row(); diff --git a/plugins/feature/map/mapmodel.h b/plugins/feature/map/mapmodel.h index 3db617eb4..0a12350c0 100644 --- a/plugins/feature/map/mapmodel.h +++ b/plugins/feature/map/mapmodel.h @@ -62,6 +62,8 @@ public: void allUpdated(); MapItem *findMapItem(const QObject *source, const QString& name); + MapItem *findMapItem(const QString& name); + QModelIndex findMapItemIndex(const QString& name); QHash roleNames() const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; @@ -235,9 +237,6 @@ public: Q_INVOKABLE void moveToFront(int oldRow); Q_INVOKABLE void moveToBack(int oldRow); - ObjectMapItem *findMapItem(const QString& name); - QModelIndex findMapItemIndex(const QString& name); - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index de8b90f92..1848fdc7b 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -31,6 +31,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("AIS"), QStringLiteral("APRS"), QStringLiteral("APTDemod"), + QStringLiteral("DSCDemod"), QStringLiteral("FT8Demod"), QStringLiteral("HeatMap"), QStringLiteral("ILSDemod"), @@ -46,6 +47,7 @@ const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.feature.ais"), QStringLiteral("sdrangel.feature.aprs"), QStringLiteral("sdrangel.channel.aptdemod"), + QStringLiteral("sdrangel.channel.dscdemod"), QStringLiteral("sdrangel.channel.ft8demod"), QStringLiteral("sdrangel.channel.heatmap"), QStringLiteral("sdrangel.channel.ilsdemod"), @@ -79,6 +81,7 @@ MapSettings::MapSettings() : MapItemSettings *aprsSettings = new MapItemSettings("APRS", true, QColor(255, 255, 0), true, false, 11); aprsSettings->m_extrapolate = 0; m_itemSettings.insert("APRS", aprsSettings); + m_itemSettings.insert("DSCDemod", new MapItemSettings("DSCDemod", true, QColor(181, 230, 29), true, true, 3)); m_itemSettings.insert("StarTracker", new MapItemSettings("StarTracker", true, QColor(230, 230, 230), true, true, 3)); m_itemSettings.insert("SatelliteTracker", new MapItemSettings("SatelliteTracker", true, QColor(0, 0, 255), true, false, 0, modelMinPixelSize)); m_itemSettings.insert("Beacons", new MapItemSettings("Beacons", true, QColor(255, 0, 0), false, true, 8)); @@ -87,6 +90,7 @@ MapSettings::MapSettings() : m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", true, QColor(0, 192, 255), true, true, 8)); m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, true, 11)); + m_itemSettings.insert("VLF", new MapItemSettings("VLF", false, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("AM", new MapItemSettings("AM", false, QColor(255, 0, 0), false, true, 10)); MapItemSettings *fmSettings = new MapItemSettings("FM", false, QColor(255, 0, 0), false, true, 12); diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 71cfdd26d..584223cc8 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -15,6 +15,7 @@ On top of this, it can plot data from other plugins, such as: * RF Heat Maps from the Heap Map channel, * Radials and estimated position from the VOR localizer feature, * ILS course line and glide path from the ILS Demodulator. +* DSC geographic call areas. As well as internet data sources: @@ -25,6 +26,7 @@ As well as internet data sources: * GRAVES radar, * Ionosonde station data, * Navtex transmitters. +* VLF transmitters. It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites. diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index c82864707..eca2ad3c4 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -172,6 +172,7 @@ set(sdrbase_SOURCES util/ais.cpp util/android.cpp + util/aprsfi.cpp util/aviationweather.cpp util/ax25.cpp util/aprs.cpp @@ -184,6 +185,7 @@ set(sdrbase_SOURCES util/CRC64.cpp util/csv.cpp util/db.cpp + util/dsc.cpp util/fixedtraits.cpp util/fits.cpp util/flightinformation.cpp @@ -196,6 +198,7 @@ set(sdrbase_SOURCES util/maidenhead.cpp util/message.cpp util/messagequeue.cpp + util/mmsi.cpp util/morse.cpp util/navtex.cpp util/openaip.cpp @@ -398,6 +401,7 @@ set(sdrbase_HEADERS util/ais.h util/android.h + util/aprsfi.h util/aviationweather.h util/ax25.h util/aprs.h @@ -409,6 +413,7 @@ set(sdrbase_HEADERS util/CRC64.h util/csv.h util/db.h + util/dsc.h util/doublebuffer.h util/doublebufferfifo.h util/doublebuffermultiple.h @@ -426,6 +431,7 @@ set(sdrbase_HEADERS util/maidenhead.h util/message.h util/messagequeue.h + util/mmsi.h util/morse.h util/movingaverage.h util/movingmaximum.h diff --git a/sdrbase/util/ais.cpp b/sdrbase/util/ais.cpp index 5ce502314..ddc0b5974 100644 --- a/sdrbase/util/ais.cpp +++ b/sdrbase/util/ais.cpp @@ -336,12 +336,24 @@ QString AISPositionReport::getStatusString(int status) return statuses[status]; } +QString AISPositionReport::getType() +{ + if (m_id == 1) { + return "Position report (Scheduled)"; + } else if (m_id == 2) { + return "Position report (Assigned)"; + } else { + return "Position report (Interrogated)"; + } +} + QString AISPositionReport::toString() { + QString speed = m_speedOverGround == 1022 ? ">102.2" : QString::number(m_speedOverGround); return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Status: %5") .arg(m_latitude) .arg(m_longitude) - .arg(m_speedOverGround) + .arg(speed) .arg(m_course) .arg(AISPositionReport::getStatusString(m_status)) .arg(QChar(0xb0)); @@ -444,7 +456,7 @@ AISSARAircraftPositionReport::AISSARAircraftPositionReport(QByteArray ba) : int sog = ((ba[6] & 0x3f) << 4) | ((ba[7] >> 4) & 0xf); m_speedOverGroundAvailable = sog != 1023; - m_speedOverGround = sog * 0.1f; + m_speedOverGround = sog; m_positionAccuracy = (ba[7] >> 3) & 0x1; @@ -467,12 +479,14 @@ AISSARAircraftPositionReport::AISSARAircraftPositionReport(QByteArray ba) : QString AISSARAircraftPositionReport::toString() { + QString altitude = m_altitude == 4094 ? ">4094" : QString::number(m_altitude); + QString speed = m_speedOverGround == 1022 ? ">1022" : QString::number(m_speedOverGround); return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Alt: %5 m") .arg(m_latitude) .arg(m_longitude) - .arg(m_speedOverGround) + .arg(speed) .arg(m_course) - .arg(m_altitude) + .arg(altitude) .arg(QChar(0xb0)); } @@ -520,6 +534,18 @@ AISInterrogation::AISInterrogation(QByteArray ba) : AISAssignedModeCommand::AISAssignedModeCommand(QByteArray ba) : AISMessage(ba) { + m_destinationIdA = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + m_offsetA = ((ba[8] & 0x3) << 10) | ((ba[9] & 0xff) << 2) | ((ba[10] >> 6) & 0x3); + m_incrementA = ((ba[10] & 0x3f) << 4) | ((ba[11] >> 4) & 0xf); + m_bAvailable = false; +} + +QString AISAssignedModeCommand::toString() +{ + return QString("Dest A: %1 Offset A: %2 Inc A: %3") + .arg(m_destinationIdA) + .arg(m_offsetA) + .arg(m_incrementA); } AISGNSSBroadcast::AISGNSSBroadcast(QByteArray ba) : @@ -721,6 +747,25 @@ QString AISStaticDataReport::toString() AISSingleSlotBinaryMessage::AISSingleSlotBinaryMessage(QByteArray ba) : AISMessage(ba) { + m_destinationIndicator = (ba[4] >> 1) & 1; + m_binaryDataFlag = ba[4] & 1; + if (m_destinationIndicator) { + m_destinationId = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + } + m_destinationIdAvailable = m_destinationIndicator; +} + +QString AISSingleSlotBinaryMessage::toString() +{ + QStringList s; + + s.append(QString("Destination: %1").arg(m_destinationIndicator ? "Broadcast" : "Addressed")); + s.append(QString("Flag: %1").arg(m_binaryDataFlag ? "Unstructured" : "Structured")); + if (m_destinationIdAvailable) { + s.append(QString("Destination Id: %1").arg(m_destinationId)); + } + + return s.join(" "); } AISMultipleSlotBinaryMessage::AISMultipleSlotBinaryMessage(QByteArray ba) : diff --git a/sdrbase/util/ais.h b/sdrbase/util/ais.h index 3de22e8ac..a226b04b7 100644 --- a/sdrbase/util/ais.h +++ b/sdrbase/util/ais.h @@ -79,7 +79,7 @@ public: int m_specialManoeuvre; AISPositionReport(const QByteArray ba); - virtual QString getType() override { return "Position report"; } + virtual QString getType() override; virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } virtual float getLatitude() { return m_latitude; } virtual float getLongitude() { return m_longitude; } @@ -229,8 +229,16 @@ public: class SDRBASE_API AISAssignedModeCommand : public AISMessage { public: + int m_destinationIdA; + int m_offsetA; + int m_incrementA; + int m_destinationIdB; + int m_offsetB; + int m_incrementB; + bool m_bAvailable; AISAssignedModeCommand(const QByteArray ba); virtual QString getType() override { return "Assigned mode command"; } + virtual QString toString() override; }; class SDRBASE_API AISGNSSBroadcast : public AISMessage { @@ -345,8 +353,14 @@ public: class SDRBASE_API AISSingleSlotBinaryMessage : public AISMessage { public: + bool m_destinationIndicator; + bool m_binaryDataFlag; + int m_destinationId; + bool m_destinationIdAvailable; + AISSingleSlotBinaryMessage(const QByteArray ba); virtual QString getType() override { return "Single slot binary message"; } + virtual QString toString() override; }; class SDRBASE_API AISMultipleSlotBinaryMessage : public AISMessage { diff --git a/sdrbase/util/aprsfi.cpp b/sdrbase/util/aprsfi.cpp new file mode 100644 index 000000000..00a993c26 --- /dev/null +++ b/sdrbase/util/aprsfi.cpp @@ -0,0 +1,187 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aprsfi.h" + +#include +#include +#include +#include +#include +#include + +QMutex APRSFi::m_mutex; +QHash APRSFi::m_aisCache; + +APRSFi::APRSFi(const QString& apiKey, int cacheValidMins) : + m_apiKey(apiKey), + m_cacheValidMins(cacheValidMins) +{ + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, &QNetworkAccessManager::finished, this, &APRSFi::handleReply); +} + +APRSFi::~APRSFi() +{ + disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &APRSFi::handleReply); + delete m_networkManager; +} + +APRSFi* APRSFi::create(const QString& apiKey, int cacheValidMins) +{ + return new APRSFi(apiKey, cacheValidMins); +} + +void APRSFi::getData(const QStringList& names) +{ + QStringList nonCachedNames; + QDateTime currentDateTime = QDateTime::currentDateTime(); + + QMutexLocker locker(&m_mutex); + for (const auto& name : names) + { + bool cached = false; + QList dataList; + if (m_aisCache.contains(name)) + { + const AISData& d = m_aisCache[name]; + if (d.m_dateTime.secsTo(currentDateTime) < m_cacheValidMins*60) + { + dataList.append(d); + cached = true; + } + } + if (dataList.size() > 0) { + emit dataUpdated(dataList); + } + if (!cached) { + nonCachedNames.append(name); + } + } + if (nonCachedNames.size() > 0) + { + QString nameList = nonCachedNames.join(","); + QUrl url(QString("https://api.aprs.fi/api/get")); + QUrlQuery query; + query.addQueryItem("name", nameList); + query.addQueryItem("what", "loc"); + query.addQueryItem("apikey", m_apiKey); + query.addQueryItem("format", "json"); + url.setQuery(query); + m_networkManager->get(QNetworkRequest(url)); + } +} + +void APRSFi::getData(const QString& name) +{ + QStringList names; + names.append(name); + getData(names); +} + +bool APRSFi::containsNonNull(const QJsonObject& obj, const QString &key) const +{ + if (obj.contains(key)) + { + QJsonValue val = obj.value(key); + return !val.isNull(); + } + return false; +} + +void APRSFi::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + + if (document.isObject()) + { + QJsonObject docObj = document.object(); + QDateTime receivedDateTime = QDateTime::currentDateTime(); + + if (docObj.contains(QStringLiteral("entries"))) + { + QJsonArray array = docObj.value(QStringLiteral("entries")).toArray(); + + QList data; + for (auto valRef : array) + { + if (valRef.isObject()) + { + QJsonObject obj = valRef.toObject(); + + AISData measurement; + + measurement.m_dateTime = receivedDateTime; + if (obj.contains(QStringLiteral("name"))) { + measurement.m_name = obj.value(QStringLiteral("name")).toString(); + } + if (obj.contains(QStringLiteral("mmsi"))) { + measurement.m_mmsi = obj.value(QStringLiteral("mmsi")).toString(); + } + if (containsNonNull(obj, QStringLiteral("time"))) { + measurement.m_firstTime = QDateTime::fromString(obj.value(QStringLiteral("time")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("lastTime"))) { + measurement.m_lastTime = QDateTime::fromString(obj.value(QStringLiteral("lastTime")).toString(), Qt::ISODate); + } + if (containsNonNull(obj, QStringLiteral("lat"))) { + measurement.m_latitude = obj.value(QStringLiteral("lat")).toDouble(); + } + if (containsNonNull(obj, QStringLiteral("lng"))) { + measurement.m_longitude = obj.value(QStringLiteral("lng")).toDouble(); + } + + data.append(measurement); + + if (!measurement.m_mmsi.isEmpty()) + { + QMutexLocker locker(&m_mutex); + m_aisCache.insert(measurement.m_mmsi, measurement); + } + } + else + { + qDebug() << "APRSFi::handleReply: Array element is not an object: " << valRef; + } + } + if (data.size() > 0) { + emit dataUpdated(data); + } else { + qDebug() << "APRSFi::handleReply: No data in array: " << document; + } + } + } + else + { + qDebug() << "APRSFi::handleReply: Document is not an object: " << document; + } + } + else + { + qWarning() << "APRSFi::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qWarning() << "APRSFi::handleReply: reply is null"; + } +} diff --git a/sdrbase/util/aprsfi.h b/sdrbase/util/aprsfi.h new file mode 100644 index 000000000..32caa4926 --- /dev/null +++ b/sdrbase/util/aprsfi.h @@ -0,0 +1,89 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_APRSFI_H +#define INCLUDE_APRSFI_H + +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; + +// aprs.fi API +// Allows querying APRS and AIS data +// Data can be cached to help avoid rate limiting on the server +class SDRBASE_API APRSFi : public QObject +{ + Q_OBJECT +protected: + APRSFi(const QString& apiKey, int cacheValidMins); + +public: + + struct LocationData { + QString m_name; + QDateTime m_firstTime; // First time this position was reported + QDateTime m_lastTime; // Last time this position was reported + float m_latitude; + float m_longitude; + QString m_callsign; + + QDateTime m_dateTime; // Data/time this data was received from APRS.fi + + LocationData() : + m_latitude(NAN), + m_longitude(NAN) + { + } + }; + + struct AISData : LocationData { + QString m_mmsi; + QString m_imo; + + AISData() + { + } + }; + + // Keys are free from https://aprs.fi/ - so get your own + static APRSFi* create(const QString& apiKey="184212.WhYgz2jqu3l2O", int cacheValidMins=10); + + ~APRSFi(); + void getData(const QStringList& names); + void getData(const QString& name); + +private slots: + void handleReply(QNetworkReply* reply); + +signals: + void dataUpdated(const QList& data); // Called when new data available. + +private: + bool containsNonNull(const QJsonObject& obj, const QString &key) const; + + QNetworkAccessManager *m_networkManager; + QString m_apiKey; + int m_cacheValidMins; + static QMutex m_mutex; + static QHash m_aisCache; +}; + +#endif /* INCLUDE_APRSFI_H */ diff --git a/sdrbase/util/dsc.cpp b/sdrbase/util/dsc.cpp new file mode 100644 index 000000000..ff52a1666 --- /dev/null +++ b/sdrbase/util/dsc.cpp @@ -0,0 +1,972 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "util/dsc.h" +#include "util/popcount.h" + +// "short" strings are meant to be compatible with YaDDNet + +QMap DSCMessage::m_formatSpecifierStrings = { + {GEOGRAPHIC_CALL, "Geographic call"}, + {DISTRESS_ALERT, "Distress alert"}, + {GROUP_CALL, "Group call"}, + {ALL_SHIPS, "All ships"}, + {SELECTIVE_CALL, "Selective call"}, + {AUTOMATIC_CALL, "Automatic call"} +}; + +QMap DSCMessage::m_formatSpecifierShortStrings = { + {GEOGRAPHIC_CALL, "AREA"}, + {DISTRESS_ALERT, "DIS"}, + {GROUP_CALL, "GRP"}, + {ALL_SHIPS, "ALL"}, + {SELECTIVE_CALL, "SEL"}, + {AUTOMATIC_CALL, "AUT"} +}; + +QMap DSCMessage::m_categoryStrings = { + {ROUTINE, "Routine"}, + {SAFETY, "Safety"}, + {URGENCY, "Urgency"}, + {DISTRESS, "Distress"} +}; + +QMap DSCMessage::m_categoryShortStrings = { + {ROUTINE, "RTN"}, + {SAFETY, "SAF"}, + {URGENCY, "URG"}, + {DISTRESS, "DIS"} +}; + +QMap DSCMessage::m_telecommand1Strings = { + {F3E_G3E_ALL_MODES_TP, "F3E (FM speech)/G3E (phase modulated speech) all modes telephony"}, + {F3E_G3E_DUPLEX_TP, "F3E (FM speech)/G3E (phase modulated speech) duplex telephony"}, + {POLLING, "Polling"}, + {UNABLE_TO_COMPLY, "Unable to comply"}, + {END_OF_CALL, "End of call"}, + {DATA, "Data"}, + {J3E_TP, "J3E (SSB) telephony"}, + {DISTRESS_ACKNOWLEDGEMENT, "Distress acknowledgement"}, + {DISTRESS_ALERT_RELAY, "Distress alert relay"}, + {F1B_J2B_TTY_FEC, "F1B (FSK) J2B (FSK via SSB) TTY FEC"}, + {F1B_J2B_TTY_AQR, "F1B (FSK) J2B (FSK via SSB) TTY AQR"}, + {TEST, "Test"}, + {POSITION_UPDATE, "Position update"}, + {NO_INFORMATION, "No information"} +}; + +QMap DSCMessage::m_telecommand1ShortStrings = { + {F3E_G3E_ALL_MODES_TP, "F3E/G3E"}, + {F3E_G3E_DUPLEX_TP, "F3E/G3E, Duplex TP"}, + {POLLING, "POLL"}, + {UNABLE_TO_COMPLY, "UNABLE TO COMPLY"}, + {END_OF_CALL, "EOC"}, + {DATA, "DATA"}, + {J3E_TP, "J3E TP"}, + {DISTRESS_ACKNOWLEDGEMENT, "DISTRESS ACK"}, + {DISTRESS_ALERT_RELAY, "DISTRESS RELAY"}, + {F1B_J2B_TTY_FEC, "F1B/J2B TTY-FEC"}, + {F1B_J2B_TTY_AQR, "F1B/J2B TTY-ARQ"}, + {TEST, "TEST"}, + {POSITION_UPDATE, "POSUPD"}, + {NO_INFORMATION, "NOINF"} +}; + +QMap DSCMessage::m_telecommand2Strings = { + {NO_REASON, "No reason"}, + {CONGESTION, "Congestion at switching centre"}, + {BUSY, "Busy"}, + {QUEUE, "Queue indication"}, + {BARRED, "Station barred"}, + {NO_OPERATOR, "No operator available"}, + {OPERATOR_UNAVAILABLE, "Operator temporarily unavailable"}, + {EQUIPMENT_DISABLED, "Equipment disabled"}, + {UNABLE_TO_USE_CHANNEL, "Unable to use proposed channel"}, + {UNABLE_TO_USE_MODE, "Unable to use proposed mode"}, + {NOT_PARTIES_TO_CONFLICT, "Ships and aircraft of States not parties to an armed conflict"}, + {MEDICAL_TRANSPORTS, "Medical transports"}, + {PAY_PHONE, "Pay-phone/public call office"}, + {FAX, "Facsimile"}, + {NO_INFORMATION_2, "No information"} +}; + +QMap DSCMessage::m_telecommand2ShortStrings = { + {NO_REASON, "NO REASON GIVEN"}, + {CONGESTION, "CONGESTION AT MARITIME CENTRE"}, + {BUSY, "BUSY"}, + {QUEUE, "QUEUE INDICATION"}, + {BARRED, "STATION BARRED"}, + {NO_OPERATOR, "NO OPERATOR AVAILABLE"}, + {OPERATOR_UNAVAILABLE, "OPERATOR TEMPORARILY UNAVAILABLE"}, + {EQUIPMENT_DISABLED, "EQUIPMENT DISABLED"}, + {UNABLE_TO_USE_CHANNEL, "UNABLE TO USE PROPOSED CHANNEL"}, + {UNABLE_TO_USE_MODE, "UNABLE TO USE PROPOSED MODE"}, + {NOT_PARTIES_TO_CONFLICT, "SHIPS/AIRCRAFT OF STATES NOT PARTIES TO ARMED CONFLICT"}, + {MEDICAL_TRANSPORTS, "MEDICAL TRANSPORTS"}, + {PAY_PHONE, "PAY-PHONE/PUBLIC CALL OFFICE"}, + {FAX, "FAX/DATA ACCORDING ITU-R M1081"}, + {NO_INFORMATION_2, "NOINF"} +}; + +QMap DSCMessage::m_distressNatureStrings = { + {FIRE, "Fire, explosion"}, + {FLOODING, "Flooding"}, + {COLLISION, "Collision"}, + {GROUNDING, "Grounding"}, + {LISTING, "Listing"}, + {SINKING, "Sinking"}, + {ADRIFT, "Adrift"}, + {UNDESIGNATED, "Undesignated"}, + {ABANDONING_SHIP, "Abandoning ship"}, + {PIRACY, "Piracy, armed attack"}, + {MAN_OVERBOARD, "Man overboard"}, + {EPIRB, "EPIRB"} +}; + +QMap DSCMessage::m_endOfSignalStrings = { + {REQ, "Req ACK"}, + {ACK, "ACK"}, + {EOS, "EOS"} +}; + +QMap DSCMessage::m_endOfSignalShortStrings = { + {REQ, "REQ"}, + {ACK, "ACK"}, + {EOS, "EOS"} +}; + +DSCMessage::DSCMessage(const QByteArray& data, QDateTime dateTime) : + m_dateTime(dateTime), + m_data(data) +{ + decode(data); +} + +QString DSCMessage::toString(const QString separator) const +{ + QStringList s; + + s.append(QString("Format specifier: %1").arg(formatSpecifier())); + + if (m_hasAddress) { + s.append(QString("Address: %1").arg(m_address)); + } + if (m_hasCategory) { + s.append(QString("Category: %1").arg(category())); + } + + s.append(QString("Self Id: %1").arg(m_selfId)); + + if (m_hasTelecommand1) { + s.append(QString("Telecommand 1: %1").arg(telecommand1(m_telecommand1))); + } + if (m_hasTelecommand2) { + s.append(QString("Telecommand 2: %1").arg(telecommand2(m_telecommand2))); + } + + if (m_hasDistressId) { + s.append(QString("Distress Id: %1").arg(m_distressId)); + } + if (m_hasDistressNature) + { + s.append(QString("Distress nature: %1").arg(distressNature(m_distressNature))); + s.append(QString("Distress coordinates: %1").arg(m_position)); + } + else if (m_hasPosition) + { + s.append(QString("Position: %1").arg(m_position)); + } + + if (m_hasFrequency1) { + s.append(QString("RX Frequency: %1Hz").arg(m_frequency1)); + } + if (m_hasChannel1) { + s.append(QString("RX Channel: %1").arg(m_channel1)); + } + if (m_hasFrequency2) { + s.append(QString("TX Frequency: %1Hz").arg(m_frequency2)); + } + if (m_hasChannel2) { + s.append(QString("TX Channel: %1").arg(m_channel2)); + } + if (m_hasNumber) { + s.append(QString("Phone Number: %1").arg(m_number)); + } + + if (m_hasTime) { + s.append(QString("Time: %1").arg(m_time.toString())); + } + if (m_hasSubsequenceComms) { + s.append(QString("Subsequent comms: %1").arg(telecommand1(m_subsequenceComms))); + } + + return s.join(separator); +} + +QString DSCMessage::toYaddNetFormat(const QString& id, qint64 frequency) const +{ + QStringList s; + + // rx_id + s.append(QString("[%1]").arg(id)); + // rx_freq + float frequencyKHZ = frequency / 1000.0f; + s.append(QString("%1").arg(frequencyKHZ, 0, 'f', 1)); + // fmt + s.append(formatSpecifier(true)); + // to + if (m_hasAddress) + { + if (m_formatSpecifier == GEOGRAPHIC_CALL) + { + char ns = m_addressLatitude >= 0 ? 'N' : 'S'; + char ew = m_addressLongitude >= 0 ? 'E' : 'W'; + int lat = abs(m_addressLatitude); + int lon = abs(m_addressLongitude); + s.append(QString("AREA %2%1%6=>%4%1 %3%1%7=>%5%1") + .arg(QChar(0xb0)) // degree + .arg(lat, 2, 10, QChar('0')) + .arg(lon, 3, 10, QChar('0')) + .arg(m_addressLatAngle, 2, 10, QChar('0')) + .arg(m_addressLonAngle, 2, 10, QChar('0')) + .arg(ns) + .arg(ew)); + } + else + { + s.append(m_address); + } + } + else + { + s.append(""); + } + // cat + s.append(category(true)); + // from + s.append(m_selfId); + + // tc1 + if (m_hasTelecommand1) { + s.append(telecommand1(m_telecommand1, true)); + } else { + s.append("--"); + } + // tc2 + if (m_hasTelecommand2) { + s.append(telecommand2(m_telecommand2, true)); + } else { + s.append("--"); + } + // distress fields don't appear to be used! + // freq + if (m_hasFrequency1 && m_hasFrequency2) { + s.append(QString("%1/%2KHz").arg(m_frequency1/1000.0, 7, 'f', 1, QChar('0')).arg(m_frequency2/1000.0, 7, 'f', 1, QChar('0'))); + } else if (m_hasFrequency1) { + s.append(QString("%1KHz").arg(m_frequency1/1000.0, 7, 'f', 1, QChar('0'))); + } else if (m_hasFrequency2) { + s.append(QString("%1KHz").arg(m_frequency2/1000.0, 7, 'f', 1, QChar('0'))); + } else if (m_hasChannel1 && m_hasChannel2) { + s.append(QString("%1/%2").arg(m_channel1).arg(m_channel2)); + } else if (m_hasChannel1) { + s.append(QString("%1").arg(m_channel1)); + } else if (m_hasChannel2) { + s.append(QString("%1").arg(m_channel2)); + } else { + s.append("--"); + } + // pos + if (m_hasPosition) { + s.append(m_position); // FIXME: Format?? + } else { + s.append("--"); // Sometimes this is " -- ". in YaDD Why? + } + + // eos + s.append(endOfSignal(m_eos, true)); + // ecc + s.append(QString("ECC %1 %2").arg(m_calculatedECC).arg(m_eccOk ? "OK" : "ERR")); + + return s.join(";"); +} + +QString DSCMessage::formatSpecifier(bool shortString) const +{ + if (shortString) + { + if (m_formatSpecifierShortStrings.contains(m_formatSpecifier)) { + return m_formatSpecifierShortStrings[m_formatSpecifier]; + } else { + return QString("UNK/ERR").arg(m_formatSpecifier); + } + } + else + { + if (m_formatSpecifierStrings.contains(m_formatSpecifier)) { + return m_formatSpecifierStrings[m_formatSpecifier]; + } else { + return QString("Unknown (%1)").arg(m_formatSpecifier); + } + } +} + + +QString DSCMessage::category(bool shortString) const +{ + if (shortString) + { + if (m_categoryShortStrings.contains(m_category)) { + return m_categoryShortStrings[m_category]; + } else { + return QString("UNK/ERR").arg(m_category); + } + } + else + { + if (!m_hasCategory) { + return "N/A"; + } else if (m_categoryStrings.contains(m_category)) { + return m_categoryStrings[m_category]; + } else { + return QString("Unknown (%1)").arg(m_category); + } + } +} + +QString DSCMessage::telecommand1(FirstTelecommand telecommand, bool shortString) +{ + if (shortString) + { + if (m_telecommand1ShortStrings.contains(telecommand)) { + return m_telecommand1ShortStrings[telecommand]; + } else { + return QString("UNK/ERR").arg(telecommand); + } + } + else + { + if (m_telecommand1Strings.contains(telecommand)) { + return m_telecommand1Strings[telecommand]; + } else { + return QString("Unknown (%1)").arg(telecommand); + } + } +} + +QString DSCMessage::telecommand2(SecondTelecommand telecommand, bool shortString) +{ + if (shortString) + { + if (m_telecommand2ShortStrings.contains(telecommand)) { + return m_telecommand2ShortStrings[telecommand]; + } else { + return QString("UNK/ERR").arg(telecommand); + } + } + else + { + if (m_telecommand2Strings.contains(telecommand)) { + return m_telecommand2Strings[telecommand]; + } else { + return QString("Unknown (%1)").arg(telecommand); + } + } +} + +QString DSCMessage::distressNature(DistressNature nature) +{ + if (m_distressNatureStrings.contains(nature)) { + return m_distressNatureStrings[nature]; + } else { + return QString("Unknown (%1)").arg(nature); + } +} + +QString DSCMessage::endOfSignal(EndOfSignal eos, bool shortString) +{ + if (shortString) + { + if (m_endOfSignalShortStrings.contains(eos)) { + return m_endOfSignalShortStrings[eos]; + } else { + return QString("UNK/ERR").arg(eos); + } + } + else + { + if (m_endOfSignalStrings.contains(eos)) { + return m_endOfSignalStrings[eos]; + } else { + return QString("Unknown (%1)").arg(eos); + } + } +} + + +QString DSCMessage::symbolsToDigits(const QByteArray data, int startIdx, int length) +{ + QString s; + + for (int i = 0; i < length; i++) + { + QString digits = QString("%1").arg((int)data[startIdx+i], 2, 10, QChar('0')); + s = s.append(digits); + } + + return s; +} + +QString DSCMessage::formatCoordinates(int latitude, int longitude) +{ + QString lat, lon; + if (latitude >= 0) { + lat = QString("%1%2N").arg(latitude).arg(QChar(0xb0)); + } else { + lat = QString("%1%2S").arg(-latitude).arg(QChar(0xb0)); + } + if (longitude >= 0) { + lon = QString("%1%2E").arg(longitude).arg(QChar(0xb0)); + } else { + lon = QString("%1%2W").arg(-longitude).arg(QChar(0xb0)); + } + return QString("%1 %2").arg(lat).arg(lon); +} + +void DSCMessage::decode(const QByteArray& data) +{ + int idx = 0; + + // Format specifier + m_formatSpecifier = (FormatSpecifier) data[idx++]; + m_formatSpecifierMatch = m_formatSpecifier == data[idx++]; + + // Address and category + if (m_formatSpecifier != DISTRESS_ALERT) + { + if (m_formatSpecifier != ALL_SHIPS) + { + m_address = symbolsToDigits(data, idx, 5); + idx += 5; + m_hasAddress = true; + + if (m_formatSpecifier == SELECTIVE_CALL) + { + m_address = formatAddress(m_address); + } + else if (m_formatSpecifier == GEOGRAPHIC_CALL) + { + // Address definines a geographic rectangle. We have NW coord + 2 angles + QChar azimuthSector = m_address[0]; + m_addressLatitude = m_address[1].digitValue() * 10 + m_address[2].digitValue(); // In degrees + m_addressLongitude = m_address[3].digitValue() * 100 + m_address[4].digitValue() * 10 + m_address[5].digitValue(); // In degrees + switch (azimuthSector.toLatin1()) + { + case '0': // NE + break; + case '1': // NW + m_addressLongitude = -m_addressLongitude; + break; + case '2': // SE + m_addressLatitude = -m_addressLatitude; + break; + case '3': // SW + m_addressLongitude = -m_addressLongitude; + m_addressLatitude = -m_addressLatitude; + break; + default: + break; + } + m_addressLatAngle = m_address[6].digitValue() * 10 + m_address[7].digitValue(); + m_addressLonAngle = m_address[8].digitValue() * 10 + m_address[9].digitValue(); + + int latitude2 = m_addressLatitude + m_addressLatAngle; + int longitude2 = m_addressLongitude + m_addressLonAngle; + + /*m_address = QString("Lat %2%1 Lon %3%1 %4%5%6%1 %4%7%8%1") + .arg(QChar(0xb0)) // degree + .arg(m_addressLatitude) + .arg(m_addressLongitude) + .arg(QChar(0x0394)) // delta + .arg(QChar(0x03C6)) // phi + .arg(m_addressLatAngle) + .arg(QChar(0x03BB)) // lambda + .arg(m_addressLonAngle);*/ + m_address = QString("%1 - %2") + .arg(formatCoordinates(m_addressLatitude, m_addressLongitude)) + .arg(formatCoordinates(latitude2, longitude2)); + } + } + else + { + m_hasAddress = false; + } + m_category = (Category) data[idx++]; + m_hasCategory = true; + } + else + { + m_hasAddress = false; + m_hasCategory = true; + } + + // Self Id + m_selfId = symbolsToDigits(data, idx, 5); + m_selfId = formatAddress(m_selfId); + idx += 5; + + // Telecommands + if (m_formatSpecifier != DISTRESS_ALERT) + { + m_telecommand1 = (FirstTelecommand) data[idx++]; + m_hasTelecommand1 = true; + + if (m_category != DISTRESS) // Not Distress Alert Ack / Relay + { + m_telecommand2 = (SecondTelecommand) data[idx++]; + m_hasTelecommand2 = true; + } + else + { + m_hasTelecommand2 = false; + } + } + else + { + m_hasTelecommand1 = false; + m_hasTelecommand2 = false; + } + + // ID of source of distress for relays and acks + if (m_hasCategory && m_category == DISTRESS) + { + m_distressId = symbolsToDigits(data, idx, 5); + m_distressId = formatAddress(m_distressId); + idx += 5; + m_hasDistressId = true; + } + else + { + m_hasDistressId = false; + } + + if (m_formatSpecifier == DISTRESS_ALERT) + { + m_distressNature = (DistressNature) data[idx++]; + m_position = formatCoordinates(symbolsToDigits(data, idx, 5)); + idx += 5; + m_hasDistressNature = true; + m_hasPosition = true; + + m_hasFrequency1 = false; + m_hasChannel1 = false; + m_hasFrequency2 = false; + m_hasChannel2 = false; + } + else if (m_hasCategory && (m_category != DISTRESS)) + { + m_hasDistressNature = false; + // Frequency or position + if (data[idx] == 55) + { + // Position 6 + m_position = formatCoordinates(symbolsToDigits(data, idx, 5)); + idx += 5; + m_hasPosition = true; + + m_hasFrequency1 = false; + m_hasChannel1 = false; + m_hasFrequency2 = false; + m_hasChannel2 = false; + } + else + { + m_hasPosition = false; + // Frequency + m_frequency1 = 0; + decodeFrequency(data, idx, m_frequency1, m_channel1); + m_hasFrequency1 = m_frequency1 != 0; + m_hasChannel1 = !m_channel1.isEmpty(); + + if (m_formatSpecifier != AUTOMATIC_CALL) + { + m_frequency2 = 0; + decodeFrequency(data, idx, m_frequency2, m_channel2); + m_hasFrequency2 = m_frequency2 != 0; + m_hasChannel2 = !m_channel2.isEmpty(); + } + else + { + m_hasFrequency2 = false; + m_hasChannel2 = false; + } + } + } + else + { + m_hasDistressNature = false; + m_hasPosition = false; + m_hasFrequency1 = false; + m_hasChannel1 = false; + m_hasFrequency2 = false; + m_hasChannel2 = false; + } + + if (m_formatSpecifier == AUTOMATIC_CALL) + { + signed char oddEven = data[idx++]; + int len = data.size() - idx - 2; // EOS + ECC + m_number = symbolsToDigits(data, idx, len); + idx += len; + if (oddEven == 105) { // Is number an odd number? + m_number = m_number.mid(1); // Drop leading digit (which should be a 0) + } + m_hasNumber = true; + } + else + { + m_hasNumber = false; + } + + // Time + if ( (m_formatSpecifier == DISTRESS_ALERT) + || (m_hasCategory && (m_category == DISTRESS)) + //|| (m_formatSpecifier == SELECTIVE_CALL) && (m_category == SAFETY) && (m_telecommand1 == POSITION_UPDATE) && (m_telecommand2 == 126) && (m_frequency == pos4)) + ) + { + // 8 8 8 8 for no time + QString time = symbolsToDigits(data, idx, 2); + if (time != "8888") + { + m_time = QTime(time.left(2).toInt(), time.right(2).toInt()); + m_hasTime = true; + } + else + { + m_hasTime = false; + } + // FIXME: Convert to QTime? + } + else + { + m_hasTime = false; + } + + // Subsequent communications + if ((m_formatSpecifier == DISTRESS_ALERT) || (m_hasCategory && (m_category == DISTRESS))) + { + m_subsequenceComms = (FirstTelecommand)data[idx++]; + m_hasSubsequenceComms = true; + } + else + { + m_hasSubsequenceComms = false; + } + + m_eos = (EndOfSignal) data[idx++]; + m_ecc = data[idx++]; + + checkECC(data); + + // Indicate message as being invalid if any unexpected data, too long, or ECC didn't match + if ( m_formatSpecifierStrings.contains(m_formatSpecifier) + && (!m_hasCategory || (m_hasCategory && m_categoryStrings.contains(m_category))) + && (!m_hasTelecommand1 || (m_hasTelecommand1 && m_telecommand1Strings.contains(m_telecommand1))) + && (!m_hasTelecommand2 || (m_hasTelecommand2 && m_telecommand2Strings.contains(m_telecommand2))) + && (!m_hasDistressNature || (m_hasDistressNature && m_distressNatureStrings.contains(m_distressNature))) + && m_endOfSignalStrings.contains(m_eos) + && (!data.contains(-1)) + && (data.size() < DSCDecoder::m_maxBytes) + && m_eccOk + ) { + m_valid = true; + } else { + m_valid = false; + } + +} + +void DSCMessage::checkECC(const QByteArray& data) +{ + m_calculatedECC = 0; + // Only use one format specifier and one EOS + for (int i = 1; i < data.size() - 1; i++) { + m_calculatedECC ^= data[i]; + } + m_eccOk = m_calculatedECC == m_ecc; +} + +void DSCMessage::decodeFrequency(const QByteArray& data, int& idx, int& frequency, QString& channel) +{ + // No frequency information is indicated by 126 repeated 3 times + if ((data[idx] == 126) && (data[idx+1] == 126) && (data[idx+2] == 126)) + { + idx += 3; + return; + } + + // Extract frequency digits + QString s = symbolsToDigits(data, idx, 3); + idx += 3; + if (s[0] == '4') + { + s = s.append(symbolsToDigits(data, idx, 1)); + idx++; + } + + if ((s[0] == '0') || (s[0] == '1') || (s[0] == '2')) + { + frequency = s.toInt() * 100; + } + else if (s[0] == '3') + { + channel = "CH" + s.mid(1); // HF/MF + } + else if (s[0] == '4') + { + frequency = s.mid(1).toInt() * 10; // Frequency in multiples of 10Hz + } + else if (s[0] == '9') + { + channel = "CH" + s.mid(2) + "VHF"; // VHF + } +} + +QString DSCMessage::formatAddress(const QString &address) const +{ + // First 9 digits should be MMSI + // Last digit should always be 0, except for ITU-R M.1080, which allows 10th digit to specify different equipement on same vessel + if (address.right(1) == "0") { + return address.left(9); + } else { + return QString("%1-%2").arg(address.left(9)).arg(address.right(1)); + } +} + +QString DSCMessage::formatCoordinates(const QString& coords) +{ + if (coords == "9999999999") + { + return "Not available"; + } + else + { + QChar quadrant = coords[0]; + QString latitude = QString("%1%3%2\'") + .arg(coords.mid(1, 2)) + .arg(coords.mid(3, 2)) + .arg(QChar(0xb0)); + QString longitude = QString("%1%3%2\'") + .arg(coords.mid(1, 3)) + .arg(coords.mid(4, 2)) + .arg(QChar(0xb0)); + switch (quadrant.toLatin1()) + { + case '0': + latitude = latitude.append('N'); + longitude = longitude.append('E'); + break; + case '1': + latitude = latitude.append('N'); + longitude = longitude.append('W'); + break; + case '2': + latitude = latitude.append('S'); + longitude = longitude.append('E'); + break; + case '3': + latitude = latitude.append('S'); + longitude = longitude.append('W'); + break; + } + return QString("%1 %2").arg(latitude).arg(longitude); + } +} + +// Doesn't include 125 111 125 as these will have be detected already, in DSDDemodSink +const signed char DSCDecoder::m_expectedSymbols[] = { + 110, + 125, 109, + 125, 108, + 125, 107, + 125, 106 +}; + +int DSCDecoder::m_maxBytes = 40; // Max bytes in any message + +void DSCDecoder::init(int offset) +{ + if (offset == 0) + { + m_state = FILL_DX; + } + else + { + m_phaseIdx = offset; + m_state = PHASING; + } + m_idx = 0; + m_errors = 0; + m_bytes = QByteArray(); + m_eos = false; +} + +bool DSCDecoder::decodeSymbol(signed char symbol) +{ + bool ret = false; + + switch (m_state) + { + case PHASING: + // Check if received phasing signals are as expected + if (symbol != m_expectedSymbols[9-m_phaseIdx]) { + m_errors++; + } + m_phaseIdx--; + if (m_phaseIdx == 0) { + m_state = FILL_DX; + } + break; + + case FILL_DX: + // Fill up buffer + m_buf[m_idx++] = symbol; + if (m_idx == BUFFER_SIZE) + { + m_state = RX; + m_idx = 0; + } + else + { + m_state = FILL_RX; + } + break; + + case FILL_RX: + if ( ((m_idx == 1) && (symbol != 106)) + || ((m_idx == 2) && (symbol != 105)) + ) + { + m_errors++; + } + m_state = FILL_DX; + break; + + case RX: + { + signed char a = selectSymbol(m_buf[m_idx], symbol); + + if (DSCMessage::m_endOfSignalStrings.contains((DSCMessage::EndOfSignal) a)) { + m_state = DX_EOS; + } else { + m_state = DX; + } + + if (m_bytes.size() > m_maxBytes) + { + ret = true; + m_state = NO_EOS; + } + } + break; + + case DX: + // Save received character in buffer + m_buf[m_idx] = symbol; + m_idx = (m_idx + 1) % BUFFER_SIZE; + m_state = RX; + break; + + case DX_EOS: + // Save, EOS symbol + m_buf[m_idx] = symbol; + m_idx = (m_idx + 1) % BUFFER_SIZE; + m_state = RX_EOS; + break; + + case RX_EOS: + selectSymbol(m_buf[m_idx], symbol); + m_state = DONE; + ret = true; + break; + + case DONE: + case NO_EOS: + break; + + } + + return ret; +} + +// Reverse order of bits in a byte +unsigned char DSCDecoder::reverse(unsigned char b) +{ + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +// Convert 10 bits to a symbol +// Returns -1 if error detected +signed char DSCDecoder::bitsToSymbol(unsigned int bits) +{ + signed char data = reverse(bits >> 3) >> 1; + int zeros = 7-popcount(data); + int expectedZeros = bits & 0x7; + if (zeros == expectedZeros) { + return data; + } else { + return -1; + } +} + +// Decode 10-bits to symbols then remove errors using repeated symbols +bool DSCDecoder::decodeBits(int bits) +{ + signed char symbol = bitsToSymbol(bits); + //qDebug() << "Bits2sym: " << Qt::hex << bits << Qt::hex << symbol; + return decodeSymbol(symbol); +} + +// Select time diversity symbol without errors +signed char DSCDecoder::selectSymbol(signed char dx, signed char rx) +{ + signed char s; + if (dx != -1) + { + s = dx; // First received character has no detectable error + if (dx != rx) { + m_errors++; + } + } + else if (rx != -1) + { + s = rx; // Second received character has no detectable error + m_errors++; + } + else + { + s = '*'; // Both received characters have errors + m_errors += 2; + } + m_bytes.append(s); + + return s; +} diff --git a/sdrbase/util/dsc.h b/sdrbase/util/dsc.h new file mode 100644 index 000000000..04007dd49 --- /dev/null +++ b/sdrbase/util/dsc.h @@ -0,0 +1,234 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_UTIL_DSC_H +#define INCLUDE_UTIL_DSC_H + +#include "export.h" + +#include +#include +#include + +// Digital Select Calling +// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.493-15-201901-I!!PDF-E.pdf + +class SDRBASE_API DSCDecoder { + +public: + + void init(int offset); + bool decodeBits(int bits); + QByteArray getMessage() const { return m_bytes; } + int getErrors() const { return m_errors; } + + static int m_maxBytes; + +private: + + static const int BUFFER_SIZE = 3; + signed char m_buf[3]; + enum State { + PHASING, + FILL_DX, + FILL_RX, + DX, + RX, + DX_EOS, + RX_EOS, + DONE, + NO_EOS + } m_state; + int m_idx; + int m_errors; + int m_phaseIdx; + bool m_eos; + static const signed char m_expectedSymbols[]; + + QByteArray m_bytes; + + bool decodeSymbol(signed char symbol); + static signed char bitsToSymbol(unsigned int bits); + static unsigned char reverse(unsigned char b); + signed char selectSymbol(signed char dx, signed char rx); + +}; + +class SDRBASE_API DSCMessage { +public: + + enum FormatSpecifier { + GEOGRAPHIC_CALL = 102, + DISTRESS_ALERT = 112, + GROUP_CALL = 114, + ALL_SHIPS = 116, + SELECTIVE_CALL = 120, + AUTOMATIC_CALL = 123 + }; + + enum Category { + ROUTINE = 100, + SAFETY = 108, + URGENCY = 110, + DISTRESS = 112 + }; + + enum FirstTelecommand { + F3E_G3E_ALL_MODES_TP = 100, + F3E_G3E_DUPLEX_TP = 101, + POLLING = 103, + UNABLE_TO_COMPLY = 104, + END_OF_CALL = 105, + DATA = 106, + J3E_TP = 109, + DISTRESS_ACKNOWLEDGEMENT = 110, + DISTRESS_ALERT_RELAY = 112, + F1B_J2B_TTY_FEC = 113, + F1B_J2B_TTY_AQR = 115, + TEST = 118, + POSITION_UPDATE = 121, + NO_INFORMATION = 126 + }; + + enum SecondTelecommand { + NO_REASON = 100, + CONGESTION = 101, + BUSY = 102, + QUEUE = 103, + BARRED = 104, + NO_OPERATOR = 105, + OPERATOR_UNAVAILABLE = 106, + EQUIPMENT_DISABLED = 107, + UNABLE_TO_USE_CHANNEL = 108, + UNABLE_TO_USE_MODE = 109, + NOT_PARTIES_TO_CONFLICT = 110, + MEDICAL_TRANSPORTS = 111, + PAY_PHONE = 112, + FAX = 113, + NO_INFORMATION_2 = 126 + }; + + enum DistressNature { + FIRE = 100, + FLOODING = 101, + COLLISION = 102, + GROUNDING = 103, + LISTING = 104, + SINKING = 105, + ADRIFT = 106, + UNDESIGNATED = 107, + ABANDONING_SHIP = 108, + PIRACY = 109, + MAN_OVERBOARD = 110, + EPIRB = 112 + }; + + enum EndOfSignal { + REQ = 117, + ACK = 122, + EOS = 127 + }; + + static QMap m_formatSpecifierStrings; + static QMap m_formatSpecifierShortStrings; + static QMap m_categoryStrings; + static QMap m_categoryShortStrings; + static QMap m_telecommand1Strings; + static QMap m_telecommand1ShortStrings; + static QMap m_telecommand2Strings; + static QMap m_telecommand2ShortStrings; + static QMap m_distressNatureStrings; + static QMap m_endOfSignalStrings; + static QMap m_endOfSignalShortStrings; + + FormatSpecifier m_formatSpecifier; + bool m_formatSpecifierMatch; + QString m_address; + bool m_hasAddress; + int m_addressLatitude; // For GEOGRAPHIC_CALL + int m_addressLongitude; + int m_addressLatAngle; + int m_addressLonAngle; + + Category m_category; + bool m_hasCategory; + QString m_selfId; + FirstTelecommand m_telecommand1; + bool m_hasTelecommand1; + SecondTelecommand m_telecommand2; + bool m_hasTelecommand2; + + QString m_distressId; + bool m_hasDistressId; + + DistressNature m_distressNature; + bool m_hasDistressNature; + + QString m_position; + bool m_hasPosition; + + int m_frequency1; // Rx + bool m_hasFrequency1; + QString m_channel1; + bool m_hasChannel1; + int m_frequency2; // Tx + bool m_hasFrequency2; + QString m_channel2; + bool m_hasChannel2; + + QString m_number; // Phone number + bool m_hasNumber; + + QTime m_time; + bool m_hasTime; + + FirstTelecommand m_subsequenceComms; + bool m_hasSubsequenceComms; + + EndOfSignal m_eos; + signed char m_ecc; // Error checking code (parity) + signed char m_calculatedECC; + bool m_eccOk; + bool m_valid; // Data is within defined values + + QDateTime m_dateTime; // Date/time when received + QByteArray m_data; + + DSCMessage(const QByteArray& data, QDateTime dateTime); + QString toString(const QString separator = " ") const; + QString toYaddNetFormat(const QString& id, qint64 frequency) const; + QString formatSpecifier(bool shortString=false) const; + QString category(bool shortString=false) const; + + static QString telecommand1(FirstTelecommand telecommand, bool shortString=false); + static QString telecommand2(SecondTelecommand telecommand, bool shortString=false); + static QString distressNature(DistressNature nature); + static QString endOfSignal(EndOfSignal eos, bool shortString=false); + +protected: + + QString symbolsToDigits(const QByteArray data, int startIdx, int length); + QString formatCoordinates(int latitude, int longitude); + void decode(const QByteArray& data); + void checkECC(const QByteArray& data); + void decodeFrequency(const QByteArray& data, int& idx, int& frequency, QString& channel); + QString formatAddress(const QString &address) const; + QString formatCoordinates(const QString& coords); + +}; + +#endif /* INCLUDE_UTIL_DSC_H */ diff --git a/sdrbase/util/mmsi.cpp b/sdrbase/util/mmsi.cpp new file mode 100644 index 000000000..384213ca6 --- /dev/null +++ b/sdrbase/util/mmsi.cpp @@ -0,0 +1,422 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/mmsi.h" +#include "util/osndb.h" + +// https://www.itu.int/en/ITU-R/terrestrial/fmd/Pages/mid.aspx +// Names used match up with names used for flags in ADS-B directory +QMap MMSI::m_mid = { + {201, "albania"}, + {202, "andorra"}, + {203, "austria"}, + {204, "portugal"}, + {205, "belgium"}, + {206, "belarus"}, + {207, "bulgaria"}, + {208, "vatican_city"}, + {209, "cyprus"}, + {210, "cyprus"}, + {211, "germany"}, + {212, "cyprus"}, + {213, "georgia"}, + {214, "moldova"}, + {215, "malta"}, + {216, "armenia"}, + {218, "germany"}, + {219, "denmark"}, + {220, "denmark"}, + {224, "spain"}, + {225, "spain"}, + {226, "france"}, + {227, "france"}, + {228, "france"}, + {229, "malta"}, + {230, "finland"}, + {231, "denmark"}, + {232, "united_kingdom"}, + {233, "united_kingdom"}, + {234, "united_kingdom"}, + {235, "united_kingdom"}, + {236, "united_kingdom"}, + {237, "greece"}, + {238, "croatia"}, + {239, "greece"}, + {240, "greece"}, + {241, "greece"}, + {242, "morocco"}, + {243, "hungary"}, + {244, "netherlands"}, + {245, "netherlands"}, + {246, "netherlands"}, + {247, "italy"}, + {248, "malta"}, + {249, "malta"}, + {250, "ireland"}, + {251, "iceland"}, + {252, "liechtenstein"}, + {253, "luxembourg"}, + {254, "monaco"}, + {255, "portugal"}, + {256, "malta"}, + {257, "norway"}, + {258, "norway"}, + {259, "norway"}, + {261, "poland"}, + {262, "montenegro"}, + {263, "portugal"}, + {264, "romania"}, + {265, "sweden"}, + {266, "sweden"}, + {267, "slovakia"}, + {268, "san_marino"}, + {269, "switzerland"}, + {270, "czech_republic"}, + {271, "turkey"}, + {272, "ukraine"}, + {273, "russia"}, + {274, "macedonia"}, + {275, "latvia"}, + {276, "estonia"}, + {277, "slovenia"}, + {279, "serbia"}, + {301, "united_kingdom"}, + {303, "united_states"}, + {304, "antigua_and_barbuda"}, + {305, "antigua_and_barbuda"}, + {306, "netherlands"}, + {307, "netherlands"}, + {308, "bahamas"}, + {309, "bahamas"}, + {310, "bermuda"}, + {311, "bahamas"}, + {312, "belize"}, + {314, "barbados"}, + {316, "canada"}, + {319, "cayman_isles"}, + {321, "costa_rica"}, + {323, "cuba"}, + {325, "dominica"}, + {327, "dominican_republic"}, + {329, "france"}, + {330, "grenada"}, + {331, "denmark"}, // greenland + {332, "guatemala"}, + {334, "honduras"}, + {336, "haiti"}, + {338, "united_states"}, + {339, "jamaica"}, + {341, "st_kitts_and_nevis"}, + {343, "st_lucia"}, + {345, "mexico"}, + {347, "france"}, // martinique + {348, "united_kingdom"}, // montserrat + {350, "nicaragua"}, + {351, "panama"}, + {352, "panama"}, + {353, "panama"}, + {354, "panama"}, + {355, "panama"}, + {356, "panama"}, + {357, "panama"}, + {358, "united_states"}, // puerto_rico + {359, "el_salvador"}, + {361, "france"}, + {362, "trinidad_and_tobago"}, + {364, "turks_and_caicos"}, + {366, "united_states"}, + {367, "united_states"}, + {368, "united_states"}, + {369, "united_states"}, + {370, "panama"}, + {371, "panama"}, + {372, "panama"}, + {373, "panama"}, + {374, "panama"}, + {375, "st_vincent"}, + {376, "st_vincent"}, + {377, "st_vincent"}, + {378, "virgin_isles"}, + {401, "afghanistan"}, + {403, "saudi_arabia"}, + {405, "bangladesh"}, + {408, "bahrain"}, + {410, "bhutan"}, + {412, "china"}, + {413, "china"}, + {414, "china"}, + {416, "taiwan"}, + {417, "sri_lanka"}, + {419, "india"}, + {422, "iran"}, + {423, "azerbaijan"}, + {425, "iraq"}, + {428, "israel"}, + {431, "japan"}, + {432, "japan"}, + {434, "turkmenistan"}, + {436, "kazakhstan"}, + {437, "uzbekistan"}, + {438, "jordan"}, + {440, "korea_south"}, + {441, "korea_south"}, + {443, "palestine"}, + {445, "korea_north"}, + {447, "kuwait"}, + {450, "lebanon"}, + {451, "kyrgyzstan"}, + {453, "china"}, // macao + {455, "maldives"}, + {457, "mongolia"}, + {459, "nepal"}, + {461, "oman"}, + {463, "pakistan"}, + {466, "qatar"}, + {468, "syria"}, + {470, "united_arab_emirates"}, + {471, "united_arab_emirates"}, + {472, "tajikistan"}, + {473, "yemen"}, + {474, "yemen"}, + {477, "hong_kong"}, + {478, "bosnia"}, + {501, "france"}, + {503, "australia"}, + {506, "myanmar"}, + {508, "brunei"}, + {510, "micronesia"}, + {511, "palau"}, + {512, "new_zealand"}, + {514, "cambodia"}, + {515, "cambodia"}, + {516, "australia"}, + {518, "cook_islands"}, + {520, "fiji"}, + {523, "australia"}, + {525, "indonesia"}, + {529, "kiribati"}, + {531, "laos"}, + {533, "malaysia"}, + {536, "united_states"}, + {538, "marshall islands"}, + {540, "france"}, + {542, "new_zealand"}, + {544, "nauru"}, + {546, "france"}, + {548, "philippines"}, + {550, "timorleste"}, + {553, "papua_new_guinea"}, + {555, "united_kingdom"}, + {557, "solomon_islands"}, + {559, "united_states"}, // american_samoa + {561, "samoa"}, + {563, "singapore"}, + {564, "singapore"}, + {565, "singapore"}, + {566, "singapore"}, + {567, "thailand"}, + {570, "tonga"}, + {572, "tuvalu"}, + {574, "vietnam"}, + {576, "vanuatu"}, + {577, "vanuatu"}, + {578, "france"}, + {601, "south_africa"}, + {603, "angola"}, + {605, "algeria"}, + {607, "france"}, + {608, "united_kingdom"}, // ascension_island + {609, "burundi"}, + {610, "benin"}, + {611, "botswana"}, + {612, "central_african_republic"}, + {613, "cameroun"}, // cameroon + {615, "congoroc"}, + {616, "comoros"}, + {617, "cape_verde"}, + {618, "france"}, + {619, "ivory_coast"}, + {620, "comoros"}, + {621, "djibouti"}, + {622, "egypt"}, + {624, "ethiopia"}, + {625, "eritrea"}, + {626, "gabon"}, + {627, "ghana"}, + {629, "gambia"}, + {630, "guinea_bissau"}, + {631, "equatorial_guinea"}, + {632, "guinea"}, + {633, "burkina_faso"}, + {634, "kenya"}, + {635, "france"}, + {636, "liberia"}, + {637, "liberia"}, + {638, "south_sudan"}, + {642, "libya"}, + {644, "lesotho"}, + {645, "mauritius"}, + {647, "madagascar"}, + {649, "mali"}, + {650, "mozambique"}, + {654, "mauritania"}, + {655, "malawi"}, + {656, "niger"}, + {657, "nigeria"}, + {659, "namibia"}, + {660, "france"}, // reunion + {661, "rwanda"}, + {662, "sudan"}, + {663, "senegal"}, + {664, "seychelles"}, + {665, "united_kingdom"}, // saint_helena + {666, "somalia"}, + {667, "sierra_leone"}, + {668, "sao_tome_principe"}, + {669, "swaziland"}, // eswatini + {670, "chad"}, + {671, "togo"}, + {672, "tunisia"}, + {674, "tanzania"}, + {675, "uganda"}, + {676, "congodrc"}, + {677, "tanzania"}, + {678, "zambia"}, + {679, "zimbabwe"}, + {701, "argentina"}, + {710, "brazil"}, + {720, "bolivia"}, + {725, "chile"}, + {730, "colombia"}, + {735, "ecuador"}, + {740, "falkland_isles"}, + {745, "france"}, // guiana + {750, "guyana"}, + {755, "paraguay"}, + {760, "peru"}, + {765, "suriname"}, + {770, "uruguay"}, + {775, "venezuela"}, +}; + +QString MMSI::getMID(const QString &mmsi) +{ + if (mmsi.startsWith("00") || mmsi.startsWith("99") || mmsi.startsWith("98")) { + return mmsi.mid(2, 3); + } else if (mmsi.startsWith("0") || mmsi.startsWith("8")) { + return mmsi.mid(1, 3); + } else if (mmsi.startsWith("111")) { + return mmsi.mid(3, 3); + } else { + return mmsi.left(3); + } +} + +QString MMSI::getCountry(const QString &mmsi) +{ + return m_mid[MMSI::getMID(mmsi).toInt()]; +} + +void MMSI::checkFlags() +{ + // Loop through all MIDs and check to see if we have a flag icon + for (auto id : m_mid.keys()) + { + QString country = m_mid.value(id); + QString path = QString(":/flags/%1.bmp").arg(country); + QResource res(path); + if (!res.isValid()) { + qDebug() << "MMSI::checkFlags: Resource invalid " << path; + } + } +} + +QIcon *MMSI::getFlagIcon(const QString &mmsi) +{ + QString country = getCountry(mmsi); + return AircraftInformation::getFlagIcon(country); +} + +QString MMSI::getFlagIconURL(const QString &mmsi) +{ + QString country = getCountry(mmsi); + return AircraftInformation::getFlagIconURL(country); +} + +QString MMSI::getCategory(const QString &mmsi) +{ + switch (mmsi[0].toLatin1()) + { + case '0': + if (mmsi.startsWith("00")) { + return "Coast"; + } else { + return "Group"; // Group of ships + } + case '1': + // Search and rescue + if (mmsi[6] == '1') { + return "SAR Aircraft"; + } else if (mmsi[6] == '5') { + return "SAR Helicopter"; + } else { + return "SAR"; + } + case '8': + return "Handheld"; + case '9': + if (mmsi.startsWith("970")) + { + return "SAR"; + } + else if (mmsi.startsWith("972")) + { + return "Man overboard"; + } + else if (mmsi.startsWith("974")) + { + return "EPIRB"; // Emergency Becaon + } + else if (mmsi.startsWith("979")) + { + return "AMRD"; // Autonomous + } + else if (mmsi.startsWith("98")) + { + return "Craft with parent ship"; + } + else if (mmsi.startsWith("99")) + { + if (mmsi[5] == '1') { + return "Physical AtoN"; + } else if (mmsi[5] == '6') { + return "Virtual AtoN"; + } else if (mmsi[5] == '8') { + return "Mobile AtoN"; + } else { + return "AtoN"; // Aid-to-navigation + } + } + break; + default: + return "Ship"; // Vessel better? + } + return "Unknown"; +} diff --git a/sdrbase/util/mmsi.h b/sdrbase/util/mmsi.h new file mode 100644 index 000000000..14ed823ab --- /dev/null +++ b/sdrbase/util/mmsi.h @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef UTIL_MMSI_H +#define UTIL_MMSI_H + +#include +#include + +#include "export.h" + +// Maritime mobile service identities (basically ship identifiers) +// MMSIs defined by ITU-R M.585 +// https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.585-9-202205-I!!PDF-E.pdf + +class SDRBASE_API MMSI { + +public: + + static QString getMID(const QString &mmsi); + static QString getCountry(const QString &mmsi); + static QString getCategory(const QString &mmsi); + static QIcon *getFlagIcon(const QString &mmsi); + static QString getFlagIconURL(const QString &mmsi); + +private: + + static QMap m_mid; + + static void checkFlags(); +}; + +#endif /* UTIL_MMSI_H */ diff --git a/sdrbase/util/movingaverage.h b/sdrbase/util/movingaverage.h index b37afd1e2..758528b5a 100644 --- a/sdrbase/util/movingaverage.h +++ b/sdrbase/util/movingaverage.h @@ -120,6 +120,7 @@ class MovingAverageUtilVar double asDouble() const { return m_total * m_samplesSizeInvD; } float asFloat() const { return m_total * m_samplesSizeInvF; } operator T() const { return m_total / m_samples.size(); } + T instantAverage() const { return m_total / (m_num_samples == 0 ? 1 : m_num_samples); } private: std::vector m_samples; diff --git a/sdrbase/util/osndb.cpp b/sdrbase/util/osndb.cpp index 5c6998a7f..0b556cda9 100644 --- a/sdrbase/util/osndb.cpp +++ b/sdrbase/util/osndb.cpp @@ -527,6 +527,7 @@ QIcon *AircraftInformation::getAirlineIcon(const QString &operatorICAO) return icon; } } + QString AircraftInformation::getFlagIconPath(const QString &country) { QString endPath = QString("/flags/%1.bmp").arg(country); @@ -550,6 +551,15 @@ QString AircraftInformation::getFlagIconPath(const QString &country) return QString(); } +QString AircraftInformation::getFlagIconURL(const QString &country) +{ + QString path = getFlagIconPath(country); + if (path.startsWith(':')) { + path = "qrc://" + path.mid(1); + } + return path; +} + QIcon *AircraftInformation::getFlagIcon(const QString &country) { if (m_flagIcons.contains(country)) diff --git a/sdrbase/util/osndb.h b/sdrbase/util/osndb.h index 9a6ede114..1b2f5fb9f 100644 --- a/sdrbase/util/osndb.h +++ b/sdrbase/util/osndb.h @@ -69,6 +69,7 @@ struct SDRBASE_API AircraftInformation { static QIcon *getAirlineIcon(const QString &operatorICAO); static QString getFlagIconPath(const QString &country); + static QString getFlagIconURL(const QString &country); // Try to find an flag logo based on a country static QIcon *getFlagIcon(const QString &country); diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 94d15364d..c9cfff591 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -4493,6 +4493,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setDoa2Settings(new SWGSDRangel::SWGDOA2Settings()); channelSettings->getDoa2Settings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "DSCDemodSettings") + { + channelSettings->setDscDemodSettings(new SWGSDRangel::SWGDSCDemodSettings()); + channelSettings->getDscDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "DSDDemodSettings") { channelSettings->setDsdDemodSettings(new SWGSDRangel::SWGDSDDemodSettings()); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 500713f76..c5e93a86e 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -41,6 +41,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channel.demoddatv", "DATVDemodSettings"}, {"sdrangel.channel.dabdemod", "DABDemodSettings"}, {"sdrangel.channel.doa2", "DOA2Settings"}, + {"sdrangel.channel.dscdemod", "DSCDemodSettings"}, {"sdrangel.channel.dsddemod", "DSDDemodSettings"}, {"sdrangel.channel.filesink", "FileSinkSettings"}, {"sdrangel.channeltx.filesource", "FileSourceSettings"}, @@ -155,6 +156,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"DATVMod", "DATVModSettings"}, {"DABDemod", "DABDemodSettings"}, {"DOA2", "DOA2Settings"}, + {"DSCDemod", "DSCDemodSettings"}, {"DSDDemod", "DSDDemodSettings"}, {"FileSink", "FileSinkSettings"}, {"FileSource", "FileSourceSettings"}, diff --git a/sdrgui/CMakeLists.txt b/sdrgui/CMakeLists.txt index 3e8adea62..ebaa856d6 100644 --- a/sdrgui/CMakeLists.txt +++ b/sdrgui/CMakeLists.txt @@ -44,6 +44,7 @@ set(sdrgui_SOURCES gui/fftwisdomdialog.cpp gui/flowlayout.cpp gui/framelesswindowresizer.cpp + gui/frequencydelegate.cpp gui/glscope.cpp gui/glscopegui.cpp gui/glshadercolors.cpp @@ -157,6 +158,7 @@ set(sdrgui_HEADERS gui/fftwisdomdialog.h gui/flowlayout.h gui/framelesswindowresizer.h + gui/frequencydelegate.h gui/glscope.h gui/glscopegui.h gui/glshadercolors.h diff --git a/sdrgui/gui/frequencydelegate.cpp b/sdrgui/gui/frequencydelegate.cpp new file mode 100644 index 000000000..3b063ad81 --- /dev/null +++ b/sdrgui/gui/frequencydelegate.cpp @@ -0,0 +1,58 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "frequencydelegate.h" + +FrequencyDelegate::FrequencyDelegate(QString units, int precision, bool group) : + m_units(units), + m_precision(precision), + m_group(group) +{ +} + +QString FrequencyDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + bool ok; + qlonglong v = value.toLongLong(&ok); + if (ok) + { + double d; + if (m_units == "GHz") { + d = v / 1000000000.0; + } else if (m_units == "MHz") { + d = v / 1000000.0; + } else if (m_units == "kHz") { + d = v / 1000.0; + } else { + d = v; + } + + QLocale l(locale); + if (m_group) { + l.setNumberOptions(l.numberOptions() & ~QLocale::OmitGroupSeparator); + } else { + l.setNumberOptions(l.numberOptions() | QLocale::OmitGroupSeparator); + } + QString s = l.toString(d, 'f', m_precision); + + return QString("%1 %2").arg(s).arg(m_units); + } + else + { + return value.toString(); + } +} diff --git a/sdrgui/gui/frequencydelegate.h b/sdrgui/gui/frequencydelegate.h new file mode 100644 index 000000000..4899bbee6 --- /dev/null +++ b/sdrgui/gui/frequencydelegate.h @@ -0,0 +1,39 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// 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 . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef SDRGUI_GUI_FREQUENCYDELGATE_H +#define SDRGUI_GUI_FREQUENCYDELGATE_H + +#include + +#include "export.h" + +// Delegate for table to display frequency +class SDRGUI_API FrequencyDelegate : public QStyledItemDelegate { + +public: + FrequencyDelegate(QString units = "kHz", int precision=1, bool group=true); + virtual QString displayText(const QVariant &value, const QLocale &locale) const override; + +private: + QString m_units; + int m_precision; + bool m_group; + +}; + +#endif // SDRGUI_GUI_FREQUENCYDELGATE_H diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 5c55e0b53..1489c596a 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -35,6 +35,8 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/DATVMod.yaml#/DATVModReport" DOA2Report: $ref: "http://swgserver:8081/api/swagger/include/DOA2.yaml#/DOA2Report" + DSCDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/DSCDemod.yaml#/DSCDemodReport" DSDDemodReport: $ref: "http://swgserver:8081/api/swagger/include/DSDDemod.yaml#/DSDDemodReport" IEEE_802_15_4_ModReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index daa36f511..7f671b0d2 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -51,6 +51,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/DABDemod.yaml#/DABDemodSettings" DOA2Settings: $ref: "http://swgserver:8081/api/swagger/include/DOA2.yaml#/DOA2Settings" + DSCDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/DSCDemod.yaml#/DSCDemodSettings" DSDDemodSettings: $ref: "http://swgserver:8081/api/swagger/include/DSDDemod.yaml#/DSDDemodSettings" FileSinkSettings: diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index e47906200..a99447f1a 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -56,6 +56,8 @@ SWGChannelReport::SWGChannelReport() { m_datv_mod_report_isSet = false; doa2_report = nullptr; m_doa2_report_isSet = false; + dsc_demod_report = nullptr; + m_dsc_demod_report_isSet = false; dsd_demod_report = nullptr; m_dsd_demod_report_isSet = false; ieee_802_15_4_mod_report = nullptr; @@ -156,6 +158,8 @@ SWGChannelReport::init() { m_datv_mod_report_isSet = false; doa2_report = new SWGDOA2Report(); m_doa2_report_isSet = false; + dsc_demod_report = new SWGDSCDemodReport(); + m_dsc_demod_report_isSet = false; dsd_demod_report = new SWGDSDDemodReport(); m_dsd_demod_report_isSet = false; ieee_802_15_4_mod_report = new SWGIEEE_802_15_4_ModReport(); @@ -264,6 +268,9 @@ SWGChannelReport::cleanup() { if(doa2_report != nullptr) { delete doa2_report; } + if(dsc_demod_report != nullptr) { + delete dsc_demod_report; + } if(dsd_demod_report != nullptr) { delete dsd_demod_report; } @@ -401,6 +408,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&doa2_report, pJson["DOA2Report"], "SWGDOA2Report", "SWGDOA2Report"); + ::SWGSDRangel::setValue(&dsc_demod_report, pJson["DSCDemodReport"], "SWGDSCDemodReport", "SWGDSCDemodReport"); + ::SWGSDRangel::setValue(&dsd_demod_report, pJson["DSDDemodReport"], "SWGDSDDemodReport", "SWGDSDDemodReport"); ::SWGSDRangel::setValue(&ieee_802_15_4_mod_report, pJson["IEEE_802_15_4_ModReport"], "SWGIEEE_802_15_4_ModReport", "SWGIEEE_802_15_4_ModReport"); @@ -523,6 +532,9 @@ SWGChannelReport::asJsonObject() { if((doa2_report != nullptr) && (doa2_report->isSet())){ toJsonValue(QString("DOA2Report"), doa2_report, obj, QString("SWGDOA2Report")); } + if((dsc_demod_report != nullptr) && (dsc_demod_report->isSet())){ + toJsonValue(QString("DSCDemodReport"), dsc_demod_report, obj, QString("SWGDSCDemodReport")); + } if((dsd_demod_report != nullptr) && (dsd_demod_report->isSet())){ toJsonValue(QString("DSDDemodReport"), dsd_demod_report, obj, QString("SWGDSDDemodReport")); } @@ -763,6 +775,16 @@ SWGChannelReport::setDoa2Report(SWGDOA2Report* doa2_report) { this->m_doa2_report_isSet = true; } +SWGDSCDemodReport* +SWGChannelReport::getDscDemodReport() { + return dsc_demod_report; +} +void +SWGChannelReport::setDscDemodReport(SWGDSCDemodReport* dsc_demod_report) { + this->dsc_demod_report = dsc_demod_report; + this->m_dsc_demod_report_isSet = true; +} + SWGDSDDemodReport* SWGChannelReport::getDsdDemodReport() { return dsd_demod_report; @@ -1130,6 +1152,9 @@ SWGChannelReport::isSet(){ if(doa2_report && doa2_report->isSet()){ isObjectUpdated = true; break; } + if(dsc_demod_report && dsc_demod_report->isSet()){ + isObjectUpdated = true; break; + } if(dsd_demod_report && dsd_demod_report->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h index 863e38004..cdbac4782 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -34,6 +34,7 @@ #include "SWGDATVDemodReport.h" #include "SWGDATVModReport.h" #include "SWGDOA2Report.h" +#include "SWGDSCDemodReport.h" #include "SWGDSDDemodReport.h" #include "SWGFT8DemodReport.h" #include "SWGFileSinkReport.h" @@ -128,6 +129,9 @@ public: SWGDOA2Report* getDoa2Report(); void setDoa2Report(SWGDOA2Report* doa2_report); + SWGDSCDemodReport* getDscDemodReport(); + void setDscDemodReport(SWGDSCDemodReport* dsc_demod_report); + SWGDSDDemodReport* getDsdDemodReport(); void setDsdDemodReport(SWGDSDDemodReport* dsd_demod_report); @@ -270,6 +274,9 @@ private: SWGDOA2Report* doa2_report; bool m_doa2_report_isSet; + SWGDSCDemodReport* dsc_demod_report; + bool m_dsc_demod_report_isSet; + SWGDSDDemodReport* dsd_demod_report; bool m_dsd_demod_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index 849d66b0a..9ddb27740 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -70,6 +70,8 @@ SWGChannelSettings::SWGChannelSettings() { m_dab_demod_settings_isSet = false; doa2_settings = nullptr; m_doa2_settings_isSet = false; + dsc_demod_settings = nullptr; + m_dsc_demod_settings_isSet = false; dsd_demod_settings = nullptr; m_dsd_demod_settings_isSet = false; file_sink_settings = nullptr; @@ -194,6 +196,8 @@ SWGChannelSettings::init() { m_dab_demod_settings_isSet = false; doa2_settings = new SWGDOA2Settings(); m_doa2_settings_isSet = false; + dsc_demod_settings = new SWGDSCDemodSettings(); + m_dsc_demod_settings_isSet = false; dsd_demod_settings = new SWGDSDDemodSettings(); m_dsd_demod_settings_isSet = false; file_sink_settings = new SWGFileSinkSettings(); @@ -329,6 +333,9 @@ SWGChannelSettings::cleanup() { if(doa2_settings != nullptr) { delete doa2_settings; } + if(dsc_demod_settings != nullptr) { + delete dsc_demod_settings; + } if(dsd_demod_settings != nullptr) { delete dsd_demod_settings; } @@ -495,6 +502,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&doa2_settings, pJson["DOA2Settings"], "SWGDOA2Settings", "SWGDOA2Settings"); + ::SWGSDRangel::setValue(&dsc_demod_settings, pJson["DSCDemodSettings"], "SWGDSCDemodSettings", "SWGDSCDemodSettings"); + ::SWGSDRangel::setValue(&dsd_demod_settings, pJson["DSDDemodSettings"], "SWGDSDDemodSettings", "SWGDSDDemodSettings"); ::SWGSDRangel::setValue(&file_sink_settings, pJson["FileSinkSettings"], "SWGFileSinkSettings", "SWGFileSinkSettings"); @@ -648,6 +657,9 @@ SWGChannelSettings::asJsonObject() { if((doa2_settings != nullptr) && (doa2_settings->isSet())){ toJsonValue(QString("DOA2Settings"), doa2_settings, obj, QString("SWGDOA2Settings")); } + if((dsc_demod_settings != nullptr) && (dsc_demod_settings->isSet())){ + toJsonValue(QString("DSCDemodSettings"), dsc_demod_settings, obj, QString("SWGDSCDemodSettings")); + } if((dsd_demod_settings != nullptr) && (dsd_demod_settings->isSet())){ toJsonValue(QString("DSDDemodSettings"), dsd_demod_settings, obj, QString("SWGDSDDemodSettings")); } @@ -973,6 +985,16 @@ SWGChannelSettings::setDoa2Settings(SWGDOA2Settings* doa2_settings) { this->m_doa2_settings_isSet = true; } +SWGDSCDemodSettings* +SWGChannelSettings::getDscDemodSettings() { + return dsc_demod_settings; +} +void +SWGChannelSettings::setDscDemodSettings(SWGDSCDemodSettings* dsc_demod_settings) { + this->dsc_demod_settings = dsc_demod_settings; + this->m_dsc_demod_settings_isSet = true; +} + SWGDSDDemodSettings* SWGChannelSettings::getDsdDemodSettings() { return dsd_demod_settings; @@ -1411,6 +1433,9 @@ SWGChannelSettings::isSet(){ if(doa2_settings && doa2_settings->isSet()){ isObjectUpdated = true; break; } + if(dsc_demod_settings && dsc_demod_settings->isSet()){ + isObjectUpdated = true; break; + } if(dsd_demod_settings && dsd_demod_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index f0ddcab8d..8cf514dba 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -39,6 +39,7 @@ #include "SWGDATVDemodSettings.h" #include "SWGDATVModSettings.h" #include "SWGDOA2Settings.h" +#include "SWGDSCDemodSettings.h" #include "SWGDSDDemodSettings.h" #include "SWGFT8DemodSettings.h" #include "SWGFileSinkSettings.h" @@ -159,6 +160,9 @@ public: SWGDOA2Settings* getDoa2Settings(); void setDoa2Settings(SWGDOA2Settings* doa2_settings); + SWGDSCDemodSettings* getDscDemodSettings(); + void setDscDemodSettings(SWGDSCDemodSettings* dsc_demod_settings); + SWGDSDDemodSettings* getDsdDemodSettings(); void setDsdDemodSettings(SWGDSDDemodSettings* dsd_demod_settings); @@ -337,6 +341,9 @@ private: SWGDOA2Settings* doa2_settings; bool m_doa2_settings_isSet; + SWGDSCDemodSettings* dsc_demod_settings; + bool m_dsc_demod_settings_isSet; + SWGDSDDemodSettings* dsd_demod_settings; bool m_dsd_demod_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.cpp new file mode 100644 index 000000000..d7e8fef98 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.cpp @@ -0,0 +1,131 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGDSCDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGDSCDemodReport::SWGDSCDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGDSCDemodReport::SWGDSCDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGDSCDemodReport::~SWGDSCDemodReport() { + this->cleanup(); +} + +void +SWGDSCDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGDSCDemodReport::cleanup() { + + +} + +SWGDSCDemodReport* +SWGDSCDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGDSCDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGDSCDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGDSCDemodReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_channel_sample_rate_isSet){ + obj->insert("channelSampleRate", QJsonValue(channel_sample_rate)); + } + + return obj; +} + +float +SWGDSCDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGDSCDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGDSCDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGDSCDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGDSCDemodReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.h new file mode 100644 index 000000000..8b3707f4e --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGDSCDemodReport.h @@ -0,0 +1,64 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGDSCDemodReport.h + * + * DSCDemod + */ + +#ifndef SWGDSCDemodReport_H_ +#define SWGDSCDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGDSCDemodReport: public SWGObject { +public: + SWGDSCDemodReport(); + SWGDSCDemodReport(QString* json); + virtual ~SWGDSCDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGDSCDemodReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getChannelSampleRate(); + void setChannelSampleRate(qint32 channel_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 channel_sample_rate; + bool m_channel_sample_rate_isSet; + +}; + +} + +#endif /* SWGDSCDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.cpp new file mode 100644 index 000000000..90d0ef2d2 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.cpp @@ -0,0 +1,584 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGDSCDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGDSCDemodSettings::SWGDSCDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGDSCDemodSettings::SWGDSCDemodSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + filter_invalid = 0; + m_filter_invalid_isSet = false; + filter_column = 0; + m_filter_column_isSet = false; + filter = nullptr; + m_filter_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = nullptr; + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = nullptr; + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = nullptr; + m_scope_config_isSet = false; + channel_marker = nullptr; + m_channel_marker_isSet = false; + rollup_state = nullptr; + m_rollup_state_isSet = false; +} + +SWGDSCDemodSettings::~SWGDSCDemodSettings() { + this->cleanup(); +} + +void +SWGDSCDemodSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + filter_invalid = 0; + m_filter_invalid_isSet = false; + filter_column = 0; + m_filter_column_isSet = false; + filter = new QString(""); + m_filter_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = new QString(""); + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + log_filename = new QString(""); + m_log_filename_isSet = false; + log_enabled = 0; + m_log_enabled_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + scope_config = new SWGGLScope(); + m_scope_config_isSet = false; + channel_marker = new SWGChannelMarker(); + m_channel_marker_isSet = false; + rollup_state = new SWGRollupState(); + m_rollup_state_isSet = false; +} + +void +SWGDSCDemodSettings::cleanup() { + + + + + if(filter != nullptr) { + delete filter; + } + + if(udp_address != nullptr) { + delete udp_address; + } + + if(log_filename != nullptr) { + delete log_filename; + } + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + if(scope_config != nullptr) { + delete scope_config; + } + if(channel_marker != nullptr) { + delete channel_marker; + } + if(rollup_state != nullptr) { + delete rollup_state; + } +} + +SWGDSCDemodSettings* +SWGDSCDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGDSCDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&filter_invalid, pJson["filterInvalid"], "qint32", ""); + + ::SWGSDRangel::setValue(&filter_column, pJson["filterColumn"], "qint32", ""); + + ::SWGSDRangel::setValue(&filter, pJson["filter"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&log_filename, pJson["logFilename"], "QString", "QString"); + + ::SWGSDRangel::setValue(&log_enabled, pJson["logEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&stream_index, pJson["streamIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&scope_config, pJson["scopeConfig"], "SWGGLScope", "SWGGLScope"); + + ::SWGSDRangel::setValue(&channel_marker, pJson["channelMarker"], "SWGChannelMarker", "SWGChannelMarker"); + + ::SWGSDRangel::setValue(&rollup_state, pJson["rollupState"], "SWGRollupState", "SWGRollupState"); + +} + +QString +SWGDSCDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGDSCDemodSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_input_frequency_offset_isSet){ + obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); + } + if(m_rf_bandwidth_isSet){ + obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); + } + if(m_filter_invalid_isSet){ + obj->insert("filterInvalid", QJsonValue(filter_invalid)); + } + if(m_filter_column_isSet){ + obj->insert("filterColumn", QJsonValue(filter_column)); + } + if(filter != nullptr && *filter != QString("")){ + toJsonValue(QString("filter"), filter, obj, QString("QString")); + } + if(m_udp_enabled_isSet){ + obj->insert("udpEnabled", QJsonValue(udp_enabled)); + } + if(udp_address != nullptr && *udp_address != QString("")){ + toJsonValue(QString("udpAddress"), udp_address, obj, QString("QString")); + } + if(m_udp_port_isSet){ + obj->insert("udpPort", QJsonValue(udp_port)); + } + if(log_filename != nullptr && *log_filename != QString("")){ + toJsonValue(QString("logFilename"), log_filename, obj, QString("QString")); + } + if(m_log_enabled_isSet){ + obj->insert("logEnabled", QJsonValue(log_enabled)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_stream_index_isSet){ + obj->insert("streamIndex", QJsonValue(stream_index)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + if((scope_config != nullptr) && (scope_config->isSet())){ + toJsonValue(QString("scopeConfig"), scope_config, obj, QString("SWGGLScope")); + } + if((channel_marker != nullptr) && (channel_marker->isSet())){ + toJsonValue(QString("channelMarker"), channel_marker, obj, QString("SWGChannelMarker")); + } + if((rollup_state != nullptr) && (rollup_state->isSet())){ + toJsonValue(QString("rollupState"), rollup_state, obj, QString("SWGRollupState")); + } + + return obj; +} + +qint64 +SWGDSCDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGDSCDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGDSCDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGDSCDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getFilterInvalid() { + return filter_invalid; +} +void +SWGDSCDemodSettings::setFilterInvalid(qint32 filter_invalid) { + this->filter_invalid = filter_invalid; + this->m_filter_invalid_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getFilterColumn() { + return filter_column; +} +void +SWGDSCDemodSettings::setFilterColumn(qint32 filter_column) { + this->filter_column = filter_column; + this->m_filter_column_isSet = true; +} + +QString* +SWGDSCDemodSettings::getFilter() { + return filter; +} +void +SWGDSCDemodSettings::setFilter(QString* filter) { + this->filter = filter; + this->m_filter_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGDSCDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGDSCDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGDSCDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGDSCDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +QString* +SWGDSCDemodSettings::getLogFilename() { + return log_filename; +} +void +SWGDSCDemodSettings::setLogFilename(QString* log_filename) { + this->log_filename = log_filename; + this->m_log_filename_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getLogEnabled() { + return log_enabled; +} +void +SWGDSCDemodSettings::setLogEnabled(qint32 log_enabled) { + this->log_enabled = log_enabled; + this->m_log_enabled_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGDSCDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGDSCDemodSettings::getTitle() { + return title; +} +void +SWGDSCDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGDSCDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGDSCDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGDSCDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGDSCDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGDSCDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGDSCDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGDSCDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGDSCDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +SWGGLScope* +SWGDSCDemodSettings::getScopeConfig() { + return scope_config; +} +void +SWGDSCDemodSettings::setScopeConfig(SWGGLScope* scope_config) { + this->scope_config = scope_config; + this->m_scope_config_isSet = true; +} + +SWGChannelMarker* +SWGDSCDemodSettings::getChannelMarker() { + return channel_marker; +} +void +SWGDSCDemodSettings::setChannelMarker(SWGChannelMarker* channel_marker) { + this->channel_marker = channel_marker; + this->m_channel_marker_isSet = true; +} + +SWGRollupState* +SWGDSCDemodSettings::getRollupState() { + return rollup_state; +} +void +SWGDSCDemodSettings::setRollupState(SWGRollupState* rollup_state) { + this->rollup_state = rollup_state; + this->m_rollup_state_isSet = true; +} + + +bool +SWGDSCDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_filter_invalid_isSet){ + isObjectUpdated = true; break; + } + if(m_filter_column_isSet){ + isObjectUpdated = true; break; + } + if(filter && *filter != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_enabled_isSet){ + isObjectUpdated = true; break; + } + if(udp_address && *udp_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_port_isSet){ + isObjectUpdated = true; break; + } + if(log_filename && *log_filename != QString("")){ + isObjectUpdated = true; break; + } + if(m_log_enabled_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_stream_index_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + if(scope_config && scope_config->isSet()){ + isObjectUpdated = true; break; + } + if(channel_marker && channel_marker->isSet()){ + isObjectUpdated = true; break; + } + if(rollup_state && rollup_state->isSet()){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.h new file mode 100644 index 000000000..40500ddf2 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGDSCDemodSettings.h @@ -0,0 +1,182 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 7.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGDSCDemodSettings.h + * + * DSCDemod + */ + +#ifndef SWGDSCDemodSettings_H_ +#define SWGDSCDemodSettings_H_ + +#include + + +#include "SWGChannelMarker.h" +#include "SWGGLScope.h" +#include "SWGRollupState.h" +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGDSCDemodSettings: public SWGObject { +public: + SWGDSCDemodSettings(); + SWGDSCDemodSettings(QString* json); + virtual ~SWGDSCDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGDSCDemodSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + qint32 getFilterInvalid(); + void setFilterInvalid(qint32 filter_invalid); + + qint32 getFilterColumn(); + void setFilterColumn(qint32 filter_column); + + QString* getFilter(); + void setFilter(QString* filter); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + QString* getLogFilename(); + void setLogFilename(QString* log_filename); + + qint32 getLogEnabled(); + void setLogEnabled(qint32 log_enabled); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getStreamIndex(); + void setStreamIndex(qint32 stream_index); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + SWGGLScope* getScopeConfig(); + void setScopeConfig(SWGGLScope* scope_config); + + SWGChannelMarker* getChannelMarker(); + void setChannelMarker(SWGChannelMarker* channel_marker); + + SWGRollupState* getRollupState(); + void setRollupState(SWGRollupState* rollup_state); + + + virtual bool isSet() override; + +private: + qint64 input_frequency_offset; + bool m_input_frequency_offset_isSet; + + float rf_bandwidth; + bool m_rf_bandwidth_isSet; + + qint32 filter_invalid; + bool m_filter_invalid_isSet; + + qint32 filter_column; + bool m_filter_column_isSet; + + QString* filter; + bool m_filter_isSet; + + qint32 udp_enabled; + bool m_udp_enabled_isSet; + + QString* udp_address; + bool m_udp_address_isSet; + + qint32 udp_port; + bool m_udp_port_isSet; + + QString* log_filename; + bool m_log_filename_isSet; + + qint32 log_enabled; + bool m_log_enabled_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + qint32 stream_index; + bool m_stream_index_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + + SWGGLScope* scope_config; + bool m_scope_config_isSet; + + SWGChannelMarker* channel_marker; + bool m_channel_marker_isSet; + + SWGRollupState* rollup_state; + bool m_rollup_state_isSet; + +}; + +} + +#endif /* SWGDSCDemodSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index c628a7f18..4945e7ee9 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -99,6 +99,8 @@ #include "SWGDATVModSettings.h" #include "SWGDOA2Report.h" #include "SWGDOA2Settings.h" +#include "SWGDSCDemodReport.h" +#include "SWGDSCDemodSettings.h" #include "SWGDSDDemodReport.h" #include "SWGDSDDemodSettings.h" #include "SWGDVSerialDevice.h" @@ -780,6 +782,16 @@ namespace SWGSDRangel { obj->init(); return obj; } + if(QString("SWGDSCDemodReport").compare(type) == 0) { + SWGDSCDemodReport *obj = new SWGDSCDemodReport(); + obj->init(); + return obj; + } + if(QString("SWGDSCDemodSettings").compare(type) == 0) { + SWGDSCDemodSettings *obj = new SWGDSCDemodSettings(); + obj->init(); + return obj; + } if(QString("SWGDSDDemodReport").compare(type) == 0) { SWGDSDDemodReport *obj = new SWGDSDDemodReport(); obj->init();