diff --git a/doc/img/VORDemod_plugin.png b/doc/img/VORDemod_plugin.png new file mode 100644 index 000000000..d670f9186 Binary files /dev/null and b/doc/img/VORDemod_plugin.png differ diff --git a/doc/img/VORDemod_plugin.xcf b/doc/img/VORDemod_plugin.xcf new file mode 100644 index 000000000..66cc233dc Binary files /dev/null and b/doc/img/VORDemod_plugin.xcf differ diff --git a/doc/img/VORDemod_plugin_map.png b/doc/img/VORDemod_plugin_map.png new file mode 100644 index 000000000..2414ab81d Binary files /dev/null and b/doc/img/VORDemod_plugin_map.png differ diff --git a/doc/img/VORDemod_plugin_table.png b/doc/img/VORDemod_plugin_table.png new file mode 100644 index 000000000..b1cc2a355 Binary files /dev/null and b/doc/img/VORDemod_plugin_table.png differ diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 205d211bd..9aa289a57 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(localsink) add_subdirectory(filesink) add_subdirectory(freqtracker) add_subdirectory(demodchirpchat) +add_subdirectory(demodvor) if(LIBDSDCC_FOUND AND LIBMBE_FOUND) add_subdirectory(demoddsd) diff --git a/plugins/channelrx/demodadsb/adsbdemodgui.cpp b/plugins/channelrx/demodadsb/adsbdemodgui.cpp index 9eece6bbe..8571c9f84 100644 --- a/plugins/channelrx/demodadsb/adsbdemodgui.cpp +++ b/plugins/channelrx/demodadsb/adsbdemodgui.cpp @@ -34,6 +34,7 @@ #include "plugin/pluginapi.h" #include "util/simpleserializer.h" #include "util/db.h" +#include "util/units.h" #include "gui/basicchannelsettingsdialog.h" #include "gui/devicestreamselectiondialog.h" #include "gui/crightclickenabler.h" @@ -141,36 +142,6 @@ static int cprN(double lat, int odd) return 1; } -static Real feetToMetres(Real feet) -{ - return feet * 0.3048f; -} - -static int feetToMetresInt(Real feet) -{ - return (int)std::round(feetToMetres(feet)); -} - -static Real knotsToKPH(Real knots) -{ - return knots * 1.852f; -} - -static int knotsToKPHInt(Real knots) -{ - return (int)std::round(knotsToKPH(knots)); -} - -static Real feetPerMinToMetresPerSecond(Real fpm) -{ - return fpm * 0.00508f; -} - -static int feetPerMinToMetresPerSecondInt(Real fpm) -{ - return (int)std::round(feetPerMinToMetresPerSecond(fpm)); -} - // Can't use std::fmod, as that works differently for negative numbers (See C.2.6.2) static Real modulus(double x, double y) { @@ -188,7 +159,7 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const QGeoCoordinate coords; coords.setLatitude(m_aircrafts[row]->m_latitude); coords.setLongitude(m_aircrafts[row]->m_longitude); - coords.setAltitude(feetToMetres(m_aircrafts[row]->m_altitude)); + coords.setAltitude(Units::feetToMetres(m_aircrafts[row]->m_altitude)); return QVariant::fromValue(coords); } else if (role == AircraftModel::headingRole) @@ -220,14 +191,14 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const if (m_aircrafts[row]->m_altitudeValid) { if (m_aircrafts[row]->m_gui->useSIUints()) - list.append(QString("Altitude: %1 (m)").arg(feetToMetresInt(m_aircrafts[row]->m_altitude))); + list.append(QString("Altitude: %1 (m)").arg(Units::feetToIntegerMetres(m_aircrafts[row]->m_altitude))); else list.append(QString("Altitude: %1 (ft)").arg(m_aircrafts[row]->m_altitude)); } if (m_aircrafts[row]->m_speedValid) { if (m_aircrafts[row]->m_gui->useSIUints()) - list.append(QString("%1: %2 (kph)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(knotsToKPHInt(m_aircrafts[row]->m_speed))); + list.append(QString("%1: %2 (kph)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(Units::knotsToIntegerKPH(m_aircrafts[row]->m_speed))); else list.append(QString("%1: %2 (kn)").arg(m_aircrafts[row]->m_speedTypeNames[m_aircrafts[row]->m_speedType]).arg(m_aircrafts[row]->m_speed)); } @@ -239,7 +210,7 @@ QVariant AircraftModel::data(const QModelIndex &index, int role) const if (m_aircrafts[row]->m_gui->useSIUints()) { - rate = feetPerMinToMetresPerSecondInt(m_aircrafts[row]->m_verticalRate); + rate = Units::feetPerMinToIntegerMetresPerSecond(m_aircrafts[row]->m_verticalRate); units = QString("m/s"); } else @@ -372,7 +343,7 @@ QVariant AirportModel::data(const QModelIndex &index, int role) const QGeoCoordinate coords; coords.setLatitude(m_airports[row]->m_latitude); coords.setLongitude(m_airports[row]->m_longitude); - coords.setAltitude(feetToMetres(m_airports[row]->m_elevation)); + coords.setAltitude(Units::feetToMetres(m_airports[row]->m_elevation)); return QVariant::fromValue(coords); } else if (role == AirportModel::airportDataRole) @@ -463,7 +434,7 @@ void ADSBDemodGUI::updatePosition(Aircraft *aircraft) m_aircraftModel.addAircraft(aircraft); } // Calculate range, azimuth and elevation to aircraft from station - m_azEl.setTarget(aircraft->m_latitude, aircraft->m_longitude, feetToMetres(aircraft->m_altitude)); + m_azEl.setTarget(aircraft->m_latitude, aircraft->m_longitude, Units::feetToMetres(aircraft->m_altitude)); m_azEl.calculate(); aircraft->m_range = m_azEl.getDistance(); aircraft->m_azimuth = m_azEl.getAzimuth(); @@ -479,7 +450,7 @@ void ADSBDemodGUI::updatePosition(Aircraft *aircraft) bool ADSBDemodGUI::updateLocalPosition(Aircraft *aircraft, double latitude, double longitude, bool surfacePosition) { // Calculate range to aircraft from station - m_azEl.setTarget(latitude, longitude, feetToMetres(aircraft->m_altitude)); + m_azEl.setTarget(latitude, longitude, Units::feetToMetres(aircraft->m_altitude)); m_azEl.calculate(); // Don't use the full 333km, as there may be some error in station position @@ -851,7 +822,7 @@ void ADSBDemodGUI::handleADSB( // } aircraft->m_speedType = Aircraft::GS; aircraft->m_speedValid = true; - aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? knotsToKPHInt(aircraft->m_speed) : (int)std::round(aircraft->m_speed)); + aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : (int)std::round(aircraft->m_speed)); } else if (movement == 124) { @@ -878,7 +849,7 @@ void ADSBDemodGUI::handleADSB( aircraft->m_altitude = alt_ft; aircraft->m_altitudeValid = true; // setData rather than setText so it sorts numerically - aircraft->m_altitudeItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? feetToMetresInt(aircraft->m_altitude) : aircraft->m_altitude); + aircraft->m_altitudeItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::feetToIntegerMetres(aircraft->m_altitude) : aircraft->m_altitude); } int f = (data[6] >> 2) & 1; // CPR odd/even frame - should alternate every 0.2s @@ -1041,7 +1012,7 @@ void ADSBDemodGUI::handleADSB( aircraft->m_speedType = Aircraft::GS; aircraft->m_speedValid = true; aircraft->m_headingItem->setData(Qt::DisplayRole, aircraft->m_heading); - aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? knotsToKPHInt(aircraft->m_speed) : aircraft->m_speed); + aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : aircraft->m_speed); } else { @@ -1061,14 +1032,14 @@ void ADSBDemodGUI::handleADSB( aircraft->m_speed = as; aircraft->m_speedType = as_t ? Aircraft::IAS : Aircraft::TAS; aircraft->m_speedValid = true; - aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? knotsToKPHInt(aircraft->m_speed) : aircraft->m_speed); + aircraft->m_speedItem->setData(Qt::DisplayRole, m_settings.m_siUnits ? Units::knotsToIntegerKPH(aircraft->m_speed) : aircraft->m_speed); } int s_vr = (data[8] >> 3) & 1; // Vertical rate sign int vr = ((data[8] & 0x7) << 6) | ((data[9] >> 2) & 0x3f); // Vertical rate aircraft->m_verticalRate = (vr-1)*64*(s_vr?-1:1); aircraft->m_verticalRateValid = true; if (m_settings.m_siUnits) - aircraft->m_verticalRateItem->setData(Qt::DisplayRole, feetPerMinToMetresPerSecondInt(aircraft->m_verticalRate)); + aircraft->m_verticalRateItem->setData(Qt::DisplayRole, Units::feetPerMinToIntegerMetresPerSecond(aircraft->m_verticalRate)); else aircraft->m_verticalRateItem->setData(Qt::DisplayRole, aircraft->m_verticalRate); } @@ -1637,7 +1608,7 @@ void ADSBDemodGUI::updateAirports() AirportInformation *airportInfo = i.value(); // Calculate distance and az/el to airport from My Position - azEl.setTarget(airportInfo->m_latitude, airportInfo->m_longitude, feetToMetres(airportInfo->m_elevation)); + azEl.setTarget(airportInfo->m_latitude, airportInfo->m_longitude, Units::feetToMetres(airportInfo->m_elevation)); azEl.calculate(); // Only display airport if in range diff --git a/plugins/channelrx/demodadsb/csv.h b/plugins/channelrx/demodadsb/csv.h index 770c38345..0a3716dc7 100644 --- a/plugins/channelrx/demodadsb/csv.h +++ b/plugins/channelrx/demodadsb/csv.h @@ -25,6 +25,10 @@ static inline char *csvNext(char **pp) { char *p = *pp; + + if (p[0] == '\0') + return nullptr; + char *start = p; while ((*p != ',') && (*p != '\n')) diff --git a/plugins/channelrx/demodvor/CMakeLists.txt b/plugins/channelrx/demodvor/CMakeLists.txt new file mode 100644 index 000000000..845692499 --- /dev/null +++ b/plugins/channelrx/demodvor/CMakeLists.txt @@ -0,0 +1,65 @@ +project(vor) + +set(vor_SOURCES + vordemod.cpp + vordemodsettings.cpp + vordemodbaseband.cpp + vordemodsink.cpp + vordemodplugin.cpp + vordemodwebapiadapter.cpp + vordemodreport.cpp +) + +set(vor_HEADERS + vordemod.h + vordemodsettings.h + vordemodbaseband.h + vordemodsink.h + vordemodplugin.h + vordemodwebapiadapter.h + vordemodreport.h +) + + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(vor_SOURCES + ${vor_SOURCES} + vordemodgui.cpp + vordemodgui.ui + map.qrc + icons.qrc + ) + set(vor_HEADERS + ${vor_HEADERS} + vordemodgui.h + navaid.h + ../demodadsb/csv.h + ) + + set(TARGET_NAME demodvor) + set(TARGET_LIB "Qt5::Widgets" Qt5::Quick Qt5::QuickWidgets Qt5::Positioning) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodvorsrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${vor_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channelrx/demodvor/icons.qrc b/plugins/channelrx/demodvor/icons.qrc new file mode 100644 index 000000000..ca8be748f --- /dev/null +++ b/plugins/channelrx/demodvor/icons.qrc @@ -0,0 +1,6 @@ + + + icons/compass.png + icons/vor.png + + diff --git a/plugins/channelrx/demodvor/icons/compass.png b/plugins/channelrx/demodvor/icons/compass.png new file mode 100644 index 000000000..1415a50f0 Binary files /dev/null and b/plugins/channelrx/demodvor/icons/compass.png differ diff --git a/plugins/channelrx/demodvor/icons/vor.png b/plugins/channelrx/demodvor/icons/vor.png new file mode 100644 index 000000000..ac66aad04 Binary files /dev/null and b/plugins/channelrx/demodvor/icons/vor.png differ diff --git a/plugins/channelrx/demodvor/map.qrc b/plugins/channelrx/demodvor/map.qrc new file mode 100644 index 000000000..b0018f5b9 --- /dev/null +++ b/plugins/channelrx/demodvor/map.qrc @@ -0,0 +1,10 @@ + + + map/map.qml + map/MapStation.qml + map/antenna.png + map/VOR.png + map/VOR-DME.png + map/VORTAC.png + + diff --git a/plugins/channelrx/demodvor/map/MapStation.qml b/plugins/channelrx/demodvor/map/MapStation.qml new file mode 100644 index 000000000..a69346e46 --- /dev/null +++ b/plugins/channelrx/demodvor/map/MapStation.qml @@ -0,0 +1,40 @@ +import QtQuick 2.12 +import QtLocation 5.12 +import QtPositioning 5.12 + +MapQuickItem { + id: station + property string stationName // Name of the station, E.g. Home + + coordinate: QtPositioning.coordinate(51.5, 0.125) // Location of the antenna (QTH) - London + zoomLevel: 11 + + anchorPoint.x: image.width/2 + anchorPoint.y: image.height/2 + + sourceItem: Grid { + columns: 1 + Grid { + horizontalItemAlignment: Grid.AlignHCenter + layer.enabled: true + layer.smooth: true + Image { + id: image + source: "antenna.png" + } + Rectangle { + id: bubble + color: "lightblue" + border.width: 1 + width: text.width * 1.3 + height: text.height * 1.3 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: stationName + } + } + } + } +} diff --git a/plugins/channelrx/demodvor/map/VOR-DME.png b/plugins/channelrx/demodvor/map/VOR-DME.png new file mode 100644 index 000000000..c2e2c412d Binary files /dev/null and b/plugins/channelrx/demodvor/map/VOR-DME.png differ diff --git a/plugins/channelrx/demodvor/map/VOR.png b/plugins/channelrx/demodvor/map/VOR.png new file mode 100644 index 000000000..0d6949fde Binary files /dev/null and b/plugins/channelrx/demodvor/map/VOR.png differ diff --git a/plugins/channelrx/demodvor/map/VORTAC.png b/plugins/channelrx/demodvor/map/VORTAC.png new file mode 100644 index 000000000..f7df0a0b3 Binary files /dev/null and b/plugins/channelrx/demodvor/map/VORTAC.png differ diff --git a/plugins/channelrx/demodvor/map/antenna.png b/plugins/channelrx/demodvor/map/antenna.png new file mode 100644 index 000000000..f13c91881 Binary files /dev/null and b/plugins/channelrx/demodvor/map/antenna.png differ diff --git a/plugins/channelrx/demodvor/map/map.qml b/plugins/channelrx/demodvor/map/map.qml new file mode 100644 index 000000000..0d6eb0508 --- /dev/null +++ b/plugins/channelrx/demodvor/map/map.qml @@ -0,0 +1,115 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtLocation 5.12 +import QtPositioning 5.12 + +Item { + id: qmlMap + property int vorZoomLevel: 11 + + Plugin { + id: mapPlugin + name: "osm" + } + + Map { + id: map + objectName: "map" + anchors.fill: parent + plugin: mapPlugin + center: QtPositioning.coordinate(51.5, 0.125) // London + zoomLevel: 10 + + + MapItemView { + model: vorModel + delegate: vorRadialComponent + } + + MapStation { + id: station + objectName: "station" + stationName: "Home" + coordinate: QtPositioning.coordinate(51.5, 0.125) + } + + MapItemView { + model: vorModel + delegate: vorComponent + } + + onZoomLevelChanged: { + if (zoomLevel > 11) { + station.zoomLevel = zoomLevel + vorZoomLevel = zoomLevel + } else { + station.zoomLevel = 11 + vorZoomLevel = 11 + } + } + + } + + Component { + id: vorRadialComponent + MapPolyline { + line.width: 2 + line.color: 'gray' + path: vorRadial + } + } + + Component { + id: vorComponent + MapQuickItem { + id: vor + anchorPoint.x: image.width/2 + anchorPoint.y: bubble.height/2 + coordinate: position + zoomLevel: vorZoomLevel + + sourceItem: Grid { + columns: 1 + Grid { + horizontalItemAlignment: Grid.AlignHCenter + verticalItemAlignment: Grid.AlignVCenter + columnSpacing: 5 + layer.enabled: true + layer.smooth: true + Image { + id: image + source: vorImage + MouseArea { + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: (mouse) => { + selected = !selected + } + } + } + Rectangle { + id: bubble + color: bubbleColour + border.width: 1 + width: text.width + 5 + height: text.height + 5 + radius: 5 + Text { + id: text + anchors.centerIn: parent + text: vorData + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onDoubleClicked: (mouse) => { + selected = !selected + } + } + } + } + } + } + } + +} diff --git a/plugins/channelrx/demodvor/navaid.h b/plugins/channelrx/demodvor/navaid.h new file mode 100644 index 000000000..f770b281c --- /dev/null +++ b/plugins/channelrx/demodvor/navaid.h @@ -0,0 +1,368 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_NAVAID_H +#define INCLUDE_NAVAID_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "util/units.h" +#include "../demodadsb/csv.h" + +#define OURAIRPORTS_NAVAIDS_URL "https://ourairports.com/data/navaids.csv" + +#define OPENAIP_NAVAIDS_URL "https://www.openaip.net/customer_export_akfshb9237tgwiuvb4tgiwbf/%1_nav.aip" + +struct NavAid { + + int m_id; + QString m_ident; // 2 or 3 character ident + QString m_type; // VOR, VOR-DME or VORTAC + QString m_name; + float m_latitude; + float m_longitude; + float m_elevation; + int m_frequencykHz; + QString m_channel; + int m_range; // Nautical miles + float m_magneticDeclination; + bool m_alignedTrueNorth; // Is the VOR aligned to true North, rather than magnetic (may be the case at high latitudes) + + static QString trimQuotes(const QString s) + { + if (s.startsWith('\"') && s.endsWith('\"')) + return s.mid(1, s.size() - 2); + else + return s; + } + + int getRangeMetres() + { + return Units::nauticalMilesToIntegerMetres((float)m_range); + } + + // OpenAIP XML file + static void readNavAidsXML(QHash *navAidInfo, const QString &filename) + { + QFile file(filename); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QXmlStreamReader xmlReader(&file); + + while(!xmlReader.atEnd() && !xmlReader.hasError()) + { + if (xmlReader.readNextStartElement()) + { + if (xmlReader.name() == "NAVAID") + { + QStringRef typeRef = xmlReader.attributes().value("TYPE"); + if ((typeRef == QLatin1String("VOR")) + || (typeRef== QLatin1String("VOR-DME")) + || (typeRef == QLatin1String("VORTAC"))) + { + QString type = typeRef.toString(); + int identifier = 0; + QString name; + QString id; + float lat = 0.0f; + float lon = 0.0f; + float elevation = 0.0f; + int frequency = 0; + QString channel; + int range = 25; + float declination = 0.0f; + bool alignedTrueNorth = false; + while(xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("IDENTIFIER")) + identifier = xmlReader.readElementText().toInt(); + else if (xmlReader.name() == QLatin1String("NAME")) + name = xmlReader.readElementText(); + else if (xmlReader.name() == QLatin1String("ID")) + id = xmlReader.readElementText(); + else if (xmlReader.name() == QLatin1String("GEOLOCATION")) + { + while(xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("LAT")) + lat = xmlReader.readElementText().toFloat(); + else if (xmlReader.name() == QLatin1String("LON")) + lon = xmlReader.readElementText().toFloat(); + else if (xmlReader.name() == QLatin1String("ELEV")) + elevation = xmlReader.readElementText().toFloat(); + else + xmlReader.skipCurrentElement(); + } + } + else if (xmlReader.name() == QLatin1String("RADIO")) + { + while(xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("FREQUENCY")) + frequency = (int)(xmlReader.readElementText().toFloat() * 1000); + else if (xmlReader.name() == QLatin1String("CHANNEL")) + channel = xmlReader.readElementText(); + else + xmlReader.skipCurrentElement(); + } + } + else if (xmlReader.name() == QLatin1String("PARAMS")) + { + while(xmlReader.readNextStartElement()) + { + if (xmlReader.name() == QLatin1String("RANGE")) + range = xmlReader.readElementText().toInt(); + else if (xmlReader.name() == QLatin1String("DECLINATION")) + declination = xmlReader.readElementText().toFloat(); + else if (xmlReader.name() == QLatin1String("ALIGNEDTOTRUENORTH")) + alignedTrueNorth = xmlReader.readElementText() == "TRUE"; + else + xmlReader.skipCurrentElement(); + } + } + else + xmlReader.skipCurrentElement(); + } + NavAid *vor = new NavAid(); + vor->m_id = identifier; + vor->m_ident = id; + // Check idents conform to our filtering rules + if (vor->m_ident.size() < 2) + qDebug() << "Warning: VOR Ident less than 2 characters: " << vor->m_ident; + else if (vor->m_ident.size() > 3) + qDebug() << "Warning: VOR Ident greater than 3 characters: " << vor->m_ident; + vor->m_type = type; + vor->m_name = name; + vor->m_frequencykHz = frequency; + vor->m_channel = channel; + vor->m_latitude = lat; + vor->m_longitude = lon; + vor->m_elevation = elevation; + vor->m_range = range; + vor->m_magneticDeclination = declination; + vor->m_alignedTrueNorth = alignedTrueNorth; + navAidInfo->insert(identifier, vor); + } + } + } + } + + file.close(); + } + else + qDebug() << "NavAid::readNavAidsXML: Could not open " << filename << " for reading."; + } + + // Read OurAirport's NavAids CSV file + // See comments for readOSNDB + static QHash *readNavAidsDB(const QString &filename) + { + int cnt = 0; + QHash *navAidInfo = nullptr; + + // Column numbers used for the data as of 2020/10/28 + int idCol = 0; + int identCol = 2; + int typeCol = 4; + int nameCol = 3; + int frequencyCol = 5; + int latitudeCol = 6; + int longitudeCol = 7; + int elevationCol = 8; + int powerCol = 18; + + qDebug() << "NavAid::readNavAidsDB: " << filename; + + FILE *file; + QByteArray utfFilename = filename.toUtf8(); + if ((file = fopen(utfFilename.constData(), "r")) != NULL) + { + char row[2048]; + + if (fgets(row, sizeof(row), file)) + { + navAidInfo = new QHash(); + navAidInfo->reserve(15000); + + // Read header + int idx = 0; + char *p = strtok(row, ","); + while (p != NULL) + { + if (!strcmp(p, "id")) + idCol = idx; + else if (!strcmp(p, "ident")) + identCol = idx; + else if (!strcmp(p, "type")) + typeCol = idx; + else if (!strcmp(p, "name")) + nameCol = idx; + else if (!strcmp(p, "frequency_khz")) + frequencyCol = idx; + else if (!strcmp(p, "latitude_deg")) + latitudeCol = idx; + else if (!strcmp(p, "longitude_deg")) + longitudeCol = idx; + else if (!strcmp(p, "elevation_ft")) + elevationCol = idx; + else if (!strcmp(p, "power")) + powerCol = idx; + p = strtok(NULL, ","); + idx++; + } + // Read data + while (fgets(row, sizeof(row), file)) + { + int id = 0; + char *idString = NULL; + char *ident = NULL; + size_t identLen = 0; + char *type = NULL; + size_t typeLen = 0; + char *name = NULL; + size_t nameLen = 0; + char *frequencyString = NULL; + int frequency; + float latitude = 0.0f; + char *latitudeString = NULL; + size_t latitudeLen = 0; + float longitude = 0.0f; + char *longitudeString = NULL; + size_t longitudeLen = 0; + float elevation = 0.0f; + char *elevationString = NULL; + size_t elevationLen = 0; + char *power = NULL; + size_t powerLen = 0; + + char *q = row; + idx = 0; + while ((p = csvNext(&q)) != nullptr) + { + // Read strings, stripping quotes + if (idx == idCol) + { + idString = p; + idString[strlen(idString)] = '\0'; + id = strtol(idString, NULL, 10); + } + else if ((idx == identCol) && (p[0] == '\"')) + { + ident = p+1; + identLen = strlen(ident)-1; + ident[identLen] = '\0'; + } + else if ((idx == typeCol) && (p[0] == '\"')) + { + type = p+1; + typeLen = strlen(type)-1; + type[typeLen] = '\0'; + } + else if ((idx == nameCol) && (p[0] == '\"')) + { + name = p+1; + nameLen = strlen(name)-1; + name[nameLen] = '\0'; + } + if (idx == frequencyCol) + { + frequencyString = p; + frequencyString[strlen(frequencyString)] = '\0'; + frequency = strtol(frequencyString, NULL, 10); + } + else if (idx == latitudeCol) + { + latitudeString = p; + latitudeLen = strlen(latitudeString)-1; + latitudeString[latitudeLen] = '\0'; + latitude = atof(latitudeString); + } + else if (idx == longitudeCol) + { + longitudeString = p; + longitudeLen = strlen(longitudeString)-1; + longitudeString[longitudeLen] = '\0'; + longitude = atof(longitudeString); + } + else if (idx == elevationCol) + { + elevationString = p; + elevationLen = strlen(elevationString)-1; + elevationString[elevationLen] = '\0'; + elevation = atof(elevationString); + } + else if ((idx == powerCol) && (p[0] == '\"')) + { + power = p+1; + powerLen = strlen(power)-1; + power[powerLen] = '\0'; + } + idx++; + } + + // For now, we only want VORs + if (type && !strncmp(type, "VOR", 3)) + { + NavAid *vor = new NavAid(); + vor->m_id = id; + vor->m_ident = QString(ident); + // Check idents conform to our filtering rules + if (vor->m_ident.size() < 2) + qDebug() << "Warning: VOR Ident less than 2 characters: " << vor->m_ident; + else if (vor->m_ident.size() > 3) + qDebug() << "Warning: VOR Ident greater than 3 characters: " << vor->m_ident; + vor->m_type = QString(type); + vor->m_name = QString(name); + vor->m_frequencykHz = frequency; + vor->m_latitude = latitude; + vor->m_longitude = longitude; + vor->m_elevation = elevation; + if (power && !strcmp(power, "HIGH")) + vor->m_range = 100; + else if (power && !strcmp(power, "MEDIUM")) + vor->m_range = 40; + else + vor->m_range = 25; + vor->m_magneticDeclination = 0.0f; + vor->m_alignedTrueNorth = false; + navAidInfo->insert(id, vor); + cnt++; + } + } + } + fclose(file); + } + else + qDebug() << "NavAid::readNavAidsDB: Failed to open " << filename; + + qDebug() << "NavAid::readNavAidsDB: Read " << cnt << " VORs"; + + return navAidInfo; + } + +}; + +#endif // INCLUDE_NAVAID_H diff --git a/plugins/channelrx/demodvor/readme.md b/plugins/channelrx/demodvor/readme.md new file mode 100644 index 000000000..d037d5930 --- /dev/null +++ b/plugins/channelrx/demodvor/readme.md @@ -0,0 +1,89 @@ +

VOR demodulator plugin

+ +

Introduction

+ +This plugin can be used to demodulate VOR (VHF omnidirectional range) navaids (navigation aids). VORs are radio naviation aids in the VHF 108 - 117.975MHz band commonly used for aircraft navigation. + +VORs transmit two 30Hz signals, one AM at the VOR center frequency and one FM on a 9960Hz sub-carrier. The FM reference signal's phase is set so 0 degrees corresponds to magnetic north from the VOR (Some VORs at high latitudes use true North). The phase of the AM variable signal is such that the phase difference to the reference signal corresponds to the bearing from the VOR to the receiver. For example, if a receiver is North from the VOR, the AM and FM 30Hz signals will be received in phase. If a receiver is East from the VOR, the phase difference will be 90 degrees. + +VORs also transmit a Morse code ident signal at a 1020Hz offset. This is a 2 or 3 character identifier used to identify the VOR, as multiple VORs can be transmitted on the same frequency. For example, the VOR at London Heathrow transmits .-.. --- -. for LON. The Morse code ident is typically transmitted at 10 seconds intervals at between 7 and 10 wpm. VORs that are under maintainance may transmit TST. + +Some VORs also transmit an AM voice identification or information signal between 300-3kHz. + +This plugin can demodulate all four signals from multiple VORs simultaneously, allowing your position to be determined and plotted on a map. It can also demodulate the Morse code ident signal and and check they are correct for each VOR. The Morse code ident and any voice signal will also be heard as audio. + +Note that for aircraft, there is typically a direct line-of-sight to the VOR. This is unlikely to be the case when using an SDR on the ground. To get good results, ideally you want to be on a nice high hill or close to the VOR. + +

Interface

+ +![VOR Demodulator plugin GUI](../../../doc/img/VORDemod_plugin.png) + +

1: Level meter in dB

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

2: Channel power

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

3: Audio mute and audio output select

+ +Left click on this button to toggle audio mute for this channel. The button will light up in green if the squelch is open. This helps identifying which channels are active in a multi-channel configuration. + +If you right click on it it will open a dialog to select the audio output device. See [audio management documentation](../../../sdrgui/audio.md) for details. + +

4: Download VOR Database

+ +Pressing this button downloads the OpenAIP.net Navaid database, which contains the details (position, frequencies, name and ident) for each VOR. This needs to be performed at least once. + +

5: Draw Radials Adjusted for Magnetic Declination

+ +When checked, radials on the map will drawn adjusted for magnetic declination. For example, if a VOR has a magnetic declination of 5 degrees, and the radial is calculated at 0 degrees, the radial will be drawn to magnetic North, i.e. -5 degress from true North. If not checked, the same radial would be drawn to true North (i.e 0 degrees), which may result in a less accurate position estimate. + +

6: Morse ident threshold

+ +This is the Morse code ident threshold, expressed as a linear signal to noise (SNR) ratio. This is effectively the signal level required for the Morse demodulator to detect a dot or dash. Setting this to low values will allow the Morse demodulator to detect weak signals, but it also increases the likelyhood that noise will incorrectly be interpreted as a signal, resulting in invalid idents being reported. + +

7: Squelch threshold

+ +This is the squelch threshold in dB. The average total power received in the signal bandwidth before demodulation is compared to this value and the squelch input is open above this value. It can be varied continuously in 0.1 dB steps from 0.0 to -100.0 dB using the dial button. + +

8: Volume

+ +This is the volume of the audio signal from 0.0 (mute) to 10.0 (maximum). It can be varied continuously in 0.1 steps using the dial button. + +

VOR Table

+ +The VOR table displays information about selected VORs. To select or deselect a VOR, double click it on the map. The information displayed includes: + +![VOR Demodulator Table](../../../doc/img/VORDemod_plugin_table.png) + +* Name - The name of the VOR. For example: 'LONDON'. +* Freq (MHz) - The center frequency the VOR transmits on in MHz. +* Offset (kHz) - This is the current difference between the VOR's center frequency and SDRangle's device center frequency. If displayed in red, the VOR is out of range and it's signal will not be able to be received. +* Ident - A 2 or 3 character identifier for the VOR. For example: 'LON'. +* Morse - The Morse code identifier for the VOR. For example: '.-.. --- -.' +* RX Ident - This contains the demodulated ident. If it matches the expected ident, it will be displayed in green, if not, it will be displayed in red. If an ident is received that is not 2 or 3 characters, it will not be displayed, but the last received ident will be displayed in yellow. +* RX Morse - This contains the demodulated Morse code ident. Colour coding is as for RX Ident. +* Radial - This contains the demodulated radial direction in degrees (unadjusted for magnetic declination). If there is a low confidence the value is correct (due to a weak signal), it will be displayed in red. +* Ref (dB) - This displays the magnitude of the received 30Hz FM reference signal in dB. +* Var (dB) - This displays the mangitude of the received 30Hz AM variable signal in dB. +* Mute - This button allows you to mute or unmute the audio from the corresponding VOR. + +

Map

+ +The map displays the locations of each VOR, with an information box containing the information about the VOR, such as it's name, frequency, ident (in text and Morse), range and magnetic declination. + +To initialise the VORs on the map, first set your position using the Preferences > My position menu, then open the VOR Demodulator channel (close and reopen it, if already open). Then press the Download VOR Database button (This only needs to be performed once). The map should then display VORs in your vicinity. + +Double clicking on a VOR will select and add it to the list of VORs to demodulate. It will be added to the VOR table and will be highlighted green. Double clicking a selected VOR, will remove it from the list of VORs to demodulate and it will be removed from the VOR table. + +When a signal from a VOR is correctly being demodulated, a radial line will be drawn on the map, at the angle corresponding to the phase difference between the AM and FM 30Hz signals. Your receiver should be somewhere along this radial line. The length of the radial line is set according to the range of the VOR as recorded in the database, which is valid for aircraft at altitude. Range on the ground will be considerably less. An approximate position for the receiver is where the radial lines from two or more VORs intersect. + +![VOR Demodulator Map](../../../doc/img/VORDemod_plugin_map.png) + +

Attribution

+ +Icons by Denelson83 and mamayer, via Wikimedia Commons and RULI from the Noun Project https://thenounproject.com/ diff --git a/plugins/channelrx/demodvor/vordemod.cpp b/plugins/channelrx/demodvor/vordemod.cpp new file mode 100644 index 000000000..17f5d680a --- /dev/null +++ b/plugins/channelrx/demodvor/vordemod.cpp @@ -0,0 +1,506 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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 "vordemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGVORDemodSettings.h" +#include "SWGChannelReport.h" +#include "SWGVORDemodReport.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(VORDemod::MsgConfigureVORDemod, Message) + +const char * const VORDemod::m_channelIdURI = "sdrangel.channel.vordemod"; +const char * const VORDemod::m_channelId = "VORDemod"; + +VORDemod::VORDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new VORDemodBaseband(); + m_basebandSink->moveToThread(&m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +VORDemod::~VORDemod() +{ + qDebug("VORDemod::~VORDemod"); + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +uint32_t VORDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void VORDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void VORDemod::start() +{ + qDebug("VORDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + VORDemodBaseband::MsgConfigureVORDemodBaseband *msg = VORDemodBaseband::MsgConfigureVORDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void VORDemod::stop() +{ + qDebug("VORDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool VORDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureVORDemod::match(cmd)) + { + MsgConfigureVORDemod& cfg = (MsgConfigureVORDemod&) cmd; + qDebug() << "VORDemod::handleMessage: MsgConfigureVORDemod"; + 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() << "VORDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + // Forward to GUI if any + if (m_guiMessageQueue) + { + rep = new DSPSignalNotification(notif); + m_guiMessageQueue->push(rep); + } + + return true; + } + else + { + return false; + } +} + +void VORDemod::applySettings(const VORDemodSettings& settings, bool force) +{ + qDebug() << "VORDemod::applySettings:" + << " m_volume: " << settings.m_volume + << " m_squelch: " << settings.m_squelch + << " m_audioMute: " << settings.m_audioMute + << " m_audioDeviceName: " << settings.m_audioDeviceName + << " 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 ((m_settings.m_squelch != settings.m_squelch) || force) { + reverseAPIKeys.append("squelch"); + } + if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) { + reverseAPIKeys.append("audioDeviceName"); + } + + if ((m_settings.m_audioMute != settings.m_audioMute) || force) { + reverseAPIKeys.append("audioMute"); + } + + if ((m_settings.m_volume != settings.m_volume) || force) { + reverseAPIKeys.append("volume"); + } + + 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"); + } + + if ((m_settings.m_identThreshold != settings.m_identThreshold) || force) { + reverseAPIKeys.append("identThreshold"); + } + + if ((m_settings.m_magDecAdjust != settings.m_magDecAdjust) || force) { + reverseAPIKeys.append("magDecAdjust"); + } + + VORDemodBaseband::MsgConfigureVORDemodBaseband *msg = VORDemodBaseband::MsgConfigureVORDemodBaseband::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 (m_featuresSettingsFeedback.size() > 0) { + featuresSendSettings(reverseAPIKeys, settings, force); + } + + m_settings = settings; +} + +QByteArray VORDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool VORDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureVORDemod *msg = MsgConfigureVORDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureVORDemod *msg = MsgConfigureVORDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int VORDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setVorDemodSettings(new SWGSDRangel::SWGVORDemodSettings()); + response.getVorDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int VORDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + VORDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureVORDemod *msg = MsgConfigureVORDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("VORDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureVORDemod *msgToGUI = MsgConfigureVORDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void VORDemod::webapiUpdateChannelSettings( + VORDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("audioMute")) { + settings.m_audioMute = response.getVorDemodSettings()->getAudioMute() != 0; + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getVorDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("squelch")) { + settings.m_squelch = response.getVorDemodSettings()->getSquelch(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getVorDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("volume")) { + settings.m_volume = response.getVorDemodSettings()->getVolume(); + } + if (channelSettingsKeys.contains("audioDeviceName")) { + settings.m_audioDeviceName = *response.getVorDemodSettings()->getAudioDeviceName(); + } + + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getVorDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getVorDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getVorDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getVorDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getVorDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getVorDemodSettings()->getReverseApiChannelIndex(); + } + if (channelSettingsKeys.contains("identThreshold")) { + settings.m_identThreshold = response.getVorDemodSettings()->getIdentThreshold(); + } + if (channelSettingsKeys.contains("magDecAdjust")) { + settings.m_magDecAdjust = response.getVorDemodSettings()->getMagDecAdjust() != 0; + } +} + +int VORDemod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setVorDemodReport(new SWGSDRangel::SWGVORDemodReport()); + response.getVorDemodReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +void VORDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const VORDemodSettings& settings) +{ + response.getVorDemodSettings()->setAudioMute(settings.m_audioMute ? 1 : 0); + response.getVorDemodSettings()->setRgbColor(settings.m_rgbColor); + response.getVorDemodSettings()->setSquelch(settings.m_squelch); + response.getVorDemodSettings()->setVolume(settings.m_volume); + + if (response.getVorDemodSettings()->getTitle()) { + *response.getVorDemodSettings()->getTitle() = settings.m_title; + } else { + response.getVorDemodSettings()->setTitle(new QString(settings.m_title)); + } + + if (response.getVorDemodSettings()->getAudioDeviceName()) { + *response.getVorDemodSettings()->getAudioDeviceName() = settings.m_audioDeviceName; + } else { + response.getVorDemodSettings()->setAudioDeviceName(new QString(settings.m_audioDeviceName)); + } + + response.getVorDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getVorDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getVorDemodSettings()->getReverseApiAddress()) { + *response.getVorDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getVorDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getVorDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getVorDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getVorDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + response.getVorDemodSettings()->setIdentThreshold(settings.m_identThreshold); + response.getVorDemodSettings()->setMagDecAdjust(settings.m_magDecAdjust ? 1 : 0); +} + +void VORDemod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + + response.getVorDemodReport()->setChannelPowerDb(CalcDb::dbPower(magsqAvg)); + response.getVorDemodReport()->setSquelch(m_basebandSink->getSquelchOpen() ? 1 : 0); + response.getVorDemodReport()->setAudioSampleRate(m_basebandSink->getAudioSampleRate()); +} + +void VORDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const VORDemodSettings& 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 VORDemod::featuresSendSettings(QList& channelSettingsKeys, const VORDemodSettings& settings, bool force) +{ + QList::iterator it = m_featuresSettingsFeedback.begin(); + MainCore *mainCore = MainCore::instance(); + + for (; it != m_featuresSettingsFeedback.end(); ++it) + { + if (mainCore->existsFeature(*it)) + { + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + Feature::MsgChannelSettings *msg = Feature::MsgChannelSettings::create( + this, + channelSettingsKeys, + swgChannelSettings, + force + ); + + (*it)->getInputMessageQueue()->push(msg); + } + else + { + m_featuresSettingsFeedback.removeOne(*it); + } + } +} + +void VORDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const VORDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("VORDemod")); + swgChannelSettings->setVorDemodSettings(new SWGSDRangel::SWGVORDemodSettings()); + SWGSDRangel::SWGVORDemodSettings *swgVORDemodSettings = swgChannelSettings->getVorDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("audioMute") || force) { + swgVORDemodSettings->setAudioMute(settings.m_audioMute ? 1 : 0); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgVORDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("squelch") || force) { + swgVORDemodSettings->setSquelch(settings.m_squelch); + } + if (channelSettingsKeys.contains("title") || force) { + swgVORDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("volume") || force) { + swgVORDemodSettings->setVolume(settings.m_volume); + } + if (channelSettingsKeys.contains("audioDeviceName") || force) { + swgVORDemodSettings->setAudioDeviceName(new QString(settings.m_audioDeviceName)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgVORDemodSettings->setStreamIndex(settings.m_streamIndex); + } + if (channelSettingsKeys.contains("identThreshold") || force) { + swgVORDemodSettings->setAudioMute(settings.m_identThreshold); + } + if (channelSettingsKeys.contains("magDecAdjust") || force) { + swgVORDemodSettings->setAudioMute(settings.m_magDecAdjust ? 1 : 0); + } +} + +void VORDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "VORDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("VORDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/channelrx/demodvor/vordemod.h b/plugins/channelrx/demodvor/vordemod.h new file mode 100644 index 000000000..0246296a5 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemod.h @@ -0,0 +1,160 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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_VORDEMOD_H +#define INCLUDE_VORDEMOD_H + +#include + +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "vordemodbaseband.h" +#include "vordemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; + +class VORDemod : public BasebandSampleSink, public ChannelAPI { + Q_OBJECT +public: + class MsgConfigureVORDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const VORDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureVORDemod* create(const VORDemodSettings& settings, bool force) + { + return new MsgConfigureVORDemod(settings, force); + } + + private: + VORDemodSettings m_settings; + bool m_force; + + MsgConfigureVORDemod(const VORDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + VORDemod(DeviceAPI *deviceAPI); + virtual ~VORDemod(); + virtual void destroy() { delete this; } + + 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 bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return 0; } + + 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 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 VORDemodSettings& settings); + + static void webapiUpdateChannelSettings( + VORDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + uint32_t getAudioSampleRate() const { return m_basebandSink->getAudioSampleRate(); } + double getMagSq() const { return m_basebandSink->getMagSq(); } + bool getSquelchOpen() const { return m_basebandSink->getSquelchOpen(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } + void setMessageQueueToGUI(MessageQueue* queue) override { + BasebandSampleSink::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; + VORDemodBaseband* m_basebandSink; + VORDemodSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const VORDemodSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const VORDemodSettings& settings, bool force); + void featuresSendSettings(QList& channelSettingsKeys, const VORDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const VORDemodSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + +}; + +#endif // INCLUDE_VORDEMOD_H diff --git a/plugins/channelrx/demodvor/vordemodbaseband.cpp b/plugins/channelrx/demodvor/vordemodbaseband.cpp new file mode 100644 index 000000000..41911eb34 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodbaseband.cpp @@ -0,0 +1,282 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 "vordemodbaseband.h" +#include "vordemodreport.h" + +MESSAGE_CLASS_DEFINITION(VORDemodBaseband::MsgConfigureVORDemodBaseband, Message) + +VORDemodBaseband::VORDemodBaseband() : + m_running(false), + m_mutex(QMutex::Recursive), + m_messageQueueToGUI(nullptr), + m_basebandSampleRate(0) +{ + qDebug("VORDemodBaseband::VORDemodBaseband"); + + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + + // FIXME: If we remove this audio stops working when this demod is closed + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(&m_audioFifoBug, getInputMessageQueue()); +} + +VORDemodBaseband::~VORDemodBaseband() +{ + m_inputMessageQueue.clear(); + + for (int i = 0; i < m_sinks.size(); i++) + { + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_sinks[i]->getAudioFifo()); + delete m_sinks[i]; + } + m_sinks.clear(); + + // FIXME: If we remove this audio stops working when this demod is closed + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(&m_audioFifoBug); + + for (int i = 0; i < m_channelizers.size(); i++) + delete m_channelizers[i]; + m_channelizers.clear(); +} + +void VORDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void VORDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &VORDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void VORDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &VORDemodBaseband::handleData + ); + m_running = false; +} + +void VORDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void VORDemodBaseband::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) { + for (int i = 0; i < m_channelizers.size(); i++) + m_channelizers[i]->feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + for (int i = 0; i < m_channelizers.size(); i++) + m_channelizers[i]->feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void VORDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool VORDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureVORDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureVORDemodBaseband& cfg = (MsgConfigureVORDemodBaseband&) cmd; + qDebug() << "VORDemodBaseband::handleMessage: MsgConfigureVORDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "VORDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate() << " centerFrequency: " << notif.getCenterFrequency(); + m_centerFrequency = notif.getCenterFrequency(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(m_basebandSampleRate)); + + return true; + } + else + { + return false; + } +} + +// Calculate offset of VOR center frequency from sample source center frequency +void VORDemodBaseband::calculateOffset(VORDemodSink *sink) +{ + int frequencyOffset = sink->m_vorFrequencyHz - m_centerFrequency; + bool outOfBand = std::abs(frequencyOffset)+VORDEMOD_CHANNEL_BANDWIDTH > (m_basebandSampleRate/2); + + if (m_messageQueueToGUI != nullptr) + { + VORDemodReport::MsgReportFreqOffset *msg = VORDemodReport::MsgReportFreqOffset::create(sink->m_subChannelId, frequencyOffset, outOfBand); + m_messageQueueToGUI->push(msg); + } + + sink->m_frequencyOffset = frequencyOffset; + sink->m_outOfBand = outOfBand; +} + +void VORDemodBaseband::applySettings(const VORDemodSettings& settings, bool force) +{ + // Remove sub-channels no longer needed + for (int i = 0; i < m_sinks.size(); i++) + { + if (!settings.m_subChannelSettings.contains(m_sinks[i]->m_subChannelId)) + { + qDebug() << "VORDemodBaseband::applySettings: Removing sink " << m_sinks[i]->m_subChannelId; + VORDemodSink *sink = m_sinks[i]; + DSPEngine::instance()->getAudioDeviceManager()->removeAudioSink(m_sinks[i]->getAudioFifo()); + m_sinks.removeAt(i); + delete sink; + DownChannelizer *channelizer = m_channelizers[i]; + m_channelizers.removeAt(i); + delete channelizer; + } + } + + // Add new sub channels + QHash::const_iterator itr = settings.m_subChannelSettings.begin(); + while (itr != settings.m_subChannelSettings.end()) + { + VORDemodSubChannelSettings *subChannelSettings = itr.value(); + int j; + for (j = 0; j < m_sinks.size(); j++) + { + if (subChannelSettings->m_id == m_sinks[j]->m_subChannelId) + break; + } + if (j == m_sinks.size()) + { + // Add a sub-channel sink + qDebug() << "VORDemodBaseband::applySettings: Adding sink " << subChannelSettings->m_id; + VORDemodSink *sink = new VORDemodSink(settings, subChannelSettings->m_id, m_messageQueueToGUI); + DownChannelizer *channelizer = new DownChannelizer(sink); + channelizer->setBasebandSampleRate(m_basebandSampleRate); + DSPEngine::instance()->getAudioDeviceManager()->addAudioSink(sink->getAudioFifo(), getInputMessageQueue()); + sink->applyAudioSampleRate(DSPEngine::instance()->getAudioDeviceManager()->getOutputSampleRate()); + + m_sinks.append(sink); + m_channelizers.append(channelizer); + + calculateOffset(sink); + + channelizer->setChannelization(VORDEMOD_CHANNEL_SAMPLE_RATE, sink->m_frequencyOffset); + sink->applyChannelSettings(channelizer->getChannelSampleRate(), channelizer->getChannelFrequencyOffset(), true); + sink->applyAudioSampleRate(sink->getAudioSampleRate()); + } + ++itr; + } + + if (force) + { + for (int i = 0; i < m_sinks.size(); i++) + { + m_channelizers[i]->setChannelization(VORDEMOD_CHANNEL_SAMPLE_RATE, m_sinks[i]->m_frequencyOffset); + m_sinks[i]->applyChannelSettings(m_channelizers[i]->getChannelSampleRate(), m_channelizers[i]->getChannelFrequencyOffset()); + m_sinks[i]->applyAudioSampleRate(m_sinks[i]->getAudioSampleRate()); // reapply in case of channel sample rate change + } + } + + if ((settings.m_audioDeviceName != m_settings.m_audioDeviceName) || force) + { + AudioDeviceManager *audioDeviceManager = DSPEngine::instance()->getAudioDeviceManager(); + int audioDeviceIndex = audioDeviceManager->getOutputDeviceIndex(settings.m_audioDeviceName); + for (int i = 0; i < m_sinks.size(); i++) + { + audioDeviceManager->removeAudioSink(m_sinks[i]->getAudioFifo()); + audioDeviceManager->addAudioSink(m_sinks[i]->getAudioFifo(), getInputMessageQueue(), audioDeviceIndex); + int audioSampleRate = audioDeviceManager->getOutputSampleRate(audioDeviceIndex); + + if (m_sinks[i]->getAudioSampleRate() != audioSampleRate) + { + m_sinks[i]->applyAudioSampleRate(audioSampleRate); + } + } + } + + for (int i = 0; i < m_sinks.size(); i++) + m_sinks[i]->applySettings(settings, force); + + m_settings = settings; +} + +void VORDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_basebandSampleRate = sampleRate; + for (int i = 0; i < m_sinks.size(); i++) + { + m_channelizers[i]->setBasebandSampleRate(sampleRate); + calculateOffset(m_sinks[i]); + m_sinks[i]->applyChannelSettings(m_channelizers[i]->getChannelSampleRate(), m_channelizers[i]->getChannelFrequencyOffset()); + m_sinks[i]->applyAudioSampleRate(m_sinks[i]->getAudioSampleRate()); // reapply in case of channel sample rate change + } +} diff --git a/plugins/channelrx/demodvor/vordemodbaseband.h b/plugins/channelrx/demodvor/vordemodbaseband.h new file mode 100644 index 000000000..72323f8bd --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodbaseband.h @@ -0,0 +1,136 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_VORDEMODBASEBAND_H +#define INCLUDE_VORDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "vordemodsink.h" + +class DownChannelizer; + +class VORDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureVORDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const VORDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureVORDemodBaseband* create(const VORDemodSettings& settings, bool force) + { + return new MsgConfigureVORDemodBaseband(settings, force); + } + + private: + VORDemodSettings m_settings; + bool m_force; + + MsgConfigureVORDemodBaseband(const VORDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + VORDemodBaseband(); + ~VORDemodBaseband(); + 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) { + avg = 0.0; + peak = 0.0; + nbSamples = 0; + for (int i = 0; i < m_sinks.size(); i++) + { + double avg1, peak1; + int nbSamples1; + m_sinks[i]->getMagSqLevels(avg1, peak1, nbSamples1); + if (avg1 > avg) + { + avg = avg1; + nbSamples = nbSamples1; + } + avg += avg1; + if (peak1 > peak) + peak = peak1; + } + } + void setMessageQueueToGUI(MessageQueue *messageQueue) { + m_messageQueueToGUI = messageQueue; + for (int i = 0; i < m_sinks.size(); i++) + m_sinks[i]->setMessageQueueToGUI(messageQueue); + } + bool getSquelchOpen() const { + for (int i = 0; i < m_sinks.size(); i++) + { + if (m_sinks[i]->getSquelchOpen()) + return true; + } + return false; + } + int getAudioSampleRate() const { + if (m_sinks.size() > 0) + return m_sinks[0]->getAudioSampleRate(); + else + return 48000; + } + void setBasebandSampleRate(int sampleRate); + double getMagSq() const { + if (m_sinks.size() > 0) + return m_sinks[0]->getMagSq(); + else + return 0.0; + } + bool isRunning() const { return m_running; } + +private: + SampleSinkFifo m_sampleFifo; + QList m_channelizers; + QList m_sinks; + AudioFifo m_audioFifoBug; // FIXME: Removing this results in audio stopping when demod is closed + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + VORDemodSettings m_settings; + bool m_running; + QMutex m_mutex; + MessageQueue *m_messageQueueToGUI; + int m_basebandSampleRate; + int m_centerFrequency; + + bool handleMessage(const Message& cmd); + void calculateOffset(VORDemodSink *sink); + void applySettings(const VORDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_VORDEMODBASEBAND_H diff --git a/plugins/channelrx/demodvor/vordemodgui.cpp b/plugins/channelrx/demodvor/vordemodgui.cpp new file mode 100644 index 000000000..0bd14eff3 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodgui.cpp @@ -0,0 +1,1420 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 "vordemodgui.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_vordemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "util/morse.h" +#include "util/units.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/dspengine.h" +#include "gui/crightclickenabler.h" +#include "gui/audioselectdialog.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" + +#include "vordemod.h" +#include "vordemodreport.h" +#include "vordemodsink.h" + +#define VOR_COL_NAME 0 +#define VOR_COL_FREQUENCY 1 +#define VOR_COL_OFFSET 2 +#define VOR_COL_IDENT 3 +#define VOR_COL_MORSE 4 +#define VOR_COL_RX_IDENT 5 +#define VOR_COL_RX_MORSE 6 +#define VOR_COL_RADIAL 7 +#define VOR_COL_REF_MAG 8 +#define VOR_COL_VAR_MAG 9 +#define VOR_COL_MUTE 10 + +static const char *countryCodes[] = { + "ad", + "ae", + "af", + "ag", + "ai", + "al", + "am", + "an", + "ao", + "aq", + "ar", + "as", + "at", + "au", + "aw", + "ax", + "az", + "ba", + "bb", + "bd", + "be", + "bf", + "bg", + "bh", + "bi", + "bj", + "bl", + "bm", + "bn", + "bo", + "bq", + "br", + "bs", + "bt", + "bv", + "bw", + "by", + "bz", + "ca", + "cc", + "cd", + "cf", + "cg", + "ch", + "ci", + "ck", + "cl", + "cm", + "cn", + "co", + "cr", + "cu", + "cv", + "cw", + "cx", + "cy", + "cz", + "de", + "dj", + "dk", + "dm", + "do", + "dz", + "ec", + "ee", + "eg", + "eh", + "er", + "es", + "et", + "fi", + "fj", + "fk", + "fm", + "fo", + "fr", + "ga", + "gb", + "ge", + "gf", + "gg", + "gh", + "gi", + "gl", + "gm", + "gn", + "gp", + "gq", + "gr", + "gs", + "gt", + "gu", + "gw", + "gy", + "hk", + "hm", + "hn", + "hr", + "hu", + "id", + "ie", + "il", + "im", + "in", + "io", + "iq", + "ir", + "is", + "it", + "je", + "jm", + "jo", + "jp", + "ke", + "kg", + "kh", + "ki", + "km", + "kn", + "kp", + "kr", + "kw", + "ky", + "kz", + "la", + "lb", + "lc", + "li", + "lk", + "lr", + "ls", + "lt", + "lu", + "lv", + "ly", + "ma", + "mc", + "md", + "me", + "mf", + "mg", + "mh", + "mk", + "ml", + "mm", + "mn", + "mo", + "mp", + "mq", + "mr", + "ms", + "mt", + "mu", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nc", + "ne", + "nf", + "ng", + "ni", + "nl", + "no", + "np", + "nr", + "nu", + "nz", + "om", + "pa", + "pe", + "pf", + "pg", + "ph", + "pk", + "pl", + "pm", + "pn", + "pr", + "ps", + "pt", + "pw", + "py", + "qa", + "re", + "ro", + "rs", + "ru", + "rw", + "sa", + "sb", + "sc", + "sd", + "se", + "sg", + "sh", + "si", + "sj", + "sk", + "sl", + "sm", + "sn", + "so", + "sr", + "ss", + "st", + "sv", + "sx", + "sy", + "sz", + "tc", + "td", + "tf", + "tg", + "th", + "tj", + "tk", + "tl", + "tm", + "tn", + "to", + "tr", + "tt", + "tv", + "tw", + "tz", + "ua", + "ug", + "um", + "us", + "uy", + "uz", + "va", + "vc", + "ve", + "vg", + "vi", + "vn", + "vu", + "wf", + "ws", + "ye", + "yt", + "za", + "zm", + "zw", + nullptr +}; + +// Lats and longs in decimal degrees. Distance in metres. Bearing in degrees. +// https://www.movable-type.co.uk/scripts/latlong.html +static void calcRadialEndPoint(float startLatitude, float startLongitude, float distance, float bearing, float &endLatitude, float &endLongitude) +{ + double startLatRad = startLatitude*M_PI/180.0; + double startLongRad = startLongitude*M_PI/180.0; + double theta = bearing*M_PI/180.0; + double earthRadius = 6378137.0; // At equator + double delta = distance/earthRadius; + double endLatRad = std::asin(sin(startLatRad)*cos(delta) + cos(startLatRad)*sin(delta)*cos(theta)); + double endLongRad = startLongRad + std::atan2(sin(theta)*sin(delta)*cos(startLatRad), cos(delta) - sin(startLatRad)*sin(endLatRad)); + endLatitude = endLatRad*180.0/M_PI; + endLongitude = endLongRad*180.0/M_PI; +} + +// Calculate intersection point along two radials +// https://www.movable-type.co.uk/scripts/latlong.html +static bool calcIntersectionPoint(float lat1, float lon1, float bearing1, float lat2, float lon2, float bearing2, float &intersectLat, float &intersectLon) +{ + + double lat1Rad = Units::degreesToRadians(lat1); + double lon1Rad = Units::degreesToRadians(lon1); + double lat2Rad = Units::degreesToRadians(lat2); + double lon2Rad = Units::degreesToRadians(lon2); + double theta13 = Units::degreesToRadians(bearing1); + double theta23 = Units::degreesToRadians(bearing2); + + double deltaLat = lat1Rad - lat2Rad; + double deltaLon = lon1Rad - lon2Rad; + double sindlat = sin(deltaLat/2.0); + double sindlon = sin(deltaLon/2.0); + double cosLat1 = cos(lat1Rad); + double cosLat2 = cos(lat2Rad); + double delta12 = 2.0 * asin(sqrt(sindlat*sindlat+cosLat1*cosLat2*sindlon*sindlon)); + if (abs(delta12) < std::numeric_limits::epsilon()) + return false; + + double sinLat1 = sin(lat1Rad); + double sinLat2 = sin(lat2Rad); + double sinDelta12 = sin(delta12); + double cosDelta12 = cos(delta12); + double thetaA = acos((sinLat2-sinLat1*cosDelta12)/(sinDelta12*cosLat1)); + double thetaB = acos((sinLat1-sinLat2*cosDelta12)/(sinDelta12*cosLat2)); + + double theta12, theta21; + if (sin(lon2Rad-lon1Rad) > 0.0) + { + theta12 = thetaA; + theta21 = 2.0*M_PI-thetaB; + } + else + { + theta12 = 2.0*M_PI-thetaA; + theta21 = thetaB; + } + double alpha1 = theta13 - theta12; + double alpha2 = theta21 - theta23; + double sinAlpha1 = sin(alpha1); + double sinAlpha2 = sin(alpha2); + if ((sinAlpha1 == 0.0) && (sinAlpha2 == 0.0)) + return false; + if (sinAlpha1*sinAlpha2 < 0.0) + return false; + double cosAlpha1 = cos(alpha1); + double cosAlpha2 = cos(alpha2); + double cosAlpha3 = -cosAlpha1*cosAlpha2+sinAlpha1*sinAlpha2*cos(delta12); + double delta13 = atan2(sin(delta12)*sinAlpha1*sinAlpha2, cosAlpha2+cosAlpha1*cosAlpha3); + double lat3Rad = asin(sinLat1*cos(delta13)+cosLat1*sin(delta13)*cos(theta13)); + double lon3Rad = lon1Rad + atan2(sin(theta13)*sin(delta13)*cosLat1, cos(delta13)-sinLat1*sin(lat3Rad)); + + intersectLat = Units::radiansToDegress(lat3Rad); + intersectLon = Units::radiansToDegress(lon3Rad); + + return true; +} + +VORGUI::VORGUI(NavAid *navAid, VORDemodGUI *gui) : + m_navAid(navAid), + m_gui(gui) +{ + // These are deleted by QTableWidget + m_nameItem = new QTableWidgetItem(); + m_frequencyItem = new QTableWidgetItem(); + m_offsetItem = new QTableWidgetItem(); + m_radialItem = new QTableWidgetItem(); + m_identItem = new QTableWidgetItem(); + m_morseItem = new QTableWidgetItem(); + m_rxIdentItem = new QTableWidgetItem(); + m_rxMorseItem = new QTableWidgetItem(); + m_varMagItem = new QTableWidgetItem(); + m_refMagItem = new QTableWidgetItem(); + + m_muteItem = new QWidget(); + + m_muteButton = new QToolButton(); + m_muteButton->setCheckable(true); + m_muteButton->setChecked(false); + m_muteButton->setToolTip("Mute/unmute audio from this VOR"); + m_muteButton->setIcon(m_gui->m_muteIcon); + + QHBoxLayout* pLayout = new QHBoxLayout(m_muteItem); + pLayout->addWidget(m_muteButton); + pLayout->setAlignment(Qt::AlignCenter); + pLayout->setContentsMargins(0, 0, 0, 0); + m_muteItem->setLayout(pLayout); + + connect(m_muteButton, &QPushButton::toggled, this, &VORGUI::on_audioMute_toggled); + + m_coordinates.push_back(QVariant::fromValue(*new QGeoCoordinate(m_navAid->m_latitude, m_navAid->m_longitude, Units::feetToMetres(m_navAid->m_elevation)))); +} + +void VORGUI::on_audioMute_toggled(bool checked) +{ + m_gui->m_settings.m_subChannelSettings.value(m_navAid->m_id)->m_audioMute = checked; + m_gui->applySettings(); +} + +QVariant VORModel::data(const QModelIndex &index, int role) const +{ + int row = index.row(); + if ((row < 0) || (row >= m_vors.count())) + return QVariant(); + if (role == VORModel::positionRole) + { + // Coordinates to display the VOR icon at + QGeoCoordinate coords; + coords.setLatitude(m_vors[row]->m_latitude); + coords.setLongitude(m_vors[row]->m_longitude); + coords.setAltitude(Units::feetToMetres(m_vors[row]->m_elevation)); + return QVariant::fromValue(coords); + } + else if (role == VORModel::vorDataRole) + { + // Create the text to go in the bubble next to the VOR + QStringList list; + list.append(QString("Name: %1").arg(m_vors[row]->m_name)); + list.append(QString("Frequency: %1 MHz").arg(m_vors[row]->m_frequencykHz / 1000.0f, 0, 'f', 1)); + if (m_vors[row]->m_channel != "") + list.append(QString("Channel: %1").arg(m_vors[row]->m_channel)); + list.append(QString("Ident: %1 %2").arg(m_vors[row]->m_ident).arg(Morse::toSpacedUnicodeMorse(m_vors[row]->m_ident))); + list.append(QString("Range: %1 nm").arg(m_vors[row]->m_range)); + if (m_vors[row]->m_alignedTrueNorth) + list.append(QString("Magnetic declination: Aligned to true North")); + else if (m_vors[row]->m_magneticDeclination != 0.0f) + list.append(QString("Magnetic declination: %1%2").arg(std::round(m_vors[row]->m_magneticDeclination)).arg(QChar(0x00b0))); + QString data = list.join("\n"); + return QVariant::fromValue(data); + } + else if (role == VORModel::vorImageRole) + { + // Select an image to use for the VOR + return QVariant::fromValue(QString("/demodvor/map/%1.png").arg(m_vors[row]->m_type)); + } + else if (role == VORModel::bubbleColourRole) + { + // Select a background colour for the text bubble next to the VOR + if (m_selected[row]) + return QVariant::fromValue(QColor("lightgreen")); + else + return QVariant::fromValue(QColor("lightblue")); + } + else if (role == VORModel::vorRadialRole) + { + // Draw a radial line from centre of VOR outwards at the demodulated angle + if (m_radialsVisible && m_selected[row] && (m_vorGUIs[row] != nullptr) && (m_radials[row] != -1.0f)) + { + QVariantList list; + + list.push_back(m_vorGUIs[row]->m_coordinates[0]); // Centre of VOR + + float endLat, endLong; + float bearing; + if (m_gui->m_settings.m_magDecAdjust && !m_vors[row]->m_alignedTrueNorth) + bearing = m_radials[row] - m_vors[row]->m_magneticDeclination; + else + bearing = m_radials[row]; + calcRadialEndPoint(m_vors[row]->m_latitude, m_vors[row]->m_longitude, m_vors[row]->getRangeMetres(), bearing, endLat, endLong); + list.push_back(QVariant::fromValue(*new QGeoCoordinate(endLat, endLong, Units::feetToMetres(m_vors[row]->m_elevation)))); + + return list; + } + else + return QVariantList(); + } + else if (role == VORModel::selectedRole) + return QVariant::fromValue(m_selected[row]); + return QVariant(); +} + +bool VORModel::setData(const QModelIndex &index, const QVariant& value, int role) +{ + int row = index.row(); + if ((row < 0) || (row >= m_vors.count())) + return false; + if (role == VORModel::selectedRole) + { + bool selected = value.toBool(); + VORGUI *vorGUI; + if (selected == true) + { + vorGUI = new VORGUI(m_vors[row], m_gui); + m_vorGUIs[row] = vorGUI; + } + else + vorGUI = m_vorGUIs[row]; + m_gui->selectVOR(vorGUI, selected); + m_selected[row] = selected; + emit dataChanged(index, index); + if (!selected) + { + delete vorGUI; + m_vorGUIs[row] = nullptr; + } + return true; + } + return true; +} + +// Find intersection between first two selected radials +bool VORModel::findIntersection(float &lat, float &lon) +{ + if (m_vors.count() > 2) + { + float lat1, lon1, bearing1, valid1 = false; + float lat2, lon2, bearing2, valid2 = false; + + for (int i = 0; i < m_vors.count(); i++) + { + if (m_selected[i] && (m_radials[i] >= 0.0)) + { + if (!valid1) + { + lat1 = m_vors[i]->m_latitude; + lon1 = m_vors[i]->m_longitude; + if (m_gui->m_settings.m_magDecAdjust && !m_vors[i]->m_alignedTrueNorth) + bearing1 = m_radials[i] - m_vors[i]->m_magneticDeclination; + else + bearing1 = m_radials[i]; + valid1 = true; + } + else + { + lat2 = m_vors[i]->m_latitude; + lon2 = m_vors[i]->m_longitude; + if (m_gui->m_settings.m_magDecAdjust && !m_vors[i]->m_alignedTrueNorth) + bearing2 = m_radials[i] - m_vors[i]->m_magneticDeclination; + else + bearing2 = m_radials[i]; + valid2 = true; + break; + } + } + } + + if (valid1 && valid2) + { + return calcIntersectionPoint(lat1, lon1, bearing1, lat2, lon2, bearing2, lat, lon); + } + } + + return false; +} + +void VORDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing spaces are for sort arrow + QString morse("---- ---- ----"); + int row = ui->vorData->rowCount(); + ui->vorData->setRowCount(row + 1); + ui->vorData->setItem(row, VOR_COL_NAME, new QTableWidgetItem("White Sulphur Springs")); + ui->vorData->setItem(row, VOR_COL_FREQUENCY, new QTableWidgetItem("Freq (MHz) ")); + ui->vorData->setItem(row, VOR_COL_OFFSET, new QTableWidgetItem("Offset (kHz) ")); + ui->vorData->setItem(row, VOR_COL_IDENT, new QTableWidgetItem("Ident ")); + ui->vorData->setItem(row, VOR_COL_MORSE, new QTableWidgetItem(Morse::toSpacedUnicode(morse))); + ui->vorData->setItem(row, VOR_COL_RADIAL, new QTableWidgetItem("Radial (o) ")); + ui->vorData->setItem(row, VOR_COL_RX_IDENT, new QTableWidgetItem("RX Ident ")); + ui->vorData->setItem(row, VOR_COL_RX_MORSE, new QTableWidgetItem(Morse::toSpacedUnicode(morse))); + ui->vorData->setItem(row, VOR_COL_VAR_MAG, new QTableWidgetItem("Var (dB) ")); + ui->vorData->setItem(row, VOR_COL_REF_MAG, new QTableWidgetItem("Ref (dB) ")); + ui->vorData->setItem(row, VOR_COL_MUTE, new QTableWidgetItem("Mute")); + ui->vorData->resizeColumnsToContents(); + ui->vorData->removeRow(row); +} + +// Columns in table reordered +void VORDemodGUI::vorData_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 VORDemodGUI::vorData_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 VORDemodGUI::columnSelectMenu(QPoint pos) +{ + menu->popup(ui->vorData->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void VORDemodGUI::columnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->vorData->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *VORDemodGUI::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; +} + +// Called when a VOR is selected on the map +void VORDemodGUI::selectVOR(VORGUI *vorGUI, bool selected) +{ + if (selected) + { + m_selectedVORs.insert(vorGUI->m_navAid->m_id, vorGUI); + ui->vorData->setSortingEnabled(false); + int row = ui->vorData->rowCount(); + ui->vorData->setRowCount(row + 1); + ui->vorData->setItem(row, VOR_COL_NAME, vorGUI->m_nameItem); + ui->vorData->setItem(row, VOR_COL_FREQUENCY, vorGUI->m_frequencyItem); + ui->vorData->setItem(row, VOR_COL_OFFSET, vorGUI->m_offsetItem); + ui->vorData->setItem(row, VOR_COL_IDENT, vorGUI->m_identItem); + ui->vorData->setItem(row, VOR_COL_MORSE, vorGUI->m_morseItem); + ui->vorData->setItem(row, VOR_COL_RADIAL, vorGUI->m_radialItem); + ui->vorData->setItem(row, VOR_COL_RX_IDENT, vorGUI->m_rxIdentItem); + ui->vorData->setItem(row, VOR_COL_RX_MORSE, vorGUI->m_rxMorseItem); + ui->vorData->setItem(row, VOR_COL_VAR_MAG, vorGUI->m_varMagItem); + ui->vorData->setItem(row, VOR_COL_REF_MAG, vorGUI->m_refMagItem); + ui->vorData->setCellWidget(row, VOR_COL_MUTE, vorGUI->m_muteItem); + vorGUI->m_nameItem->setText(vorGUI->m_navAid->m_name); + vorGUI->m_identItem->setText(vorGUI->m_navAid->m_ident); + vorGUI->m_morseItem->setText(Morse::toSpacedUnicodeMorse(vorGUI->m_navAid->m_ident)); + vorGUI->m_frequencyItem->setData(Qt::DisplayRole, vorGUI->m_navAid->m_frequencykHz / 1000.0); + ui->vorData->setSortingEnabled(true); + + // Add to settings to create corresponding demodulator + VORDemodSubChannelSettings *subChannelSettings = new VORDemodSubChannelSettings(); + subChannelSettings->m_id = vorGUI->m_navAid->m_id; + subChannelSettings->m_frequency = vorGUI->m_navAid->m_frequencykHz * 1000; + subChannelSettings->m_audioMute = false; + m_settings.m_subChannelSettings.insert(vorGUI->m_navAid->m_id, subChannelSettings); + applySettings(); + } + else + { + m_selectedVORs.remove(vorGUI->m_navAid->m_id); + ui->vorData->removeRow(vorGUI->m_nameItem->row()); + // Remove from settings to remove corresponding demodulator + VORDemodSubChannelSettings *subChannelSettings = m_settings.m_subChannelSettings.value(vorGUI->m_navAid->m_id); + m_settings.m_subChannelSettings.remove(vorGUI->m_navAid->m_id); + delete subChannelSettings; + applySettings(); + } +} + +void VORDemodGUI::updateVORs() +{ + m_vorModel.removeAllVORs(); + QHash::iterator i = m_vors->begin(); + AzEl azEl = m_azEl; + + while (i != m_vors->end()) + { + NavAid *vor = i.value(); + + // Calculate distance to VOR from My Position + azEl.setTarget(vor->m_latitude, vor->m_longitude, Units::feetToMetres(vor->m_elevation)); + azEl.calculate(); + + // Only display VOR if in range + if (azEl.getDistance() <= 200000) + { + m_vorModel.addVOR(vor); + } + ++i; + } +} + +VORDemodGUI* VORDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + VORDemodGUI* gui = new VORDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void VORDemodGUI::destroy() +{ + delete this; +} + +void VORDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray VORDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool VORDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +bool VORDemodGUI::handleMessage(const Message& message) +{ + if (VORDemod::MsgConfigureVORDemod::match(message)) + { + qDebug("VORDemodGUI::handleMessage: VORDemod::MsgConfigureVORDemod"); + const VORDemod::MsgConfigureVORDemod& cfg = (VORDemod::MsgConfigureVORDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (DSPSignalNotification::match(message)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) message; + m_basebandSampleRate = notif.getSampleRate(); + return true; + } + else if (VORDemodReport::MsgReportRadial::match(message)) + { + VORDemodReport::MsgReportRadial& report = (VORDemodReport::MsgReportRadial&) message; + int subChannelId = report.getSubChannelId(); + + VORGUI *vorGUI = m_selectedVORs.value(subChannelId); + + // Display radial and signal magnitudes in table + + Real varMagDB = std::round(20.0*std::log10(report.getVarMag())); + Real refMagDB = std::round(20.0*std::log10(report.getRefMag())); + + bool validRadial = (refMagDB > m_settings.m_refThresholdDB) && (varMagDB > m_settings.m_varThresholdDB); + + vorGUI->m_radialItem->setData(Qt::DisplayRole, std::round(report.getRadial())); + if (validRadial) + vorGUI->m_radialItem->setForeground(QBrush(Qt::white)); + else + vorGUI->m_radialItem->setForeground(QBrush(Qt::red)); + + vorGUI->m_refMagItem->setData(Qt::DisplayRole, refMagDB); + if (refMagDB > m_settings.m_refThresholdDB) + vorGUI->m_refMagItem->setForeground(QBrush(Qt::white)); + else + vorGUI->m_refMagItem->setForeground(QBrush(Qt::red)); + + vorGUI->m_varMagItem->setData(Qt::DisplayRole, varMagDB); + if (varMagDB > m_settings.m_varThresholdDB) + vorGUI->m_varMagItem->setForeground(QBrush(Qt::white)); + else + vorGUI->m_varMagItem->setForeground(QBrush(Qt::red)); + + // Update radial on map + m_vorModel.setRadial(subChannelId, validRadial, report.getRadial()); + + return true; + } + else if (VORDemodReport::MsgReportFreqOffset::match(message)) + { + VORDemodReport::MsgReportFreqOffset& report = (VORDemodReport::MsgReportFreqOffset&) message; + int subChannelId = report.getSubChannelId(); + + VORGUI *vorGUI = m_selectedVORs.value(subChannelId); + + vorGUI->m_offsetItem->setData(Qt::DisplayRole, report.getFreqOffset() / 1000.0); + if (report.getOutOfBand()) + { + vorGUI->m_offsetItem->setForeground(QBrush(Qt::red)); + // Clear other fields as data is now invalid + vorGUI->m_radialItem->setText(""); + vorGUI->m_refMagItem->setText(""); + vorGUI->m_varMagItem->setText(""); + m_vorModel.setRadial(subChannelId, false, -1.0f); + } + else + vorGUI->m_offsetItem->setForeground(QBrush(Qt::white)); + } + else if (VORDemodReport::MsgReportIdent::match(message)) + { + VORDemodReport::MsgReportIdent& report = (VORDemodReport::MsgReportIdent&) message; + int subChannelId = report.getSubChannelId(); + + VORGUI *vorGUI = m_selectedVORs.value(subChannelId); + + QString ident = report.getIdent(); + // Convert Morse to a string + QString identString = Morse::toString(ident); + // Idents should only be two or three characters, so filter anything else + // other than TEST which indicates a VOR is under maintainance (may also be TST) + if (((identString.size() >= 2) && (identString.size() <= 3)) || (identString == "TEST")) + { + vorGUI->m_rxIdentItem->setText(identString); + vorGUI->m_rxMorseItem->setText(Morse::toSpacedUnicode(ident)); + if (vorGUI->m_navAid->m_ident == identString) + { + // Set colour to green if matching expected ident + vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::green)); + vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::green)); + } + else + { + // Set colour to green if not matching expected ident + vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::red)); + vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::red)); + } + } + else + { + // Set yellow to indicate we've filtered something (unless red) + if (vorGUI->m_rxIdentItem->foreground().color() != Qt::red) + { + vorGUI->m_rxIdentItem->setForeground(QBrush(Qt::yellow)); + vorGUI->m_rxMorseItem->setForeground(QBrush(Qt::yellow)); + } + } + return true; + } + + return false; +} + +void VORDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void VORDemodGUI::channelMarkerChangedByCursor() +{ +} + +void VORDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void VORDemodGUI::on_thresh_valueChanged(int value) +{ + ui->threshText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_identThreshold = value / 10.0; + applySettings(); +} + +void VORDemodGUI::on_volume_valueChanged(int value) +{ + ui->volumeText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_volume = value / 10.0; + applySettings(); +} + +void VORDemodGUI::on_squelch_valueChanged(int value) +{ + ui->squelchText->setText(QString("%1 dB").arg(value)); + m_settings.m_squelch = value; + applySettings(); +} + +void VORDemodGUI::on_audioMute_toggled(bool checked) +{ + m_settings.m_audioMute = checked; + applySettings(); +} + +qint64 VORDemodGUI::fileAgeInDays(QString filename) +{ + QFile file(filename); + if (file.exists()) + { + QDateTime modified = file.fileTime(QFileDevice::FileModificationTime); + if (modified.isValid()) + return modified.daysTo(QDateTime::currentDateTime()); + else + return -1; + } + return -1; +} + +bool VORDemodGUI::confirmDownload(QString filename) +{ + qint64 age = fileAgeInDays(filename); + if ((age == -1) || (age > 100)) + return true; + else + { + QMessageBox::StandardButton reply; + if (age == 0) + reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded today. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); + else if (age == 1) + reply = QMessageBox::question(this, "Confirm download", "This file was last downloaded yesterday. Are you sure you wish to redownload it?", QMessageBox::Yes|QMessageBox::No); + else + reply = QMessageBox::question(this, "Confirm download", QString("This file was last downloaded %1 days ago. Are you sure you wish to redownload this file?").arg(age), QMessageBox::Yes|QMessageBox::No); + return reply == QMessageBox::Yes; + } +} + +QString VORDemodGUI::getDataDir() +{ + // Get directory to store app data in + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + // First dir is writable + return locations[0]; +} + +QString VORDemodGUI::getOpenAIPVORDBFilename(int i) +{ + if (countryCodes[i] != nullptr) + return getDataDir() + "/" + countryCodes[i] + "_nav.aip"; + else + return ""; +} + +QString VORDemodGUI::getOpenAIPVORDBURL(int i) +{ + if (countryCodes[i] != nullptr) + return QString(OPENAIP_NAVAIDS_URL).arg(countryCodes[i]); + else + return ""; +} + +QString VORDemodGUI::getVORDBFilename() +{ + return getDataDir() + "/vorDatabase.csv"; +} + +void VORDemodGUI::updateDownloadProgress(qint64 bytesRead, qint64 totalBytes) +{ + m_progressDialog->setMaximum(totalBytes); + m_progressDialog->setValue(bytesRead); +} + +void VORDemodGUI::downloadFinished(const QString& filename, bool success) +{ + if (success) + { + if (filename == getVORDBFilename()) + { + m_vors = NavAid::readNavAidsDB(filename); + if (m_vors != nullptr) + updateVORs(); + m_progressDialog->close(); + m_progressDialog = nullptr; + } + else if (filename == getOpenAIPVORDBFilename(m_countryIndex)) + { + m_countryIndex++; + if (countryCodes[m_countryIndex] != nullptr) + { + QString vorDBFile = getOpenAIPVORDBFilename(m_countryIndex); + QString urlString = getOpenAIPVORDBURL(m_countryIndex); + QUrl dbURL(urlString); + m_progressDialog->setLabelText(QString("Downloading %1.").arg(urlString)); + m_progressDialog->setValue(m_countryIndex); + m_dlm.download(dbURL, vorDBFile); + } + else + { + readNavAids(); + if (m_vors != nullptr) + updateVORs(); + m_progressDialog->close(); + m_progressDialog = nullptr; + } + } + else + { + qDebug() << "VORDemodGUI::downloadFinished: Unexpected filename: " << filename; + m_progressDialog->close(); + m_progressDialog = nullptr; + } + } + else + { + qDebug() << "VORDemodGUI::downloadFinished: Failed: " << filename; + m_progressDialog->close(); + m_progressDialog = nullptr; + QMessageBox::warning(this, "Download failed", QString("Failed to download %1").arg(filename)); + } +} + +void VORDemodGUI::on_getOurAirportsVORDB_clicked(bool checked) +{ + (void) checked; + + // Don't try to download while already in progress + if (m_progressDialog == nullptr) + { + QString vorDBFile = getVORDBFilename(); + if (confirmDownload(vorDBFile)) + { + // Download OurAirports navaid database to disk + QUrl dbURL(QString(OURAIRPORTS_NAVAIDS_URL)); + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setAttribute(Qt::WA_DeleteOnClose); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setMinimumDuration(500); + m_progressDialog->setLabelText(QString("Downloading %1.").arg(OURAIRPORTS_NAVAIDS_URL)); + QNetworkReply *reply = m_dlm.download(dbURL, vorDBFile); + connect(reply, SIGNAL(downloadProgress(qint64,qint64)), this, SLOT(updateDownloadProgress(qint64,qint64))); + } + } +} + +void VORDemodGUI::on_getOpenAIPVORDB_clicked(bool checked) +{ + (void) checked; + + // Don't try to download while already in progress + if (m_progressDialog == nullptr) + { + m_countryIndex = 0; + QString vorDBFile = getOpenAIPVORDBFilename(m_countryIndex); + if (confirmDownload(vorDBFile)) + { + // Download OpenAIP XML to disk + QString urlString = getOpenAIPVORDBURL(m_countryIndex); + QUrl dbURL(urlString); + m_progressDialog = new QProgressDialog(this); + m_progressDialog->setAttribute(Qt::WA_DeleteOnClose); + m_progressDialog->setCancelButton(nullptr); + m_progressDialog->setMinimumDuration(500); + m_progressDialog->setMaximum(sizeof(countryCodes)/sizeof(countryCodes[0])); + m_progressDialog->setValue(0); + m_progressDialog->setLabelText(QString("Downloading %1.").arg(urlString)); + m_dlm.download(dbURL, vorDBFile); + } + } +} + +void VORDemodGUI::readNavAids() +{ + m_vors = new QHash(); + for (int countryIndex = 0; countryCodes[countryIndex] != nullptr; countryIndex++) + { + QString vorDBFile = getOpenAIPVORDBFilename(countryIndex); + NavAid::readNavAidsXML(m_vors, vorDBFile); + } +} + +void VORDemodGUI::on_magDecAdjust_clicked(bool checked) +{ + m_settings.m_magDecAdjust = checked; + m_vorModel.allVORUpdated(); + applySettings(); +} + +void VORDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void VORDemodGUI::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.move(p); + 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); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_vorDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +VORDemodGUI::VORDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::VORDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true), + m_squelchOpen(false), + m_tickCount(0), + m_progressDialog(nullptr), + m_vorModel(this), + m_vors(nullptr) +{ + ui->setupUi(this); + + ui->map->rootContext()->setContextProperty("vorModel", &m_vorModel); + ui->map->setSource(QUrl(QStringLiteral("qrc:/demodvor/map/map.qml"))); + + m_muteIcon.addPixmap(QPixmap("://sound_off.png"), QIcon::Normal, QIcon::On); + m_muteIcon.addPixmap(QPixmap("://sound_on.png"), QIcon::Normal, QIcon::Off); + + setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(&m_dlm, &HttpDownloadManager::downloadComplete, this, &VORDemodGUI::downloadFinished); + + m_vorDemod = reinterpret_cast(rxChannel); + m_vorDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + CRightClickEnabler *audioMuteRightClickEnabler = new CRightClickEnabler(ui->audioMute); + connect(audioMuteRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(audioSelect())); + + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(2*48000); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("VOR 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_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Get station position + Real stationLatitude = MainCore::instance()->getSettings().getLatitude(); + Real stationLongitude = MainCore::instance()->getSettings().getLongitude(); + Real stationAltitude = MainCore::instance()->getSettings().getAltitude(); + m_azEl.setLocation(stationLatitude, stationLongitude, stationAltitude); + + // Centre map at My Position + QQuickItem *item = ui->map->rootObject(); + QObject *object = item->findChild("map"); + if(object != NULL) + { + QGeoCoordinate coords = object->property("center").value(); + coords.setLatitude(stationLatitude); + coords.setLongitude(stationLongitude); + object->setProperty("center", QVariant::fromValue(coords)); + } + // Move antenna icon to My Position to start with + QObject *stationObject = item->findChild("station"); + if(stationObject != NULL) + { + QGeoCoordinate coords = stationObject->property("coordinate").value(); + coords.setLatitude(stationLatitude); + coords.setLongitude(stationLongitude); + coords.setAltitude(stationAltitude); + stationObject->setProperty("coordinate", QVariant::fromValue(coords)); + stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName())); + } + + // Read in VOR information if it exists + bool useOurAirports = false; + if (useOurAirports) + { + m_vors = NavAid::readNavAidsDB(getVORDBFilename()); + ui->getOpenAIPVORDB->setVisible(false); + } + else + { + readNavAids(); + ui->getOurAirportsVORDB->setVisible(false); + } + if (m_vors != nullptr) + updateVORs(); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->vorData->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->vorData->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + menu = new QMenu(ui->vorData); + for (int i = 0; i < ui->vorData->horizontalHeader()->count(); i++) + { + QString text = ui->vorData->horizontalHeaderItem(i)->text(); + menu->addAction(createCheckableItem(text, i, true)); + } + ui->vorData->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->vorData->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(columnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->vorData->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(vorData_sectionMoved(int, int, int))); + connect(ui->vorData->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(vorData_sectionResized(int, int, int))); + + displaySettings(); + applySettings(true); +} + +VORDemodGUI::~VORDemodGUI() +{ + delete ui; +} + +void VORDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void VORDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + VORDemod::MsgConfigureVORDemod* message = VORDemod::MsgConfigureVORDemod::create( m_settings, force); + m_vorDemod->getInputMessageQueue()->push(message); + } +} + +void VORDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setBandwidth(m_basebandSampleRate > 0 ? m_basebandSampleRate : 2*48000); + 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()); + + blockApplySettings(true); + + ui->thresh->setValue(m_settings.m_identThreshold * 10.0); + ui->threshText->setText(QString("%1").arg(m_settings.m_identThreshold, 0, 'f', 1)); + + ui->volume->setValue(m_settings.m_volume * 10.0); + ui->volumeText->setText(QString("%1").arg(m_settings.m_volume, 0, 'f', 1)); + + ui->squelch->setValue(m_settings.m_squelch); + ui->squelchText->setText(QString("%1 dB").arg(m_settings.m_squelch)); + + ui->audioMute->setChecked(m_settings.m_audioMute); + + displayStreamIndex(); + + // Order and size columns + QHeaderView *header = ui->vorData->horizontalHeader(); + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + { + bool hidden = m_settings.m_columnSizes[i] == 0; + header->setSectionHidden(i, hidden); + menu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_columnSizes[i] > 0) + ui->vorData->setColumnWidth(i, m_settings.m_columnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_columnIndexes[i]); + } + + blockApplySettings(false); +} + +void VORDemodGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void VORDemodGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void VORDemodGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void VORDemodGUI::audioSelect() +{ + qDebug("VORDemodGUI::audioSelect"); + AudioSelectDialog audioSelect(DSPEngine::instance()->getAudioDeviceManager(), m_settings.m_audioDeviceName); + audioSelect.exec(); + + if (audioSelect.m_selected) + { + m_settings.m_audioDeviceName = audioSelect.m_audioDeviceName; + applySettings(); + } +} + +void VORDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_vorDemod->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)); + } + + int audioSampleRate = m_vorDemod->getAudioSampleRate(); + bool squelchOpen = m_vorDemod->getSquelchOpen(); + + if (squelchOpen != m_squelchOpen) + { + if (audioSampleRate < 0) { + ui->audioMute->setStyleSheet("QToolButton { background-color : red; }"); + } else if (squelchOpen) { + ui->audioMute->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + } + + m_squelchOpen = squelchOpen; + } + + // Try to determine position, based on intersection of two radials + if (m_tickCount % 50) + { + float lat, lon; + + if (m_vorModel.findIntersection(lat, lon)) + { + // Move antenna icon to estimated position + QQuickItem *item = ui->map->rootObject(); + QObject *stationObject = item->findChild("station"); + if(stationObject != NULL) + { + QGeoCoordinate coords = stationObject->property("coordinate").value(); + coords.setLatitude(lat); + coords.setLongitude(lon); + stationObject->setProperty("coordinate", QVariant::fromValue(coords)); + stationObject->setProperty("stationName", QVariant::fromValue(MainCore::instance()->getSettings().getStationName())); + } + } + } + + m_tickCount++; +} diff --git a/plugins/channelrx/demodvor/vordemodgui.h b/plugins/channelrx/demodvor/vordemodgui.h new file mode 100644 index 000000000..741c9a4fe --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodgui.h @@ -0,0 +1,291 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_VORDEMODGUI_H +#define INCLUDE_VORDEMODGUI_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "dsp/movingaverage.h" +#include "util/messagequeue.h" +#include "util/httpdownloadmanager.h" +#include "util/azel.h" +#include "vordemodsettings.h" +#include "navaid.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class VORDemod; +class VORDemodGUI; + +namespace Ui { + class VORDemodGUI; +} +class VORDemodGUI; + +// Table items for each VOR +class VORGUI : public QObject { + Q_OBJECT +public: + NavAid *m_navAid; + QVariantList m_coordinates; + VORDemodGUI *m_gui; + + QTableWidgetItem *m_nameItem; + QTableWidgetItem *m_frequencyItem; + QTableWidgetItem *m_offsetItem; + QTableWidgetItem *m_identItem; + QTableWidgetItem *m_morseItem; + QTableWidgetItem *m_radialItem; + QTableWidgetItem *m_rxIdentItem; + QTableWidgetItem *m_rxMorseItem; + QTableWidgetItem *m_varMagItem; + QTableWidgetItem *m_refMagItem; + QWidget *m_muteItem; + QToolButton *m_muteButton; + + VORGUI(NavAid *navAid, VORDemodGUI *gui); +private slots: + void on_audioMute_toggled(bool checked); +}; + +// VOR model used for each VOR on the map +class VORModel : public QAbstractListModel { + Q_OBJECT + +public: + using QAbstractListModel::QAbstractListModel; + enum MarkerRoles{ + positionRole = Qt::UserRole + 1, + vorDataRole = Qt::UserRole + 2, + vorImageRole = Qt::UserRole + 3, + vorRadialRole = Qt::UserRole + 4, + bubbleColourRole = Qt::UserRole + 5, + selectedRole = Qt::UserRole + 6 + }; + + VORModel(VORDemodGUI *gui) : + m_gui(gui), + m_radialsVisible(true) + { + } + + Q_INVOKABLE void addVOR(NavAid *vor) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_vors.append(vor); + m_selected.append(false); + m_radials.append(-1.0f); + m_vorGUIs.append(nullptr); + endInsertRows(); + } + + int rowCount(const QModelIndex &parent = QModelIndex()) const override { + Q_UNUSED(parent) + return m_vors.count(); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + bool setData(const QModelIndex &index, const QVariant& value, int role = Qt::EditRole) override; + + Qt::ItemFlags flags(const QModelIndex &index) const override { + (void) index; + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; + } + + void allVORUpdated() { + for (int i = 0; i < m_vors.count(); i++) + { + QModelIndex idx = index(i); + emit dataChanged(idx, idx); + } + } + + void removeVOR(NavAid *vor) { + int row = m_vors.indexOf(vor); + if (row >= 0) + { + beginRemoveRows(QModelIndex(), row, row); + m_vors.removeAt(row); + m_selected.removeAt(row); + m_radials.removeAt(row); + m_vorGUIs.removeAt(row); + endRemoveRows(); + } + } + + void removeAllVORs() { + beginRemoveRows(QModelIndex(), 0, m_vors.count()); + m_vors.clear(); + m_selected.clear(); + m_radials.clear(); + m_vorGUIs.clear(); + endRemoveRows(); + } + + QHash roleNames() const { + QHash roles; + roles[positionRole] = "position"; + roles[vorDataRole] = "vorData"; + roles[vorImageRole] = "vorImage"; + roles[vorRadialRole] = "vorRadial"; + roles[bubbleColourRole] = "bubbleColour"; + roles[selectedRole] = "selected"; + return roles; + } + + void setRadialsVisible(bool radialsVisible) + { + m_radialsVisible = radialsVisible; + allVORUpdated(); + } + + void setRadial(int id, bool valid, Real radial) + { + for (int i = 0; i < m_vors.count(); i++) + { + if (m_vors[i]->m_id == id) + { + if (valid) + m_radials[i] = radial; + else + m_radials[i] = -1; // -1 to indicate invalid + QModelIndex idx = index(i); + emit dataChanged(idx, idx); + break; + } + } + } + + bool findIntersection(float &lat, float &lon); + +private: + VORDemodGUI *m_gui; + bool m_radialsVisible; + QList m_vors; + QList m_selected; + QList m_radials; + QList m_vorGUIs; +}; + +class VORDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static VORDemodGUI* 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; } + void selectVOR(VORGUI *vorGUI, bool selected); + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + friend class VORGUI; + friend class VORModel; + + Ui::VORDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + VORDemodSettings m_settings; + bool m_doApplySettings; + + VORDemod* m_vorDemod; + bool m_squelchOpen; + int m_basebandSampleRate; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QMenu *menu; // Column select context menu + HttpDownloadManager m_dlm; + QProgressDialog *m_progressDialog; + int m_countryIndex; + VORModel m_vorModel; + QHash *m_vors; + QHash m_selectedVORs; + AzEl m_azEl; // Position of station + QIcon m_muteIcon; + + explicit VORDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~VORDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked); + + void calculateFreqOffset(VORGUI *vorGUI); + void calculateFreqOffsets(); + void updateVORs(); + QString getOpenAIPVORDBURL(int i); + QString getOpenAIPVORDBFilename(int i); + QString getVORDBFilename(); + void readNavAids(); + // Move to util + QString getDataDir(); + qint64 fileAgeInDays(QString filename); + bool confirmDownload(QString filename); + +private slots: + void on_thresh_valueChanged(int value); + void on_volume_valueChanged(int value); + void on_squelch_valueChanged(int value); + void on_audioMute_toggled(bool checked); + void on_getOurAirportsVORDB_clicked(bool checked = false); + void on_getOpenAIPVORDB_clicked(bool checked = false); + void on_magDecAdjust_clicked(bool checked = false); + void vorData_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void vorData_sectionResized(int logicalIndex, int oldSize, int newSize); + void columnSelectMenu(QPoint pos); + void columnSelectMenuChecked(bool checked = false); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void updateDownloadProgress(qint64 bytesRead, qint64 totalBytes); + void downloadFinished(const QString& filename, bool success); + void handleInputMessages(); + void audioSelect(); + void tick(); +}; + +#endif // INCLUDE_VORDEMODGUI_H diff --git a/plugins/channelrx/demodvor/vordemodgui.ui b/plugins/channelrx/demodvor/vordemodgui.ui new file mode 100644 index 000000000..e9b4c4487 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodgui.ui @@ -0,0 +1,617 @@ + + + VORDemodGUI + + + + 0 + 0 + 398 + 893 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + VOR Demodulator + + + VOR Demodulator + + + + + 0 + 0 + 390 + 61 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + Qt::Vertical + + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + Left: Mute/Unmute audio Right: view/select audio device + + + ... + + + + :/sound_on.png + :/sound_off.png:/sound_on.png + + + true + + + + + + + + + + + true + + + Download OurAirports VOR database + + + + + + + :/demodvor/icons/vor.png:/demodvor/icons/vor.png + + + + + + + Download OpenAIP VOR database + + + + + + + :/demodvor/icons/vor.png:/demodvor/icons/vor.png + + + + + + + Draw radials adjusted for magnetic declination + + + + + + + :/demodvor/icons/compass.png:/demodvor/icons/compass.png + + + true + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 20 + 0 + + + + MT + + + + + + + + 24 + 24 + + + + Morse code ident threshold (Linear SNR) + + + 0 + + + 100 + + + 0 + + + + + + + + 0 + 0 + + + + + 25 + 0 + + + + 10.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Sq + + + + + + + + 24 + 24 + + + + Audio squelch threshold (dB) + + + -100 + + + 0 + + + -40 + + + + + + + + 40 + 0 + + + + -100dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Vol + + + + + + + + 24 + 24 + + + + Audio volume + + + 100 + + + + + + + + 25 + 0 + + + + 10.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 110 + 391 + 140 + + + + + 0 + 0 + + + + VORs + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + + Name + + + Name of the VOR + + + + + Freq (MHz) + + + Frequency of the VOR in MHz + + + + + Offset (kHz) + + + Offset of the VOR's frequency from the current center frequency. Red indicates out of range. + + + + + Ident + + + Ident for the VOR + + + + + Morse + + + Morse code ident for the VOR + + + + + RX Ident + + + Received ident + + + + + RX Morse + + + Received Morse code ident + + + + + Radial (°) + + + Calculated radial from the VOR + + + + + Ref (dB) + + + Magnitude of received reference signal in dB + + + + + Var (dB) + + + Magnitude of received variable signal in dB + + + + + Mute + + + Mute/unmute audio from selected VORs + + + + + + + + + + 0 + 260 + 391 + 581 + + + + + 0 + 0 + + + + Map + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 0 + 0 + + + + + 100 + 500 + + + + VOR map + + + QQuickWidget::SizeRootObjectToView + + + + + + + + + + + + + + QQuickWidget + QWidget +
QtQuickWidgets/QQuickWidget
+
+ + RollupWidget + QWidget +
gui/rollupwidget.h
+ 1 +
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+
+ + audioMute + getOurAirportsVORDB + vorData + map + + + + + + + +
diff --git a/plugins/channelrx/demodvor/vordemodplugin.cpp b/plugins/channelrx/demodvor/vordemodplugin.cpp new file mode 100644 index 000000000..c7382e118 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 "vordemodgui.h" +#endif +#include "vordemod.h" +#include "vordemodwebapiadapter.h" +#include "vordemodplugin.h" + +const PluginDescriptor VORDemodPlugin::m_pluginDescriptor = { + VORDemod::m_channelId, + QStringLiteral("VOR Demodulator"), + QStringLiteral("6.1.0"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +VORDemodPlugin::VORDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& VORDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void VORDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(VORDemod::m_channelIdURI, VORDemod::m_channelId, this); +} + +void VORDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + VORDemod *instance = new VORDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* VORDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* VORDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return VORDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* VORDemodPlugin::createChannelWebAPIAdapter() const +{ + return new VORDemodWebAPIAdapter(); +} diff --git a/plugins/channelrx/demodvor/vordemodplugin.h b/plugins/channelrx/demodvor/vordemodplugin.h new file mode 100644 index 000000000..a43c13c99 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_VORDEMODPLUGIN_H +#define INCLUDE_VORDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class VORDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.vordemod") + +public: + explicit VORDemodPlugin(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_VORDEMODPLUGIN_H diff --git a/plugins/channelrx/demodvor/vordemodreport.cpp b/plugins/channelrx/demodvor/vordemodreport.cpp new file mode 100644 index 000000000..6ac03f99a --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodreport.cpp @@ -0,0 +1,22 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "vordemodreport.h" + +MESSAGE_CLASS_DEFINITION(VORDemodReport::MsgReportFreqOffset, Message) +MESSAGE_CLASS_DEFINITION(VORDemodReport::MsgReportRadial, Message) +MESSAGE_CLASS_DEFINITION(VORDemodReport::MsgReportIdent, Message) diff --git a/plugins/channelrx/demodvor/vordemodreport.h b/plugins/channelrx/demodvor/vordemodreport.h new file mode 100644 index 000000000..88a067d4e --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodreport.h @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_VORDEMODREPORT_H +#define INCLUDE_VORDEMODREPORT_H + +#include + +#include "util/message.h" + +class VORDemodReport : public QObject +{ + Q_OBJECT +public: + class MsgReportRadial : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSubChannelId() const { return m_subChannelId; } + float getRadial() const { return m_radial; } + float getRefMag() const { return m_refMag; } + float getVarMag() const { return m_varMag; } + + static MsgReportRadial* create(int subChannelId, float radial, float refMag, float varMag) + { + return new MsgReportRadial(subChannelId, radial, refMag, varMag); + } + + private: + int m_subChannelId; + float m_radial; + float m_refMag; + float m_varMag; + + MsgReportRadial(int subChannelId, float radial, float refMag, float varMag) : + Message(), + m_subChannelId(subChannelId), + m_radial(radial), + m_refMag(refMag), + m_varMag(varMag) + { + } + }; + + class MsgReportFreqOffset : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSubChannelId() const { return m_subChannelId; } + int getFreqOffset() const { return m_freqOffset; } + bool getOutOfBand() const { return m_outOfBand; } + + static MsgReportFreqOffset* create(int subChannelId, int freqOffset, bool outOfBand) + { + return new MsgReportFreqOffset(subChannelId, freqOffset, outOfBand); + } + + private: + int m_subChannelId; + int m_freqOffset; + bool m_outOfBand; + + MsgReportFreqOffset(int subChannelId, int freqOffset, bool outOfBand) : + Message(), + m_subChannelId(subChannelId), + m_freqOffset(freqOffset), + m_outOfBand(outOfBand) + { + } + }; + + class MsgReportIdent : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getSubChannelId() const { return m_subChannelId; } + QString getIdent() const { return m_ident; } + + static MsgReportIdent* create(int subChannelId, QString ident) + { + return new MsgReportIdent(subChannelId, ident); + } + + private: + int m_subChannelId; + QString m_ident; + + MsgReportIdent(int subChannelId, QString ident) : + Message(), + m_subChannelId(subChannelId), + m_ident(ident) + { + } + }; + +public: + VORDemodReport() {} + ~VORDemodReport() {} +}; + +#endif // INCLUDE_VORDEMODREPORT_H diff --git a/plugins/channelrx/demodvor/vordemodsettings.cpp b/plugins/channelrx/demodvor/vordemodsettings.cpp new file mode 100644 index 000000000..e0c7e35ab --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodsettings.cpp @@ -0,0 +1,157 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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 "vordemodsettings.h" + +VORDemodSettings::VORDemodSettings() : + m_channelMarker(0) +{ + resetToDefaults(); +} + +void VORDemodSettings::resetToDefaults() +{ + m_squelch = -60.0; + m_volume = 2.0; + m_audioMute = false; + m_rgbColor = QColor(255, 255, 0).rgb(); + m_title = "VOR Demodulator"; + m_audioDeviceName = AudioDeviceManager::m_defaultDeviceName; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + + m_identThreshold = 2.0; + m_refThresholdDB = -45.0; + m_varThresholdDB = -90.0; + m_magDecAdjust = true; + + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + { + m_columnIndexes[i] = i; + m_columnSizes[i] = -1; // Autosize + } +} + +QByteArray VORDemodSettings::serialize() const +{ + SimpleSerializer s(1); + s.writeS32(3, m_streamIndex); + s.writeS32(4, m_volume*10); + s.writeS32(5, m_squelch); + + if (m_channelMarker) { + s.writeBlob(6, m_channelMarker->serialize()); + } + + s.writeU32(7, m_rgbColor); + s.writeString(9, m_title); + s.writeString(11, m_audioDeviceName); + s.writeBool(14, m_useReverseAPI); + s.writeString(15, m_reverseAPIAddress); + s.writeU32(16, m_reverseAPIPort); + s.writeU32(17, m_reverseAPIDeviceIndex); + s.writeU32(18, m_reverseAPIChannelIndex); + + s.writeReal(20, m_identThreshold); + s.writeReal(21, m_refThresholdDB); + s.writeReal(22, m_varThresholdDB); + s.writeBool(23, m_magDecAdjust); + + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + s.writeS32(100 + i, m_columnIndexes[i]); + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + s.writeS32(200 + i, m_columnSizes[i]); + + return s.final(); +} + +bool VORDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + qint32 tmp; + uint32_t utmp; + QString strtmp; + + d.readS32(3, &m_streamIndex, 0); + d.readS32(4, &tmp, 20); + m_volume = tmp * 0.1; + d.readS32(5, &tmp, -40); + m_squelch = tmp; + d.readBlob(6, &bytetmp); + + if (m_channelMarker) { + m_channelMarker->deserialize(bytetmp); + } + + d.readU32(7, &m_rgbColor); + d.readString(9, &m_title, "VOR Demodulator"); + d.readString(11, &m_audioDeviceName, AudioDeviceManager::m_defaultDeviceName); + d.readBool(14, &m_useReverseAPI, false); + d.readString(15, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(16, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(17, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(18, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + d.readReal(20, &m_identThreshold, 2.0); + d.readReal(21, &m_refThresholdDB, -45.0); + d.readReal(22, &m_varThresholdDB, -90.0); + d.readBool(23, &m_magDecAdjust, true); + + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + d.readS32(100 + i, &m_columnIndexes[i], i); + for (int i = 0; i < VORDEMOD_COLUMNS; i++) + d.readS32(200 + i, &m_columnSizes[i], -1); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + + diff --git a/plugins/channelrx/demodvor/vordemodsettings.h b/plugins/channelrx/demodvor/vordemodsettings.h new file mode 100644 index 000000000..9b74ad6fa --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodsettings.h @@ -0,0 +1,69 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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_VORDEMODSETTINGS_H +#define INCLUDE_VORDEMODSETTINGS_H + +#include +#include + +class Serializable; + +// Number of columns in the table +#define VORDEMOD_COLUMNS 11 + +struct VORDemodSubChannelSettings { + int m_id; //!< Unique VOR identifier (from database) + int m_frequency; //!< Frequency the VOR is on + bool m_audioMute; //!< Mute the audio from this VOR +}; + +struct VORDemodSettings +{ + Real m_squelch; + Real m_volume; + bool m_audioMute; + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + QString m_audioDeviceName; + 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; + + Real m_identThreshold; //!< Linear SNR threshold for Morse demodulator + Real m_refThresholdDB; //!< Threshold in dB for valid VOR reference signal + Real m_varThresholdDB; //!< Threshold in dB for valid VOR variable signal + bool m_magDecAdjust; //!< Adjust for magnetic declination when drawing radials on the map + + int m_columnIndexes[VORDEMOD_COLUMNS];//!< How the columns are ordered in the table + int m_columnSizes[VORDEMOD_COLUMNS]; //!< Size of the coumns in the table + + QHash m_subChannelSettings; + + VORDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_VORDEMODSETTINGS_H */ diff --git a/plugins/channelrx/demodvor/vordemodsink.cpp b/plugins/channelrx/demodvor/vordemodsink.cpp new file mode 100644 index 000000000..272836971 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodsink.cpp @@ -0,0 +1,446 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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 "audio/audiooutputdevice.h" +#include "dsp/dspengine.h" +#include "util/db.h" +#include "util/stepfunctions.h" +#include "util/morse.h" +#include "util/units.h" + +#include "vordemodsink.h" +#include "vordemodreport.h" + +VORDemodSink::VORDemodSink(const VORDemodSettings& settings, int subChannel, + MessageQueue *messageQueueToGUI) : + m_channelFrequencyOffset(0), + m_outOfBand(true), + m_channelSampleRate(VORDEMOD_CHANNEL_SAMPLE_RATE), + m_audioSampleRate(48000), + m_squelchCount(0), + m_squelchOpen(false), + m_squelchDelayLine(9600), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToGUI(messageQueueToGUI), + m_volumeAGC(0.003), + m_audioFifo(48000), + m_refPrev(0.0f), + m_movingAverageIdent(5000), + m_prevBit(0), + m_bitTime(0), + m_varGoertzel(30, VORDEMOD_CHANNEL_SAMPLE_RATE), + m_refGoertzel(30, VORDEMOD_CHANNEL_SAMPLE_RATE) +{ + m_audioBuffer.resize(1<<14); + m_audioBufferFill = 0; + + m_magsq = 0.0; + + qDebug() << "Sink " << subChannel; + if (subChannel >= 0) + { + m_subChannelId = subChannel; + m_vorFrequencyHz = settings.m_subChannelSettings[subChannel]->m_frequency; + + applySettings(settings, true); + } +} + +VORDemodSink::~VORDemodSink() +{ +} + +void VORDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + if (m_outOfBand) + return; + + 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; + } + } + } + + if (m_audioBufferFill > 0) + { + uint res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + + if (res != m_audioBufferFill) { + qDebug("VORDemodSink::feed: %u/%u tail samples written", res, m_audioBufferFill); + } + + m_audioBufferFill = 0; + } +} + +void VORDemodSink::processOneAudioSample(Complex &ci) +{ + Real re = ci.real() / SDR_RX_SCALEF; + Real im = ci.imag() / SDR_RX_SCALEF; + Real magsq = re*re + im*im; + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + + m_magsqCount++; + + m_squelchDelayLine.write(magsq); + + if (m_magsq < m_squelchLevel) + { + if (m_squelchCount > 0) { + m_squelchCount--; + } + } + else + { + if (m_squelchCount < (unsigned int)m_audioSampleRate / 10) { + m_squelchCount++; + } + } + + qint16 sample; + + m_squelchOpen = (m_squelchCount >= (unsigned int)m_audioSampleRate / 20); + + if (m_squelchOpen && !m_settings.m_audioMute && !m_settings.m_subChannelSettings.value(m_subChannelId)->m_audioMute) + { + Real demod; + + { + demod = sqrt(m_squelchDelayLine.readBack(m_audioSampleRate/20)); + m_volumeAGC.feed(demod); + demod = (demod - m_volumeAGC.getValue()) / m_volumeAGC.getValue(); + } + + demod = m_bandpass.filter(demod); + + Real attack = (m_squelchCount - 0.05f * m_audioSampleRate) / (0.05f * m_audioSampleRate); + sample = demod * StepFunctions::smootherstep(attack) * (m_audioSampleRate/24) * m_settings.m_volume; + } + else + { + sample = 0; + } + + m_audioBuffer[m_audioBufferFill].l = sample; + m_audioBuffer[m_audioBufferFill].r = sample; + ++m_audioBufferFill; + + if (m_audioBufferFill >= m_audioBuffer.size()) + { + uint res = m_audioFifo.write((const quint8*)&m_audioBuffer[0], m_audioBufferFill); + + if (res != m_audioBufferFill) + { + qDebug("VORDemodSink::processOneAudioSample: %u/%u audio samples written", res, m_audioBufferFill); + m_audioFifo.clear(); + } + + m_audioBufferFill = 0; + } +} + + +void VORDemodSink::processOneSample(Complex &ci) +{ + Complex ca; + + // Resample as audio + if (m_audioInterpolatorDistance < 1.0f) // interpolate + { + while (!m_audioInterpolator.interpolate(&m_audioInterpolatorDistanceRemain, ci, &ca)) + { + processOneAudioSample(ca); + m_audioInterpolatorDistanceRemain += m_audioInterpolatorDistance; + } + } + else // decimate + { + if (m_audioInterpolator.decimate(&m_audioInterpolatorDistanceRemain, ci, &ca)) + { + processOneAudioSample(ca); + m_audioInterpolatorDistanceRemain += m_audioInterpolatorDistance; + } + } + + Real re = ci.real() / SDR_RX_SCALEF; + Real im = ci.imag() / SDR_RX_SCALEF; + Real magsq = re*re + im*im; + + // AM Demod + Real mag = std::sqrt(magsq); + + // Calculate phase of 30Hz variable AM signal + double varPhase; + double varMag; + if (m_varGoertzel.size() == VORDEMOD_CHANNEL_SAMPLE_RATE - 1) + { + m_varGoertzel.goertzel(mag); + varPhase = Units::radiansToDegress(m_varGoertzel.phase()); + varMag = m_varGoertzel.mag(); + m_varGoertzel.reset(); + } + else + m_varGoertzel.filter(mag); + + Complex magc(mag, 0.0); + + // Mix reference sub-carrier down to 0Hz + Complex fm0 = magc; + fm0 *= m_ncoRef.nextIQ(); + // Filter other signals + Complex fmfilt = m_lowpassRef.filter(fm0); + + // FM demod + Real phi = std::arg(std::conj(m_refPrev) * fmfilt); + m_refPrev = fmfilt; + + // Calculate phase of 30Hz reference FM signal + if (m_refGoertzel.size() == VORDEMOD_CHANNEL_SAMPLE_RATE - 1) + { + m_refGoertzel.goertzel(phi); + float phaseDeg = Units::radiansToDegress(m_refGoertzel.phase()); + double refMag = m_refGoertzel.mag(); + int groupDelay = (301-1)/2; + float filterPhaseShift = 360.0*30.0*groupDelay/VORDEMOD_CHANNEL_SAMPLE_RATE; + float shiftedPhase = phaseDeg + filterPhaseShift; + + // Calculate difference in phase, which is the radial + float phaseDifference = shiftedPhase - varPhase; + if (phaseDifference < 0.0) + phaseDifference += 360.0; + else if (phaseDifference >= 360.0) + phaseDifference -= 360.0; + + // qDebug() << "Ref phase: " << phaseDeg << " var phase " << varPhase; + + if (getMessageQueueToGUI()) + { + VORDemodReport::MsgReportRadial *msg = VORDemodReport::MsgReportRadial::create(m_subChannelId, phaseDifference, refMag, varMag); + getMessageQueueToGUI()->push(msg); + } + + m_refGoertzel.reset(); + } + else + m_refGoertzel.filter(phi); + + // Ident demod + // Remove ident sub-carrier offset + Complex c1 = magc; + c1 *= m_ncoIdent.nextIQ(); + // Filter other signals + Complex c2 = std::abs(m_lowpassIdent.filter(c1)); + + // Filter noise with moving average (moving average preserves edges) + m_movingAverageIdent(c2.real()); + Real mav = m_movingAverageIdent.asFloat(); + + // Caclulate noise floor + if (mav > m_identMaxs[m_binCnt]) + m_identMaxs[m_binCnt] = mav; + m_binSampleCnt++; + if (m_binSampleCnt >= m_samplesPerDot10wpm/2) + { + // Calc minimum of maximums + m_identNoise = 1.0f; + for (int i = 0; i < m_identBins; i++) + { + m_identNoise = std::min(m_identNoise, m_identMaxs[i]); + } + m_binSampleCnt = 0; + m_binCnt++; + if (m_binCnt == m_identBins) + m_binCnt = 0; + m_identMaxs[m_binCnt] = 0.0f; + + // Prevent divide by zero + if (m_identNoise == 0.0f) + m_identNoise = 1e-20f; + } + + // CW demod + int bit = (mav / m_identNoise) >= m_settings.m_identThreshold; + if ((m_prevBit == 0) && (bit == 1)) + { + if (m_bitTime > 7*m_samplesPerDot10wpm) + { + if (m_ident != "") + { + qDebug() << m_ident << " " << Morse::toString(m_ident); + if (getMessageQueueToGUI()) + { + VORDemodReport::MsgReportIdent *msg = VORDemodReport::MsgReportIdent::create(m_subChannelId, m_ident); + getMessageQueueToGUI()->push(msg); + } + m_ident = ""; + } + } + else if (m_bitTime > 2.5*m_samplesPerDot10wpm) + { + m_ident.append(" "); + } + m_bitTime = 0; + } + else if (bit == 1) + { + m_bitTime++; + } + else if ((m_prevBit == 1) && (bit == 0)) + { + if (m_bitTime > 2*m_samplesPerDot10wpm) + { + m_ident.append("-"); + } + else if (m_bitTime > 0.2*m_samplesPerDot10wpm) + { + m_ident.append("."); + } + m_bitTime = 0; + } + else + { + m_bitTime++; + if (m_bitTime > 10*m_samplesPerDot7wpm) + { + m_ident = m_ident.simplified(); + if (m_ident != "") + { + qDebug() << m_ident << " " << Morse::toString(m_ident); + if (getMessageQueueToGUI()) + { + VORDemodReport::MsgReportIdent *msg = VORDemodReport::MsgReportIdent::create(m_subChannelId, m_ident); + getMessageQueueToGUI()->push(msg); + } + m_ident = ""; + } + m_bitTime = 0; + } + } + m_prevBit = bit; +} + +void VORDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "VORDemodSink::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, VORDEMOD_CHANNEL_BANDWIDTH); + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) channelSampleRate / (Real) VORDEMOD_CHANNEL_SAMPLE_RATE; + + m_samplesPerDot7wpm = VORDEMOD_CHANNEL_SAMPLE_RATE*60/(50*7); + m_samplesPerDot10wpm = VORDEMOD_CHANNEL_SAMPLE_RATE*60/(50*10); + + m_ncoIdent.setFreq(-1020, VORDEMOD_CHANNEL_SAMPLE_RATE); // +-50Hz source offset allowed + m_ncoRef.setFreq(-9960, VORDEMOD_CHANNEL_SAMPLE_RATE); + m_lowpassIdent.create(301, VORDEMOD_CHANNEL_SAMPLE_RATE, 100.0f); + m_lowpassRef.create(301, VORDEMOD_CHANNEL_SAMPLE_RATE, 600.0f); // Max deviation is 480Hz + m_movingAverageIdent.resize(m_samplesPerDot10wpm/5); // Needs to be short enough for noise floor calculation + + m_binSampleCnt = 0; + m_binCnt = 0; + m_identNoise = 0.0001f; + for (int i = 0; i < m_identBins; i++) + { + m_identMaxs[i] = 0.0f; + } + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; +} + +void VORDemodSink::applySettings(const VORDemodSettings& settings, bool force) +{ + qDebug() << "VORDemodSink::applySettings:" + << " m_volume: " << settings.m_volume + << " m_squelch: " << settings.m_squelch + << " m_audioMute: " << settings.m_audioMute + << " m_audioDeviceName: " << settings.m_audioDeviceName + << " force: " << force; + + if ((m_settings.m_squelch != settings.m_squelch) || force) { + m_squelchLevel = CalcDb::powerFromdB(settings.m_squelch); + } + + m_settings = settings; +} + +void VORDemodSink::applyAudioSampleRate(int sampleRate) +{ + if (sampleRate < 0) + { + qWarning("VORDemodSink::applyAudioSampleRate: invalid sample rate: %d", sampleRate); + return; + } + + qDebug("VORDemodSink::applyAudioSampleRate: sampleRate: %d m_channelSampleRate: %d", sampleRate, m_channelSampleRate); + + // (ICAO Annex 10 3.3.6.3) - Optional voice audio is 300Hz to 3kHz + m_audioInterpolator.create(16, VORDEMOD_CHANNEL_SAMPLE_RATE, 3000.0f); + m_audioInterpolatorDistanceRemain = 0; + m_audioInterpolatorDistance = (Real) VORDEMOD_CHANNEL_SAMPLE_RATE / (Real) sampleRate; + m_bandpass.create(301, sampleRate, 300.0f, 3000.0f); + m_audioFifo.setSize(sampleRate); + m_squelchDelayLine.resize(sampleRate/5); + + m_volumeAGC.resizeNew(sampleRate/10, 0.003f); + + m_audioSampleRate = sampleRate; +} diff --git a/plugins/channelrx/demodvor/vordemodsink.h b/plugins/channelrx/demodvor/vordemodsink.h new file mode 100644 index 000000000..d6243b4e7 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodsink.h @@ -0,0 +1,156 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 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_VORDEMODSINK_H +#define INCLUDE_VORDEMODSINK_H + +#include "dsp/channelsamplesink.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/agc.h" +#include "dsp/firfilter.h" +#include "dsp/goertzel.h" +#include "audio/audiofifo.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" + +#include "vordemodsettings.h" + +#include + +// Highest frequency is the FM subcarrier at up to ~11kHz +// However, old VORs can have 0.005% frequency offset, which is 6kHz +#define VORDEMOD_CHANNEL_BANDWIDTH 18000 +// Sample rate needs to be at least twice the above +// Also need to consider impact frequency resolution of Goertzel filters +// May as well make it a common audio rate, to possibly avoid decimation +#define VORDEMOD_CHANNEL_SAMPLE_RATE 48000 + +class VORDemodSink : public ChannelSampleSink { +public: + VORDemodSink(const VORDemodSettings& settings, int subChannel, + MessageQueue *messageQueueToGUI); + ~VORDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const VORDemodSettings& settings, bool force = false); + void setMessageQueueToGUI(MessageQueue *messageQueue) { m_messageQueueToGUI = messageQueue; } + void applyAudioSampleRate(int sampleRate); + + int getAudioSampleRate() const { return m_audioSampleRate; } + double getMagSq() const { return m_magsq; } + bool getSquelchOpen() const { return m_squelchOpen; } + AudioFifo *getAudioFifo() { return &m_audioFifo; } + + 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; + } + + int m_subChannelId; // The id for the VOR this demod sink was created for + int m_vorFrequencyHz; // The VORs frequency + int m_frequencyOffset; // Different between sample source center frequeny and VOR center frequency + int m_channelFrequencyOffset; + bool m_outOfBand; + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + VORDemodSettings m_settings; + int m_channelSampleRate; + int m_audioSampleRate; + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + Real m_squelchLevel; + uint32_t m_squelchCount; + bool m_squelchOpen; + DoubleBufferFIFO m_squelchDelayLine; + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MessageQueue *m_messageQueueToGUI; + + MovingAverageUtil m_movingAverage; + SimpleAGC<4800> m_volumeAGC; + Bandpass m_bandpass; + + Interpolator m_audioInterpolator; + Real m_audioInterpolatorDistance; + Real m_audioInterpolatorDistanceRemain; + AudioVector m_audioBuffer; + AudioFifo m_audioFifo; + uint32_t m_audioBufferFill; + + NCO m_ncoIdent; + NCO m_ncoRef; + Lowpass m_lowpassRef; + Lowpass m_lowpassIdent; + Complex m_refPrev; + MovingAverageUtilVar m_movingAverageIdent; + static const int m_identBins = 10; + Real m_identMins[m_identBins]; + Real m_identMin; + Real m_identMaxs[m_identBins]; + Real m_identNoise; + int m_binSampleCnt; + int m_binCnt; + int m_samplesPerDot7wpm; + int m_samplesPerDot10wpm; + int m_prevBit; + int m_bitTime; + QString m_ident; + Goertzel m_varGoertzel; + Goertzel m_refGoertzel; + + void processOneSample(Complex &ci); + void processOneAudioSample(Complex &ci); + MessageQueue *getMessageQueueToGUI() { return m_messageQueueToGUI; } +}; + +#endif // INCLUDE_VORDEMODSINK_H diff --git a/plugins/channelrx/demodvor/vordemodwebapiadapter.cpp b/plugins/channelrx/demodvor/vordemodwebapiadapter.cpp new file mode 100644 index 000000000..ee8d54ef7 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodwebapiadapter.cpp @@ -0,0 +1,51 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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 "vordemod.h" +#include "vordemodwebapiadapter.h" + +VORDemodWebAPIAdapter::VORDemodWebAPIAdapter() +{} + +VORDemodWebAPIAdapter::~VORDemodWebAPIAdapter() +{} + +int VORDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setVorDemodSettings(new SWGSDRangel::SWGVORDemodSettings()); + response.getVorDemodSettings()->init(); + VORDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int VORDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + VORDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodvor/vordemodwebapiadapter.h b/plugins/channelrx/demodvor/vordemodwebapiadapter.h new file mode 100644 index 000000000..94a1513d6 --- /dev/null +++ b/plugins/channelrx/demodvor/vordemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 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_VORDEMOD_WEBAPIADAPTER_H +#define INCLUDE_VORDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "vordemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class VORDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + VORDemodWebAPIAdapter(); + virtual ~VORDemodWebAPIAdapter(); + + 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: + VORDemodSettings m_settings; +}; + +#endif // INCLUDE_VORDEMOD_WEBAPIADAPTER_H diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 8837ef993..1105edb59 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -122,6 +122,7 @@ set(sdrbase_SOURCES dsp/interpolator.cpp dsp/glscopesettings.cpp dsp/glspectrumsettings.cpp + dsp/goertzel.cpp dsp/hbfilterchainconverter.cpp dsp/hbfiltertraits.cpp dsp/mimochannel.cpp @@ -173,6 +174,7 @@ set(sdrbase_SOURCES util/lfsr.cpp util/message.cpp util/messagequeue.cpp + util/morse.cpp util/prettyprint.cpp util/rtpsink.cpp util/syncmessenger.cpp @@ -181,6 +183,7 @@ set(sdrbase_SOURCES util/serialutil.cpp #util/spinlock.cpp util/uid.cpp + util/units.cpp util/timeutil.cpp plugin/plugininterface.cpp @@ -271,6 +274,7 @@ set(sdrbase_HEADERS dsp/glscopesettings.h dsp/glspectruminterface.h dsp/glspectrumsettings.h + dsp/goertzel.h dsp/hbfilterchainconverter.h dsp/iirfilter.h dsp/interpolator.h @@ -348,6 +352,7 @@ set(sdrbase_HEADERS util/lfsr.h util/message.h util/messagequeue.h + util/morse.h util/movingaverage.h util/prettyprint.h util/rtpsink.h @@ -357,6 +362,7 @@ set(sdrbase_HEADERS util/serialutil.h #util/spinlock.h util/uid.h + util/units.h util/timeutil.h webapi/webapiadapter.h diff --git a/sdrbase/dsp/goertzel.cpp b/sdrbase/dsp/goertzel.cpp new file mode 100644 index 000000000..1dccefcb2 --- /dev/null +++ b/sdrbase/dsp/goertzel.cpp @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "goertzel.h" + +Goertzel::Goertzel(double frequency, int sampleRate) : + m_s0(0.0), + m_s1(0.0), + m_s2(0.0), + m_z(0.0), + m_sampleCount(0) +{ + m_a = 2.0 * M_PI * frequency / sampleRate; + m_b = 2.0 * cos(m_a); + m_c = std::complex(cos(-m_a), sin(-m_a)); +} + +void Goertzel::reset() +{ + m_s0 = 0.0; + m_s1 = 0.0; + m_s2 = 0.0; + m_z = 0.0; + m_sampleCount = 0; +} + +void Goertzel::filter(double sample) +{ + m_s0 = m_b * m_s1 - m_s2 + sample; + m_s2 = m_s1; + m_s1 = m_s0; + m_sampleCount++; +} + +std::complex Goertzel::goertzel(double lastSample) +{ + m_s0 = m_b * m_s1 - m_s2 + lastSample; + m_sampleCount++; + + std::complex y = m_s0 - m_s1 * m_c; + double x = -m_a * (m_sampleCount - 1.0); + std::complex d(cos(x), sin(x)); + + double scale = m_sampleCount / 2.0; + m_z = y * d / scale; + return m_z; +} + +double Goertzel::mag() +{ + return std::abs(m_z); +} + +double Goertzel::phase() +{ + return std::arg(m_z); +} diff --git a/sdrbase/dsp/goertzel.h b/sdrbase/dsp/goertzel.h new file mode 100644 index 000000000..3ffb18a66 --- /dev/null +++ b/sdrbase/dsp/goertzel.h @@ -0,0 +1,46 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_GOERTZEL_H +#define INCLUDE_GOERTZEL_H + +#include + +#include "export.h" + +// Goertzel filter for calculting discrete Fourier transform for a single frequency +// Implementation supports non-integer multiples of fundamental frequency, see: +// https://asp-eurasipjournals.springeropen.com/track/pdf/10.1186/1687-6180-2012-56.pdf +class SDRBASE_API Goertzel +{ +public: + Goertzel(double frequency, int sampleRate); + void reset(); + void filter(double sample); + std::complex goertzel(double lastSample); + double mag(); + double phase(); + int size() { return m_sampleCount; } + +private: + double m_s0, m_s1, m_s2; + double m_a, m_b; + std::complex m_c, m_z; + int m_sampleCount; +}; + +#endif // INCLUDE_GOERTZEL_H diff --git a/sdrbase/util/morse.cpp b/sdrbase/util/morse.cpp new file mode 100644 index 000000000..c9a9cf59f --- /dev/null +++ b/sdrbase/util/morse.cpp @@ -0,0 +1,203 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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/morse.h" + +// ITU encoding with a few extras +const struct Morse::ASCIIToMorse Morse::m_asciiToMorse[] = { + {' ', " "}, + {'!', "-.-.--"}, // Non-ITU + {'\"', ".-..-."}, + {'#', ""}, + {'$', "...-..-"}, // Non-ITU + {'%', ""}, + {'&', ".-..."}, + {'\'', ".----."}, + {'(', "-.--."}, + {')', "-.--.-"}, + {'*', "_.._"}, + {'+', ".-.-."}, + {',', "--..--"}, + {'-', "-....-"}, + {'.', ".-.-.-"}, + {'/', "-..-."}, + {'0', "-----"}, + {'1', ".----"}, + {'2', "..---"}, + {'3', "...--"}, + {'4', "....-"}, + {'5', "....."}, + {'6', "-...."}, + {'7', "--..."}, + {'8', "---.."}, + {'9', "----."}, + {':', "---..."}, + {';', "-.-.-."}, // Non-ITU + {'<', ""}, + {'=', "-...-"}, + {'>', ""}, + {'?', "..--.."}, + {'@', ".--.-."}, + {'A', ".-"}, + {'B', "-..."}, + {'C', "-.-."}, + {'D', "-.."}, + {'E', "."}, + {'F', "..-."}, + {'G', "--."}, + {'H', "...."}, + {'I', ".."}, + {'J', ".---"}, + {'K', "-.-"}, + {'L', ".-.."}, + {'M', "--"}, + {'N', "-."}, + {'O', "---"}, + {'P', ".--."}, + {'Q', "--.-"}, + {'R', ".-."}, + {'S', "..."}, + {'T', "-"}, + {'U', "..-"}, + {'V', "...-"}, + {'W', ".--"}, + {'X', "-..-"}, + {'Y', "-.--"}, + {'Z', "--.."}, + {'[', ""}, + {'\\', ""}, + {']', ""}, + {'^', ""}, + {'_', "..--.-"}, // Non-ITU + {'`', ""}, + {'a', ".-"}, + {'b', "-..."}, + {'c', "-.-."}, + {'d', "-.."}, + {'e', "."}, + {'f', "..-."}, + {'g', "--."}, + {'h', "...."}, + {'i', ".."}, + {'j', ".---"}, + {'k', "-.-"}, + {'l', ".-.."}, + {'m', "--"}, + {'n', "-."}, + {'o', "---"}, + {'p', ".--."}, + {'q', "--.-"}, + {'r', ".-."}, + {'s', "..."}, + {'t', "-"}, + {'u', "..-"}, + {'v', "...-"}, + {'w', ".--"}, + {'x', "-..-"}, + {'y', "-.--"}, + {'z', "--.."}, + {'{', ""}, + {'|', ""}, + {'}', ""}, + {'~', ""} +}; + +// Convert ASCII character to Morse code sequence consisting of . and - characters +QString Morse::toMorse(char ascii) +{ + char firstChar = m_asciiToMorse[0].ascii; + if ((ascii >= firstChar) && (ascii < 127)) + return QString(m_asciiToMorse[ascii - firstChar].morse); + else + return QString(); +} + +// Convert string to Morse code sequence consisting of . and - characters separated by spaces +QString Morse::toMorse(QString &string) +{ + QStringList list; + for (int i = 0; i < string.size(); i++) + { + if (i != 0) + list.append(" "); + list.append(toMorse(string.at(i).toLatin1())); + } + return list.join(""); +} + +// Converts Morse code sequence using ASCII . and - to Unicode bullet and minus sign +// which are horizontally aligned, so look nicer in GUIs +QString Morse::toUnicode(QString &morse) +{ + return morse.replace(QChar('.'), QChar(0x2022)).replace(QChar('-'), QChar(0x2212)); +} + +// Converts a string to a unicode Morse sequence with extra space characters between +// dots and dashes to improve readability in GUIs +QString Morse::toSpacedUnicode(QString &morse) +{ + QString temp = toUnicode(morse); + for (int i = 0; i < temp.size(); i+=2) + temp.insert(i, ' '); + return temp; +} + +// Converts a string to a unicode Morse sequence +QString Morse::toUnicodeMorse(QString &string) +{ + QString ascii = toMorse(string); + return ascii.replace(QChar('.'), QChar(0x2022)).replace(QChar('-'), QChar(0x2212)); +} + +// Converts a string to a unicode Morse sequence with spacing between dots and dashes +QString Morse::toSpacedUnicodeMorse(QString &string) +{ + QString temp = toUnicodeMorse(string); + for (int i = 0; i < temp.size(); i+=2) + temp.insert(i, ' '); + return temp; +} + +#define COUNT_OF(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x]))))) + +// Converts a Morse sequence to an ASCII character. -1 if no mapping found. +int Morse::toASCII(QString &morse) +{ + for (int i = 0; i < COUNT_OF(m_asciiToMorse); i++) + { + if (morse == m_asciiToMorse[i].morse) + return m_asciiToMorse[i].ascii; + } + return -1; +} + +// Converts a sequence of Morse code to a string. Unknown Morse codes are ignored. +QString Morse::toString(QString &morse) +{ + QString string(""); + QStringList groups = morse.split(" "); + for (int i = 0; i < groups.size(); i++) + { + int c = Morse::toASCII(groups[i]); + if ((c != -1) && (groups[i] != "")) + string.append(c); + } + return string; +} diff --git a/sdrbase/util/morse.h b/sdrbase/util/morse.h new file mode 100644 index 000000000..28857c104 --- /dev/null +++ b/sdrbase/util/morse.h @@ -0,0 +1,47 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_MORSE_H +#define INCLUDE_MORSE_H + +#include + +#include "export.h" + +// Morse (ITU) code utils for converting between strings and Morse code and vice-versa +class SDRBASE_API Morse +{ +public: + static QString toMorse(char asciiChar); + static QString toMorse(QString &string); + static QString toUnicode(QString &morse); + static QString toSpacedUnicode(QString &morse); + static QString toUnicodeMorse(QString &string); + static QString toSpacedUnicodeMorse(QString &string); + static int toASCII(QString &morse); + static QString toString(QString &morse); + +private: + struct ASCIIToMorse { + char ascii; + const char *morse; + }; + + const static ASCIIToMorse m_asciiToMorse[]; +}; + +#endif // INCLUDE_MORSE_H diff --git a/sdrbase/util/units.cpp b/sdrbase/util/units.cpp new file mode 100644 index 000000000..7fc936c53 --- /dev/null +++ b/sdrbase/util/units.cpp @@ -0,0 +1,18 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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 "units.h" diff --git a/sdrbase/util/units.h b/sdrbase/util/units.h new file mode 100644 index 000000000..4285f26e0 --- /dev/null +++ b/sdrbase/util/units.h @@ -0,0 +1,82 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 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_UNITS_H +#define INCLUDE_UNITS_H + +#include + +#include "export.h" + +// Unit conversions +class SDRBASE_API Units +{ +public: + + static inline float feetToMetres(float feet) + { + return feet * 0.3048f; + } + + static inline int feetToIntegerMetres(float feet) + { + return (int)std::round(feetToMetres(feet)); + } + + static inline float nauticalMilesToMetres(float nauticalMiles) + { + return nauticalMiles * 1855.0f; + } + + static inline int nauticalMilesToIntegerMetres(float nauticalMiles) + { + return (int)std::round(nauticalMilesToMetres(nauticalMiles)); + } + + static float knotsToKPH(float knots) + { + return knots * 1.852f; + } + + static int knotsToIntegerKPH(float knots) + { + return (int)std::round(knotsToKPH(knots)); + } + + static float feetPerMinToMetresPerSecond(float fpm) + { + return fpm * 0.00508f; + } + + static int feetPerMinToIntegerMetresPerSecond(float fpm) + { + return (int)std::round(feetPerMinToMetresPerSecond(fpm)); + } + + static float degreesToRadians(float degrees) + { + return degrees * ((float)M_PI) / 180.0f; + } + + static float radiansToDegress(float radians) + { + return radians * 180.0f / ((float)M_PI); + } + +}; + +#endif // INCLUDE_UNITS_H diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 2556a07e4..c174184a4 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -3956,6 +3956,11 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setUdpSinkSettings(new SWGSDRangel::SWGUDPSinkSettings()); channelSettings->getUdpSinkSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "VORDemodSettings") + { + channelSettings->setVorDemodSettings(new SWGSDRangel::SWGVORDemodSettings()); + channelSettings->getVorDemodSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "WFMDemodSettings") { channelSettings->setWfmDemodSettings(new SWGSDRangel::SWGWFMDemodSettings()); @@ -4542,6 +4547,7 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.setSsbModSettings(nullptr); channelSettings.setUdpSourceSettings(nullptr); channelSettings.setUdpSinkSettings(nullptr); + channelSettings.setVorDemodSettings(nullptr); channelSettings.setWfmDemodSettings(nullptr); channelSettings.setWfmModSettings(nullptr); } @@ -4565,6 +4571,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.setSsbModReport(nullptr); channelReport.setUdpSourceReport(nullptr); channelReport.setUdpSinkReport(nullptr); + channelReport.setVorDemodReport(nullptr); channelReport.setWfmDemodReport(nullptr); channelReport.setWfmModReport(nullptr); } diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 86abdc6c8..255691d8b 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -22,6 +22,7 @@ #include "webapiutils.h" const QMap WebAPIUtils::m_channelURIToSettingsKey = { + {"sdrangel.channel.adsbdemod", "ADSBDemodSettings"}, {"sdrangel.channel.amdemod", "AMDemodSettings"}, {"de.maintech.sdrangelove.channel.am", "AMDemodSettings"}, // remap {"sdrangel.channeltx.modam", "AMModSettings"}, @@ -57,6 +58,7 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channeltx.udpsink", "UDPSinkSettings"}, // remap {"sdrangel.channel.udpsink", "UDPSinkSettings"}, {"sdrangel.channel.udpsrc", "UDPSourceSettings"}, // remap + {"sdrangel.channel.vordemod", "VORDemodSettings"}, {"sdrangel.channel.wfmdemod", "WFMDemodSettings"}, {"de.maintech.sdrangelove.channel.wfm", "WFMDemodSettings"}, // remap {"sdrangel.channeltx.modwfm", "WFMModSettings"}, @@ -114,6 +116,7 @@ const QMap WebAPIUtils::m_deviceIdToSettingsKey = { }; const QMap WebAPIUtils::m_channelTypeToSettingsKey = { + {"ADSBDemod", "ADSBDemodSettings"}, {"AMDemod", "AMDemodSettings"}, {"AMMod", "AMModSettings"}, {"ATVDemod", "ATVDemodSettings"}, @@ -141,6 +144,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"SSBDemod", "SSBDemodSettings"}, {"UDPSink", "UDPSinkSettings"}, {"UDPSource", "UDPSourceSettings"}, + {"VORDemod", "VORDemodSettings"}, {"WFMDemod", "WFMDemodSettings"}, {"WFMMod", "WFMModSettings"}, {"BeamSteeringCWMod", "BeamSteeringCWModSettings"}, diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index d2ae747d1..80da8869e 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -57,6 +57,8 @@ ChannelReport: $ref: "http://swgserver:8081/api/swagger/include/UDPSource.yaml#/UDPSourceReport" UDPSinkReport: $ref: "http://swgserver:8081/api/swagger/include/UDPSink.yaml#/UDPSinkReport" + VORDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/VORDemod.yaml#/VORDemodReport" WFMDemodReport: $ref: "http://swgserver:8081/api/swagger/include/WFMDemod.yaml#/WFMDemodReport" WFMModReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index 988a60704..96d318d6e 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -79,6 +79,8 @@ ChannelSettings: $ref: "http://swgserver:8081/api/swagger/include/UDPSource.yaml#/UDPSourceSettings" UDPSinkSettings: $ref: "http://swgserver:8081/api/swagger/include/UDPSink.yaml#/UDPSinkSettings" + VORDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/VORDemod.yaml#/VORDemodSettings" WFMDemodSettings: $ref: "http://swgserver:8081/api/swagger/include/WFMDemod.yaml#/WFMDemodSettings" WFMModSettings: diff --git a/swagger/sdrangel/api/swagger/include/VORDemod.yaml b/swagger/sdrangel/api/swagger/include/VORDemod.yaml new file mode 100644 index 000000000..475979a9d --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/VORDemod.yaml @@ -0,0 +1,51 @@ +VORDemodSettings: + description: VORDemod + properties: + squelch: + description: power squelch threshold in decibels + type: number + format: float + volume: + type: number + format: float + audioMute: + type: integer + rgbColor: + type: integer + title: + type: string + audioDeviceName: + type: string + streamIndex: + description: MIMO channel. Not relevant when connected to SI (single Rx). + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer + identThreshold: + description: Morse code ident threshold (linear SNR) + type: integer + magDecAdjust: + description: Adjust radial lines on map for magnetic declination of VOR + type: integer + +VORDemodReport: + description: VORDemod + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + squelch: + description: squelch status (1 if open else 0) + type: integer + audioSampleRate: + type: integer diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index f3035e734..0da072d21 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -78,6 +78,8 @@ SWGChannelReport::SWGChannelReport() { m_udp_source_report_isSet = false; udp_sink_report = nullptr; m_udp_sink_report_isSet = false; + vor_demod_report = nullptr; + m_vor_demod_report_isSet = false; wfm_demod_report = nullptr; m_wfm_demod_report_isSet = false; wfm_mod_report = nullptr; @@ -140,6 +142,8 @@ SWGChannelReport::init() { m_udp_source_report_isSet = false; udp_sink_report = new SWGUDPSinkReport(); m_udp_sink_report_isSet = false; + vor_demod_report = new SWGVORDemodReport(); + m_vor_demod_report_isSet = false; wfm_demod_report = new SWGWFMDemodReport(); m_wfm_demod_report_isSet = false; wfm_mod_report = new SWGWFMModReport(); @@ -221,6 +225,9 @@ SWGChannelReport::cleanup() { if(udp_sink_report != nullptr) { delete udp_sink_report; } + if(vor_demod_report != nullptr) { + delete vor_demod_report; + } if(wfm_demod_report != nullptr) { delete wfm_demod_report; } @@ -290,6 +297,8 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&udp_sink_report, pJson["UDPSinkReport"], "SWGUDPSinkReport", "SWGUDPSinkReport"); + ::SWGSDRangel::setValue(&vor_demod_report, pJson["VORDemodReport"], "SWGVORDemodReport", "SWGVORDemodReport"); + ::SWGSDRangel::setValue(&wfm_demod_report, pJson["WFMDemodReport"], "SWGWFMDemodReport", "SWGWFMDemodReport"); ::SWGSDRangel::setValue(&wfm_mod_report, pJson["WFMModReport"], "SWGWFMModReport", "SWGWFMModReport"); @@ -385,6 +394,9 @@ SWGChannelReport::asJsonObject() { if((udp_sink_report != nullptr) && (udp_sink_report->isSet())){ toJsonValue(QString("UDPSinkReport"), udp_sink_report, obj, QString("SWGUDPSinkReport")); } + if((vor_demod_report != nullptr) && (vor_demod_report->isSet())){ + toJsonValue(QString("VORDemodReport"), vor_demod_report, obj, QString("SWGVORDemodReport")); + } if((wfm_demod_report != nullptr) && (wfm_demod_report->isSet())){ toJsonValue(QString("WFMDemodReport"), wfm_demod_report, obj, QString("SWGWFMDemodReport")); } @@ -645,6 +657,16 @@ SWGChannelReport::setUdpSinkReport(SWGUDPSinkReport* udp_sink_report) { this->m_udp_sink_report_isSet = true; } +SWGVORDemodReport* +SWGChannelReport::getVorDemodReport() { + return vor_demod_report; +} +void +SWGChannelReport::setVorDemodReport(SWGVORDemodReport* vor_demod_report) { + this->vor_demod_report = vor_demod_report; + this->m_vor_demod_report_isSet = true; +} + SWGWFMDemodReport* SWGChannelReport::getWfmDemodReport() { return wfm_demod_report; @@ -745,6 +767,9 @@ SWGChannelReport::isSet(){ if(udp_sink_report && udp_sink_report->isSet()){ isObjectUpdated = true; break; } + if(vor_demod_report && vor_demod_report->isSet()){ + isObjectUpdated = true; break; + } if(wfm_demod_report && wfm_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 e6d4bf3d8..989f0b51f 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -45,6 +45,7 @@ #include "SWGSigMFFileSinkReport.h" #include "SWGUDPSinkReport.h" #include "SWGUDPSourceReport.h" +#include "SWGVORDemodReport.h" #include "SWGWFMDemodReport.h" #include "SWGWFMModReport.h" #include @@ -142,6 +143,9 @@ public: SWGUDPSinkReport* getUdpSinkReport(); void setUdpSinkReport(SWGUDPSinkReport* udp_sink_report); + SWGVORDemodReport* getVorDemodReport(); + void setVorDemodReport(SWGVORDemodReport* vor_demod_report); + SWGWFMDemodReport* getWfmDemodReport(); void setWfmDemodReport(SWGWFMDemodReport* wfm_demod_report); @@ -227,6 +231,9 @@ private: SWGUDPSinkReport* udp_sink_report; bool m_udp_sink_report_isSet; + SWGVORDemodReport* vor_demod_report; + bool m_vor_demod_report_isSet; + SWGWFMDemodReport* wfm_demod_report; bool m_wfm_demod_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index 54419e3ef..c29a7a711 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -98,6 +98,8 @@ SWGChannelSettings::SWGChannelSettings() { m_udp_source_settings_isSet = false; udp_sink_settings = nullptr; m_udp_sink_settings_isSet = false; + vor_demod_settings = nullptr; + m_vor_demod_settings_isSet = false; wfm_demod_settings = nullptr; m_wfm_demod_settings_isSet = false; wfm_mod_settings = nullptr; @@ -180,6 +182,8 @@ SWGChannelSettings::init() { m_udp_source_settings_isSet = false; udp_sink_settings = new SWGUDPSinkSettings(); m_udp_sink_settings_isSet = false; + vor_demod_settings = new SWGVORDemodSettings(); + m_vor_demod_settings_isSet = false; wfm_demod_settings = new SWGWFMDemodSettings(); m_wfm_demod_settings_isSet = false; wfm_mod_settings = new SWGWFMModSettings(); @@ -287,6 +291,9 @@ SWGChannelSettings::cleanup() { if(udp_sink_settings != nullptr) { delete udp_sink_settings; } + if(vor_demod_settings != nullptr) { + delete vor_demod_settings; + } if(wfm_demod_settings != nullptr) { delete wfm_demod_settings; } @@ -376,6 +383,8 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&udp_sink_settings, pJson["UDPSinkSettings"], "SWGUDPSinkSettings", "SWGUDPSinkSettings"); + ::SWGSDRangel::setValue(&vor_demod_settings, pJson["VORDemodSettings"], "SWGVORDemodSettings", "SWGVORDemodSettings"); + ::SWGSDRangel::setValue(&wfm_demod_settings, pJson["WFMDemodSettings"], "SWGWFMDemodSettings", "SWGWFMDemodSettings"); ::SWGSDRangel::setValue(&wfm_mod_settings, pJson["WFMModSettings"], "SWGWFMModSettings", "SWGWFMModSettings"); @@ -501,6 +510,9 @@ SWGChannelSettings::asJsonObject() { if((udp_sink_settings != nullptr) && (udp_sink_settings->isSet())){ toJsonValue(QString("UDPSinkSettings"), udp_sink_settings, obj, QString("SWGUDPSinkSettings")); } + if((vor_demod_settings != nullptr) && (vor_demod_settings->isSet())){ + toJsonValue(QString("VORDemodSettings"), vor_demod_settings, obj, QString("SWGVORDemodSettings")); + } if((wfm_demod_settings != nullptr) && (wfm_demod_settings->isSet())){ toJsonValue(QString("WFMDemodSettings"), wfm_demod_settings, obj, QString("SWGWFMDemodSettings")); } @@ -861,6 +873,16 @@ SWGChannelSettings::setUdpSinkSettings(SWGUDPSinkSettings* udp_sink_settings) { this->m_udp_sink_settings_isSet = true; } +SWGVORDemodSettings* +SWGChannelSettings::getVorDemodSettings() { + return vor_demod_settings; +} +void +SWGChannelSettings::setVorDemodSettings(SWGVORDemodSettings* vor_demod_settings) { + this->vor_demod_settings = vor_demod_settings; + this->m_vor_demod_settings_isSet = true; +} + SWGWFMDemodSettings* SWGChannelSettings::getWfmDemodSettings() { return wfm_demod_settings; @@ -991,6 +1013,9 @@ SWGChannelSettings::isSet(){ if(udp_sink_settings && udp_sink_settings->isSet()){ isObjectUpdated = true; break; } + if(vor_demod_settings && vor_demod_settings->isSet()){ + isObjectUpdated = true; break; + } if(wfm_demod_settings && wfm_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 677413676..333bd5588 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -53,6 +53,7 @@ #include "SWGSigMFFileSinkSettings.h" #include "SWGUDPSinkSettings.h" #include "SWGUDPSourceSettings.h" +#include "SWGVORDemodSettings.h" #include "SWGWFMDemodSettings.h" #include "SWGWFMModSettings.h" #include @@ -180,6 +181,9 @@ public: SWGUDPSinkSettings* getUdpSinkSettings(); void setUdpSinkSettings(SWGUDPSinkSettings* udp_sink_settings); + SWGVORDemodSettings* getVorDemodSettings(); + void setVorDemodSettings(SWGVORDemodSettings* vor_demod_settings); + SWGWFMDemodSettings* getWfmDemodSettings(); void setWfmDemodSettings(SWGWFMDemodSettings* wfm_demod_settings); @@ -295,6 +299,9 @@ private: SWGUDPSinkSettings* udp_sink_settings; bool m_udp_sink_settings_isSet; + SWGVORDemodSettings* vor_demod_settings; + bool m_vor_demod_settings_isSet; + SWGWFMDemodSettings* wfm_demod_settings; bool m_wfm_demod_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 8c1faa313..162309701 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -221,6 +221,8 @@ #include "SWGUSRPInputSettings.h" #include "SWGUSRPOutputReport.h" #include "SWGUSRPOutputSettings.h" +#include "SWGVORDemodReport.h" +#include "SWGVORDemodSettings.h" #include "SWGWFMDemodReport.h" #include "SWGWFMDemodSettings.h" #include "SWGWFMModReport.h" @@ -856,6 +858,12 @@ namespace SWGSDRangel { if(QString("SWGUSRPOutputSettings").compare(type) == 0) { return new SWGUSRPOutputSettings(); } + if(QString("SWGVORDemodReport").compare(type) == 0) { + return new SWGVORDemodReport(); + } + if(QString("SWGVORDemodSettings").compare(type) == 0) { + return new SWGVORDemodSettings(); + } if(QString("SWGWFMDemodReport").compare(type) == 0) { return new SWGWFMDemodReport(); } diff --git a/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.cpp new file mode 100644 index 000000000..b94efbd83 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.cpp @@ -0,0 +1,154 @@ +/** + * 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: 6.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 "SWGVORDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGVORDemodReport::SWGVORDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGVORDemodReport::SWGVORDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + squelch = 0; + m_squelch_isSet = false; + audio_sample_rate = 0; + m_audio_sample_rate_isSet = false; +} + +SWGVORDemodReport::~SWGVORDemodReport() { + this->cleanup(); +} + +void +SWGVORDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + squelch = 0; + m_squelch_isSet = false; + audio_sample_rate = 0; + m_audio_sample_rate_isSet = false; +} + +void +SWGVORDemodReport::cleanup() { + + + +} + +SWGVORDemodReport* +SWGVORDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGVORDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&squelch, pJson["squelch"], "qint32", ""); + + ::SWGSDRangel::setValue(&audio_sample_rate, pJson["audioSampleRate"], "qint32", ""); + +} + +QString +SWGVORDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGVORDemodReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_squelch_isSet){ + obj->insert("squelch", QJsonValue(squelch)); + } + if(m_audio_sample_rate_isSet){ + obj->insert("audioSampleRate", QJsonValue(audio_sample_rate)); + } + + return obj; +} + +float +SWGVORDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGVORDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGVORDemodReport::getSquelch() { + return squelch; +} +void +SWGVORDemodReport::setSquelch(qint32 squelch) { + this->squelch = squelch; + this->m_squelch_isSet = true; +} + +qint32 +SWGVORDemodReport::getAudioSampleRate() { + return audio_sample_rate; +} +void +SWGVORDemodReport::setAudioSampleRate(qint32 audio_sample_rate) { + this->audio_sample_rate = audio_sample_rate; + this->m_audio_sample_rate_isSet = true; +} + + +bool +SWGVORDemodReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_squelch_isSet){ + isObjectUpdated = true; break; + } + if(m_audio_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.h new file mode 100644 index 000000000..b66202648 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGVORDemodReport.h @@ -0,0 +1,70 @@ +/** + * 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: 6.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. + */ + +/* + * SWGVORDemodReport.h + * + * VORDemod + */ + +#ifndef SWGVORDemodReport_H_ +#define SWGVORDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGVORDemodReport: public SWGObject { +public: + SWGVORDemodReport(); + SWGVORDemodReport(QString* json); + virtual ~SWGVORDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGVORDemodReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getSquelch(); + void setSquelch(qint32 squelch); + + qint32 getAudioSampleRate(); + void setAudioSampleRate(qint32 audio_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 squelch; + bool m_squelch_isSet; + + qint32 audio_sample_rate; + bool m_audio_sample_rate_isSet; + +}; + +} + +#endif /* SWGVORDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.cpp new file mode 100644 index 000000000..094a56148 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.cpp @@ -0,0 +1,413 @@ +/** + * 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: 6.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 "SWGVORDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGVORDemodSettings::SWGVORDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGVORDemodSettings::SWGVORDemodSettings() { + squelch = 0.0f; + m_squelch_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + audio_mute = 0; + m_audio_mute_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + audio_device_name = nullptr; + m_audio_device_name_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; + ident_threshold = 0; + m_ident_threshold_isSet = false; + mag_dec_adjust = 0; + m_mag_dec_adjust_isSet = false; +} + +SWGVORDemodSettings::~SWGVORDemodSettings() { + this->cleanup(); +} + +void +SWGVORDemodSettings::init() { + squelch = 0.0f; + m_squelch_isSet = false; + volume = 0.0f; + m_volume_isSet = false; + audio_mute = 0; + m_audio_mute_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + audio_device_name = new QString(""); + m_audio_device_name_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; + ident_threshold = 0; + m_ident_threshold_isSet = false; + mag_dec_adjust = 0; + m_mag_dec_adjust_isSet = false; +} + +void +SWGVORDemodSettings::cleanup() { + + + + + if(title != nullptr) { + delete title; + } + if(audio_device_name != nullptr) { + delete audio_device_name; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + + +} + +SWGVORDemodSettings* +SWGVORDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGVORDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&squelch, pJson["squelch"], "float", ""); + + ::SWGSDRangel::setValue(&volume, pJson["volume"], "float", ""); + + ::SWGSDRangel::setValue(&audio_mute, pJson["audioMute"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&audio_device_name, pJson["audioDeviceName"], "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(&ident_threshold, pJson["identThreshold"], "qint32", ""); + + ::SWGSDRangel::setValue(&mag_dec_adjust, pJson["magDecAdjust"], "qint32", ""); + +} + +QString +SWGVORDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGVORDemodSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_squelch_isSet){ + obj->insert("squelch", QJsonValue(squelch)); + } + if(m_volume_isSet){ + obj->insert("volume", QJsonValue(volume)); + } + if(m_audio_mute_isSet){ + obj->insert("audioMute", QJsonValue(audio_mute)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(audio_device_name != nullptr && *audio_device_name != QString("")){ + toJsonValue(QString("audioDeviceName"), audio_device_name, 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(m_ident_threshold_isSet){ + obj->insert("identThreshold", QJsonValue(ident_threshold)); + } + if(m_mag_dec_adjust_isSet){ + obj->insert("magDecAdjust", QJsonValue(mag_dec_adjust)); + } + + return obj; +} + +float +SWGVORDemodSettings::getSquelch() { + return squelch; +} +void +SWGVORDemodSettings::setSquelch(float squelch) { + this->squelch = squelch; + this->m_squelch_isSet = true; +} + +float +SWGVORDemodSettings::getVolume() { + return volume; +} +void +SWGVORDemodSettings::setVolume(float volume) { + this->volume = volume; + this->m_volume_isSet = true; +} + +qint32 +SWGVORDemodSettings::getAudioMute() { + return audio_mute; +} +void +SWGVORDemodSettings::setAudioMute(qint32 audio_mute) { + this->audio_mute = audio_mute; + this->m_audio_mute_isSet = true; +} + +qint32 +SWGVORDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGVORDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGVORDemodSettings::getTitle() { + return title; +} +void +SWGVORDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +QString* +SWGVORDemodSettings::getAudioDeviceName() { + return audio_device_name; +} +void +SWGVORDemodSettings::setAudioDeviceName(QString* audio_device_name) { + this->audio_device_name = audio_device_name; + this->m_audio_device_name_isSet = true; +} + +qint32 +SWGVORDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGVORDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGVORDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGVORDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGVORDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGVORDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGVORDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGVORDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGVORDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGVORDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGVORDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGVORDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +qint32 +SWGVORDemodSettings::getIdentThreshold() { + return ident_threshold; +} +void +SWGVORDemodSettings::setIdentThreshold(qint32 ident_threshold) { + this->ident_threshold = ident_threshold; + this->m_ident_threshold_isSet = true; +} + +qint32 +SWGVORDemodSettings::getMagDecAdjust() { + return mag_dec_adjust; +} +void +SWGVORDemodSettings::setMagDecAdjust(qint32 mag_dec_adjust) { + this->mag_dec_adjust = mag_dec_adjust; + this->m_mag_dec_adjust_isSet = true; +} + + +bool +SWGVORDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_squelch_isSet){ + isObjectUpdated = true; break; + } + if(m_volume_isSet){ + isObjectUpdated = true; break; + } + if(m_audio_mute_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(audio_device_name && *audio_device_name != 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(m_ident_threshold_isSet){ + isObjectUpdated = true; break; + } + if(m_mag_dec_adjust_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.h new file mode 100644 index 000000000..342b4fe5d --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGVORDemodSettings.h @@ -0,0 +1,137 @@ +/** + * 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: 6.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. + */ + +/* + * SWGVORDemodSettings.h + * + * VORDemod + */ + +#ifndef SWGVORDemodSettings_H_ +#define SWGVORDemodSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGVORDemodSettings: public SWGObject { +public: + SWGVORDemodSettings(); + SWGVORDemodSettings(QString* json); + virtual ~SWGVORDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGVORDemodSettings* fromJson(QString &jsonString) override; + + float getSquelch(); + void setSquelch(float squelch); + + float getVolume(); + void setVolume(float volume); + + qint32 getAudioMute(); + void setAudioMute(qint32 audio_mute); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + QString* getAudioDeviceName(); + void setAudioDeviceName(QString* audio_device_name); + + 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); + + qint32 getIdentThreshold(); + void setIdentThreshold(qint32 ident_threshold); + + qint32 getMagDecAdjust(); + void setMagDecAdjust(qint32 mag_dec_adjust); + + + virtual bool isSet() override; + +private: + float squelch; + bool m_squelch_isSet; + + float volume; + bool m_volume_isSet; + + qint32 audio_mute; + bool m_audio_mute_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + QString* audio_device_name; + bool m_audio_device_name_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; + + qint32 ident_threshold; + bool m_ident_threshold_isSet; + + qint32 mag_dec_adjust; + bool m_mag_dec_adjust_isSet; + +}; + +} + +#endif /* SWGVORDemodSettings_H_ */