diff --git a/.appveyor.yml b/.appveyor.yml index bce5c11b2..e7893e089 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -84,7 +84,7 @@ for: qml-module-qtlocation qml-module-qtpositioning qml-module-qtquick-window2 qml-module-qtquick-dialogs \ qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtgraphicaleffects \ libqt5serialport5-dev qtdeclarative5-dev qtpositioning5-dev qtlocation5-dev \ - libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libqt5svg5-dev libfaad-dev zlib1g-dev \ + libqt5charts5-dev libqt5texttospeech5-dev libqt5gamepad5-dev libqt5svg5-dev libfaad-dev libflac-dev zlib1g-dev \ libusb-1.0-0-dev libhidapi-dev libboost-all-dev libasound2-dev libopencv-dev libopencv-imgcodecs-dev \ libxml2-dev bison flex ffmpeg libpostproc-dev libavcodec-dev libavformat-dev \ libopus-dev libcodec2-dev libairspy-dev libhackrf-dev \ diff --git a/cmake/Modules/FindFLAC.cmake b/cmake/Modules/FindFLAC.cmake new file mode 100644 index 000000000..f6af1b52a --- /dev/null +++ b/cmake/Modules/FindFLAC.cmake @@ -0,0 +1,31 @@ +IF(NOT FLAC_FOUND) + INCLUDE(FindPkgConfig) + PKG_CHECK_MODULES(PC_FLAC flac) + + FIND_PATH( + FLAC_INCLUDE_DIR + NAMES FLAC/stream_encoder.h + HINTS ${PC_FLAC_INCLUDE_DIRS} + PATHS /usr/local/include + /usr/include + ) + + FIND_LIBRARY( + FLAC_LIBRARY + NAMES FLAC + libFLAC + HINTS ${FLAC_DIR}/lib + ${PC_FLAC_LIBRARY_DIRS} + PATHS /usr/local/lib + /usr/lib + /usr/lib64 + ) + + message(STATUS "FLAC LIBRARY " ${FLAC_LIBRARY}) + message(STATUS "FLAC INCLUDE DIR " ${FLAC_INCLUDE_DIR}) + + INCLUDE(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(FLAC DEFAULT_MSG FLAC_LIBRARY FLAC_INCLUDE_DIR) + MARK_AS_ADVANCED(FLAC_LIBRARY FLAC_INCLUDE_DIR) + +ENDIF(NOT FLAC_FOUND) diff --git a/debian/control b/debian/control index 710d236a6..2498d2c48 100644 --- a/debian/control +++ b/debian/control @@ -45,6 +45,7 @@ Build-Depends: debhelper (>= 9), flex, ffmpeg, libfaad-dev, + libflac-dev, libavcodec-dev, libavformat-dev, libopus-dev, diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 618a85899..1b1070471 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -851,6 +851,37 @@ if(ENABLE_FEATURE_SATELLITETRACKER OR ENABLE_CHANNELRX_DEMODAPT) endif () endif () +if(ENABLE_CHANNELRX_REMOTETCPSINK) + if (WIN32) + set(FLAC_LIBRARIES "${SDRANGEL_BINARY_LIB_DIR}/FLAC.lib" CACHE INTERNAL "") + elseif (LINUX) + set(FLAC_LIBRARIES "${EXTERNAL_BUILD_LIBRARIES}/lib${LIB_SUFFIX}/libFLAC${CMAKE_SHARED_LIBRARY_SUFFIX}" CACHE INTERNAL "") + elseif (EMSCRIPTEN) + set(FLAC_LIBRARIES "${EXTERNAL_BUILD_LIBRARIES}/flac/src/flac-build/src/libFLAC/libFLAC.a" CACHE INTERNAL "") + endif() + ExternalProject_Add(flac + GIT_REPOSITORY https://github.com/xiph/flac.git + PREFIX "${EXTERNAL_BUILD_LIBRARIES}/flac" + CMAKE_ARGS ${COMMON_CMAKE_ARGS} -DINSTALL_MANPAGES=OFF -D=BUILD_SHARED_LIBS=ON -DWITH_FORTIFY_SOURCE=OFF -DWITH_STACK_PROTECTOR=PFF -DBUILD_PROGRAMS=OFF -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF -DWITH_OGG=OFF -DBUILD_DOCS=OFF + BUILD_BYPRODUCTS "${FLAC_LIBRARIES}" + INSTALL_COMMAND "" + TEST_COMMAND "" + ) + ExternalProject_Get_Property(flac source_dir binary_dir) + set_global(FLAC_DEPENDS flac) + set_global_cache(FLAC_FOUND ON) + set(FLAC_EXTERNAL ON CACHE INTERNAL "") + set(FLAC_INCLUDE_DIR "${EXTERNAL_BUILD_LIBRARIES}/flac/src/flac/include" CACHE INTERNAL "") + if (WIN32) + install(FILES "${SDRANGEL_BINARY_BIN_DIR}/FLAC${CMAKE_SHARED_LIBRARY_SUFFIX}" DESTINATION "${INSTALL_LIB_DIR}") + elseif (APPLE) + set(FLAC_LIBRARIES "${binary_dir}/flac/FLAC${CMAKE_SHARED_LIBRARY_SUFFIX}" CACHE INTERNAL "") + install(DIRECTORY "${binary_dir}/libFLAC" DESTINATION "${INSTALL_LIB_DIR}" + FILES_MATCHING PATTERN "libFLAC*${CMAKE_SHARED_LIBRARY_SUFFIX}") + set(MACOS_EXTERNAL_LIBS_FIXUP "${MACOS_EXTERNAL_LIBS_FIXUP};${binary_dir}/libFLAC") + endif () +endif () + # For Morse Decoder feature if(ENABLE_FEATURE_MORSEDECODER) if (WIN32) diff --git a/plugins/channelrx/remotetcpsink/CMakeLists.txt b/plugins/channelrx/remotetcpsink/CMakeLists.txt index 2b42dc98e..4d1884a49 100644 --- a/plugins/channelrx/remotetcpsink/CMakeLists.txt +++ b/plugins/channelrx/remotetcpsink/CMakeLists.txt @@ -7,6 +7,7 @@ set(remotetcpsink_SOURCES remotetcpsinksettings.cpp remotetcpsinkwebapiadapter.cpp remotetcpsinkplugin.cpp + socket.cpp ) set(remotetcpsink_HEADERS @@ -17,10 +18,13 @@ set(remotetcpsink_HEADERS remotetcpsinkwebapiadapter.h remotetcpsinkplugin.h remotetcpprotocol.h + socket.h ) include_directories( ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client + ${FLAC_INCLUDE_DIR} + ${ZLIB_INCLUDE_DIRS} ) if(NOT SERVER_MODE) @@ -28,10 +32,13 @@ if(NOT SERVER_MODE) ${remotetcpsink_SOURCES} remotetcpsinkgui.cpp remotetcpsinkgui.ui + remotetcpsinksettingsdialog.cpp + remotetcpsinksettingsdialog.ui ) set(remotetcpsink_HEADERS ${remotetcpsink_HEADERS} remotetcpsinkgui.h + remotetcpsinksettingsdialog.h ) set(TARGET_NAME ${PLUGINS_PREFIX}remotetcpsink) set(TARGET_LIB "Qt::Widgets") @@ -44,16 +51,25 @@ else() set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) endif() -add_library(${TARGET_NAME} SHARED - ${remotetcpsink_SOURCES} -) +if(NOT Qt6_FOUND) + add_library(${TARGET_NAME} ${remotetcpsink_SOURCES}) +else() + qt_add_plugin(${TARGET_NAME} CLASS_NAME RemoteTCPSinkPlugin ${remotetcpsink_SOURCES}) +endif() -target_link_libraries(${TARGET_NAME} +if(NOT BUILD_SHARED_LIBS) + set_property(GLOBAL APPEND PROPERTY STATIC_PLUGINS_PROPERTY ${TARGET_NAME}) +endif() + +target_link_libraries(${TARGET_NAME} PRIVATE Qt::Core + Qt::WebSockets ${TARGET_LIB} sdrbase ${TARGET_LIB_GUI} swagger + ${FLAC_LIBRARIES} + ${ZLIB_LIBRARIES} ) install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channelrx/remotetcpsink/readme.md b/plugins/channelrx/remotetcpsink/readme.md index aefe35deb..1c10da99f 100644 --- a/plugins/channelrx/remotetcpsink/readme.md +++ b/plugins/channelrx/remotetcpsink/readme.md @@ -1,11 +1,25 @@ -

Remote TCP sink channel plugin

+

Remote TCP Sink Channel Plugin

Introduction

-This plugin sends I/Q samples from the baseband via TCP/IP across a network to a client application. -The client application could be SDRangel using the [Remote TCP Input](../../samplesource/remotetcpinput/readme.md) plugin or a rtl_tcp compatible application. +The Remote TCP Sink Channel plugin sends I/Q samples from the baseband via TCP/IP or a Secure WebSocket across a network to a client application. +The client application could be SDRangel using the [Remote TCP Input](../../samplesource/remotetcpinput/readme.md) plugin or an rtl_tcp compatible application. This means that applications using rtl_tcp protocol can connect to the wide variety of SDRs supported by SDRangel. +While the plugin supports the RTL0 protocol for compatibility with older applications, the newer SDRA protocol supports the following additional features: + +- Different bit depths (8, 16, 24 or 32), +- Additional settings, such as decimation, frequency offset and channel gain, +- Device settings can be sent to the client for display, +- IQ compression, using FLAC or zlib, to reduce network bandwidth, +- IQ squelch, to reduce network bandwidth when no signal is being received, +- Real-time forwarding of device/antenna position and direction to client, +- Text messaging between clients and server. + +The Remote TCP Sink can support multiple clients connected simultaneously, with a user-defined maximum client limit. Clients can also have a time limit applied. + +Connection details can optionally be sent to a public database at https://sdrangel.org to allow operation as a WebSDR. Public servers are viewable on the [Map Feature](../../feature/map/readme.md). +

Interface

![Remote TCP sink channel plugin GUI](../../../doc/img/RemoteTCPSink.png) @@ -20,25 +34,85 @@ This is used to select the desired part of the signal when the channel sample ra Sets a gain figure in dB that is applied to I/Q samples before transmission via TCP/IP. This option may be useful for amplifying very small signals from SDRs with high-dynamic range (E.g. 24-bits), when the network sample bit-depth is 8-bits. -

3: Sample rate

+

3: Channel power

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

4: Level meter in dB

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

5: IQ Squelch

+ +Check to enable IQ squelch. When IQ squelch is enabled, if the channel power falls below the specified power level (6), +the plugin will squelch (suppress) all the signal and noise in the channel, +so that it can be transmitted at a very high compression ratio, reducing network bandwidth. + +This option is particularly suitable for packetised data, where the client doesn't need to receive the noise between packets. + +

6: IQ Squelch power level

+ +Sets the power level in dB, below which, IQ data will be squelched. + +

7: IQ Squelch gate time

+ +Sets the IQ squelch gate time. The units can be us (microseconds), ms (milliseconds) or s (seconds). + +

8: IQ Squelch indicator

+ +When IQ squelch is enabled, the icon will have a green background when a signal above the power level (6) is being transmitted and a grey background when the signal is squelched. + +

9: Sample rate

Specifies the channel and network sample rate in samples per second. If this is different from the baseband sample rate, the baseband signal will be decimated to the specified rate. -

4: Sample bit depth

+

10: Sample bit depth

Specifies number of bits per I/Q sample transmitted via TCP/IP. -

5: IP address

+

11: IP address

IP address of the local network interface on which the server will listen for TCP/IP connections from network clients. Use 0.0.0.0 for any interface. -

6: Port

+

12: Port

TCP port on which the server will listen for connections. -

7: Protocol

+

13: Protocol

-Specifies the protocol used for sending IQ samples and metadata to clients via TCP/IP. +Specifies the protocol used for sending IQ samples and metadata to clients: - RTL0: Compatible with rtl_tcp - limited to 8-bit IQ data. -- SDRA: Enhanced version of protocol that allows device settings to be sent to clients and for higher bit depths to be used (8, 16, 24 and 32). +- SDRA: Enhanced version of protocol via TCP Socket. +- SDRA wss: SDRA protocol via a Secure Web Socket instead of a TCP Socket. You should use this with the WebAssembly version of SDRangel. + +

14: Display Settings

+ +Click to open the Settings Dialog. + +

15: Remote Control

+ +When checked, remote clients will be able to change device settings. When unchecked, client requests to change settings will be ignored. + +

16: TX

+ +When pressed, the text message (18) will be transmitted to the clients specified by (17). + +

17: TX Address

+ +Specifies the TCP/IP address and port of the client that the message should be transmitted to, or ALL, if it should be transmitted to all clients. + +

18: TX Message

+ +Specifies a text message to transmit to clients, when the TX button (16) is pressed. + +

19: RX Messages

+ +Displays text messages received from clients. + +

20: Connection Log

+ +Displays a the IP addresses and TCP port numbers of clients that have connected, along with when they connected and disconnected +and how long they were connected for. diff --git a/plugins/channelrx/remotetcpsink/remotetcpprotocol.h b/plugins/channelrx/remotetcpsink/remotetcpprotocol.h index 4803a2190..fb9e7b02d 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpprotocol.h +++ b/plugins/channelrx/remotetcpsink/remotetcpprotocol.h @@ -105,13 +105,23 @@ public: setChannelFreqOffset = 0xc4, setChannelGain = 0xc5, setSampleBitDepth = 0xc6, // Bit depth for samples sent over network + setIQSquelchEnabled = 0xc7, + setIQSquelch = 0xc8, + setIQSquelchGate = 0xc9, //setAntenna? //setLOOffset? + sendMessage = 0xd0, + sendBlacklistedMessage = 0xd1, + dataIQ = 0xf0, // Uncompressed IQ data + dataIQFLAC = 0xf1, // IQ data compressed with FLAC + dataIQzlib = 0xf2, // IQ data compressed with zlib + dataPosition = 0xf3, // Lat, Long, Alt of anntenna + dataDirection = 0xf4 // Az/El of antenna }; static const int m_rtl0MetaDataSize = 12; static const int m_rsp0MetaDataSize = 45; - static const int m_sdraMetaDataSize = 64; + static const int m_sdraMetaDataSize = 128; static void encodeInt16(quint8 *p, qint16 data) { @@ -132,7 +142,7 @@ public: encodeUInt32(p, (quint32)data); } - static qint16 extractInt16(quint8 *p) + static qint16 extractInt16(const quint8 *p) { qint16 data; data = (p[1] & 0xff) @@ -140,7 +150,7 @@ public: return data; } - static quint32 extractUInt32(quint8 *p) + static quint32 extractUInt32(const quint8 *p) { quint32 data; data = (p[3] & 0xff) @@ -150,7 +160,7 @@ public: return data; } - static qint32 extractInt32(quint8 *p) + static qint32 extractInt32(const quint8 *p) { return (qint32)extractUInt32(p); } @@ -167,7 +177,7 @@ public: p[7] = data & 0xff; } - static quint64 extractUInt64(quint8 *p) + static quint64 extractUInt64(const quint8 *p) { quint64 data; data = (p[7] & 0xff) @@ -181,6 +191,27 @@ public: return data; } + static void encodeFloat(quint8 *p, float data) + { + quint32 t; + + memcpy(&t, &data, 4); + + encodeUInt32(p, t); + } + + static float extractFloat(const quint8 *p) + { + quint32 t; + float f; + + t = extractUInt32(p); + + memcpy(&f, &t, 4); + + return f; + } + }; #endif /* PLUGINS_CHANNELRX_REMOTETCPSINK_REMOTETCPPROTOCOL_H_ */ diff --git a/plugins/channelrx/remotetcpsink/remotetcpsink.cpp b/plugins/channelrx/remotetcpsink/remotetcpsink.cpp index 999ee8765..bd9a5084e 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsink.cpp +++ b/plugins/channelrx/remotetcpsink/remotetcpsink.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "SWGChannelSettings.h" #include "SWGWorkspaceInfo.h" @@ -33,13 +35,16 @@ #include "dsp/devicesamplemimo.h" #include "device/deviceapi.h" #include "settings/serializable.h" +#include "channel/channelwebapiutils.h" #include "maincore.h" #include "remotetcpsinkbaseband.h" MESSAGE_CLASS_DEFINITION(RemoteTCPSink::MsgConfigureRemoteTCPSink, Message) MESSAGE_CLASS_DEFINITION(RemoteTCPSink::MsgReportConnection, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPSink::MsgReportDisconnect, Message) MESSAGE_CLASS_DEFINITION(RemoteTCPSink::MsgReportBW, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPSink::MsgSendMessage, Message) const char* const RemoteTCPSink::m_channelIdURI = "sdrangel.channel.remotetcpsink"; const char* const RemoteTCPSink::m_channelId = "RemoteTCPSink"; @@ -47,7 +52,9 @@ const char* const RemoteTCPSink::m_channelId = "RemoteTCPSink"; RemoteTCPSink::RemoteTCPSink(DeviceAPI *deviceAPI) : ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), m_deviceAPI(deviceAPI), - m_basebandSampleRate(0) + m_basebandSampleRate(0), + m_clients(0), + m_removeRequest(nullptr) { setObjectName(m_channelId); @@ -77,7 +84,21 @@ RemoteTCPSink::RemoteTCPSink(DeviceAPI *deviceAPI) : RemoteTCPSink::~RemoteTCPSink() { - qDebug("RemoteTCPSinkBaseband::~RemoteTCPSink"); + qDebug("RemoteTCPSink::~RemoteTCPSink"); + + // Wait until remove listing request is finished + if (m_removeRequest && !m_removeRequest->isFinished()) + { + qDebug() << "RemoteTCPSink::~RemoteTCPSink: Waiting for remove listing request to finish"; + QEventLoop loop; + connect(m_removeRequest, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + } + + if (m_basebandSink->isRunning()) { + stop(); + } + QObject::disconnect( m_networkManager, &QNetworkAccessManager::finished, @@ -88,10 +109,6 @@ RemoteTCPSink::~RemoteTCPSink() m_deviceAPI->removeChannelSinkAPI(this); m_deviceAPI->removeChannelSink(this); - if (m_basebandSink->isRunning()) { - stop(); - } - m_basebandSink->deleteLater(); } @@ -130,6 +147,8 @@ void RemoteTCPSink::start() if (m_basebandSampleRate != 0) { m_basebandSink->setBasebandSampleRate(m_basebandSampleRate); } + + updatePublicListing(); } void RemoteTCPSink::stop() @@ -138,6 +157,9 @@ void RemoteTCPSink::stop() m_basebandSink->stopWork(); m_thread.quit(); m_thread.wait(); + if (m_settings.m_public) { + removePublicListing(m_settings.m_publicAddress, m_settings.m_publicPort); + } } bool RemoteTCPSink::handleMessage(const Message& cmd) @@ -146,7 +168,7 @@ bool RemoteTCPSink::handleMessage(const Message& cmd) { MsgConfigureRemoteTCPSink& cfg = (MsgConfigureRemoteTCPSink&) cmd; qDebug() << "RemoteTCPSink::handleMessage: MsgConfigureRemoteTCPSink"; - applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce(), cfg.getRemoteChange()); + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce(), cfg.getRestartRequired()); return true; } @@ -166,6 +188,28 @@ bool RemoteTCPSink::handleMessage(const Message& cmd) return true; } + else if (MsgSendMessage::match(cmd)) + { + MsgSendMessage& msg = (MsgSendMessage&) cmd; + + // Forward to the sink + m_basebandSink->getInputMessageQueue()->push(MsgSendMessage::create(msg.getAddress(), msg.getPort(), msg.getCallsign(), msg.getText(), msg.getBroadcast())); + return true; + } + else if (MsgReportConnection::match(cmd)) + { + MsgReportConnection& msg = (MsgReportConnection&) cmd; + m_clients = msg.getClients(); + updatePublicListing(); + return true; + } + else if (MsgReportDisconnect::match(cmd)) + { + MsgReportDisconnect& msg = (MsgReportDisconnect&) cmd; + m_clients = msg.getClients(); + updatePublicListing(); + return true; + } else { return false; @@ -208,7 +252,7 @@ void RemoteTCPSink::setCenterFrequency(qint64 frequency) } } -void RemoteTCPSink::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool remoteChange) +void RemoteTCPSink::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool restartRequired) { qDebug() << "RemoteTCPSink::applySettings:" << " settingsKeys: " << settingsKeys @@ -221,7 +265,7 @@ void RemoteTCPSink::applySettings(const RemoteTCPSinkSettings& settings, const Q << " m_protocol: " << settings.m_protocol << " m_streamIndex: " << settings.m_streamIndex << " force: " << force - << " remoteChange: " << remoteChange; + << " restartRequired: " << restartRequired; if (settingsKeys.contains("streamIndex")) { @@ -236,7 +280,7 @@ void RemoteTCPSink::applySettings(const RemoteTCPSinkSettings& settings, const Q } } - MsgConfigureRemoteTCPSink *msg = MsgConfigureRemoteTCPSink::create(settings, settingsKeys, force, remoteChange); + MsgConfigureRemoteTCPSink *msg = MsgConfigureRemoteTCPSink::create(settings, settingsKeys, force, restartRequired); m_basebandSink->getInputMessageQueue()->push(msg); if (settings.m_useReverseAPI) @@ -256,11 +300,31 @@ void RemoteTCPSink::applySettings(const RemoteTCPSinkSettings& settings, const Q sendChannelSettings(pipes, settingsKeys, settings, force); } + // Do we need to remove old listing + bool removeListing = false; + if (m_settings.m_public) + { + if ((settingsKeys.contains("public") || force) && !settings.m_public) { + removeListing = true; + } + if ((settingsKeys.contains("publicAddress") || force) && (settings.m_publicAddress != m_settings.m_publicAddress)) { + removeListing = true; + } + if ((settingsKeys.contains("publicPort") || force) && (settings.m_publicPort != m_settings.m_publicPort)) { + removeListing = true; + } + } + if (removeListing) { + removePublicListing(m_settings.m_publicAddress, m_settings.m_publicPort); + } + if (force) { m_settings = settings; } else { m_settings.applySettings(settingsKeys, settings); } + + updatePublicListing(); } int RemoteTCPSink::webapiSettingsGet( @@ -571,6 +635,10 @@ void RemoteTCPSink::networkManagerFinished(QNetworkReply *reply) qDebug("RemoteTCPSink::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); } + if (reply == m_removeRequest) { + m_removeRequest = nullptr; + } + reply->deleteLater(); } @@ -586,3 +654,88 @@ void RemoteTCPSink::handleIndexInDeviceSetChanged(int index) .arg(index); m_basebandSink->setFifoLabel(fifoLabel); } + +void RemoteTCPSink::removePublicListing(const QString& address, quint16 port) +{ + QUrl url = QUrl("https://sdrangel.org/websdr/removedb.php"); + + QNetworkRequest request; + request.setUrl(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QJsonObject json; + json.insert("address", address); + json.insert("port", port); + + QJsonDocument doc(json); + QByteArray data = doc.toJson(); + + m_removeRequest = m_networkManager->post(request, data); +} + +void RemoteTCPSink::updatePublicListing() +{ + if (!m_settings.m_public || !m_thread.isRunning()) { + return; + } + + QUrl url = QUrl("https://sdrangel.org/websdr/updatedb.php"); + + QNetworkRequest request; + request.setUrl(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + // Get device position + float latitude, longitude, altitude; + + if (!ChannelWebAPIUtils::getDevicePosition(getDeviceSetIndex(), latitude, longitude, altitude)) + { + latitude = MainCore::instance()->getSettings().getLatitude(); + longitude = MainCore::instance()->getSettings().getLongitude(); + altitude = MainCore::instance()->getSettings().getAltitude(); + } + // FIXME: Optionally slightly obfuscate position + + // Get antenna direction + double azimuth = m_settings.m_azimuth; + double elevation = m_settings.m_elevation; + if (!m_settings.m_isotropic && !m_settings.m_rotator.isEmpty() && (m_settings.m_rotator != "None")) + { + unsigned int rotatorFeatureSetIndex; + unsigned int rotatorFeatureIndex; + + if (MainCore::getFeatureIndexFromId(m_settings.m_rotator, rotatorFeatureSetIndex, rotatorFeatureIndex)) + { + ChannelWebAPIUtils::getFeatureReportValue(rotatorFeatureSetIndex, rotatorFeatureIndex, "currentAzimuth", azimuth); + ChannelWebAPIUtils::getFeatureReportValue(rotatorFeatureSetIndex, rotatorFeatureIndex, "currentElevation", elevation); + } + } + + QString device = MainCore::instance()->getDevice(getDeviceSetIndex())->getHardwareId(); + + QJsonObject json; + json.insert("address", m_settings.m_publicAddress); + json.insert("port", m_settings.m_publicPort); + json.insert("minFrequency", m_settings.m_minFrequency); + json.insert("maxFrequency", m_settings.m_maxFrequency); + json.insert("maxSampleRate", m_settings.m_maxSampleRate); + json.insert("device", device); + json.insert("antenna", m_settings.m_antenna); + json.insert("remoteControl", (int) m_settings.m_remoteControl); + json.insert("stationName", MainCore::instance()->getSettings().getStationName()); + json.insert("location", m_settings.m_location); + json.insert("latitude", latitude); + json.insert("longitude", longitude); + json.insert("altitude", altitude); + json.insert("isotropic", (int) m_settings.m_isotropic); + json.insert("azimuth", azimuth); + json.insert("elevation", elevation); + json.insert("clients", m_clients); + json.insert("maxClients", m_settings.m_maxClients); + json.insert("timeLimit", m_settings.m_timeLimit); + + QJsonDocument doc(json); + QByteArray data = doc.toJson(); + + m_networkManager->post(request, data); +} diff --git a/plugins/channelrx/remotetcpsink/remotetcpsink.h b/plugins/channelrx/remotetcpsink/remotetcpsink.h index 9533053d3..f36923dd7 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsink.h +++ b/plugins/channelrx/remotetcpsink/remotetcpsink.h @@ -42,25 +42,25 @@ public: const RemoteTCPSinkSettings& getSettings() const { return m_settings; } const QList& getSettingsKeys() const { return m_settingsKeys; } bool getForce() const { return m_force; } - bool getRemoteChange() const { return m_remoteChange; } + bool getRestartRequired() const { return m_restartRequired; } - static MsgConfigureRemoteTCPSink* create(const RemoteTCPSinkSettings& settings, const QList& settingsKeys, bool force, bool remoteChange = false) + static MsgConfigureRemoteTCPSink* create(const RemoteTCPSinkSettings& settings, const QList& settingsKeys, bool force, bool restartRequired = false) { - return new MsgConfigureRemoteTCPSink(settings, settingsKeys, force, remoteChange); + return new MsgConfigureRemoteTCPSink(settings, settingsKeys, force, restartRequired); } private: RemoteTCPSinkSettings m_settings; QList m_settingsKeys; bool m_force; - bool m_remoteChange; // This change of settings was requested by a remote client, so no need to restart server + bool m_restartRequired; - MsgConfigureRemoteTCPSink(const RemoteTCPSinkSettings& settings, const QList& settingsKeys, bool force, bool remoteChange) : + MsgConfigureRemoteTCPSink(const RemoteTCPSinkSettings& settings, const QList& settingsKeys, bool force, bool restartRequired) : Message(), m_settings(settings), m_settingsKeys(settingsKeys), m_force(force), - m_remoteChange(remoteChange) + m_restartRequired(restartRequired) { } }; @@ -69,39 +69,117 @@ public: public: int getClients() const { return m_clients; } + const QHostAddress& getAddress() const { return m_address; } + int getPort() const { return m_port; } - static MsgReportConnection* create(int clients) + + static MsgReportConnection* create(int clients, const QHostAddress& address, quint16 port) { - return new MsgReportConnection(clients); + return new MsgReportConnection(clients, address, port); } private: int m_clients; + QHostAddress m_address; + quint16 m_port; - MsgReportConnection(int clients) : + MsgReportConnection(int clients, const QHostAddress& address, quint16 port) : Message(), - m_clients(clients) + m_clients(clients), + m_address(address), + m_port(port) { } }; - // Message to report actual transmit bandwidth in bits per second + class MsgReportDisconnect : public Message { + MESSAGE_CLASS_DECLARATION + + public: + int getClients() const { return m_clients; } + const QHostAddress& getAddress() const { return m_address; } + quint16 getPort() const { return m_port; } + + static MsgReportDisconnect* create(int clients, const QHostAddress& address, quint16 port) + { + return new MsgReportDisconnect(clients, address, port); + } + + private: + int m_clients; + QHostAddress m_address; + quint16 m_port; + + MsgReportDisconnect(int clients, const QHostAddress& address, quint16 port) : + Message(), + m_clients(clients), + m_address(address), + m_port(port) + { } + }; + + // Message to report actual transmit bandwidth in bits per second and compression ratio class MsgReportBW : public Message { MESSAGE_CLASS_DECLARATION public: float getBW() const { return m_bw; } + float getNetworkBW() const { return m_networkBW; } + qint64 getBytesUncompressed() const { return m_bytesUncompressed; } + qint64 getBytesCompressed() const { return m_bytesCompressed; } + qint64 getBytesTransmitted() const { return m_bytesTransmitted; } - static MsgReportBW* create(float bw) + static MsgReportBW* create(float bw, float networkBW, qint64 bytesUncompressed, qint64 bytesCompressed, qint64 bytesTransmitted) { - return new MsgReportBW(bw); + return new MsgReportBW(bw, networkBW, bytesUncompressed, bytesCompressed, bytesTransmitted); } private: float m_bw; + float m_networkBW; + qint64 m_bytesUncompressed; + qint64 m_bytesCompressed; + qint64 m_bytesTransmitted; - MsgReportBW(float bw) : + MsgReportBW(float bw, float networkBW, qint64 bytesUncompressed, qint64 bytesCompressed, qint64 bytesTransmitted) : Message(), - m_bw(bw) + m_bw(bw), + m_networkBW(networkBW), + m_bytesUncompressed(bytesUncompressed), + m_bytesCompressed(bytesCompressed), + m_bytesTransmitted(bytesTransmitted) + { } + }; + + // Send a text message to a client (or received message from client) + class MsgSendMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QHostAddress getAddress() const { return m_address; } + quint16 getPort() const { return m_port; } + const QString& getCallsign() const { return m_callsign; } + const QString& getText() const { return m_text; } + bool getBroadcast() const { return m_broadcast; } + + static MsgSendMessage* create(QHostAddress address, quint16 port, const QString& callsign, const QString& text, bool broadcast) + { + return new MsgSendMessage(address, port, callsign, text, broadcast); + } + + private: + QHostAddress m_address; + quint16 m_port; + QString m_callsign; + QString m_text; + bool m_broadcast; + + MsgSendMessage(QHostAddress address, quint16 port, const QString& callsign, const QString& text, bool broadcast) : + Message(), + m_address(address), + m_port(port), + m_callsign(callsign), + m_text(text), + m_broadcast(broadcast) { } }; @@ -164,7 +242,17 @@ public: uint32_t getNumberOfDeviceStreams() const; int getBasebandSampleRate() const { return m_basebandSampleRate; } - void setMessageQueueToGUI(MessageQueue* queue) override { + bool getSquelchOpen() const { return m_basebandSink && m_basebandSink->getSquelchOpen(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_basebandSink) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } else { + avg = 0.0; peak = 0.0; nbSamples = 1; + } + } + void setMessageQueueToGUI(MessageQueue* queue) final { ChannelAPI::setMessageQueueToGUI(queue); m_basebandSink->setMessageQueueToGUI(queue); } @@ -183,8 +271,11 @@ private: QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + int m_clients; // Number of clients currently connected + QNetworkReply *m_removeRequest; + virtual bool handleMessage(const Message& cmd); - void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool remoteChange = false); + void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool restartRequired = false); void webapiReverseSendSettings(const QStringList& channelSettingsKeys, const RemoteTCPSinkSettings& settings, bool force); void sendChannelSettings( const QList& pipes, @@ -198,6 +289,8 @@ private: const RemoteTCPSinkSettings& settings, bool force ); + void removePublicListing(const QString& address, quint16 port); + void updatePublicListing(); private slots: void networkManagerFinished(QNetworkReply *reply); diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.cpp b/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.cpp index c23ce0985..95373af9b 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.cpp +++ b/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.cpp @@ -128,7 +128,7 @@ bool RemoteTCPSinkBaseband::handleMessage(const Message& cmd) RemoteTCPSink::MsgConfigureRemoteTCPSink& cfg = (RemoteTCPSink::MsgConfigureRemoteTCPSink&) cmd; qDebug() << "RemoteTCPSinkBaseband::handleMessage: MsgConfigureRemoteTCPSink"; - applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce(), cfg.getRemoteChange()); + applySettings(cfg.getSettings(), cfg.getSettingsKeys(), cfg.getForce(), cfg.getRestartRequired()); return true; } @@ -141,13 +141,21 @@ bool RemoteTCPSinkBaseband::handleMessage(const Message& cmd) return true; } + else if (RemoteTCPSink::MsgSendMessage::match(cmd)) + { + RemoteTCPSink::MsgSendMessage& msg = (RemoteTCPSink::MsgSendMessage&) cmd; + + m_sink.sendMessage(msg.getAddress(), msg.getPort(), msg.getCallsign(), msg.getText(), msg.getBroadcast()); + + return true; + } else { return false; } } -void RemoteTCPSinkBaseband::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool remoteChange) +void RemoteTCPSinkBaseband::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool restartRequired) { qDebug() << "RemoteTCPSinkBaseband::applySettings:" << "m_channelSampleRate:" << settings.m_channelSampleRate @@ -160,7 +168,7 @@ void RemoteTCPSinkBaseband::applySettings(const RemoteTCPSinkSettings& settings, m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); } - m_sink.applySettings(settings, settingsKeys, force, remoteChange); + m_sink.applySettings(settings, settingsKeys, force, restartRequired); if (force) { m_settings = settings; } else { diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.h b/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.h index 88ffe7337..a3710b6c4 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.h +++ b/plugins/channelrx/remotetcpsink/remotetcpsinkbaseband.h @@ -47,6 +47,8 @@ public: MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication int getChannelSampleRate() const; + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { m_sink.getMagSqLevels(avg, peak, nbSamples); } + bool getSquelchOpen() const { return m_sink.getSquelchOpen(); } void setMessageQueueToGUI(MessageQueue *messageQueue) { m_sink.setMessageQueueToGUI(messageQueue); } void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } void setBasebandSampleRate(int sampleRate); @@ -64,7 +66,7 @@ private: QRecursiveMutex m_mutex; bool handleMessage(const Message& cmd); - void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool remoteChange = false); + void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool restartRequired = false); private slots: void handleInputMessages(); diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.cpp b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.cpp index 5da31d488..4dbb9773a 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.cpp +++ b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 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 // @@ -15,19 +15,25 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include #include #include +#include #include "device/deviceuiset.h" #include "gui/basicchannelsettingsdialog.h" #include "gui/dialpopup.h" #include "gui/dialogpositioner.h" +#include "gui/perioddial.h" #include "dsp/dspcommands.h" +#include "util/db.h" +#include "maincore.h" #include "remotetcpsinkgui.h" #include "remotetcpsink.h" #include "ui_remotetcpsinkgui.h" +#include "remotetcpsinksettingsdialog.h" + +const QString RemoteTCPSinkGUI::m_dateTimeFormat = "yyyy.MM.dd hh:mm:ss"; RemoteTCPSinkGUI* RemoteTCPSinkGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *channelRx) { @@ -102,11 +108,74 @@ QString RemoteTCPSinkGUI::displayScaledF(float value, char type, int precision, } } +void RemoteTCPSinkGUI::resizeTable() +{ + QDateTime dateTime = QDateTime::currentDateTime(); + QString dateTimeString = dateTime.toString(m_dateTimeFormat); + int row = ui->connections->rowCount(); + ui->connections->setRowCount(row + 1); + ui->connections->setItem(row, CONNECTIONS_COL_ADDRESS, new QTableWidgetItem("255.255.255.255")); + ui->connections->setItem(row, CONNECTIONS_COL_PORT, new QTableWidgetItem("65535")); + ui->connections->setItem(row, CONNECTIONS_COL_CONNECTED, new QTableWidgetItem(dateTimeString)); + ui->connections->setItem(row, CONNECTIONS_COL_DISCONNECTED, new QTableWidgetItem(dateTimeString)); + ui->connections->setItem(row, CONNECTIONS_COL_TIME, new QTableWidgetItem("1000 d")); + ui->connections->resizeColumnsToContents(); + ui->connections->removeRow(row); +} + +void RemoteTCPSinkGUI::addConnection(const QHostAddress& address, int port) +{ + QDateTime dateTime = QDateTime::currentDateTime(); + + int row = ui->connections->rowCount(); + ui->connections->setRowCount(row + 1); + + ui->connections->setItem(row, CONNECTIONS_COL_ADDRESS, new QTableWidgetItem(address.toString())); + ui->connections->setItem(row, CONNECTIONS_COL_PORT, new QTableWidgetItem(QString::number(port))); + ui->connections->setItem(row, CONNECTIONS_COL_CONNECTED, new QTableWidgetItem(dateTime.toString(m_dateTimeFormat))); + ui->connections->setItem(row, CONNECTIONS_COL_DISCONNECTED, new QTableWidgetItem("")); + ui->connections->setItem(row, CONNECTIONS_COL_TIME, new QTableWidgetItem("")); +} + +void RemoteTCPSinkGUI::removeConnection(const QHostAddress& address, int port) +{ + QString addressString = address.toString(); + QString portString = QString::number(port); + + for (int row = 0; row < ui->connections->rowCount(); row++) + { + if ((ui->connections->item(row, CONNECTIONS_COL_ADDRESS)->text() == addressString) + && (ui->connections->item(row, CONNECTIONS_COL_PORT)->text() == portString) + && (ui->connections->item(row, CONNECTIONS_COL_DISCONNECTED)->text().isEmpty())) + { + QDateTime connected = QDateTime::fromString(ui->connections->item(row, CONNECTIONS_COL_CONNECTED)->text(), m_dateTimeFormat); + QDateTime disconnected = QDateTime::currentDateTime(); + QString dateTimeString = disconnected.toString(m_dateTimeFormat); + QString time; + int secs = connected.secsTo(disconnected); + if (secs < 60) { + time = QString("%1 s").arg(secs); + } else if (secs < 60 * 60) { + time = QString("%1 m").arg(secs / 60); + } else if (secs < 60 * 60 * 24) { + time = QString("%1 h").arg(secs / 60 / 60); + } else { + time = QString("%1 d").arg(secs / 60 / 60 / 24); + } + + ui->connections->item(row, CONNECTIONS_COL_DISCONNECTED)->setText(dateTimeString); + ui->connections->item(row, CONNECTIONS_COL_TIME)->setText(time); + break; + } + } +} + bool RemoteTCPSinkGUI::handleMessage(const Message& message) { if (RemoteTCPSink::MsgConfigureRemoteTCPSink::match(message)) { const RemoteTCPSink::MsgConfigureRemoteTCPSink& cfg = (RemoteTCPSink::MsgConfigureRemoteTCPSink&) message; + if ((cfg.getSettings().m_channelSampleRate != m_settings.m_channelSampleRate) || (cfg.getSettings().m_sampleBits != m_settings.m_sampleBits)) { m_bwAvg.reset(); @@ -126,14 +195,52 @@ bool RemoteTCPSinkGUI::handleMessage(const Message& message) else if (RemoteTCPSink::MsgReportConnection::match(message)) { const RemoteTCPSink::MsgReportConnection& report = (RemoteTCPSink::MsgReportConnection&) message; - ui->clients->setText(QString("%1").arg(report.getClients())); + + ui->clients->setText(QString("%1/%2").arg(report.getClients()).arg(m_settings.m_maxClients)); + QString ip = QString("%1:%2").arg(report.getAddress().toString()).arg(report.getPort()); + if (ui->txAddress->findText(ip) == -1) { + ui->txAddress->addItem(ip); + } + addConnection(report.getAddress(), report.getPort()); + + return true; + } + else if (RemoteTCPSink::MsgReportDisconnect::match(message)) + { + const RemoteTCPSink::MsgReportDisconnect& report = (RemoteTCPSink::MsgReportDisconnect&) message; + + ui->clients->setText(QString("%1/%2").arg(report.getClients()).arg(m_settings.m_maxClients)); + QString ip = QString("%1:%2").arg(report.getAddress().toString()).arg(report.getPort()); + int idx = ui->txAddress->findText(ip); + if (idx != -1) { + ui->txAddress->removeItem(idx); + } + removeConnection(report.getAddress(), report.getPort()); + return true; } else if (RemoteTCPSink::MsgReportBW::match(message)) { const RemoteTCPSink::MsgReportBW& report = (RemoteTCPSink::MsgReportBW&) message; + m_bwAvg(report.getBW()); - ui->bw->setText(QString("%1bps").arg(displayScaledF(m_bwAvg.instantAverage(), 'f', 3, true))); + m_networkBWAvg(report.getNetworkBW()); + + QString text = QString("%1bps").arg(displayScaledF(m_bwAvg.instantAverage(), 'f', 1, true)); + + if (!m_settings.m_iqOnly && (report.getBytesUncompressed() > 0)) + { + float compressionSaving = 1.0f - (report.getBytesCompressed() / (float) report.getBytesUncompressed()); + m_compressionAvg(compressionSaving); + + QString compressionText = QString(" %1%").arg((int) std::round(m_compressionAvg.instantAverage() * 100.0f)); + text.append(compressionText); + } + + QString networkBWText = QString(" %1bps").arg(displayScaledF(m_networkBWAvg.instantAverage(), 'f', 1, true)); + text.append(networkBWText); + + ui->bw->setText(text); return true; } else if (DSPSignalNotification::match(message)) @@ -150,6 +257,25 @@ bool RemoteTCPSinkGUI::handleMessage(const Message& message) return true; } + else if (RemoteTCPSink::MsgSendMessage::match(message)) + { + RemoteTCPSink::MsgSendMessage& msg = (RemoteTCPSink::MsgSendMessage&) message; + QString address = QString("%1:%2").arg(msg.getAddress().toString()).arg(msg.getPort()); + QString callsign = msg.getCallsign(); + QString text = msg.getText(); + bool broadcast = msg.getBroadcast(); + + // Display received message in GUI + ui->messages->addItem(QString("%1/%2> %3").arg(address).arg(callsign).arg(text)); + ui->messages->scrollToBottom(); + + // Forward to other clients + if (broadcast) { + m_remoteSink->getInputMessageQueue()->push(RemoteTCPSink::MsgSendMessage::create(msg.getAddress(), msg.getPort(), callsign, text, broadcast)); + } + + return true; + } else { return false; @@ -157,12 +283,14 @@ bool RemoteTCPSinkGUI::handleMessage(const Message& message) } RemoteTCPSinkGUI::RemoteTCPSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *channelrx, QWidget* parent) : - ChannelGUI(parent), - ui(new Ui::RemoteTCPSinkGUI), - m_pluginAPI(pluginAPI), - m_deviceUISet(deviceUISet), - m_basebandSampleRate(0), - m_deviceCenterFrequency(0) + ChannelGUI(parent), + ui(new Ui::RemoteTCPSinkGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_basebandSampleRate(0), + m_deviceCenterFrequency(0), + m_tickCount(0), + m_squelchOpen(false) { setAttribute(Qt::WA_DeleteOnClose, true); m_helpURL = "plugins/channelrx/remotetcpsink/readme.md"; @@ -177,6 +305,8 @@ RemoteTCPSinkGUI::RemoteTCPSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISe m_remoteSink->setMessageQueueToGUI(getInputMessageQueue()); m_basebandSampleRate = m_remoteSink->getBasebandSampleRate(); + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + m_channelMarker.blockSignals(true); m_channelMarker.setColor(m_settings.m_rgbColor); m_channelMarker.setCenterFrequency(0); @@ -189,12 +319,17 @@ RemoteTCPSinkGUI::RemoteTCPSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISe m_deviceUISet->addChannelMarker(&m_channelMarker); + ui->txAddress->clear(); + ui->txAddress->addItem("All"); + ui->channelSampleRate->setColorMapper(ColorMapper(ColorMapper::GrayGreenYellow)); ui->channelSampleRate->setValueRange(8, 0, 99999999); ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); +#ifndef __EMSCRIPTEN__ // Add all IP addresses for (const QHostAddress& address: QNetworkInterface::allAddresses()) { @@ -202,11 +337,14 @@ RemoteTCPSinkGUI::RemoteTCPSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISe ui->dataAddress->addItem(address.toString()); } } +#endif connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + resizeTable(); + displaySettings(); makeUIConnections(); applyAllSettings(); @@ -269,12 +407,39 @@ void RemoteTCPSinkGUI::displaySettings() ui->dataAddress->addItem(m_settings.m_dataAddress); } ui->dataAddress->setCurrentText(m_settings.m_dataAddress); - ui->dataPort->setText(tr("%1").arg(m_settings.m_dataPort)); + ui->dataPort->setValue(m_settings.m_dataPort); ui->protocol->setCurrentIndex((int)m_settings.m_protocol); + ui->remoteControl->setChecked(m_settings.m_remoteControl); + ui->squelchEnabled->setChecked(m_settings.m_squelchEnabled); + displayIQOnly(); + displaySquelch(); getRollupContents()->restoreState(m_rollupState); blockApplySettings(false); } +void RemoteTCPSinkGUI::displayIQOnly() +{ + ui->messagesLayout->setEnabled(!m_settings.m_iqOnly); + ui->sendMessage->setEnabled(!m_settings.m_iqOnly); + ui->txAddress->setEnabled(!m_settings.m_iqOnly); + ui->txMessage->setEnabled(!m_settings.m_iqOnly); + ui->messagesContainer->setVisible(!m_settings.m_iqOnly); +} + +void RemoteTCPSinkGUI::displaySquelch() +{ + ui->squelch->setValue(m_settings.m_squelch); + ui->squelchText->setText(QString::number(m_settings.m_squelch)); + ui->squelch->setEnabled(m_settings.m_squelchEnabled); + ui->squelchText->setEnabled(m_settings.m_squelchEnabled); + ui->squelchUnits->setEnabled(m_settings.m_squelchEnabled); + + ui->squelchGate->setValue(m_settings.m_squelchGate); + ui->squelchGate->setEnabled(m_settings.m_squelchEnabled); + + ui->audioMute->setEnabled(m_settings.m_squelchEnabled); +} + void RemoteTCPSinkGUI::displayRateAndShift() { m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); @@ -430,20 +595,9 @@ void RemoteTCPSinkGUI::on_dataAddress_currentIndexChanged(int index) applySetting("dataAddress"); } -void RemoteTCPSinkGUI::on_dataPort_editingFinished() +void RemoteTCPSinkGUI::on_dataPort_valueChanged(int value) { - bool dataOk; - int dataPort = ui->dataPort->text().toInt(&dataOk); - - if((!dataOk) || (dataPort < 1024) || (dataPort > 65535)) - { - return; - } - else - { - m_settings.m_dataPort = dataPort; - } - + m_settings.m_dataPort = value; applySetting("dataPort"); } @@ -453,6 +607,104 @@ void RemoteTCPSinkGUI::on_protocol_currentIndexChanged(int index) applySetting("protocol"); } +void RemoteTCPSinkGUI::on_remoteControl_toggled(bool checked) +{ + m_settings.m_remoteControl = checked; + applySetting("remoteControl"); +} + +void RemoteTCPSinkGUI::on_squelchEnabled_toggled(bool checked) +{ + m_settings.m_squelchEnabled = checked; + applySetting("squelchEnabled"); + displaySquelch(); +} + +void RemoteTCPSinkGUI::on_squelch_valueChanged(int value) +{ + m_settings.m_squelch = value; + ui->squelchText->setText(QString::number(m_settings.m_squelch)); + applySetting("squelch"); +} + +void RemoteTCPSinkGUI::on_squelchGate_valueChanged(double value) +{ + m_settings.m_squelchGate = value; + applySetting("squelchGate"); +} + +void RemoteTCPSinkGUI::on_displaySettings_clicked() +{ + RemoteTCPSinkSettingsDialog dialog(&m_settings); + + new DialogPositioner(&dialog, true); + if (dialog.exec() == QDialog::Accepted) + { + applySettings(dialog.getSettingsKeys()); + displayIQOnly(); + } +} + +void RemoteTCPSinkGUI::on_sendMessage_clicked() +{ + QString message = ui->txMessage->text().trimmed(); + if (!message.isEmpty()) + { + ui->messages->addItem(QString("< %1").arg(message)); + ui->messages->scrollToBottom(); + bool broadcast = ui->txAddress->currentText() == "All"; + QHostAddress address; + quint16 port = 0; + if (!broadcast) + { + QStringList parts = ui->txAddress->currentText().split(':'); + address = QHostAddress(parts[0]); + port = parts[1].toInt(); + } + QString callsign = MainCore::instance()->getSettings().getStationName(); + m_remoteSink->getInputMessageQueue()->push(RemoteTCPSink::MsgSendMessage::create(address, port, callsign, message, broadcast)); + } +} + +void RemoteTCPSinkGUI::on_txMessage_returnPressed() +{ + on_sendMessage_clicked(); + ui->txMessage->selectAll(); +} + +void RemoteTCPSinkGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_remoteSink->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(tr("%1").arg(powDbAvg, 0, 'f', 1)); + } + + bool squelchOpen = m_remoteSink->getSquelchOpen() || !m_settings.m_squelchEnabled; + + if (squelchOpen != m_squelchOpen) + { + /*if (squelchOpen) { + ui->audioMute->setStyleSheet("QToolButton { background-color : green; }"); + } else { + ui->audioMute->setStyleSheet("QToolButton { background:rgb(79,79,79); }"); + }*/ + ui->audioMute->setChecked(!squelchOpen); + m_squelchOpen = squelchOpen; + } + + m_tickCount++; +} + void RemoteTCPSinkGUI::makeUIConnections() { QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &RemoteTCPSinkGUI::on_deltaFrequency_changed); @@ -461,8 +713,15 @@ void RemoteTCPSinkGUI::makeUIConnections() QObject::connect(ui->sampleBits, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPSinkGUI::on_sampleBits_currentIndexChanged); QObject::connect(ui->dataAddress->lineEdit(), &QLineEdit::editingFinished, this, &RemoteTCPSinkGUI::on_dataAddress_editingFinished); QObject::connect(ui->dataAddress, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPSinkGUI::on_dataAddress_currentIndexChanged); - QObject::connect(ui->dataPort, &QLineEdit::editingFinished, this, &RemoteTCPSinkGUI::on_dataPort_editingFinished); + QObject::connect(ui->dataPort, QOverload::of(&QSpinBox::valueChanged), this, &RemoteTCPSinkGUI::on_dataPort_valueChanged); QObject::connect(ui->protocol, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPSinkGUI::on_protocol_currentIndexChanged); + QObject::connect(ui->remoteControl, &ButtonSwitch::toggled, this, &RemoteTCPSinkGUI::on_remoteControl_toggled); + QObject::connect(ui->squelchEnabled, &ButtonSwitch::toggled, this, &RemoteTCPSinkGUI::on_squelchEnabled_toggled); + QObject::connect(ui->squelch, &QDial::valueChanged, this, &RemoteTCPSinkGUI::on_squelch_valueChanged); + QObject::connect(ui->squelchGate, &PeriodDial::valueChanged, this, &RemoteTCPSinkGUI::on_squelchGate_valueChanged); + QObject::connect(ui->displaySettings, &QToolButton::clicked, this, &RemoteTCPSinkGUI::on_displaySettings_clicked); + QObject::connect(ui->sendMessage, &QToolButton::clicked, this, &RemoteTCPSinkGUI::on_sendMessage_clicked); + QObject::connect(ui->txMessage, &QLineEdit::returnPressed, this, &RemoteTCPSinkGUI::on_txMessage_returnPressed); } void RemoteTCPSinkGUI::updateAbsoluteCenterFrequency() diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.h b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.h index 1dc58bd61..c44933b26 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.h +++ b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.h @@ -25,6 +25,7 @@ #include #include +#include #include "dsp/channelmarker.h" #include "channel/channelgui.h" @@ -82,9 +83,23 @@ private: bool m_doApplySettings; RemoteTCPSink* m_remoteSink; + uint32_t m_tickCount; + bool m_squelchOpen; MessageQueue m_inputMessageQueue; MovingAverageUtil m_bwAvg; + MovingAverageUtil m_compressionAvg; + MovingAverageUtil m_networkBWAvg; + + enum ConnectionsCol { + CONNECTIONS_COL_ADDRESS, + CONNECTIONS_COL_PORT, + CONNECTIONS_COL_CONNECTED, + CONNECTIONS_COL_DISCONNECTED, + CONNECTIONS_COL_TIME + }; + + static const QString m_dateTimeFormat; explicit RemoteTCPSinkGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); virtual ~RemoteTCPSinkGUI(); @@ -94,11 +109,16 @@ private: void applySettings(const QStringList& settingsKeys, bool force = false); void applyAllSettings(); void displaySettings(); + void displayIQOnly(); + void displaySquelch(); void displayRateAndShift(); bool handleMessage(const Message& message); void makeUIConnections(); QString displayScaledF(float value, char type, int precision, bool showMult); void updateAbsoluteCenterFrequency(); + void resizeTable(); + void addConnection(const QHostAddress& address, int port); + void removeConnection(const QHostAddress& address, int port); void leaveEvent(QEvent*); void enterEvent(EnterEventType*); @@ -111,10 +131,18 @@ private slots: void on_sampleBits_currentIndexChanged(int index); void on_dataAddress_editingFinished(); void on_dataAddress_currentIndexChanged(int index); - void on_dataPort_editingFinished(); + void on_dataPort_valueChanged(int value); void on_protocol_currentIndexChanged(int index); + void on_remoteControl_toggled(bool checked); + void on_squelchEnabled_toggled(bool checked); + void on_squelch_valueChanged(int value); + void on_squelchGate_valueChanged(double value); + void on_displaySettings_clicked(); + void on_sendMessage_clicked(); + void on_txMessage_returnPressed(); void onWidgetRolled(QWidget* widget, bool rollDown); void onMenuDialogCalled(const QPoint& p); + void tick(); }; #endif /* PLUGINS_CHANNELRX_REMOTETCPSINK_REMOTETCPSINKGUI_H_ */ diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.ui b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.ui index 28963c6be..72308406e 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinkgui.ui +++ b/plugins/channelrx/remotetcpsink/remotetcpsinkgui.ui @@ -7,7 +7,7 @@ 0 0 360 - 147 + 646 @@ -24,7 +24,7 @@ - 560 + 1000 16777215 @@ -41,8 +41,8 @@ 0 0 - 340 - 141 + 361 + 191 @@ -174,8 +174,217 @@ + + + + Qt::Vertical + + + + + + + + 30 + 0 + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + + + Check to enable IQ squelch + + + SQ + + + + + + + Qt::Vertical + + + + + + + + 24 + 24 + + + + IQ squelch power level in dB + + + -150 + + + 0 + + + 1 + + + + + + + + 32 + 0 + + + + -150 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + + 40 + 0 + + + + IQ squelch gate time + + + + + + + Qt::Vertical + + + + + + + false + + + Indicates when IQ squelch is open + + + ... + + + + :/sound_on.png + :/sound_off.png:/sound_on.png + + + true + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + @@ -325,21 +534,15 @@ - - - - 50 - 16777215 - - + TCP port for server to listen for connections on - - 00000 + + 1024 - - 0 + + 65535 @@ -365,9 +568,18 @@ + + + 80 + 0 + + Protocol to transmit data with + + 0 + RTL0 @@ -378,12 +590,41 @@ SDRA + + + SDRA wss + + + + + + + + Display settings dialog + + + + + + + :/listing.png:/listing.png + + + + + Allow remote control + + + RC + + + @@ -407,10 +648,10 @@ - Number of clients connected + Number of clients connected / maximum clients - 0 + 0/0 @@ -431,10 +672,14 @@ - Transmit bandwidth for a single TCP connection averaged over the last 10 seconds in bits per second + - Channel IQ bandwidth in bits per second +- Compression saving in percent +- Total outgoing network bandwidth in bits per second + +Values are averaged over the last 10 seconds - 0.000Mbps + 0.0Mbps 0% 0.0Mbps @@ -442,8 +687,151 @@ + + + + 0 + 200 + 351 + 191 + + + + Messages + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + TX + + + + + + + + 140 + 0 + + + + + 127.127.127.127:1234 + + + + + + + + + + + + + + + + + + + + + + 0 + 410 + 351 + 191 + + + + Connection Log + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Address + + + IP address of client + + + + + Port + + + TCP port number of client + + + + + Connected + + + Date and time client connected + + + + + Disconnected + + + Date and time client disconnected + + + + + Time + + + Time client was connected for + + + + + + + + + + ButtonSwitch + QToolButton +
gui/buttonswitch.h
+
RollupContents QWidget @@ -456,12 +844,24 @@
gui/valuedialz.h
1
+ + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
ValueDial QWidget
gui/valuedial.h
1
+ + PeriodDial + QWidget +
gui/perioddial.h
+ 1 +
diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksettings.cpp b/plugins/channelrx/remotetcpsink/remotetcpsinksettings.cpp index 19aeb6d26..3eabba5c4 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinksettings.cpp +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksettings.cpp @@ -38,6 +38,31 @@ void RemoteTCPSinkSettings::resetToDefaults() m_dataAddress = "0.0.0.0"; m_dataPort = 1234; m_protocol = SDRA; + m_iqOnly = false; + m_compression = FLAC; + m_compressionLevel = 5; + m_blockSize = 16384; + m_squelchEnabled = false; + m_squelch = -100.0f; + m_squelchGate = 0.001f; + m_remoteControl = true; + m_maxClients = 4; + m_timeLimit = 0; + m_maxSampleRate = 10000000; + m_certificate = ""; + m_key = ""; + m_public = false; + m_publicAddress = ""; + m_publicPort = 1234; + m_minFrequency = 0; + m_maxFrequency = 2000000000; + m_antenna = ""; + m_location = ""; + m_ipBlacklist = QStringList(); + m_isotropic = true; + m_azimuth = 0.0f; + m_elevation = 0.0f; + m_rotator = "None"; m_rgbColor = QColor(140, 4, 4).rgb(); m_title = "Remote TCP sink"; m_channelMarker = nullptr; @@ -62,6 +87,33 @@ QByteArray RemoteTCPSinkSettings::serialize() const s.writeString(5, m_dataAddress); s.writeU32(6, m_dataPort); s.writeS32(7, (int)m_protocol); + s.writeBool(42, m_iqOnly); + s.writeS32(29, m_compression); + s.writeS32(38, m_compressionLevel); + s.writeS32(39, m_blockSize); + s.writeBool(40, m_squelchEnabled); + s.writeFloat(41, m_squelch); + s.writeFloat(43, m_squelchGate); + s.writeBool(23, m_remoteControl); + s.writeS32(24, m_maxClients); + s.writeS32(25, m_timeLimit); + s.writeS32(28, m_maxSampleRate); + s.writeString(26, m_certificate); + s.writeString(27, m_key); + + s.writeBool(30, m_public); + s.writeString(31, m_publicAddress); + s.writeS32(32, m_publicPort); + s.writeS64(33, m_minFrequency); + s.writeS64(34, m_maxFrequency); + s.writeString(35, m_antenna); + s.writeString(37, m_location); + s.writeList(36, m_ipBlacklist); + s.writeBool(44, m_isotropic); + s.writeFloat(45, m_azimuth); + s.writeFloat(46, m_elevation); + s.writeString(47, m_rotator); + s.writeU32(8, m_rgbColor); s.writeString(9, m_title); s.writeBool(10, m_useReverseAPI); @@ -115,6 +167,33 @@ bool RemoteTCPSinkSettings::deserialize(const QByteArray& data) m_dataPort = 1234; } d.readS32(7, (int *)&m_protocol, (int)SDRA); + d.readBool(42, &m_iqOnly, false); + d.readS32(29, (int *)&m_compression, (int)FLAC); + d.readS32(38, &m_compressionLevel, 5); + d.readS32(39, &m_blockSize, 16384); + d.readBool(40, &m_squelchEnabled, false); + d.readFloat(41, &m_squelch, -100.0f); + d.readFloat(43, &m_squelchGate, 0.001f); + d.readBool(23, &m_remoteControl, true); + d.readS32(24, &m_maxClients, 4); + d.readS32(25, &m_timeLimit, 0); + d.readS32(28, &m_maxSampleRate, 10000000); + + d.readString(26, &m_certificate, ""); + d.readString(27, &m_key, ""); + + d.readBool(30, &m_public, false); + d.readString(31, &m_publicAddress, ""); + d.readS32(32, &m_publicPort, 1234); + d.readS64(33, &m_minFrequency, 0); + d.readS64(34, &m_maxFrequency, 2000000000); + d.readString(35, &m_antenna, ""); + d.readString(37, &m_location, ""); + d.readList(36, &m_ipBlacklist); + d.readBool(44, &m_isotropic, true); + d.readFloat(45, &m_azimuth, 0.0f); + d.readFloat(46, &m_elevation, 0.0f); + d.readString(47, &m_rotator, "None"); d.readU32(8, &m_rgbColor, QColor(0, 255, 255).rgb()); d.readString(9, &m_title, "Remote TCP sink"); @@ -182,6 +261,78 @@ void RemoteTCPSinkSettings::applySettings(const QStringList& settingsKeys, const if (settingsKeys.contains("protocol")) { m_protocol = settings.m_protocol; } + if (settingsKeys.contains("iqOnly")) { + m_iqOnly = settings.m_iqOnly; + } + if (settingsKeys.contains("compression")) { + m_compression = settings.m_compression; + } + if (settingsKeys.contains("compressionLevel")) { + m_compressionLevel = settings.m_compressionLevel; + } + if (settingsKeys.contains("blockSize")) { + m_blockSize = settings.m_blockSize; + } + if (settingsKeys.contains("squelchEnabled")) { + m_squelchEnabled = settings.m_squelchEnabled; + } + if (settingsKeys.contains("squelch")) { + m_squelch = settings.m_squelch; + } + if (settingsKeys.contains("squelchGate")) { + m_squelchGate = settings.m_squelchGate; + } + if (settingsKeys.contains("remoteControl")) { + m_remoteControl = settings.m_remoteControl; + } + if (settingsKeys.contains("maxClients")) { + m_maxClients = settings.m_maxClients; + } + if (settingsKeys.contains("timeLimit")) { + m_timeLimit = settings.m_timeLimit; + } + if (settingsKeys.contains("maxSampleRate")) { + m_maxSampleRate = settings.m_maxSampleRate; + } + if (settingsKeys.contains("certificate")) { + m_certificate = settings.m_certificate; + } + if (settingsKeys.contains("key")) { + m_key = settings.m_key; + } + if (settingsKeys.contains("public")) { + m_public = settings.m_public; + } + if (settingsKeys.contains("publicAddress")) { + m_publicAddress = settings.m_publicAddress; + } + if (settingsKeys.contains("publicPort")) { + m_publicPort = settings.m_publicPort; + } + if (settingsKeys.contains("minFrequency")) { + m_minFrequency = settings.m_minFrequency; + } + if (settingsKeys.contains("maxFrequency")) { + m_maxFrequency = settings.m_maxFrequency; + } + if (settingsKeys.contains("antenna")) { + m_antenna = settings.m_antenna; + } + if (settingsKeys.contains("ipBlacklist")) { + m_ipBlacklist = settings.m_ipBlacklist; + } + if (settingsKeys.contains("isotrophic")) { + m_isotropic = settings.m_isotropic; + } + if (settingsKeys.contains("azimuth")) { + m_azimuth = settings.m_azimuth; + } + if (settingsKeys.contains("elevation")) { + m_elevation = settings.m_elevation; + } + if (settingsKeys.contains("rotator")) { + m_rotator = settings.m_rotator; + } if (settingsKeys.contains("rgbColor")) { m_rgbColor = settings.m_rgbColor; } @@ -239,6 +390,78 @@ QString RemoteTCPSinkSettings::getDebugString(const QStringList& settingsKeys, b if (settingsKeys.contains("protocol") || force) { ostr << " m_protocol: " << m_protocol; } + if (settingsKeys.contains("iqOnly") || force) { + ostr << " m_iqOnly: " << m_iqOnly; + } + if (settingsKeys.contains("compression") || force) { + ostr << " m_compression: " << m_compression; + } + if (settingsKeys.contains("compressionLevel") || force) { + ostr << " m_compressionLevel: " << m_compressionLevel; + } + if (settingsKeys.contains("blockSize") || force) { + ostr << " m_blockSize: " << m_blockSize; + } + if (settingsKeys.contains("squelchEnabled") || force) { + ostr << " m_squelchEnabled: " << m_squelchEnabled; + } + if (settingsKeys.contains("squelch") || force) { + ostr << " m_squelch: " << m_squelch; + } + if (settingsKeys.contains("squelchGate") || force) { + ostr << " m_squelchGate: " << m_squelchGate; + } + if (settingsKeys.contains("remoteControl") || force) { + ostr << " m_remoteControl: " << m_remoteControl; + } + if (settingsKeys.contains("maxClients") || force) { + ostr << " m_maxClients: " << m_maxClients; + } + if (settingsKeys.contains("timeLimit") || force) { + ostr << " m_timeLimit: " << m_timeLimit; + } + if (settingsKeys.contains("maxSampleRate") || force) { + ostr << " m_maxSampleRate: " << m_maxSampleRate; + } + if (settingsKeys.contains("certificate") || force) { + ostr << " m_certificate: " << m_certificate.toStdString(); + } + if (settingsKeys.contains("key") || force) { + ostr << " m_key: " << m_key.toStdString(); + } + if (settingsKeys.contains("public") || force) { + ostr << " m_public: " << m_public; + } + if (settingsKeys.contains("publicAddress") || force) { + ostr << " m_publicAddress: " << m_publicAddress.toStdString(); + } + if (settingsKeys.contains("publicPort") || force) { + ostr << " m_publicPort: " << m_publicPort; + } + if (settingsKeys.contains("minFrequency") || force) { + ostr << " m_minFrequency: " << m_minFrequency; + } + if (settingsKeys.contains("maxFrequency") || force) { + ostr << " m_maxFrequency: " << m_maxFrequency; + } + if (settingsKeys.contains("antenna") || force) { + ostr << " m_antenna: " << m_antenna.toStdString(); + } + if (settingsKeys.contains("ipBlacklist") || force) { + ostr << " m_ipBlacklist: " << m_ipBlacklist.join(" ").toStdString(); + } + if (settingsKeys.contains("isotrophic") || force) { + ostr << " m_isotropic: " << m_isotropic; + } + if (settingsKeys.contains("azimuth") || force) { + ostr << " m_azimuth: " << m_azimuth; + } + if (settingsKeys.contains("elevation") || force) { + ostr << " m_elevation: " << m_elevation; + } + if (settingsKeys.contains("rotator") || force) { + ostr << " m_rotator: " << m_rotator.toStdString(); + } if (settingsKeys.contains("rgbColor") || force) { ostr << " m_rgbColor: " << m_rgbColor; } diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksettings.h b/plugins/channelrx/remotetcpsink/remotetcpsinksettings.h index 7c54ea47e..74900631e 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinksettings.h +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksettings.h @@ -24,6 +24,7 @@ #include #include +#include class Serializable; @@ -31,7 +32,13 @@ struct RemoteTCPSinkSettings { enum Protocol { RTL0, // Compatible with rtl_tcp - SDRA // SDRangel remote TCP protocol which extends rtl_tcp + SDRA, // SDRangel remote TCP protocol which extends rtl_tcp + SDRA_WSS // SDRA using WebSocket Secure + }; + + enum Compressor { + FLAC, + ZLIB }; qint32 m_channelSampleRate; @@ -41,6 +48,34 @@ struct RemoteTCPSinkSettings QString m_dataAddress; uint16_t m_dataPort; enum Protocol m_protocol; + bool m_iqOnly; // Send uncompressed IQ only (No position or messages) + Compressor m_compression; // How IQ stream is compressed + int m_compressionLevel; + int m_blockSize; + bool m_squelchEnabled; + float m_squelch; + float m_squelchGate; // In seconds + bool m_remoteControl; // Whether remote control is enabled + int m_maxClients; + int m_timeLimit; // Time limit per connection in minutes, if server busy. 0 = no limit. + int m_maxSampleRate; + + QString m_certificate; // SSL certificate + QString m_key; // SSL key + + bool m_public; // Whether to list publically + QString m_publicAddress; // IP address / host for public listing + int m_publicPort; // What port number for public listing + qint64 m_minFrequency; // Minimum frequency for public listing + qint64 m_maxFrequency; // Maximum frequency for public listing + QString m_antenna; // Anntenna description for public listing + QString m_location; // Anntenna location for public listing + QStringList m_ipBlacklist; // List of IP addresses to refuse connections from + bool m_isotropic; // Antenna is isotropic + float m_azimuth; // Antenna azimuth angle + float m_elevation; // Antenna elevation angle + QString m_rotator; // Id of Rotator Controller feature to get az/el from + quint32 m_rgbColor; QString m_title; int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.cpp b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.cpp new file mode 100644 index 000000000..b9b22a107 --- /dev/null +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.cpp @@ -0,0 +1,370 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "remotetcpsinksettingsdialog.h" +#include "ui_remotetcpsinksettingsdialog.h" + +RemoteTCPSinkSettingsDialog::RemoteTCPSinkSettingsDialog(RemoteTCPSinkSettings *settings, QWidget* parent) : + QDialog(parent), + ui(new Ui::RemoteTCPSinkSettingsDialog), + m_settings(settings), + m_availableRotatorHandler({"sdrangel.feature.gs232controller"}) +{ + ui->setupUi(this); + + ui->maxClients->setValue(m_settings->m_maxClients); + ui->timeLimit->setValue(m_settings->m_timeLimit); + ui->maxSampleRate->setValue(m_settings->m_maxSampleRate); + ui->iqOnly->setChecked(m_settings->m_iqOnly); + + ui->compressor->setCurrentIndex((int) m_settings->m_compression); + ui->compressionLevel->setValue(m_settings->m_compressionLevel); + ui->blockSize->setCurrentIndex(ui->blockSize->findText(QString::number(m_settings->m_blockSize))); + + ui->certificate->setText(m_settings->m_certificate); + ui->key->setText(m_settings->m_key); + + ui->publicListing->setChecked(m_settings->m_public); + ui->publicAddress->setText(m_settings->m_publicAddress); + ui->publicPort->setValue(m_settings->m_publicPort); + ui->minFrequency->setValue(m_settings->m_minFrequency / 1000000); + ui->maxFrequency->setValue(m_settings->m_maxFrequency / 1000000); + ui->antenna->setText(m_settings->m_antenna); + ui->location->setText(m_settings->m_location); + ui->isotropic->setChecked(m_settings->m_isotropic); + ui->azimuth->setValue(m_settings->m_azimuth); + ui->elevation->setValue(m_settings->m_elevation); + ui->rotator->setCurrentText(m_settings->m_rotator); + + for (const auto& ip : m_settings->m_ipBlacklist) { + ui->ipBlacklist->addItem(ip); + } + + QObject::connect( + &m_availableRotatorHandler, + &AvailableChannelOrFeatureHandler::channelsOrFeaturesChanged, + this, + &RemoteTCPSinkSettingsDialog::rotatorsChanged + ); + m_availableRotatorHandler.scanAvailableChannelsAndFeatures(); +} + +RemoteTCPSinkSettingsDialog::~RemoteTCPSinkSettingsDialog() +{ + delete ui; +} + +void RemoteTCPSinkSettingsDialog::accept() +{ + if (!isValid()) { + return; + } + + QDialog::accept(); + + if (ui->maxClients->value() != m_settings->m_maxClients) + { + m_settings->m_maxClients = ui->maxClients->value(); + m_settingsKeys.append("maxClients"); + } + if (ui->timeLimit->value() != m_settings->m_timeLimit) + { + m_settings->m_timeLimit = ui->timeLimit->value(); + m_settingsKeys.append("timeLimit"); + } + if (ui->maxSampleRate->value() != m_settings->m_maxSampleRate) + { + m_settings->m_maxSampleRate = ui->maxSampleRate->value(); + m_settingsKeys.append("maxSampleRate"); + } + if (ui->iqOnly->isChecked() != m_settings->m_iqOnly) + { + m_settings->m_iqOnly = ui->iqOnly->isChecked(); + m_settingsKeys.append("iqOnly"); + } + RemoteTCPSinkSettings::Compressor compressor = (RemoteTCPSinkSettings::Compressor) ui->compressor->currentIndex(); + if (compressor != m_settings->m_compression) + { + m_settings->m_compression = compressor; + m_settingsKeys.append("compression"); + } + if (ui->compressionLevel->value() != m_settings->m_compressionLevel) + { + m_settings->m_compressionLevel = ui->compressionLevel->value(); + m_settingsKeys.append("compressionLevel"); + } + int blockSize = ui->blockSize->currentText().toInt(); + if (blockSize != m_settings->m_blockSize) + { + m_settings->m_blockSize = blockSize; + m_settingsKeys.append("blockSize"); + } + if (ui->certificate->text() != m_settings->m_certificate) + { + m_settings->m_certificate = ui->certificate->text(); + m_settingsKeys.append("certificate"); + } + if (ui->key->text() != m_settings->m_key) + { + m_settings->m_key = ui->key->text(); + m_settingsKeys.append("key"); + } + if (ui->publicListing->isChecked() != m_settings->m_public) + { + m_settings->m_public = ui->publicListing->isChecked(); + m_settingsKeys.append("public"); + } + if (ui->publicAddress->text() != m_settings->m_publicAddress) + { + m_settings->m_publicAddress = ui->publicAddress->text(); + m_settingsKeys.append("publicAddress"); + } + if (ui->publicPort->value() != m_settings->m_publicPort) + { + m_settings->m_publicPort = ui->publicPort->value(); + m_settingsKeys.append("publicPort"); + } + qint64 minFrequency = ui->minFrequency->value() * 1000000; + if (minFrequency != m_settings->m_minFrequency) + { + m_settings->m_minFrequency = minFrequency; + m_settingsKeys.append("minFrequency"); + } + qint64 maxFrequency = ui->maxFrequency->value() * 1000000; + if (maxFrequency != m_settings->m_maxFrequency) + { + m_settings->m_maxFrequency = maxFrequency; + m_settingsKeys.append("maxFrequency"); + } + if (ui->antenna->text() != m_settings->m_antenna) + { + m_settings->m_antenna = ui->antenna->text(); + m_settingsKeys.append("antenna"); + } + if (ui->location->text() != m_settings->m_location) + { + m_settings->m_location = ui->location->text(); + m_settingsKeys.append("location"); + } + if (ui->isotropic->isChecked() != m_settings->m_isotropic) + { + m_settings->m_isotropic = ui->isotropic->isChecked(); + m_settingsKeys.append("isotropic"); + } + if (ui->azimuth->value() != m_settings->m_azimuth) + { + m_settings->m_azimuth = ui->azimuth->value(); + m_settingsKeys.append("azimuth"); + } + if (ui->elevation->value() != m_settings->m_elevation) + { + m_settings->m_elevation = ui->elevation->value(); + m_settingsKeys.append("elevation"); + } + if (ui->rotator->currentText() != m_settings->m_rotator) + { + m_settings->m_rotator = ui->rotator->currentText(); + m_settingsKeys.append("rotator"); + } + QStringList ipBlacklist; + for (int i = 0; i < ui->ipBlacklist->count(); i++) + { + QString ip = ui->ipBlacklist->item(i)->text().trimmed(); + if (!ip.isEmpty()) { + ipBlacklist.append(ip); + } + } + if (ipBlacklist != m_settings->m_ipBlacklist) + { + m_settings->m_ipBlacklist = ipBlacklist; + m_settingsKeys.append("ipBlacklist"); + } +} + +void RemoteTCPSinkSettingsDialog::on_browseCertificate_clicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Select SSL Certificate"), + "", + tr("SSL certificate (*.cert *.pem)")); + if (!fileName.isEmpty()) { + ui->certificate->setText(fileName); + } +} + +void RemoteTCPSinkSettingsDialog::on_browseKey_clicked() +{ + QString fileName = QFileDialog::getOpenFileName(this, tr("Select SSL Key"), + "", + tr("SSL key (*.key *.pem)")); + if (!fileName.isEmpty()) { + ui->key->setText(fileName); + } +} + +void RemoteTCPSinkSettingsDialog::on_addIP_clicked() +{ + QListWidgetItem *item = new QListWidgetItem("1.1.1.1"); + item->setFlags(Qt::ItemIsEditable | item->flags()); + ui->ipBlacklist->addItem(item); + item->setSelected(true); +} + +void RemoteTCPSinkSettingsDialog::on_removeIP_clicked() +{ + qDeleteAll(ui->ipBlacklist->selectedItems()); +} + +void RemoteTCPSinkSettingsDialog::on_publicListing_toggled() +{ + displayValid(); + displayEnabled(); +} + +void RemoteTCPSinkSettingsDialog::on_publicAddress_textChanged() +{ + displayValid(); +} + +void RemoteTCPSinkSettingsDialog::on_compressor_currentIndexChanged(int index) +{ + if (index == 0) + { + // FLAC settings + ui->compressionLevel->setMaximum(8); + ui->blockSize->clear(); + ui->blockSize->addItem("4096"); + ui->blockSize->addItem("16384"); + ui->blockSize->setCurrentIndex(1); + } + else if (index == 1) + { + // zlib settings + ui->compressionLevel->setMaximum(9); + ui->blockSize->clear(); + ui->blockSize->addItem("4096"); + ui->blockSize->addItem("8192"); + ui->blockSize->addItem("16384"); + ui->blockSize->addItem("32768"); + ui->blockSize->setCurrentIndex(3); + } +} + +void RemoteTCPSinkSettingsDialog::on_iqOnly_toggled(bool checked) +{ + ui->compressionSettings->setEnabled(!checked); +} + +void RemoteTCPSinkSettingsDialog::on_isotropic_toggled(bool checked) +{ + displayEnabled(); +} + +void RemoteTCPSinkSettingsDialog::on_rotator_currentIndexChanged(int index) +{ + (void) index; + + displayEnabled(); +} + +bool RemoteTCPSinkSettingsDialog::isValid() +{ + bool valid = true; + + if (ui->publicListing->isChecked() && ui->publicAddress->text().isEmpty()) { + valid = false; + } + + return valid; +} + +void RemoteTCPSinkSettingsDialog::displayValid() +{ + bool valid = isValid(); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); +} + +void RemoteTCPSinkSettingsDialog::displayEnabled() +{ + bool enabled = ui->publicListing->isChecked(); + bool none = ui->rotator->currentText() == "None"; + bool isotropic = ui->isotropic->isChecked(); + + ui->publicAddressLabel->setEnabled(enabled); + ui->publicAddress->setEnabled(enabled); + ui->publicPort->setEnabled(enabled); + ui->frequencyLabel->setEnabled(enabled); + ui->minFrequency->setEnabled(enabled); + ui->maxFrequency->setEnabled(enabled); + ui->frequencyUnits->setEnabled(enabled); + ui->antennaLabel->setEnabled(enabled); + ui->antenna->setEnabled(enabled); + ui->locationLabel->setEnabled(enabled); + ui->location->setEnabled(enabled); + ui->isotropicLabel->setEnabled(enabled); + ui->isotropic->setEnabled(enabled); + ui->rotatorLabel->setEnabled(enabled && !isotropic); + ui->rotator->setEnabled(enabled && !isotropic); + ui->directionLabel->setEnabled(enabled && !isotropic && none); + ui->azimuthLabel->setEnabled(enabled && !isotropic && none); + ui->azimuth->setEnabled(enabled && !isotropic && none); + ui->elevationLabel->setEnabled(enabled && !isotropic && none); + ui->elevation->setEnabled(enabled && !isotropic && none); +} + +void RemoteTCPSinkSettingsDialog::rotatorsChanged(const QStringList& renameFrom, const QStringList& renameTo) +{ + AvailableChannelOrFeatureList rotators = m_availableRotatorHandler.getAvailableChannelOrFeatureList(); + updateRotatorList(rotators, renameFrom, renameTo); +} + +void RemoteTCPSinkSettingsDialog::updateRotatorList(const AvailableChannelOrFeatureList& rotators, const QStringList& renameFrom, const QStringList& renameTo) +{ + // Update rotator settting if it has been renamed + if (renameFrom.contains(m_settings->m_rotator)) { + m_settings->m_rotator = renameTo[renameFrom.indexOf(m_settings->m_rotator)]; + } + + // Update list of rotators + ui->rotator->blockSignals(true); + ui->rotator->clear(); + ui->rotator->addItem("None"); + + for (const auto& rotator : rotators) { + ui->rotator->addItem(rotator.getLongId()); + } + + // Rotator feature can be created after this plugin, so select it + // if the chosen rotator appears + int rotatorIndex = ui->rotator->findText(m_settings->m_rotator); + + if (rotatorIndex >= 0) + { + ui->rotator->setCurrentIndex(rotatorIndex); + } + else + { + ui->rotator->setCurrentIndex(0); // return to None + } + + ui->rotator->blockSignals(false); + + displayEnabled(); +} \ No newline at end of file diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.h b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.h new file mode 100644 index 000000000..c136abf23 --- /dev/null +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.h @@ -0,0 +1,65 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_REMOTETCPSINKSETTINGSDIALOG_H +#define INCLUDE_REMOTETCPSINKSETTINGSDIALOG_H + +#include + +#include "availablechannelorfeaturehandler.h" + +#include "remotetcpsinksettings.h" + +namespace Ui { + class RemoteTCPSinkSettingsDialog; +} + +class RemoteTCPSinkSettingsDialog : public QDialog { + Q_OBJECT +public: + explicit RemoteTCPSinkSettingsDialog(RemoteTCPSinkSettings *settings, QWidget* parent = nullptr); + ~RemoteTCPSinkSettingsDialog(); + + const QStringList& getSettingsKeys() const { return m_settingsKeys; }; + +private slots: + void accept(); + void on_browseCertificate_clicked(); + void on_browseKey_clicked(); + void on_addIP_clicked(); + void on_removeIP_clicked(); + void on_publicListing_toggled(); + void on_publicAddress_textChanged(); + void on_compressor_currentIndexChanged(int index); + void on_iqOnly_toggled(bool checked); + void on_isotropic_toggled(bool checked); + void on_rotator_currentIndexChanged(int index); + void rotatorsChanged(const QStringList& renameFrom, const QStringList& renameTo); + +private: + Ui::RemoteTCPSinkSettingsDialog *ui; + RemoteTCPSinkSettings *m_settings; + QStringList m_settingsKeys; + AvailableChannelOrFeatureHandler m_availableRotatorHandler; + + bool isValid(); + void displayValid(); + void displayEnabled(); + void updateRotatorList(const AvailableChannelOrFeatureList& rotators, const QStringList& renameFrom, const QStringList& renameTo); +}; + +#endif // INCLUDE_REMOTETCPSINKSETTINGSDIALOG_H diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.ui b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.ui new file mode 100644 index 000000000..269659df7 --- /dev/null +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksettingsdialog.ui @@ -0,0 +1,675 @@ + + + RemoteTCPSinkSettingsDialog + + + + 0 + 0 + 409 + 947 + + + + + 9 + + + + Settings + + + + + + Server Settings + + + + + + Maximum channel sample rate + + + 2700 + + + 100000000 + + + 10000000 + + + + + + + Time Limit + + + + + + + Max Clients + + + + + + + Connection time limit in minutes if max clients reached. 0 for no limit. + + + 100000 + + + + + + + Max Ch. Sample Rate + + + + + + + S/s + + + + + + + Maximum number of simultaneous clients + + + 0 + + + 10000 + + + + + + + mins + + + + + + + IQ only + + + + + + + Transmit uncompressed IQ only. Disables compression, position and messaging support. + + + + + + + + + + + + + Compression + + + + + + Compressor + + + + + + + + FLAC + + + + + zlib + + + + + + + + Compression Level + + + + + + + 0 - Least compression. 8 - Most compression Higher compression requires more CPU. + + + 8 + + + + + + + Block size + + + + + + + + 4096 + + + + + 16384 + + + + + + + + + + + SSL Settings + + + + + + SSL certificate + + + + + + + ... + + + + :/load.png:/load.png + + + + + + + Key + + + + + + + SSL certificate key + + + + + + + + 0 + 0 + + + + Certificate + + + + + + + ... + + + + :/load.png:/load.png + + + + + + + + + + Public Directory + + + + + + false + + + Antenna + + + + + + + false + + + Minimum recommend frequency in MHz + + + 20000 + + + + + + + false + + + Publically accessible port number + + + 1024 + + + 65535 + + + 1234 + + + + + + + false + + + Direction + + + + + + + false + + + Town and country where antenna is located + + + 255 + + + + + + + List Server + + + + + + + false + + + Isotropic + + + + + + + + + false + + + Az + + + + + + + false + + + Antenna azimuth in degrees + + + 3 + + + 360.000000000000000 + + + + + + + Qt::Vertical + + + + + + + false + + + El + + + + + + + false + + + Antenna elevation in degrees + + + 3 + + + -90.000000000000000 + + + 90.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + false + + + Maximum recommend frequency in MHz + + + 20000 + + + + + + + false + + + Address + + + + + + + false + + + Location + + + + + + + false + + + MHz + + + + + + + false + + + Publically accessible IP address or hostname + + + + + + + false + + + Check to indicate an antenna that is isotropic (non-directional) + + + + + + + + + + Whether to list the server as publically accessible + + + + + + + + + + false + + + Frequency Range + + + + + + + false + + + Antenna description + + + 255 + + + + + + + false + + + Rotator + + + + + + + false + + + Rotator feature to get antenna direction from + + + + None + + + + + + + + + + + IP Blacklist + + + + + + List of IP addresses from which connections should not be allowed + + + QAbstractItemView::MultiSelection + + + + + + + + + + + + + + + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + maxClients + timeLimit + maxSampleRate + iqOnly + compressor + compressionLevel + blockSize + certificate + browseCertificate + key + browseKey + publicListing + publicAddress + publicPort + minFrequency + maxFrequency + antenna + location + isotropic + rotator + azimuth + elevation + ipBlacklist + addIP + removeIP + + + + + + + buttonBox + accepted() + RemoteTCPSinkSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RemoteTCPSinkSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksink.cpp b/plugins/channelrx/remotetcpsink/remotetcpsinksink.cpp index b00072440..3e985cc7b 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinksink.cpp +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksink.cpp @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Jiří Pinkava // // // // This program is free software; you can redistribute it and/or modify // @@ -18,6 +18,9 @@ #include #include +#include +#include +#include #include "channel/channelwebapiutils.h" #include "device/deviceapi.h" @@ -26,22 +29,69 @@ #include "remotetcpsinksink.h" #include "remotetcpsink.h" +static FLAC__StreamEncoderWriteStatus flacWriteCallback(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, uint32_t samples, uint32_t currentFrame, void *clientData) +{ + RemoteTCPSinkSink *sink = (RemoteTCPSinkSink*) clientData; + + return sink->flacWrite(encoder, buffer, bytes, samples, currentFrame); +} + RemoteTCPSinkSink::RemoteTCPSinkSink() : - m_running(false), - m_messageQueueToGUI(nullptr), - m_messageQueueToChannel(nullptr), - m_channelFrequencyOffset(0), - m_channelSampleRate(48000), - m_linearGain(1.0f), - m_server(nullptr) + m_running(false), + m_messageQueueToGUI(nullptr), + m_messageQueueToChannel(nullptr), + m_channelFrequencyOffset(0), + m_channelSampleRate(48000), + m_linearGain(1.0f), + m_server(nullptr), + m_webSocketServer(nullptr), + m_encoder(nullptr), + m_zStreamInitialised(false), + m_zInBuf(m_zBufSize, '\0'), + m_zOutBuf(m_zBufSize, '\0'), + m_zInBufCount(0), + m_bytesUncompressed(0), + m_bytesCompressed(0), + m_bytesTransmitted(0), + m_squelchLevel(-150.0f), + m_squelchCount(0), + m_squelchOpen(false), + m_squelchDelayLine(48000/2), + m_magsq(0.0f), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_centerFrequency(0.0), + m_ppmCorrection(0), + m_biasTeeEnabled(false), + m_directSampling(false), + m_agc(false), + m_dcOffsetRemoval(false), + m_iqCorrection(false), + m_devSampleRate(0), + m_log2Decim(0), + m_gain(), + m_rfBW(0), + m_timer(this), + m_azimuth(std::numeric_limits::quiet_NaN()), + m_elevation(std::numeric_limits::quiet_NaN()) { qDebug("RemoteTCPSinkSink::RemoteTCPSinkSink"); applySettings(m_settings, QStringList(), true); applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); + + // Get updated when position changes + connect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &RemoteTCPSinkSink::preferenceChanged); + + m_timer.setSingleShot(false); + m_timer.setInterval(500); + connect(&m_timer, &QTimer::timeout, this, &RemoteTCPSinkSink::checkDeviceSettings); } RemoteTCPSinkSink::~RemoteTCPSinkSink() { + qDebug("RemoteTCPSinkSink::~RemoteTCPSinkSink"); + disconnect(&MainCore::instance()->getSettings(), &MainSettings::preferenceChanged, this, &RemoteTCPSinkSink::preferenceChanged); stop(); } @@ -71,6 +121,7 @@ void RemoteTCPSinkSink::started() QMutexLocker mutexLocker(&m_mutex); startServer(); disconnect(thread(), SIGNAL(started()), this, SLOT(started())); + m_timer.start(); } void RemoteTCPSinkSink::finished() @@ -78,6 +129,7 @@ void RemoteTCPSinkSink::finished() QMutexLocker mutexLocker(&m_mutex); stopServer(); disconnect(thread(), SIGNAL(finished()), this, SLOT(finished())); + m_timer.stop(); m_running = false; } @@ -119,19 +171,27 @@ void RemoteTCPSinkSink::feed(const SampleVector::const_iterator& begin, const Sa } } + for (const auto client : m_clients) { + client->flush(); + } + QDateTime currentDateTime = QDateTime::currentDateTime(); if (m_bwDateTime.isValid()) { - QDateTime currentDateTime = QDateTime::currentDateTime(); qint64 msecs = m_bwDateTime.msecsTo(currentDateTime) ; - if (msecs > 1000) + if (msecs >= 1000) { - float bw = (8*m_bwBytes)/(msecs/1000.0f); + float secs = msecs / 1000.0f; + float bw = (8 * m_bwBytes) / secs; + float networkBW = (8 * m_bytesTransmitted) / secs; if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgReportBW::create(bw)); + m_messageQueueToGUI->push(RemoteTCPSink::MsgReportBW::create(bw, networkBW, m_bytesUncompressed, m_bytesCompressed, m_bytesTransmitted)); } m_bwDateTime = currentDateTime; m_bwBytes = bytes; + m_bytesUncompressed = 0; + m_bytesCompressed = 0; + m_bytesTransmitted = 0; } else { @@ -140,73 +200,226 @@ void RemoteTCPSinkSink::feed(const SampleVector::const_iterator& begin, const Sa } else { - m_bwDateTime = QDateTime::currentDateTime(); + m_bwDateTime = currentDateTime; m_bwBytes = bytes; } } } +static qint32 clamp8(qint32 x) +{ + x = std::max(x, -128); + x = std::min(x, 127); + return x; +} + +static qint32 clamp16(qint32 x) +{ + x = std::max(x, -32768); + x = std::min(x, 32767); + return x; +} + +static qint32 clamp24(qint32 x) +{ + x = std::max(x, -8388608); + x = std::min(x, 8388607); + return x; +} + void RemoteTCPSinkSink::processOneSample(Complex &ci) { - if (m_settings.m_sampleBits == 8) + // Apply gain + ci = ci * m_linearGain; + + // Calculate channel power + Real re = ci.real(); + Real im = ci.imag(); + Real magsq = (re*re + im*im) / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + m_magsqPeak = std::max(magsq, m_magsqPeak); + m_magsqCount++; + + // Squelch + if (m_settings.m_squelchEnabled) { - // Transmit data as per rtl_tcp - Interleaved unsigned 8-bit IQ - quint8 iqBuf[2]; - iqBuf[0] = ((int)(ci.real() / SDR_RX_SCALEF * 256.0f * m_linearGain)) + 128; - iqBuf[1] = ((int)(ci.imag() / SDR_RX_SCALEF * 256.0f * m_linearGain)) + 128; - for (auto client : m_clients) { - client->write((const char *)iqBuf, sizeof(iqBuf)); + // Convert gate time from seconds to samples + int squelchGate = m_settings.m_squelchGate * m_channelSampleRate; + + m_squelchDelayLine.write(ci); + + if (m_magsq < m_squelchLevel) + { + if (m_squelchCount > 0) { + m_squelchCount--; + } + } + else + { + m_squelchCount = squelchGate; + } + m_squelchOpen = m_squelchCount > 0; + + if (m_squelchOpen) { + ci = m_squelchDelayLine.readBack(squelchGate); + } else { + ci = 0.0; } } - else if (m_settings.m_sampleBits == 16) + + if (!m_settings.m_iqOnly && (m_settings.m_compression == RemoteTCPSinkSettings::FLAC)) { - // Interleaved little-endian signed 16-bit IQ - quint8 iqBuf[2*2]; - qint32 i, q; - i = ((qint32)(ci.real() / SDR_RX_SCALEF * 65536.0f * m_linearGain)); - q = ((qint32)(ci.imag() / SDR_RX_SCALEF * 65536.0f * m_linearGain)); - iqBuf[1] = (i >> 8) & 0xff; - iqBuf[0] = i & 0xff; - iqBuf[3] = (q >> 8) & 0xff; - iqBuf[2] = q & 0xff; - for (auto client : m_clients) { - client->write((const char *)iqBuf, sizeof(iqBuf)); + // Compress using FLAC + FLAC__int32 iqBuf[2]; + + if (m_settings.m_sampleBits == 8) + { + iqBuf[0] = (qint32) (ci.real() / 65536.0f); + iqBuf[1] = (qint32) (ci.imag() / 65536.0f); + iqBuf[0] = clamp8(iqBuf[0]); + iqBuf[1] = clamp8(iqBuf[1]); } - } - else if (m_settings.m_sampleBits == 24) - { - // Interleaved little-endian signed 24-bit IQ - quint8 iqBuf[3*2]; - qint32 i, q; - i = ((qint32)(ci.real() * m_linearGain)); - q = ((qint32)(ci.imag() * m_linearGain)); - iqBuf[2] = (i >> 16) & 0xff; - iqBuf[1] = (i >> 8) & 0xff; - iqBuf[0] = i & 0xff; - iqBuf[5] = (q >> 16) & 0xff; - iqBuf[4] = (q >> 8) & 0xff; - iqBuf[3] = q & 0xff; - for (auto client : m_clients) { - client->write((const char *)iqBuf, sizeof(iqBuf)); + else if (m_settings.m_sampleBits == 16) + { + iqBuf[0] = (qint32) (ci.real() / 256.0f); + iqBuf[1] = (qint32) (ci.imag() / 256.0f); + iqBuf[0] = clamp16(iqBuf[0]); + iqBuf[1] = clamp16(iqBuf[1]); + } + else if (m_settings.m_sampleBits == 24) + { + iqBuf[0] = (qint32) ci.real(); + iqBuf[1] = (qint32) ci.imag(); + iqBuf[0] = clamp24(iqBuf[0]); + iqBuf[1] = clamp24(iqBuf[1]); + } + else + { + iqBuf[0] = (qint32) ci.real(); + iqBuf[1] = (qint32) ci.imag(); + } + int bytes = 2 * m_settings.m_sampleBits / 8; + m_bytesUncompressed += bytes; + + if (m_encoder && !FLAC__stream_encoder_process_interleaved(m_encoder, iqBuf, 1)) { // Number of samples in one channel + qDebug() << "RemoteTCPSinkSink::processOneSample: FLAC failed to encode:" << FLAC__stream_encoder_get_state(m_encoder); } } else { - // Interleaved little-endian signed 32-bit IQ quint8 iqBuf[4*2]; - qint32 i, q; - i = ((qint32)(ci.real() * m_linearGain)); - q = ((qint32)(ci.imag() * m_linearGain)); - iqBuf[3] = (i >> 24) & 0xff; - iqBuf[2] = (i >> 16) & 0xff; - iqBuf[1] = (i >> 8) & 0xff; - iqBuf[0] = i & 0xff; - iqBuf[7] = (q >> 24) & 0xff; - iqBuf[6] = (q >> 16) & 0xff; - iqBuf[5] = (q >> 8) & 0xff; - iqBuf[4] = q & 0xff; - for (auto client : m_clients) { - client->write((const char *)iqBuf, sizeof(iqBuf)); + + if (m_settings.m_sampleBits == 8) + { + // Transmit data as per rtl_tcp - Interleaved unsigned 8-bit IQ + iqBuf[0] = clamp8((qint32) (ci.real() / 65536.0f)) + 128; + iqBuf[1] = clamp8((qint32) (ci.imag() / 65536.0f)) + 128; + } + else if (m_settings.m_sampleBits == 16) + { + // Interleaved little-endian signed 16-bit IQ + qint32 i, q; + i = clamp16((qint32) (ci.real() / 256.0f)); + q = clamp16((qint32) (ci.imag() / 256.0f)); + iqBuf[1] = (i >> 8) & 0xff; + iqBuf[0] = i & 0xff; + iqBuf[3] = (q >> 8) & 0xff; + iqBuf[2] = q & 0xff; + } + else if (m_settings.m_sampleBits == 24) + { + // Interleaved little-endian signed 24-bit IQ + qint32 i, q; + i = clamp24((qint32) ci.real()); + q = clamp24((qint32) ci.imag()); + iqBuf[2] = (i >> 16) & 0xff; + iqBuf[1] = (i >> 8) & 0xff; + iqBuf[0] = i & 0xff; + iqBuf[5] = (q >> 16) & 0xff; + iqBuf[4] = (q >> 8) & 0xff; + iqBuf[3] = q & 0xff; + } + else + { + // Interleaved little-endian signed 32-bit IQ + qint32 i, q; + i = (qint32) ci.real(); + q = (qint32) ci.imag(); + iqBuf[3] = (i >> 24) & 0xff; + iqBuf[2] = (i >> 16) & 0xff; + iqBuf[1] = (i >> 8) & 0xff; + iqBuf[0] = i & 0xff; + iqBuf[7] = (q >> 24) & 0xff; + iqBuf[6] = (q >> 16) & 0xff; + iqBuf[5] = (q >> 8) & 0xff; + iqBuf[4] = q & 0xff; + } + + int bytes = 2 * m_settings.m_sampleBits / 8; + m_bytesUncompressed += bytes; + + if (!m_settings.m_iqOnly && (m_settings.m_compression == RemoteTCPSinkSettings::ZLIB)) + { + if (m_zStreamInitialised) + { + // Store in block buffer + memcpy(&m_zInBuf.data()[m_zInBufCount], iqBuf, bytes); + m_zInBufCount += bytes; + + if (m_zInBufCount >= m_settings.m_blockSize) + { + // Compress using zlib + m_zStream.next_in = (Bytef *) m_zInBuf.data(); + m_zStream.avail_in = m_zInBufCount; + m_zStream.next_out = (Bytef *) m_zOutBuf.data(); + m_zStream.avail_out = m_zOutBuf.size(); + int ret = deflate(&m_zStream, Z_FINISH); + + if (ret == Z_STREAM_END) { + deflateReset(&m_zStream); + } else if (ret != Z_OK) { + qDebug() << "Failed to deflate" << ret; + } + if (m_zStream.avail_in != 0) { + qDebug() << "Warning: Data still in input buffer"; + } + int compressedBytes = m_zOutBuf.size() - m_zStream.avail_out; + + //qDebug() << "zlib ret" << ret << "m_settings.m_blockSize" << m_settings.m_blockSize << "m_zInBufCount" << m_zInBufCount << "compressedBytes" << compressedBytes << "avail_in" << m_zStream.avail_in << "avail_out" << m_zStream.avail_out << " % " << round(100.0 * compressedBytes / (float) m_zInBufCount ); + + m_zInBufCount = 0; + + // Send to clients + + int clients = std::min((int) m_clients.size(), m_settings.m_maxClients); + char header[1+4]; + + header[0] = (char) RemoteTCPProtocol::dataIQzlib; + RemoteTCPProtocol::encodeUInt32((quint8 *) &header[1], compressedBytes); + + for (int i = 0; i < clients; i++) + { + m_clients[i]->write(header, sizeof(header)); + m_bytesTransmitted += sizeof(header); + m_clients[i]->write((const char *)m_zOutBuf.data(), compressedBytes); + m_bytesTransmitted += compressedBytes; + } + m_bytesCompressed += sizeof(header) + compressedBytes; + } + } + } + else + { + // Send uncompressed + int clients = std::min((int) m_clients.size(), m_settings.m_maxClients); + + for (int i = 0; i < clients; i++) + { + m_clients[i]->write((const char *)iqBuf, bytes); + m_bytesTransmitted += bytes; + } } } } @@ -232,10 +445,15 @@ void RemoteTCPSinkSink::applyChannelSettings(int channelSampleRate, int channelF m_channelSampleRate = channelSampleRate; m_channelFrequencyOffset = channelFrequencyOffset; + + m_squelchDelayLine.resize(m_settings.m_squelchGate * m_channelSampleRate + 1); } -void RemoteTCPSinkSink::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool remoteChange) +void RemoteTCPSinkSink::applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force, bool restartRequired) { + bool initFLAC = false; + bool initZLib = false; + QMutexLocker mutexLocker(&m_mutex); qDebug() << "RemoteTCPSinkSink::applySettings:" << settings.getDebugString(settingsKeys, force) << " force: " << force; @@ -252,14 +470,98 @@ void RemoteTCPSinkSink::applySettings(const RemoteTCPSinkSettings& settings, con m_interpolatorDistanceRemain = m_interpolatorDistance; } + // Update time limit for connected clients + if (settingsKeys.contains("timeLimit") && (m_settings.m_timeLimit != settings.m_timeLimit)) + { + if (settings.m_timeLimit > 0) + { + // Set new timelimit + for (int i = 0; i < m_timers.size(); i++) { + m_timers[i]->setInterval(settings.m_timeLimit * 60 * 1000); + } + // Start timers if they weren't previously started + if (m_settings.m_timeLimit == 0) + { + for (int i = 0; i < std::min((int) m_timers.size(), m_settings.m_maxClients); i++) { + m_timers[i]->start(); + } + } + } + else + { + // Stop any existing timers + for (int i = 0; i < m_timers.size(); i++) { + m_timers[i]->stop(); + } + } + } + + if ((settingsKeys.contains("compressionLevel") && (settings.m_compressionLevel != m_settings.m_compressionLevel)) + || (settingsKeys.contains("sampleBits") && (settings.m_sampleBits != m_settings.m_sampleBits)) + || (settingsKeys.contains("blockSize") && (settings.m_blockSize != m_settings.m_blockSize)) + || (settingsKeys.contains("channelSampleRate") && (settings.m_channelSampleRate != m_settings.m_channelSampleRate)) + || force) + { + initFLAC = true; + } + + if ((settingsKeys.contains("compressionLevel") && (settings.m_compressionLevel != m_settings.m_compressionLevel)) + || force) + { + initZLib = true; + } + + if (settingsKeys.contains("squelch") || force) + { + m_squelchLevel = std::pow(10.0, settings.m_squelch / 10.0); + m_movingAverage.reset(); + m_squelchCount = 0; + } + + if (settingsKeys.contains("squelchGate") || force) { + m_squelchDelayLine.resize(settings.m_squelchGate * m_channelSampleRate + 1); + } + // Do clients need to reconnect to get these updated settings? + // settingsKeys will be empty if force is set bool restart = (settingsKeys.contains("dataAddress") && (m_settings.m_dataAddress != settings.m_dataAddress)) || (settingsKeys.contains("dataPort") && (m_settings.m_dataPort != settings.m_dataPort)) + || (settingsKeys.contains("certificate") && (m_settings.m_certificate != settings.m_certificate)) + || (settingsKeys.contains("key") && (m_settings.m_key!= settings.m_key)) || (settingsKeys.contains("sampleBits") && (m_settings.m_sampleBits != settings.m_sampleBits)) || (settingsKeys.contains("protocol") && (m_settings.m_protocol != settings.m_protocol)) - || ( !remoteChange - && (settingsKeys.contains("channelSampleRate") && (m_settings.m_channelSampleRate != settings.m_channelSampleRate)) - ); + || (settingsKeys.contains("compression") && (m_settings.m_compression != settings.m_compression)) + || (settingsKeys.contains("remoteControl") && (m_settings.m_remoteControl != settings.m_remoteControl)) + || initFLAC + || restartRequired + ; + + if (!restart && (m_settings.m_protocol != RemoteTCPSinkSettings::RTL0) && !m_settings.m_iqOnly) + { + // Forward settings to clients if they've changed + if ((settingsKeys.contains("channelSampleRate") || force) && (settings.m_channelSampleRate != m_settings.m_channelSampleRate)) { + sendCommand(RemoteTCPProtocol::setChannelSampleRate, settings.m_channelSampleRate); + } + if ((settingsKeys.contains("inputFrequencyOffset") || force) && (settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset)) { + sendCommand(RemoteTCPProtocol::setChannelFreqOffset, settings.m_inputFrequencyOffset); + } + if ((settingsKeys.contains("gain") || force) && (settings.m_gain != m_settings.m_gain)) { + sendCommand(RemoteTCPProtocol::setChannelGain, settings.m_gain); + } + if ((settingsKeys.contains("sampleBits") || force) && (settings.m_sampleBits != m_settings.m_sampleBits)) { + sendCommand(RemoteTCPProtocol::setSampleBitDepth, settings.m_sampleBits); + } + if ((settingsKeys.contains("squelchEnabled") || force) && (settings.m_squelchEnabled != m_settings.m_squelchEnabled)) { + sendCommand(RemoteTCPProtocol::setIQSquelchEnabled, (quint32) settings.m_squelchEnabled); + } + if ((settingsKeys.contains("squelch") || force) && (settings.m_squelch != m_settings.m_squelch)) { + sendCommandFloat(RemoteTCPProtocol::setIQSquelch, settings.m_squelch); + } + if ((settingsKeys.contains("squelchGate") || force) && (settings.m_squelchGate != m_settings.m_squelchGate)) { + sendCommandFloat(RemoteTCPProtocol::setIQSquelchGate, settings.m_squelchGate); + } + // m_remoteControl rather than restart? + } if (force) { m_settings = settings; @@ -267,51 +569,183 @@ void RemoteTCPSinkSink::applySettings(const RemoteTCPSinkSettings& settings, con m_settings.applySettings(settingsKeys, settings); } - if (m_running && restart) { + if (m_running && (restart || force)) { startServer(); } + + if (initFLAC && (m_settings.m_compression == RemoteTCPSinkSettings::FLAC)) + { + if (m_encoder) + { + // Delete existing decoder + FLAC__stream_encoder_finish(m_encoder); + FLAC__stream_encoder_delete(m_encoder); + m_encoder = nullptr; + m_flacHeader.clear(); + } + + // Create FLAC encoder + FLAC__StreamEncoderInitStatus init_status; + m_encoder = FLAC__stream_encoder_new(); + if (m_encoder) + { + const int maxSampleRate = 176400; // Spec says max is 655350, but doesn't seem to work + FLAC__bool ok = true; + + ok &= FLAC__stream_encoder_set_verify(m_encoder, false); + ok &= FLAC__stream_encoder_set_compression_level(m_encoder, m_settings.m_compressionLevel); + ok &= FLAC__stream_encoder_set_channels(m_encoder, 2); + ok &= FLAC__stream_encoder_set_bits_per_sample(m_encoder, m_settings.m_sampleBits); + // We'll get FLAC__STREAM_ENCODER_INIT_STATUS_NOT_STREAMABLE if we use the real sample rate + if (m_settings.m_channelSampleRate < maxSampleRate) { + ok &= FLAC__stream_encoder_set_sample_rate(m_encoder, m_settings.m_channelSampleRate); + } else { + ok &= FLAC__stream_encoder_set_sample_rate(m_encoder, maxSampleRate); + } + ok &= FLAC__stream_encoder_set_total_samples_estimate(m_encoder, 0); + //ok &= FLAC__stream_encoder_set_do_mid_side_stereo(m_encoder, false); + // FLAC__MAX_BLOCK_SIZE is 65536 + // However, FLAC__format_blocksize_is_subset says anything over 16384 is not streamable + // Also, if sampleRate <= 48000, then max block size is 4608 + if (FLAC__format_blocksize_is_subset(m_settings.m_blockSize, m_settings.m_channelSampleRate)) { + ok &= FLAC__stream_encoder_set_blocksize(m_encoder, m_settings.m_blockSize); + } else { + ok &= FLAC__stream_encoder_set_blocksize(m_encoder, 4096); + } + if (ok) + { + init_status = FLAC__stream_encoder_init_stream(m_encoder, flacWriteCallback, nullptr, nullptr, nullptr, this); + if (init_status != FLAC__STREAM_ENCODER_INIT_STATUS_OK) + { + qDebug() << "RemoteTCPSinkSink::applySettings: Error initializing FLAC encoder:" << FLAC__StreamEncoderInitStatusString[init_status]; + FLAC__stream_encoder_delete(m_encoder); + m_encoder = nullptr; + } + } + else + { + qDebug() << "RemoteTCPSinkSink::applySettings: Failed to configure FLAC encoder"; + FLAC__stream_encoder_delete(m_encoder); + m_encoder = nullptr; + } + } + else + { + qDebug() << "RemoteTCPSinkSink::applySettings: Failed to allocate FLAC encoder"; + } + + m_bytesUncompressed = 0; + m_bytesCompressed = 0; + } + + if (initZLib && (m_settings.m_compression == RemoteTCPSinkSettings::ZLIB)) + { + // Intialise zlib compression + m_zStream.zalloc = Z_NULL; + m_zStream.zfree = Z_NULL; + m_zStream.opaque = Z_NULL; + m_zStream.data_type = Z_BINARY; + int windowBits = log2(m_settings.m_blockSize); + + if (Z_OK == deflateInit2(&m_zStream, m_settings.m_compressionLevel, Z_DEFLATED, windowBits, 9, Z_DEFAULT_STRATEGY)) + { + m_zStreamInitialised = true; + } + else + { + qDebug() << "RemoteTCPSinkSink::applySettings: deflateInit failed"; + m_zStreamInitialised = false; + } + + m_bytesUncompressed = 0; + m_bytesCompressed = 0; + } + } void RemoteTCPSinkSink::startServer() { stopServer(); - m_server = new QTcpServer(this); - if (!m_server->listen(QHostAddress(m_settings.m_dataAddress), m_settings.m_dataPort)) + if (m_settings.m_protocol == RemoteTCPSinkSettings::SDRA_WSS) { - qCritical() << "RemoteTCPSink failed to listen on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; - // FIXME: Report to GUI? +#ifndef QT_NO_OPENSSL + m_webSocketServer = new QWebSocketServer(QStringLiteral("Remote TCP Sink"), + QWebSocketServer::SecureMode, + this); + QSslConfiguration sslConfiguration; + qDebug() << "RemoteTCPSinkSink::startServer: SSL config: " << m_settings.m_certificate << m_settings.m_key; + QFile certFile(m_settings.m_certificate); + QFile keyFile(m_settings.m_key); + certFile.open(QIODevice::ReadOnly); + keyFile.open(QIODevice::ReadOnly); + QSslCertificate certificate(&certFile, QSsl::Pem); + QSslKey sslKey(&keyFile, QSsl::Rsa, QSsl::Pem); + certFile.close(); + keyFile.close(); + sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone); + sslConfiguration.setLocalCertificate(certificate); + sslConfiguration.setPrivateKey(sslKey); + m_webSocketServer->setSslConfiguration(sslConfiguration); + + QHostAddress address(m_settings.m_dataAddress); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) + m_webSocketServer->setSupportedSubprotocols({"binary"}); // Chrome wont connect without this - "Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received" +#endif + if (!m_webSocketServer->listen(address, m_settings.m_dataPort)) + { + qCritical() << "RemoteTCPSink failed to listen on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; + // FIXME: Report to GUI? + } + else + { + qInfo() << "RemoteTCPSink listening on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; + connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &RemoteTCPSinkSink::acceptWebConnection); + connect(m_webSocketServer, &QWebSocketServer::sslErrors, this, &RemoteTCPSinkSink::onSslErrors); + } +#else + qWarning("RemoteTCPSinkSink::startServer: SSL is not supported"); +#endif } else { - qInfo() << "RemoteTCPSink listening on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; - connect(m_server, &QTcpServer::newConnection, this, &RemoteTCPSinkSink::acceptConnection); + m_server = new QTcpServer(this); + if (!m_server->listen(QHostAddress(m_settings.m_dataAddress), m_settings.m_dataPort)) + { + qCritical() << "RemoteTCPSink failed to listen on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; + // FIXME: Report to GUI? + } + else + { + qInfo() << "RemoteTCPSink listening on" << m_settings.m_dataAddress << "port" << m_settings.m_dataPort; + connect(m_server, &QTcpServer::newConnection, this, &RemoteTCPSinkSink::acceptTCPConnection); + } } } void RemoteTCPSinkSink::stopServer() { - for (auto client : m_clients) - { - qDebug() << "RemoteTCPSinkSink::stopServer: Closing connection to client"; - client->close(); - delete client; - } - if (m_clients.size() > 0) - { - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgReportConnection::create(0)); - } - m_clients.clear(); + // Close connections to any existing clients + while (m_clients.size() > 0) { + m_clients[0]->close(); // This results in disconnected() being called, where we delete and remove from m_clients } + // Close server sockets if (m_server) { - qDebug() << "RemoteTCPSinkSink::stopServer: Closing old server"; + qDebug() << "RemoteTCPSinkSink::stopServer: Closing old socket server"; m_server->close(); - delete m_server; + m_server->deleteLater(); m_server = nullptr; } + if (m_webSocketServer) + { + qDebug() << "RemoteTCPSinkSink::stopServer: Closing old web socket server"; + m_webSocketServer->close(); + m_webSocketServer->deleteLater(); + m_webSocketServer = nullptr; + } } RemoteTCPProtocol::Device RemoteTCPSinkSink::getDevice() @@ -398,90 +832,218 @@ RemoteTCPProtocol::Device RemoteTCPSinkSink::getDevice() return RemoteTCPProtocol::UNKNOWN; } -void RemoteTCPSinkSink::acceptConnection() +void RemoteTCPSinkSink::acceptWebConnection() +{ + QMutexLocker mutexLocker(&m_mutex); + QWebSocket *client = m_webSocketServer->nextPendingConnection(); + + connect(client, &QWebSocket::binaryMessageReceived, this, &RemoteTCPSinkSink::processCommand); + connect(client, &QWebSocket::disconnected, this, &RemoteTCPSinkSink::disconnected); + qDebug() << "RemoteTCPSinkSink::acceptWebConnection: client connected"; + + // https://bugreports.qt.io/browse/QTBUG-125874 + QTimer::singleShot(200, this, [this, client] () { + QMutexLocker mutexLocker(&m_mutex); + m_clients.append(new WebSocket(client)); + acceptConnection(m_clients.last()); + }); +} + +void RemoteTCPSinkSink::acceptTCPConnection() { QMutexLocker mutexLocker(&m_mutex); QTcpSocket *client = m_server->nextPendingConnection(); - if (!client) { - qDebug() << "RemoteTCPSinkSink::acceptConnection: client is nullptr"; - return; - } - m_clients.append(client); - connect(client, &QIODevice::readyRead, this, &RemoteTCPSinkSink::processCommand); - connect(client, SIGNAL(disconnected()), this, SLOT(disconnected())); + connect(client, &QTcpSocket::disconnected, this, &RemoteTCPSinkSink::disconnected); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) connect(client, QOverload::of(&QAbstractSocket::error), this, &RemoteTCPSinkSink::errorOccurred); #else connect(client, &QAbstractSocket::errorOccurred, this, &RemoteTCPSinkSink::errorOccurred); #endif - qDebug() << "RemoteTCPSinkSink::acceptConnection: client connected"; + qDebug() << "RemoteTCPSinkSink::acceptTCPConnection: client connected"; + + QTimer::singleShot(200, this, [this, client] () { + QMutexLocker mutexLocker(&m_mutex); + m_clients.append(new TCPSocket(client)); + acceptConnection(m_clients.last()); + }); +} + +FLAC__StreamEncoderWriteStatus RemoteTCPSinkSink::flacWrite(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, uint32_t samples, uint32_t currentFrame) +{ + char header[1+4]; +//qDebug() << "RemoteTCPSinkSink::flacWrite bytes" << bytes << "samples" << samples; + // Save FLAC header for clients that connect later + if ((currentFrame == 0) && (samples == 0)) + { + m_flacHeader.append((const char *) buffer, bytes); + + // Write complete header to all clients + if (m_flacHeader.size() == m_flacHeaderSize) + { + header[0] = (char) RemoteTCPProtocol::dataIQFLAC; + RemoteTCPProtocol::encodeUInt32((quint8 *) &header[1], m_flacHeader.size()); + + for (auto client : m_clients) + { + client->write(header, sizeof(header)); + client->write(m_flacHeader.constData(), m_flacHeader.size()); + m_bytesTransmitted += sizeof(header) + m_flacHeader.size(); + client->flush(); + } + } + } + else + { + // Send compressed IQ data to max number of clients + header[0] = (char) RemoteTCPProtocol::dataIQFLAC; + RemoteTCPProtocol::encodeUInt32((quint8 *) &header[1], bytes); + int clients = std::min((int) m_clients.size(), m_settings.m_maxClients); + for (int i = 0; i < clients; i++) + { + Socket *client = m_clients[i]; + client->write(header, sizeof(header)); + client->write((const char *) buffer, bytes); + m_bytesTransmitted += sizeof(header) + bytes; + client->flush(); + } + } + m_bytesCompressed += sizeof(header) + bytes; + + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; +} + +void RemoteTCPSinkSink::acceptConnection(Socket *client) +{ if (m_settings.m_protocol == RemoteTCPSinkSettings::RTL0) { quint8 metaData[RemoteTCPProtocol::m_rtl0MetaDataSize] = {'R', 'T', 'L', '0'}; RemoteTCPProtocol::encodeUInt32(&metaData[4], getDevice()); // Tuner ID RemoteTCPProtocol::encodeUInt32(&metaData[8], 1); // Gain stages client->write((const char *)metaData, sizeof(metaData)); + m_bytesTransmitted += sizeof(metaData); + client->flush(); } else { quint8 metaData[RemoteTCPProtocol::m_sdraMetaDataSize] = {'S', 'D', 'R', 'A'}; RemoteTCPProtocol::encodeUInt32(&metaData[4], getDevice()); + // Send device/channel settings, so they can be displayed in the remote GUI - - double centerFrequency = 0.0; - qint32 ppmCorrection = 0; - quint32 flags = 0; - int biasTeeEnabled = false; - int directSampling = false; - int agc = false; - int dcOffsetRemoval = false; - int iqCorrection = false; - qint32 devSampleRate = 0; - qint32 log2Decim = 0; - qint32 gain[4] = {0, 0, 0, 0}; - qint32 rfBW = 0; - - ChannelWebAPIUtils::getCenterFrequency(m_deviceIndex, centerFrequency); - ChannelWebAPIUtils::getLOPpmCorrection(m_deviceIndex, ppmCorrection); - ChannelWebAPIUtils::getDevSampleRate(m_deviceIndex, devSampleRate); - ChannelWebAPIUtils::getSoftDecim(m_deviceIndex, log2Decim); + ChannelWebAPIUtils::getCenterFrequency(m_deviceIndex, m_centerFrequency); + ChannelWebAPIUtils::getLOPpmCorrection(m_deviceIndex, m_ppmCorrection); + ChannelWebAPIUtils::getDevSampleRate(m_deviceIndex, m_devSampleRate); + ChannelWebAPIUtils::getSoftDecim(m_deviceIndex, m_log2Decim); for (int i = 0; i < 4; i++) { - ChannelWebAPIUtils::getGain(m_deviceIndex, i, gain[i]); + ChannelWebAPIUtils::getGain(m_deviceIndex, i, m_gain[i]); } - ChannelWebAPIUtils::getRFBandwidth(m_deviceIndex, rfBW); - ChannelWebAPIUtils::getBiasTee(m_deviceIndex, biasTeeEnabled); - ChannelWebAPIUtils::getDeviceSetting(m_deviceIndex, "noModMode", directSampling); - ChannelWebAPIUtils::getAGC(m_deviceIndex, agc); - ChannelWebAPIUtils::getDCOffsetRemoval(m_deviceIndex, dcOffsetRemoval); - ChannelWebAPIUtils::getIQCorrection(m_deviceIndex, iqCorrection); - flags = (iqCorrection << 4) - | (dcOffsetRemoval << 3) - | (agc << 2) - | (directSampling << 1) - | biasTeeEnabled; + ChannelWebAPIUtils::getRFBandwidth(m_deviceIndex, m_rfBW); + ChannelWebAPIUtils::getBiasTee(m_deviceIndex, m_biasTeeEnabled); + ChannelWebAPIUtils::getDeviceSetting(m_deviceIndex, "noModMode", m_directSampling); + ChannelWebAPIUtils::getAGC(m_deviceIndex, m_agc); + ChannelWebAPIUtils::getDCOffsetRemoval(m_deviceIndex, m_dcOffsetRemoval); + ChannelWebAPIUtils::getIQCorrection(m_deviceIndex, m_iqCorrection); - RemoteTCPProtocol::encodeUInt64(&metaData[8], (quint64)centerFrequency); - RemoteTCPProtocol::encodeUInt32(&metaData[16], ppmCorrection); + quint32 flags = ((!m_settings.m_iqOnly) << 7) + | (m_settings.m_remoteControl << 6) + | (m_settings.m_squelchEnabled << 5) + | (m_iqCorrection << 4) + | (m_dcOffsetRemoval << 3) + | (m_agc << 2) + | (m_directSampling << 1) + | m_biasTeeEnabled; + + RemoteTCPProtocol::encodeUInt64(&metaData[8], (quint64)m_centerFrequency); + RemoteTCPProtocol::encodeUInt32(&metaData[16], m_ppmCorrection); RemoteTCPProtocol::encodeUInt32(&metaData[20], flags); - RemoteTCPProtocol::encodeUInt32(&metaData[24], devSampleRate); - RemoteTCPProtocol::encodeUInt32(&metaData[28], log2Decim); - RemoteTCPProtocol::encodeInt16(&metaData[32], gain[0]); - RemoteTCPProtocol::encodeInt16(&metaData[34], gain[1]); - RemoteTCPProtocol::encodeInt16(&metaData[36], gain[2]); - RemoteTCPProtocol::encodeInt16(&metaData[38], gain[3]); - RemoteTCPProtocol::encodeUInt32(&metaData[40], rfBW); + RemoteTCPProtocol::encodeUInt32(&metaData[24], m_devSampleRate); + RemoteTCPProtocol::encodeUInt32(&metaData[28], m_log2Decim); + RemoteTCPProtocol::encodeInt16(&metaData[32], m_gain[0]); + RemoteTCPProtocol::encodeInt16(&metaData[34], m_gain[1]); + RemoteTCPProtocol::encodeInt16(&metaData[36], m_gain[2]); + RemoteTCPProtocol::encodeInt16(&metaData[38], m_gain[3]); + RemoteTCPProtocol::encodeUInt32(&metaData[40], m_rfBW); RemoteTCPProtocol::encodeInt32(&metaData[44], m_settings.m_inputFrequencyOffset); RemoteTCPProtocol::encodeUInt32(&metaData[48], m_settings.m_gain); RemoteTCPProtocol::encodeUInt32(&metaData[52], m_settings.m_channelSampleRate); RemoteTCPProtocol::encodeUInt32(&metaData[56], m_settings.m_sampleBits); + RemoteTCPProtocol::encodeUInt32(&metaData[60], 1); // Protocol revision. 0=64 byte meta data, 1=128 byte meta data + RemoteTCPProtocol::encodeFloat(&metaData[64], m_settings.m_squelch); + RemoteTCPProtocol::encodeFloat(&metaData[68], m_settings.m_squelchGate); // Send API port? Not accessible via MainCore client->write((const char *)metaData, sizeof(metaData)); + m_bytesTransmitted += sizeof(metaData); + client->flush(); + + // Inform client if they are in a queue + if (!m_settings.m_iqOnly && (m_clients.size() > m_settings.m_maxClients)) { + sendQueuePosition(client, m_clients.size() - m_settings.m_maxClients); + } + + // Send existing FLAC header to new client + if (!m_settings.m_iqOnly && (m_settings.m_compression == RemoteTCPSinkSettings::FLAC) && (m_flacHeader.size() == m_flacHeaderSize)) + { + char header[1+4]; + + header[0] = (char) RemoteTCPProtocol::dataIQFLAC; + RemoteTCPProtocol::encodeUInt32((quint8 *) &header[1], m_flacHeader.size()); + client->write(header, sizeof(header)); + client->write(m_flacHeader.constData(), m_flacHeader.size()); + m_bytesTransmitted += sizeof(header) + m_flacHeader.size(); + client->flush(); + } + + // Send position / direction of antenna + sendPosition(); + if (m_settings.m_isotropic) { + sendDirection(true, std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN()); + } else if (m_settings.m_rotator == "None") { + sendDirection(false, m_settings.m_azimuth, m_settings.m_elevation); + } else { + sendRotatorDirection(true); + } } + + // Create timer to disconnect client after timelimit reached + QTimer *timer = new QTimer(); + timer->setSingleShot(true); + timer->callOnTimeout(this, [this, client] () { + qDebug() << "Disconnecting" << client->peerAddress() << "as time limit reached"; + if (m_settings.m_compression) { + sendTimeLimit(client); + } + client->close(); + }); + if (m_settings.m_timeLimit > 0) + { + timer->setInterval(m_settings.m_timeLimit * 60 * 1000); + // Only start timer if we will receive data immediately + if (m_clients.size() <= m_settings.m_maxClients) { + timer->start(); + } + } + m_timers.append(timer); + + // Close connection if blacklisted + for (const auto& ip : m_settings.m_ipBlacklist) + { + QHostAddress address(ip); + if (address == client->peerAddress()) + { + qDebug() << "Disconnecting" << client->peerAddress() << "as blacklisted"; + if (m_settings.m_compression) { + sendBlacklisted(client); + } + client->close(); + break; + } + } + + m_messageQueueToChannel->push(RemoteTCPSink::MsgReportConnection::create(m_clients.size(), client->peerAddress(), client->peerPort())); if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgReportConnection::create(m_clients.size())); + m_messageQueueToGUI->push(RemoteTCPSink::MsgReportConnection::create(m_clients.size(), client->peerAddress(), client->peerPort())); } } @@ -489,14 +1051,53 @@ void RemoteTCPSinkSink::disconnected() { QMutexLocker mutexLocker(&m_mutex); qDebug() << "RemoteTCPSinkSink::disconnected"; - QTcpSocket *client = (QTcpSocket*)sender(); - client->deleteLater(); - m_clients.removeAll(client); - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgReportConnection::create(m_clients.size())); + QObject *object = sender(); + QMutableListIterator itr(m_clients); + + // Remove from list of clients + int idx = 0; + while (itr.hasNext()) + { + Socket *socket = itr.next(); + if (socket->socket() == object) + { + itr.remove(); + delete m_timers.takeAt(idx); + m_messageQueueToChannel->push(RemoteTCPSink::MsgReportDisconnect::create(m_clients.size(), socket->peerAddress(), socket->peerPort())); + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgReportDisconnect::create(m_clients.size(), socket->peerAddress(), socket->peerPort())); + } + socket->deleteLater(); + break; + } + else + { + idx++; + } + } + + // Start timer for next waiting client + if ((idx < m_settings.m_maxClients) && (m_settings.m_timeLimit > 0)) + { + int newActiveIdx = m_settings.m_maxClients - 1; + if (newActiveIdx < m_clients.size()) { + m_timers[newActiveIdx]->start(); + } + } + + // Update other clients waiting with current queue position + for (int i = m_settings.m_maxClients; i < m_clients.size(); i++) { + sendQueuePosition(m_clients[i], i - m_settings.m_maxClients + 1); } } +#ifndef QT_NO_OPENSSL +void RemoteTCPSinkSink::onSslErrors(const QList &errors) +{ + qWarning() << "RemoteTCPSinkSink::onSslErrors: " << errors; +} +#endif + void RemoteTCPSinkSink::errorOccurred(QAbstractSocket::SocketError socketError) { qDebug() << "RemoteTCPSinkSink::errorOccurred: " << socketError; @@ -507,175 +1108,279 @@ void RemoteTCPSinkSink::errorOccurred(QAbstractSocket::SocketError socketError) }*/ } +Socket *RemoteTCPSinkSink::getSocket(QObject *object) const +{ + for (const auto client : m_clients) + { + if (client->socket() == object) { + return client; + } + } + + return nullptr; +} + void RemoteTCPSinkSink::processCommand() { QMutexLocker mutexLocker(&m_mutex); - QTcpSocket *client = (QTcpSocket*)sender(); + Socket *client = getSocket(sender()); RemoteTCPSinkSettings settings = m_settings; - quint8 cmd[5]; + while (client && (client->bytesAvailable() >= (qint64)sizeof(cmd))) { int len = client->read((char *)cmd, sizeof(cmd)); + if (len == sizeof(cmd)) { - switch (cmd[0]) + if (cmd[0] == RemoteTCPProtocol::sendMessage) { - case RemoteTCPProtocol::setCenterFrequency: - { - quint64 centerFrequency = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set center frequency " << centerFrequency; - ChannelWebAPIUtils::setCenterFrequency(m_deviceIndex, (double)centerFrequency); - break; - } - case RemoteTCPProtocol::setSampleRate: - { - int sampleRate = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set sample rate " << sampleRate; - ChannelWebAPIUtils::setDevSampleRate(m_deviceIndex, sampleRate); - if (m_settings.m_protocol == RemoteTCPSinkSettings::RTL0) + quint32 msgLen = RemoteTCPProtocol::extractUInt32(&cmd[1]); + try { - // Match channel sample rate with device sample rate for RTL0 protocol - ChannelWebAPIUtils::setSoftDecim(m_deviceIndex, 0); - settings.m_channelSampleRate = sampleRate; + char *buf = new char[msgLen]; + len = client->read((char *)buf, msgLen); + if (len == msgLen) + { + bool broadcast = (bool) buf[0]; + int i; + for (i = 1; i < (int) msgLen; i++) + { + if (buf[i] == '\0') { + break; + } + } + QString callsign = QString::fromUtf8(&buf[1]); + QString text = QString::fromUtf8(&buf[i+1]); + + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgSendMessage::create(client->peerAddress(), client->peerPort(), callsign, text, broadcast)); + } + } + else + { + qDebug() << "RemoteTCPSinkSink::processCommand: sendMessage: Failed to read" << msgLen; + } + delete[] buf; + } + catch(std::bad_alloc&) + { + qDebug() << "RemoteTCPSinkSink::processCommand: sendMessage - Failed to allocate" << msgLen; + } + } + else if (!m_settings.m_remoteControl) + { + qDebug() << "RemoteTCPSinkSink::processCommand: Ignoring command from client as remote control disabled"; + } + else + { + switch (cmd[0]) + { + case RemoteTCPProtocol::setCenterFrequency: + { + quint64 centerFrequency = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set center frequency " << centerFrequency; + ChannelWebAPIUtils::setCenterFrequency(m_deviceIndex, (double)centerFrequency); + break; + } + case RemoteTCPProtocol::setSampleRate: + { + int sampleRate = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set sample rate " << sampleRate; + ChannelWebAPIUtils::setDevSampleRate(m_deviceIndex, sampleRate); + if (m_settings.m_protocol == RemoteTCPSinkSettings::RTL0) + { + // Match channel sample rate with device sample rate for RTL0 protocol + ChannelWebAPIUtils::setSoftDecim(m_deviceIndex, 0); + settings.m_channelSampleRate = sampleRate; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false)); + } + } + break; + } + case RemoteTCPProtocol::setTunerGainMode: + // SDRangel's rtlsdr sample source always has this fixed as 1, so nothing to do + break; + case RemoteTCPProtocol::setTunerGain: // gain is gain in 10th of a dB + { + int gain = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set gain " << gain; + ChannelWebAPIUtils::setGain(m_deviceIndex, 0, gain); + break; + } + case RemoteTCPProtocol::setFrequencyCorrection: + { + int ppm = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set LO ppm correction " << ppm; + ChannelWebAPIUtils::setLOPpmCorrection(m_deviceIndex, ppm); + break; + } + case RemoteTCPProtocol::setTunerIFGain: + { + int v = RemoteTCPProtocol::extractUInt32(&cmd[1]); + int gain = (int)(qint16)(v & 0xffff); + int stage = (v >> 16) & 0xffff; + ChannelWebAPIUtils::setGain(m_deviceIndex, stage, gain); + break; + } + case RemoteTCPProtocol::setAGCMode: + { + int agc = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set AGC " << agc; + ChannelWebAPIUtils::setAGC(m_deviceIndex, agc); + break; + } + case RemoteTCPProtocol::setDirectSampling: + { + int ds = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set direct sampling " << ds; + ChannelWebAPIUtils::patchDeviceSetting(m_deviceIndex, "noModMode", ds); // RTLSDR only + break; + } + case RemoteTCPProtocol::setBiasTee: + { + int biasTee = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set bias tee " << biasTee; + ChannelWebAPIUtils::setBiasTee(m_deviceIndex, biasTee); + break; + } + case RemoteTCPProtocol::setTunerBandwidth: + { + int rfBW = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set tuner bandwidth " << rfBW; + ChannelWebAPIUtils::setRFBandwidth(m_deviceIndex, rfBW); + break; + } + case RemoteTCPProtocol::setDecimation: + { + int dec = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set decimation " << dec; + ChannelWebAPIUtils::setSoftDecim(m_deviceIndex, dec); + break; + } + case RemoteTCPProtocol::setDCOffsetRemoval: + { + int dc = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set DC offset removal " << dc; + ChannelWebAPIUtils::setDCOffsetRemoval(m_deviceIndex, dc); + break; + } + case RemoteTCPProtocol::setIQCorrection: + { + int iq = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set IQ correction " << iq; + ChannelWebAPIUtils::setIQCorrection(m_deviceIndex, iq); + break; + } + case RemoteTCPProtocol::setChannelSampleRate: + { + int channelSampleRate = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set channel sample rate " << channelSampleRate; + bool restartRequired; + if (channelSampleRate <= m_settings.m_maxSampleRate) + { + settings.m_channelSampleRate = channelSampleRate; + restartRequired = false; + } + else + { + settings.m_channelSampleRate = m_settings.m_maxSampleRate; + restartRequired = true; // Need to restart so client gets max sample rate + } if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, true)); + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, restartRequired)); } if (m_messageQueueToChannel) { - m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, true)); + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, restartRequired)); } + break; } - break; - } - case RemoteTCPProtocol::setTunerGainMode: - // SDRangel's rtlsdr sample source always has this fixed as 1, so nothing to do - break; - case RemoteTCPProtocol::setTunerGain: // gain is gain in 10th of a dB - { - int gain = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set gain " << gain; - ChannelWebAPIUtils::setGain(m_deviceIndex, 0, gain); - break; - } - case RemoteTCPProtocol::setFrequencyCorrection: - { - int ppm = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set LO ppm correction " << ppm; - ChannelWebAPIUtils::setLOPpmCorrection(m_deviceIndex, ppm); - break; - } - case RemoteTCPProtocol::setTunerIFGain: - { - int v = RemoteTCPProtocol::extractUInt32(&cmd[1]); - int gain = (int)(qint16)(v & 0xffff); - int stage = (v >> 16) & 0xffff; - ChannelWebAPIUtils::setGain(m_deviceIndex, stage, gain); - break; - } - case RemoteTCPProtocol::setAGCMode: - { - int agc = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set AGC " << agc; - ChannelWebAPIUtils::setAGC(m_deviceIndex, agc); - break; - } - case RemoteTCPProtocol::setDirectSampling: - { - int ds = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set direct sampling " << ds; - ChannelWebAPIUtils::patchDeviceSetting(m_deviceIndex, "noModMode", ds); // RTLSDR only - break; - } - case RemoteTCPProtocol::setBiasTee: - { - int biasTee = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set bias tee " << biasTee; - ChannelWebAPIUtils::setBiasTee(m_deviceIndex, biasTee); - break; - } - case RemoteTCPProtocol::setTunerBandwidth: - { - int rfBW = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set tuner bandwidth " << rfBW; - ChannelWebAPIUtils::setRFBandwidth(m_deviceIndex, rfBW); - break; - } - case RemoteTCPProtocol::setDecimation: - { - int dec = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set decimation " << dec; - ChannelWebAPIUtils::setSoftDecim(m_deviceIndex, dec); - break; - } - case RemoteTCPProtocol::setDCOffsetRemoval: - { - int dc = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set DC offset removal " << dc; - ChannelWebAPIUtils::setDCOffsetRemoval(m_deviceIndex, dc); - break; - } - case RemoteTCPProtocol::setIQCorrection: - { - int iq = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set IQ correction " << iq; - ChannelWebAPIUtils::setIQCorrection(m_deviceIndex, iq); - break; - } - case RemoteTCPProtocol::setChannelSampleRate: - { - int channelSampleRate = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set channel sample rate " << channelSampleRate; - settings.m_channelSampleRate = channelSampleRate; - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, true)); + case RemoteTCPProtocol::setChannelFreqOffset: + { + int offset = (int)RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set channel input frequency offset " << offset; + settings.m_inputFrequencyOffset = offset; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"inputFrequencyOffset"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"inputFrequencyOffset"}, false)); + } + break; } - if (m_messageQueueToChannel) { - m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"channelSampleRate"}, false, true)); + case RemoteTCPProtocol::setChannelGain: + { + int gain = (int)RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set channel gain " << gain; + settings.m_gain = gain; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"gain"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"gain"}, false)); + } + break; } - break; - } - case RemoteTCPProtocol::setChannelFreqOffset: - { - int offset = (int)RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set channel input frequency offset " << offset; - settings.m_inputFrequencyOffset = offset; - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"inputFrequencyOffset"}, false, true)); + case RemoteTCPProtocol::setSampleBitDepth: + { + int bits = RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set sample bit depth " << bits; + settings.m_sampleBits = bits; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"sampleBits"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"sampleBits"}, false)); + } + break; } - if (m_messageQueueToChannel) { - m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"inputFrequencyOffset"}, false, true)); + case RemoteTCPProtocol::setIQSquelchEnabled: + { + bool enabled = (bool) RemoteTCPProtocol::extractUInt32(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set IQ squelch enabled " << enabled; + settings.m_squelchEnabled = enabled; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelchEnabled"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelchEnabled"}, false)); + } + break; } - break; - } - case RemoteTCPProtocol::setChannelGain: - { - int gain = (int)RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set channel gain " << gain; - settings.m_gain = gain; - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"gain"}, false, true)); + case RemoteTCPProtocol::setIQSquelch: + { + float squelch = RemoteTCPProtocol::extractFloat(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set IQ squelch " << squelch; + settings.m_squelch = squelch; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelch"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelch"}, false)); + } + break; } - if (m_messageQueueToChannel) { - m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"gain"}, false, true)); + case RemoteTCPProtocol::setIQSquelchGate: + { + float squelchGate = RemoteTCPProtocol::extractFloat(&cmd[1]); + qDebug() << "RemoteTCPSinkSink::processCommand: set IQ squelch gate " << squelchGate; + settings.m_squelchGate = squelchGate; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelchGate"}, false)); + } + if (m_messageQueueToChannel) { + m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"squelchGate"}, false)); + } + break; } - break; - } - case RemoteTCPProtocol::setSampleBitDepth: - { - int bits = RemoteTCPProtocol::extractUInt32(&cmd[1]); - qDebug() << "RemoteTCPSinkSink::processCommand: set sample bit depth " << bits; - settings.m_sampleBits = bits; - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"sampleBits"}, false, true)); + default: + qDebug() << "RemoteTCPSinkSink::processCommand: unknown command " << cmd[0]; + break; } - if (m_messageQueueToChannel) { - m_messageQueueToChannel->push(RemoteTCPSink::MsgConfigureRemoteTCPSink::create(settings, {"sampleBits"}, false, true)); - } - break; - } - default: - qDebug() << "RemoteTCPSinkSink::processCommand: unknown command " << cmd[0]; - break; } } else @@ -684,3 +1389,325 @@ void RemoteTCPSinkSink::processCommand() } } } + +void RemoteTCPSinkSink::sendCommand(RemoteTCPProtocol::Command cmdId, quint32 value) +{ + QMutexLocker mutexLocker(&m_mutex); + quint8 cmd[5]; + + cmd[0] = (quint8) cmdId; + RemoteTCPProtocol::encodeUInt32(&cmd[1], value); + + for (const auto client : m_clients) + { + qint64 len = client->write((char *) cmd, sizeof(cmd)); + if (len != sizeof(cmd)) { + qDebug() << "RemoteTCPSinkSink::sendCommand: Failed to write all of message:" << len; + } + m_bytesTransmitted += sizeof(cmd); + client->flush(); + } +} + +void RemoteTCPSinkSink::sendCommandFloat(RemoteTCPProtocol::Command cmdId, float value) +{ + QMutexLocker mutexLocker(&m_mutex); + quint8 cmd[5]; + + cmd[0] = (quint8) cmdId; + RemoteTCPProtocol::encodeFloat(&cmd[1], value); + + for (const auto client : m_clients) + { + qint64 len = client->write((char *) cmd, sizeof(cmd)); + if (len != sizeof(cmd)) { + qDebug() << "RemoteTCPSinkSink::sendCommand: Failed to write all of message:" << len; + } + m_bytesTransmitted += sizeof(cmd); + client->flush(); + } +} + +void RemoteTCPSinkSink::sendMessage(QHostAddress address, quint16 port, const QString& callsign, const QString& text, bool broadcast) +{ + qint64 len; + char cmd[1+4+1]; + QByteArray callsignBytes = callsign.toUtf8(); + QByteArray textBytes = text.toUtf8(); + QByteArray bytes; + + bytes.append(callsignBytes); + bytes.append('\0'); + bytes.append(textBytes); + bytes.append('\0'); + + cmd[0] = (char) RemoteTCPProtocol::sendMessage; + RemoteTCPProtocol::encodeUInt32((quint8*) &cmd[1], bytes.size() + 1); + cmd[5] = (char) broadcast; + + for (const auto client : m_clients) + { + bool addressMatch = (address == client->peerAddress()) && (port == client->peerPort()); + if ((broadcast && !addressMatch) || (!broadcast && addressMatch)) + { + len = client->write(cmd, sizeof(cmd)); + if (len != sizeof(cmd)) { + qDebug() << "RemoteTCPSinkSink::sendMessage: Failed to write all of message header:" << len; + } + len = client->write(bytes.data(), bytes.size()); + if (len != bytes.size()) { + qDebug() << "RemoteTCPSinkSink::sendMessage: Failed to write all of message:" << len; + } + m_bytesTransmitted += sizeof(cmd) + bytes.size(); + client->flush(); + qDebug() << "RemoteTCPSinkSink::sendMessage:" << client->peerAddress() << client->peerPort() << text; + } + } +} + +void RemoteTCPSinkSink::sendQueuePosition(Socket *client, int position) +{ + QString callsign = MainCore::instance()->getSettings().getStationName(); + + sendMessage(client->peerAddress(), client->peerPort(), callsign, QString("Server busy. You are number %1 in the queue.").arg(position), false); +} + +void RemoteTCPSinkSink::sendBlacklisted(Socket *client) +{ + char cmd[1+4]; + + cmd[0] = (char) RemoteTCPProtocol::sendBlacklistedMessage; + RemoteTCPProtocol::encodeUInt32((quint8*) &cmd[1], 0); + + qint64 len = client->write(cmd, sizeof(cmd)); + if (len != sizeof(cmd)) { + qDebug() << "RemoteTCPSinkSink::sendBlacklisted: Failed to write all of message:" << len; + } + m_bytesTransmitted += sizeof(cmd); + client->flush(); +} + +void RemoteTCPSinkSink::sendTimeLimit(Socket *client) +{ + QString callsign = MainCore::instance()->getSettings().getStationName(); + + sendMessage(client->peerAddress(), client->peerPort(), callsign, "Time limit reached.", false); +} + +void RemoteTCPSinkSink::sendPosition(float latitude, float longitude, float altitude) +{ + char msg[1+4+4+4+4]; + msg[0] = (char) RemoteTCPProtocol::dataPosition; + RemoteTCPProtocol::encodeUInt32((quint8 *) &msg[1], 4+4+4); + RemoteTCPProtocol::encodeFloat((quint8 *) &msg[1+4], latitude); + RemoteTCPProtocol::encodeFloat((quint8 *) &msg[1+4+4], longitude); + RemoteTCPProtocol::encodeFloat((quint8 *) &msg[1+4+4+4], altitude); + + int clients = std::min((int) m_clients.size(), m_settings.m_maxClients); + for (int i = 0; i < clients; i++) + { + Socket *client = m_clients[i]; + client->write(msg, sizeof(msg)); + m_bytesTransmitted += sizeof(msg); + client->flush(); + } +} + +void RemoteTCPSinkSink::sendDirection(bool isotropic, float azimuth, float elevation) +{ + char msg[1+4+4+4+4]; + msg[0] = (char) RemoteTCPProtocol::dataDirection; + RemoteTCPProtocol::encodeUInt32((quint8 *) &msg[1], 4+4+4); + RemoteTCPProtocol::encodeUInt32((quint8 *) &msg[1+4], isotropic); + RemoteTCPProtocol::encodeFloat((quint8 *) &msg[1+4+4], azimuth); + RemoteTCPProtocol::encodeFloat((quint8 *) &msg[1+4+4+5], elevation); + + int clients = std::min((int) m_clients.size(), m_settings.m_maxClients); + for (int i = 0; i < clients; i++) + { + Socket *client = m_clients[i]; + client->write(msg, sizeof(msg)); + m_bytesTransmitted += sizeof(msg); + client->flush(); + } +} + +void RemoteTCPSinkSink::sendPosition() +{ + float latitude = MainCore::instance()->getSettings().getLatitude(); + float longitude = MainCore::instance()->getSettings().getLongitude(); + float altitude = MainCore::instance()->getSettings().getAltitude(); + + // Use device postion in preference to My Position + ChannelWebAPIUtils::getDevicePosition(m_deviceIndex, latitude, longitude, altitude); + + sendPosition(latitude, longitude, altitude); +} + +void RemoteTCPSinkSink::sendRotatorDirection(bool force) +{ + unsigned int rotatorFeatureSetIndex; + unsigned int rotatorFeatureIndex; + + if (MainCore::getFeatureIndexFromId(m_settings.m_rotator, rotatorFeatureSetIndex, rotatorFeatureIndex)) + { + double azimuth; + double elevation; + + if (ChannelWebAPIUtils::getFeatureReportValue(rotatorFeatureSetIndex, rotatorFeatureIndex, "currentAzimuth", azimuth) + && ChannelWebAPIUtils::getFeatureReportValue(rotatorFeatureSetIndex, rotatorFeatureIndex, "currentElevation", elevation)) + { + if (force || ((azimuth != m_azimuth) || (elevation != m_elevation))) + { + sendDirection(false, (float) azimuth, (float) elevation); + m_azimuth = azimuth; + m_elevation = elevation; + } + } + } +} + +void RemoteTCPSinkSink::preferenceChanged(int elementType) +{ + Preferences::ElementType pref = (Preferences::ElementType)elementType; + + if ((pref == Preferences::Latitude) || (pref == Preferences::Longitude) || (pref == Preferences::Altitude)) { + sendPosition(); + } +} + +// Poll for changes to device settings - FIXME: Need a signal from DeviceAPI - reverseAPI? +void RemoteTCPSinkSink::checkDeviceSettings() +{ + if ((m_settings.m_protocol != RemoteTCPSinkSettings::RTL0) && !m_settings.m_iqOnly) + { + // Forward device settings to clients if they've changed + + double centerFrequency; + if (ChannelWebAPIUtils::getCenterFrequency(m_deviceIndex, centerFrequency)) + { + if (centerFrequency != m_centerFrequency) + { + m_centerFrequency = centerFrequency; + sendCommand(RemoteTCPProtocol::setCenterFrequency, m_centerFrequency); + } + } + + int ppmCorrection; + if (ChannelWebAPIUtils::getLOPpmCorrection(m_deviceIndex, ppmCorrection)) + { + if (ppmCorrection != m_ppmCorrection) + { + m_ppmCorrection = ppmCorrection; + sendCommand(RemoteTCPProtocol::setFrequencyCorrection, m_ppmCorrection); + } + } + + int biasTeeEnabled; + if (ChannelWebAPIUtils::getBiasTee(m_deviceIndex, biasTeeEnabled)) + { + if (biasTeeEnabled != m_biasTeeEnabled) + { + m_biasTeeEnabled = biasTeeEnabled; + sendCommand(RemoteTCPProtocol::setBiasTee, m_biasTeeEnabled); + } + } + + int directSampling; + if (ChannelWebAPIUtils::getDeviceSetting(m_deviceIndex, "noModMode", directSampling)) + { + if (directSampling != m_directSampling) + { + m_directSampling = directSampling; + sendCommand(RemoteTCPProtocol::setDirectSampling, m_directSampling); + } + } + + int agc; + if (ChannelWebAPIUtils::getAGC(m_deviceIndex, agc)) + { + if (agc != m_agc) + { + m_agc = agc; + sendCommand(RemoteTCPProtocol::setAGCMode, m_agc); + } + } + + int dcOffsetRemoval; + if (ChannelWebAPIUtils::getDCOffsetRemoval(m_deviceIndex, dcOffsetRemoval)) + { + if (dcOffsetRemoval != m_dcOffsetRemoval) + { + m_dcOffsetRemoval = dcOffsetRemoval; + sendCommand(RemoteTCPProtocol::setDCOffsetRemoval, m_dcOffsetRemoval); + } + } + + int iqCorrection; + if (ChannelWebAPIUtils::getIQCorrection(m_deviceIndex, iqCorrection)) + { + if (iqCorrection != m_iqCorrection) + { + m_iqCorrection = iqCorrection; + sendCommand(RemoteTCPProtocol::setIQCorrection, m_iqCorrection); + } + } + + qint32 devSampleRate; + if (ChannelWebAPIUtils::getDevSampleRate(m_deviceIndex, devSampleRate)) + { + if (devSampleRate != m_devSampleRate) + { + m_devSampleRate = devSampleRate; + sendCommand(RemoteTCPProtocol::setSampleRate, m_devSampleRate); + } + } + + qint32 log2Decim; + if (ChannelWebAPIUtils::getSoftDecim(m_deviceIndex, log2Decim)) + { + if (log2Decim != m_log2Decim) + { + m_log2Decim = log2Decim; + sendCommand(RemoteTCPProtocol::setDecimation, m_log2Decim); + } + } + + + qint32 rfBW; + if (ChannelWebAPIUtils::getRFBandwidth(m_deviceIndex, rfBW)) + { + if (rfBW != m_rfBW) + { + m_rfBW = rfBW; + sendCommand(RemoteTCPProtocol::setTunerBandwidth, m_rfBW); + } + } + + for (int i = 0; i < 4; i++) + { + qint32 gain; + if (ChannelWebAPIUtils::getGain(m_deviceIndex, i, gain)) + { + if (gain != m_gain[i]) + { + m_gain[i] = gain; + if (i == 0) + { + sendCommand(RemoteTCPProtocol::setTunerGain, gain); + } + else + { + int v = (gain & 0xffff) | (i << 16); + sendCommand(RemoteTCPProtocol::setTunerIFGain, v); + } + } + } + } + + if (!m_settings.m_isotropic && !m_settings.m_rotator.isEmpty() && (m_settings.m_rotator != "None")) { + sendRotatorDirection(false); + } + + } +} diff --git a/plugins/channelrx/remotetcpsink/remotetcpsinksink.h b/plugins/channelrx/remotetcpsink/remotetcpsinksink.h index d012bdc6d..dd5f34739 100644 --- a/plugins/channelrx/remotetcpsink/remotetcpsinksink.h +++ b/plugins/channelrx/remotetcpsink/remotetcpsinksink.h @@ -1,5 +1,5 @@ /////////////////////////////////////////////////////////////////////////////////// -// Copyright (C) 2022-2023 Jon Beniston, M7RCE // +// Copyright (C) 2022-2024 Jon Beniston, M7RCE // // Copyright (C) 2022 Jiří Pinkava // // // // This program is free software; you can redistribute it and/or modify // @@ -23,16 +23,25 @@ #include #include #include +#include #include +#include + +#include +#include #include "dsp/channelsamplesink.h" #include "dsp/nco.h" #include "dsp/interpolator.h" #include "util/messagequeue.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" #include "remotetcpsinksettings.h" #include "remotetcpprotocol.h" +#include "socket.h" + class DeviceSampleSource; class RemoteTCPSinkSink : public QObject, public ChannelSampleSink { @@ -47,22 +56,75 @@ public: void stop(); void init(); - void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool remoteChange = false); + void applySettings(const RemoteTCPSinkSettings& settings, const QStringList& settingsKeys, bool force = false, bool restartRequired = false); void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); void setDeviceIndex(uint32_t deviceIndex) { m_deviceIndex = deviceIndex; } void setChannelIndex(uint32_t channelIndex) { m_channelIndex = channelIndex; } void setMessageQueueToGUI(MessageQueue *queue) { m_messageQueueToGUI = queue; } void setMessageQueueToChannel(MessageQueue *queue) { m_messageQueueToChannel = queue; } + void acceptConnection(Socket *client); + Socket *getSocket(QObject *object) const; + void sendCommand(RemoteTCPProtocol::Command cmd, quint32 value); + void sendCommandFloat(RemoteTCPProtocol::Command cmd, float value); + void sendMessage(QHostAddress address, quint16 port, const QString& callsign, const QString& text, bool broadcast); + + FLAC__StreamEncoderWriteStatus flacWrite(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, uint32_t samples, uint32_t currentFrame); + + bool getSquelchOpen() const { return m_squelchOpen; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + +private: + void sendQueuePosition(Socket *client, int position); + void sendBlacklisted(Socket *client); + void sendTimeLimit(Socket *client); + void sendPosition(float latitude, float longitude, float altitude); + void sendDirection(bool isotropic, float azimuth, float elevation); + void sendPosition(); + void sendRotatorDirection(bool force); private slots: - void acceptConnection(); + void acceptTCPConnection(); + void acceptWebConnection(); void disconnected(); void errorOccurred(QAbstractSocket::SocketError socketError); void processCommand(); void started(); void finished(); +#ifndef QT_NO_OPENSSL + void onSslErrors(const QList &errors); +#endif + void preferenceChanged(int elementType); + void checkDeviceSettings(); private: + + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + RemoteTCPSinkSettings m_settings; bool m_running; MessageQueue *m_messageQueueToGUI; @@ -76,7 +138,9 @@ private: QRecursiveMutex m_mutex; QTcpServer *m_server; - QList m_clients; + QWebSocketServer *m_webSocketServer; + QList m_clients; + QList m_timers; QDateTime m_bwDateTime; //!< For calculating TX bandwidth qint64 m_bwBytes; @@ -86,6 +150,52 @@ private: Real m_interpolatorDistance; Real m_interpolatorDistanceRemain; + // FLAC compression + FLAC__StreamEncoder *m_encoder; + QByteArray m_flacHeader; + static const int m_flacHeaderSize = 4 + 38 + 51; + + // Zlib compression + z_stream m_zStream; + bool m_zStreamInitialised; + QByteArray m_zInBuf; + QByteArray m_zOutBuf; + int m_zInBufCount; + static const int m_zBufSize = 32768+128; // + + qint64 m_bytesUncompressed; + qint64 m_bytesCompressed; + qint64 m_bytesTransmitted; + + Real m_squelchLevel; + int m_squelchCount; + bool m_squelchOpen; + DoubleBufferFIFO m_squelchDelayLine; + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + MovingAverageUtil m_movingAverage; + + // Device settings + double m_centerFrequency; + qint32 m_ppmCorrection; + int m_biasTeeEnabled; + int m_directSampling; + int m_agc; + int m_dcOffsetRemoval; + int m_iqCorrection; + qint32 m_devSampleRate; + qint32 m_log2Decim; + qint32 m_rfBW; + qint32 m_gain[4]; + QTimer m_timer; + + // Rotator setttings + double m_azimuth; + double m_elevation; + void startServer(); void stopServer(); void processOneSample(Complex &ci); diff --git a/plugins/channelrx/remotetcpsink/socket.cpp b/plugins/channelrx/remotetcpsink/socket.cpp new file mode 100644 index 000000000..694f2503f --- /dev/null +++ b/plugins/channelrx/remotetcpsink/socket.cpp @@ -0,0 +1,160 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "socket.h" + +Socket::Socket(QObject *socket, QObject *parent) : + QObject(parent), + m_socket(socket) +{ +} + +Socket::~Socket() +{ + delete m_socket; +} + +TCPSocket::TCPSocket(QTcpSocket *socket) : + Socket(socket) +{ +} + +qint64 TCPSocket::write(const char *data, qint64 length) +{ + QTcpSocket *socket = qobject_cast(m_socket); + + return socket->write(data, length); +} + +qint64 TCPSocket::read(char *data, qint64 length) +{ + QTcpSocket *socket = qobject_cast(m_socket); + + return socket->read(data, length); +} + +void TCPSocket::close() +{ + QTcpSocket *socket = qobject_cast(m_socket); + + socket->close(); +} + +qint64 TCPSocket::bytesAvailable() +{ + QTcpSocket *socket = qobject_cast(m_socket); + + return socket->bytesAvailable(); +} + +void TCPSocket::flush() +{ + QTcpSocket *socket = qobject_cast(m_socket); + + socket->flush(); +} + +QHostAddress TCPSocket::peerAddress() +{ + QTcpSocket *socket = qobject_cast(m_socket); + + return socket->peerAddress(); +} + +quint16 TCPSocket::peerPort() +{ + QTcpSocket *socket = qobject_cast(m_socket); + + return socket->peerPort(); +} + +WebSocket::WebSocket(QWebSocket *socket) : + Socket(socket) +{ + m_rxBuffer.reserve(64000); + m_txBuffer.reserve(64000); + connect(socket, &QWebSocket::binaryFrameReceived, this, &WebSocket::binaryFrameReceived); +} + + qint64 WebSocket::write(const char *data, qint64 length) +{ + //QWebSocket *socket = qobject_cast(m_socket); + //return socket->sendBinaryMessage(QByteArray(data, length)); + + m_txBuffer.append(data, length); + return length; +} + +void WebSocket::flush() +{ + QWebSocket *socket = qobject_cast(m_socket); + if (m_txBuffer.size() > 0) + { + qint64 len = socket->sendBinaryMessage(m_txBuffer); + if (len != m_txBuffer.size()) { + qDebug() << "WebSocket::flush: Failed to send all of message" << len << "/" << m_txBuffer.size(); + } + m_txBuffer.clear(); + } + socket->flush(); +} + +qint64 WebSocket::read(char *data, qint64 length) +{ + length = std::min(length, (qint64)m_rxBuffer.size()); + + memcpy(data, m_rxBuffer.constData(), length); + + m_rxBuffer = m_rxBuffer.mid(length); // Yep, not very efficient + + return length; +} + +void WebSocket::close() +{ + QWebSocket *socket = qobject_cast(m_socket); + + socket->close(); +} + +qint64 WebSocket::bytesAvailable() +{ + return m_rxBuffer.size(); +} + +void WebSocket::binaryFrameReceived(const QByteArray &frame, bool isLastFrame) +{ + (void) isLastFrame; + + m_rxBuffer.append(frame); +} + +QHostAddress WebSocket::peerAddress() +{ + QWebSocket *socket = qobject_cast(m_socket); + + return socket->peerAddress(); +} + +quint16 WebSocket::peerPort() +{ + QWebSocket *socket = qobject_cast(m_socket); + + return socket->peerPort(); +} diff --git a/plugins/channelrx/remotetcpsink/socket.h b/plugins/channelrx/remotetcpsink/socket.h new file mode 100644 index 000000000..48ddc3540 --- /dev/null +++ b/plugins/channelrx/remotetcpsink/socket.h @@ -0,0 +1,89 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_SOCKET_H_ +#define INCLUDE_SOCKET_H_ + +#include +#include + +// Class to allow easy use of either QTCPSocket or QWebSocket +class Socket : public QObject { + Q_OBJECT +protected: + Socket(QObject *socket, QObject *parent=nullptr); + +public: + virtual ~Socket(); + virtual qint64 write(const char *data, qint64 length) = 0; + virtual void flush() = 0; + virtual qint64 read(char *data, qint64 length) = 0; + virtual qint64 bytesAvailable() = 0; + virtual void close() = 0; + virtual QHostAddress peerAddress() = 0; + virtual quint16 peerPort() = 0; + + QObject *socket() { return m_socket; } + +protected: + + QObject *m_socket; + +}; + +class TCPSocket : public Socket { + Q_OBJECT + +public: + + TCPSocket(QTcpSocket *socket) ; + qint64 write(const char *data, qint64 length) override; + void flush() override; + qint64 read(char *data, qint64 length) override; + qint64 bytesAvailable() override; + void close() override; + QHostAddress peerAddress() override; + quint16 peerPort() override; + +}; + +class WebSocket : public Socket { + Q_OBJECT + +public: + + WebSocket(QWebSocket *socket); + qint64 write(const char *data, qint64 length) override; + void flush() override; + qint64 read(char *data, qint64 length) override; + qint64 bytesAvailable() override; + void close() override; + QHostAddress peerAddress() override; + quint16 peerPort() override; + +private slots: + + void binaryFrameReceived(const QByteArray &frame, bool isLastFrame); + +private: + + QByteArray m_rxBuffer; + QByteArray m_txBuffer; + +}; + +#endif // INCLUDE_SOCKET_H_ diff --git a/plugins/feature/map/mapgui.cpp b/plugins/feature/map/mapgui.cpp index b62565cdd..a729796cc 100644 --- a/plugins/feature/map/mapgui.cpp +++ b/plugins/feature/map/mapgui.cpp @@ -295,6 +295,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur connect(&m_kiwiSDRList, &KiwiSDRList::dataUpdated, this, &MapGUI::kiwiSDRUpdated); connect(&m_spyServerList, &SpyServerList::dataUpdated, this, &MapGUI::spyServerUpdated); + connect(&m_sdrangelServerList, &SDRangelServerList::dataUpdated, this, &MapGUI::sdrangelServerUpdated); #ifdef QT_WEBENGINE_FOUND QWebEngineSettings *settings = ui->web->settings(); @@ -309,6 +310,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur connect(profile, &QWebEngineProfile::downloadRequested, this, &MapGUI::downloadRequested); #endif +qDebug() << "Get station position"; // Get station position float stationLatitude = MainCore::instance()->getSettings().getLatitude(); float stationLongitude = MainCore::instance()->getSettings().getLongitude(); @@ -320,6 +322,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur m_polygonMapFilter.setPosition(stationPosition); m_polylineMapFilter.setPosition(stationPosition); +qDebug() << "Centre map"; // Centre map at My Position QQuickItem *item = ui->map->rootObject(); QObject *object = item->findChild("map"); @@ -331,6 +334,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur object->setProperty("center", QVariant::fromValue(coords)); } +qDebug() << "Creating antenna"; // Create antenna at My Position m_antennaMapItem.setName(new QString("Station")); m_antennaMapItem.setLatitude(stationLatitude); @@ -358,7 +362,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur setBeacons(beacons); } addIBPBeacons(); - + addNAT(); addRadioTimeTransmitters(); addRadar(); addIonosonde(); @@ -371,6 +375,7 @@ MapGUI::MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *featur addVLF(); addKiwiSDR(); addSpyServer(); + addSDRangelServer(); displaySettings(); applySettings(true); @@ -490,6 +495,44 @@ void MapGUI::addIBPBeacons() } } +// https://www.icao.int/EURNAT/EUR%20and%20NAT%20Documents/NAT%20Documents/NAT%20Documents/NAT%20Doc%20003/NAT%20Doc003%20-%20HF%20Guidance%20v3.0.0_2015.pdf +// Coords aren't precise +const QList MapGUI::m_natTransmitters = { + {"Bodo", 0, 67.26742f, 14.34990, 0}, + {"Gander", 0, 48.993056f, -54.674444f, 0}, + {"Iceland", 0, 64.08516f, -21.84531f, 0}, + {"New York", 0, 40.881111f, -72.647778f, 0}, + {"Santa Maria", 0, 36.995556f, -25.170556, 0}, + {"Shanwick", 0, 52.75f, -8.933333f, 0}, +}; + +// North Atlantic HF ATC ground stations +void MapGUI::addNAT() +{ + for (int i = 0; i < m_natTransmitters.size(); i++) + { + SWGSDRangel::SWGMapItem natMapItem; + // Need to suffix frequency, as there are multiple becaons with same callsign at different locations + QString name = QString("%1").arg(m_natTransmitters[i].m_callsign); + natMapItem.setName(new QString(name)); + natMapItem.setLatitude(m_natTransmitters[i].m_latitude); + natMapItem.setLongitude(m_natTransmitters[i].m_longitude); + natMapItem.setAltitude(0.0); + natMapItem.setImage(new QString("antenna.png")); + natMapItem.setImageRotation(0); + QString text = QString("NAT ATC Transmitter\nCallsign: %1") + .arg(m_natTransmitters[i].m_callsign); + natMapItem.setText(new QString(text)); + natMapItem.setModel(new QString("antenna.glb")); + natMapItem.setFixedPosition(true); + natMapItem.setOrientation(0); + natMapItem.setLabel(new QString(name)); + natMapItem.setLabelAltitudeOffset(4.5); + natMapItem.setAltitudeReference(1); + update(m_map, &natMapItem, "NAT ATC Transmitters"); + } +} + void MapGUI::addVLF() { for (int i = 0; i < VLFTransmitters::m_transmitters.size(); i++) @@ -699,6 +742,75 @@ void MapGUI::spyServerUpdated(const QList& sdrs) } } +void MapGUI::addSDRangelServer() +{ + m_sdrangelServerList.getDataPeriodically(); +} + +void MapGUI::sdrangelServerUpdated(const QList& sdrs) +{ + for (const auto& sdr : sdrs) + { + SWGSDRangel::SWGMapItem sdrangelServerMapItem; + + QString address = QString("%1:%2").arg(sdr.m_address).arg(sdr.m_port); + sdrangelServerMapItem.setName(new QString(address)); + sdrangelServerMapItem.setLatitude(sdr.m_latitude); + sdrangelServerMapItem.setLongitude(sdr.m_longitude); + sdrangelServerMapItem.setAltitude(sdr.m_altitude); + sdrangelServerMapItem.setImage(new QString("antennaangel.png")); + sdrangelServerMapItem.setImageRotation(0); + QStringList antenna; + if (!sdr.m_antenna.isEmpty()) { + antenna.append(sdr.m_antenna); + } + if (sdr.m_isotropic) { + antenna.append("Isotropic"); + } else { + antenna.append(QString("Az: %1%3 El: %2%3").arg(sdr.m_azimuth).arg(sdr.m_elevation).arg(QChar(0x00b0))); + } + + QString text = QString("SDRangel\n\nStation: %1\nDevice: %2\nAntenna: %3\nFrequency: %4 - %5\nRemote control: %6\nUsers: %7/%8") + .arg(sdr.m_stationName) + .arg(sdr.m_device) + .arg(antenna.join(" - ")) + .arg(formatFrequency(sdr.m_minFrequency)) + .arg(formatFrequency(sdr.m_maxFrequency)) + .arg(sdr.m_remoteControl ? "Yes" : "No") + .arg(sdr.m_clients) + .arg(sdr.m_maxClients) + ; + if (sdr.m_timeLimit > 0) { + text.append(QString("\nTime limit: %1 mins").arg(sdr.m_timeLimit)); + } + QString url = QString("sdrangel-server://%1").arg(address); + QString link = QString("%2").arg(url).arg(address); + text.append(QString("\nURL: %1").arg(link)); + sdrangelServerMapItem.setText(new QString(text)); + sdrangelServerMapItem.setModel(new QString("antenna.glb")); + sdrangelServerMapItem.setFixedPosition(true); + sdrangelServerMapItem.setOrientation(0); + QStringList bands; + if (sdr.m_minFrequency < 30000000) { + bands.append("HF"); + } + if ((sdr.m_minFrequency < 300000000) && (sdr.m_maxFrequency > 30000000)) { + bands.append("VHF"); + } + if ((sdr.m_minFrequency < 3000000000) && (sdr.m_maxFrequency > 300000000)) { + bands.append("UHF"); + } + if (sdr.m_maxFrequency > 3000000000) { + bands.append("SHF"); + } + QString label = QString("SDRangel %1").arg(bands.join(" ")); + sdrangelServerMapItem.setLabel(new QString(label)); + sdrangelServerMapItem.setLabelAltitudeOffset(4.5); + sdrangelServerMapItem.setAltitudeReference(1); + update(m_map, &sdrangelServerMapItem, "SDRangel"); + } +} + // Ionosonde stations void MapGUI::addIonosonde() { @@ -1582,8 +1694,13 @@ void MapGUI::applyMap2DSettings(bool reloadMap) if (!m_settings.m_osmURL.isEmpty()) { parameters["osm.mapping.custom.host"] = m_settings.m_osmURL; // E.g: "http://a.tile.openstreetmap.fr/hot/" } +#ifdef __EMSCRIPTEN__ + // Default is http://maps-redirect.qt.io/osm/5.8/ and Emscripten needs https + parameters["osm.mapping.providersrepository.address"] = QString("https://sdrangel.beniston.com/sdrangel/maps/"); +#else // Use our repo, so we can append API key parameters["osm.mapping.providersrepository.address"] = QString("http://127.0.0.1:%1/").arg(m_osmPort); +#endif // Use application specific cache, as other apps may not use API key so will have different images QString cachePath = osmCachePath(); parameters["osm.mapping.cache.directory"] = cachePath; @@ -1685,6 +1802,9 @@ void MapGUI::displayToolbar() bool narrow = this->screen()->availableGeometry().width() < 400; ui->layersMenu->setVisible(narrow); bool overlayButtons = !narrow && ((m_settings.m_mapProvider == "osm") || m_settings.m_map3DEnabled); +#ifdef __EMSCRIPTEN__ + overlayButtons = false; +#endif ui->displayRain->setVisible(overlayButtons); ui->displayClouds->setVisible(overlayButtons); ui->displaySeaMarks->setVisible(overlayButtons); @@ -2600,12 +2720,16 @@ void MapGUI::linkClicked(const QString& url) QString spyServerURL = url.mid(21); openSpyServer(spyServerURL); } + else if (url.startsWith("sdrangel-server://")) + { + QString sdrangelServerURL = url.mid(18); + openSDRangelServer(sdrangelServerURL); + } } -// Open a KiwiSDR RX device -void MapGUI::openKiwiSDR(const QString& url) +bool MapGUI::openKiwiSDRInput() { - // Create DeviceSet + // Create DeviceSet MainCore *mainCore = MainCore::instance(); unsigned int deviceSetIndex = mainCore->getDeviceSets().size(); MainCore::MsgAddDeviceSet *msg = MainCore::MsgAddDeviceSet::create(0); @@ -2634,39 +2758,43 @@ void MapGUI::openKiwiSDR(const QString& url) if (!found) { qCritical() << "MapGUI::openKiwiSDR: Failed to find KiwiSDR"; - return; + return false; } - // Wait until device is created - is there a better way? - DeviceSet *deviceSet = nullptr; - do - { - QTime dieTime = QTime::currentTime().addMSecs(100); - while (QTime::currentTime() < dieTime) { - QCoreApplication::processEvents(QEventLoop::AllEvents, 100); - } - if (mainCore->getDeviceSets().size() > deviceSetIndex) - { - deviceSet = mainCore->getDeviceSets()[deviceSetIndex]; - } - } - while (!deviceSet); - // Move to same workspace //getWorkspaceIndex(); - // Set address setting - QStringList deviceSettingsKeys = {"serverAddress"}; - SWGSDRangel::SWGDeviceSettings response; - response.init(); - SWGSDRangel::SWGKiwiSDRSettings *deviceSettings = response.getKiwiSdrSettings(); - deviceSettings->setServerAddress(new QString(url)); - QString errorMessage; - deviceSet->m_deviceAPI->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); + return true; } -// Open a RemoteTCPInput device to use for SpyServer -void MapGUI::openSpyServer(const QString& url) +// Open a KiwiSDR RX device +void MapGUI::openKiwiSDR(const QString& url) +{ + m_remoteDeviceAddress = url; + connect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::kiwiSDRDeviceSetAdded); + if (!openKiwiSDRInput()) { + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::kiwiSDRDeviceSetAdded); + } +} + +void MapGUI::kiwiSDRDeviceSetAdded(int index, DeviceAPI *device) +{ + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::kiwiSDRDeviceSetAdded); + + // FIXME: Doesn't work if we do it immediately. Settings overwritten? + QTimer::singleShot(200, [=] { + // Set address setting + QStringList deviceSettingsKeys = {"serverAddress"}; + SWGSDRangel::SWGDeviceSettings response; + response.init(); + SWGSDRangel::SWGKiwiSDRSettings *deviceSettings = response.getKiwiSdrSettings(); + deviceSettings->setServerAddress(new QString(m_remoteDeviceAddress)); + QString errorMessage; + device->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); + }); +} + +bool MapGUI::openRemoteTCPInput() { // Create DeviceSet MainCore *mainCore = MainCore::instance(); @@ -2696,39 +2824,79 @@ void MapGUI::openSpyServer(const QString& url) } if (!found) { - qCritical() << "MapGUI::openSpyServer: Failed to find RemoteTCPInput"; - return; + qCritical() << "MapGUI::openRemoteTCPInput: Failed to find RemoteTCPInput"; + return false; } - // Wait until device is created - is there a better way? - DeviceSet *deviceSet = nullptr; - do - { - QTime dieTime = QTime::currentTime().addMSecs(100); - while (QTime::currentTime() < dieTime) { - QCoreApplication::processEvents(QEventLoop::AllEvents, 100); - } - if (mainCore->getDeviceSets().size() > deviceSetIndex) - { - deviceSet = mainCore->getDeviceSets()[deviceSetIndex]; - } - } - while (!deviceSet); - // Move to same workspace //getWorkspaceIndex(); - // Set address/port setting + return true; +} + +// Open a RemoteTCPInput device to use for SpyServer +void MapGUI::openSpyServer(const QString& url) +{ QStringList address = url.split(":"); - QStringList deviceSettingsKeys = {"dataAddress", "dataPort", "protocol"}; - SWGSDRangel::SWGDeviceSettings response; - response.init(); - SWGSDRangel::SWGRemoteTCPInputSettings *deviceSettings = response.getRemoteTcpInputSettings(); - deviceSettings->setDataAddress(new QString(address[0])); - deviceSettings->setDataPort(address[1].toInt()); - deviceSettings->setProtocol(new QString("Spy Server")); - QString errorMessage; - deviceSet->m_deviceAPI->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); + m_remoteDeviceAddress = address[0]; + m_remoteDevicePort = address[1].toInt(); + connect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::spyServerDeviceSetAdded); + if (!openRemoteTCPInput()) { + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::spyServerDeviceSetAdded); + } +} + +void MapGUI::spyServerDeviceSetAdded(int index, DeviceAPI *device) +{ +qDebug() << "**************** MapGUI::spyServerDeviceSetAdded"; + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::spyServerDeviceSetAdded); + + // FIXME: Doesn't work if we do it immediately. Settings overwritten? + QTimer::singleShot(200, [=] { + // Set address/port setting + QStringList deviceSettingsKeys = {"dataAddress", "dataPort", "protocol", "overrideRemoteSettings"}; + SWGSDRangel::SWGDeviceSettings response; + response.init(); + SWGSDRangel::SWGRemoteTCPInputSettings *deviceSettings = response.getRemoteTcpInputSettings(); + deviceSettings->setDataAddress(new QString(m_remoteDeviceAddress)); + deviceSettings->setDataPort(m_remoteDevicePort); + deviceSettings->setProtocol(new QString("Spy Server")); + deviceSettings->setOverrideRemoteSettings(false); + QString errorMessage; + device->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); + }); +} + +// Open a RemoteTCPInput device to use for SDRangel +void MapGUI::openSDRangelServer(const QString& url) +{ + QStringList address = url.split(":"); + m_remoteDeviceAddress = address[0]; + m_remoteDevicePort = address[1].toInt(); + connect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::sdrangelServerDeviceSetAdded); + if (!openRemoteTCPInput()) { + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::sdrangelServerDeviceSetAdded); + } +} + +void MapGUI::sdrangelServerDeviceSetAdded(int index, DeviceAPI *device) +{ + disconnect(MainCore::instance(), &MainCore::deviceSetAdded, this, &MapGUI::sdrangelServerDeviceSetAdded); + + // FIXME: Doesn't work if we do it immediately. Settings overwritten? + QTimer::singleShot(200, [=] { + // Set address/port setting + QStringList deviceSettingsKeys = {"dataAddress", "dataPort", "protocol", "overrideRemoteSettings"}; + SWGSDRangel::SWGDeviceSettings response; + response.init(); + SWGSDRangel::SWGRemoteTCPInputSettings *deviceSettings = response.getRemoteTcpInputSettings(); + deviceSettings->setDataAddress(new QString(m_remoteDeviceAddress)); + deviceSettings->setDataPort(m_remoteDevicePort); + deviceSettings->setProtocol(new QString("SDRangel")); + deviceSettings->setOverrideRemoteSettings(false); + QString errorMessage; + device->getSampleSource()->webapiSettingsPutPatch(false, deviceSettingsKeys, response, errorMessage); + }); } #ifdef QT_WEBENGINE_FOUND @@ -2836,4 +3004,3 @@ void MapGUI::makeUIConnections() QObject::connect(ui->ibpBeacons, &QToolButton::clicked, this, &MapGUI::on_ibpBeacons_clicked); QObject::connect(ui->radiotime, &QToolButton::clicked, this, &MapGUI::on_radiotime_clicked); } - diff --git a/plugins/feature/map/mapgui.h b/plugins/feature/map/mapgui.h index f54bda778..4188698eb 100644 --- a/plugins/feature/map/mapgui.h +++ b/plugins/feature/map/mapgui.h @@ -47,6 +47,7 @@ #include "util/nasaglobalimagery.h" #include "util/kiwisdrlist.h" #include "util/spyserverlist.h" +#include "util/sdrangelserverlist.h" #include "settings/rollupstate.h" #include "availablechannelorfeaturehandler.h" @@ -169,6 +170,7 @@ public: void addIBPBeacons(); QList getRadioTimeTransmitters() { return m_radioTimeTransmitters; } void addRadioTimeTransmitters(); + void addNAT(); void addRadar(); void addIonosonde(); void addBroadcast(); @@ -182,6 +184,7 @@ public: void addVLF(); void addKiwiSDR(); void addSpyServer(); + void addSDRangelServer(); void find(const QString& target); void track3D(const QString& target); Q_INVOKABLE void supportedMapsChanged(); @@ -231,6 +234,7 @@ private: QGeoCoordinate m_lastFullUpdatePosition; KiwiSDRList m_kiwiSDRList; SpyServerList m_spyServerList; + SDRangelServerList m_sdrangelServerList; CesiumInterface *m_cesium; WebServer *m_webServer; @@ -257,6 +261,10 @@ private: QTableWidget *m_overviewWidget; QTextEdit *m_descriptionWidget; + // Settings for opening a device + QString m_remoteDeviceAddress; + quint16 m_remoteDevicePort; + explicit MapGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); virtual ~MapGUI(); @@ -282,13 +290,17 @@ private: void applyNASAGlobalImagerySettings(); void createNASAGlobalImageryView(); void displayNASAMetaData(); + bool openKiwiSDRInput(); + bool openRemoteTCPInput(); void openKiwiSDR(const QString& url); void openSpyServer(const QString& url); + void openSDRangelServer(const QString& url); QString formatFrequency(qint64 frequency) const; void updateGIRO(const QDateTime& mapDateTime); static QString getDataDir(); static const QList m_radioTimeTransmitters; + static const QList m_natTransmitters; static const QList m_vlfTransmitters; enum NASARow { @@ -359,9 +371,12 @@ private slots: void airportsUpdated(); void waypointsUpdated(); void kiwiSDRUpdated(const QList& sdrs); + void kiwiSDRDeviceSetAdded(int index, DeviceAPI *device); void spyServerUpdated(const QList& sdrs); + void spyServerDeviceSetAdded(int index, DeviceAPI *device); + void sdrangelServerUpdated(const QList& sdrs); + void sdrangelServerDeviceSetAdded(int index, DeviceAPI *device); void linkClicked(const QString& url); - }; #endif // INCLUDE_FEATURE_MAPGUI_H_ diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index 4ae6b6cbc..6c1f4a8ab 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -99,6 +99,7 @@ MapSettings::MapSettings() : m_itemSettings.insert("Radiosonde", new MapItemSettings("Radiosonde", true, QColor(102, 0, 102), true, false, 11, modelMinPixelSize)); m_itemSettings.insert("Radio Time Transmitters", new MapItemSettings("Radio Time Transmitters", true, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("Radar", new MapItemSettings("Radar", true, QColor(255, 0, 0), false, true, 8)); + m_itemSettings.insert("NAT ATC Transmitters", new MapItemSettings("NAT ATC Transmitters", false, QColor(255, 0, 0), false, true, 8)); m_itemSettings.insert("FT8Demod", new MapItemSettings("FT8Demod", true, QColor(0, 192, 255), true, true, 8)); m_itemSettings.insert("HeatMap", new MapItemSettings("HeatMap", true, QColor(102, 40, 220), true, true, 11)); m_itemSettings.insert("VLF", new MapItemSettings("VLF", false, QColor(255, 0, 0), false, true, 8)); @@ -157,8 +158,13 @@ MapSettings::MapSettings() : waypointsSettings->m_filterDistance = 500000; m_itemSettings.insert("Waypoints", waypointsSettings); - m_itemSettings.insert("KiwiSDR", new MapItemSettings("KiwiSDR", true, QColor(0, 255, 0), false, true, 8)); - m_itemSettings.insert("SpyServer", new MapItemSettings("SpyServer", true, QColor(0, 0, 255), false, true, 8)); + bool showOtherServers = true; +#ifdef __EMSCRIPTEN__ + showOtherServers = false; // Can't use without proxy +#endif + m_itemSettings.insert("KiwiSDR", new MapItemSettings("KiwiSDR", showOtherServers, QColor(0, 255, 0), false, true, 8)); + m_itemSettings.insert("SpyServer", new MapItemSettings("SpyServer", showOtherServers, QColor(0, 0, 255), false, true, 8)); + m_itemSettings.insert("SDRangel", new MapItemSettings("SDRangel", true, QColor(255, 0, 255), false, true, 8)); resetToDefaults(); } diff --git a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp index 06619e3ac..72a0f7522 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinput.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinput.cpp @@ -40,16 +40,27 @@ MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgConfigureRemoteTCPInput, Message) MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgStartStop, Message) MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgReportTCPBuffer, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgSaveReplay, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgSendMessage, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgReportPosition, Message) +MESSAGE_CLASS_DEFINITION(RemoteTCPInput::MsgReportDirection, Message) RemoteTCPInput::RemoteTCPInput(DeviceAPI *deviceAPI) : m_deviceAPI(deviceAPI), m_settings(), m_remoteInputTCPPHandler(nullptr), - m_deviceDescription("RemoteTCPInput") + m_deviceDescription("RemoteTCPInput"), + m_running(false), + m_latitude(std::numeric_limits::quiet_NaN()), + m_longitude(std::numeric_limits::quiet_NaN()), + m_altitude(std::numeric_limits::quiet_NaN()), + m_isotropic(false), + m_azimuth(std::numeric_limits::quiet_NaN()), + m_elevation(std::numeric_limits::quiet_NaN()) { m_sampleFifo.setLabel(m_deviceDescription); m_sampleFifo.setSize(48000 * 8); - m_remoteInputTCPPHandler = new RemoteTCPInputTCPHandler(&m_sampleFifo, m_deviceAPI); + m_remoteInputTCPPHandler = new RemoteTCPInputTCPHandler(&m_sampleFifo, m_deviceAPI, &m_replayBuffer); m_remoteInputTCPPHandler->moveToThread(&m_thread); m_remoteInputTCPPHandler->setMessageQueueToInput(&m_inputMessageQueue); @@ -66,6 +77,7 @@ RemoteTCPInput::RemoteTCPInput(DeviceAPI *deviceAPI) : RemoteTCPInput::~RemoteTCPInput() { + qDebug() << "RemoteTCPInput::~RemoteTCPInput"; QObject::disconnect( m_networkManager, &QNetworkAccessManager::finished, @@ -84,25 +96,45 @@ void RemoteTCPInput::destroy() void RemoteTCPInput::init() { + qDebug() << "*************** RemoteTCPInput::init"; applySettings(m_settings, QList(), true); } bool RemoteTCPInput::start() { qDebug() << "RemoteTCPInput::start"; + if (m_running) { + qDebug() << "RemoteTCPInput::stop - Already running"; + return true; + } m_remoteInputTCPPHandler->reset(); m_remoteInputTCPPHandler->start(); + qDebug() << "************ RemoteTCPInput::start" << m_settings.m_dataAddress; m_remoteInputTCPPHandler->getInputMessageQueue()->push(RemoteTCPInputTCPHandler::MsgConfigureTcpHandler::create(m_settings, QList(), true)); m_thread.start(); + m_running = true; return true; } void RemoteTCPInput::stop() { qDebug() << "RemoteTCPInput::stop"; + if (!m_running) { + // For wasm, important not to call m_remoteInputTCPPHandler->stop() twice + // as mutex can deadlock when this object is being deleted + qDebug() << "RemoteTCPInput::stop - Not running"; + return; + } m_remoteInputTCPPHandler->stop(); + qDebug() << "RemoteTCPInput::stop1"; m_thread.quit(); + qDebug() << "RemoteTCPInput::stop2"; +#ifndef __EMSCRIPTEN__ + qDebug() << "RemoteTCPInput::stop3"; m_thread.wait(); +#endif + m_running = false; + qDebug() << "RemoteTCPInput::stopped"; } QByteArray RemoteTCPInput::serialize() const @@ -120,6 +152,8 @@ bool RemoteTCPInput::deserialize(const QByteArray& data) success = false; } + qDebug() << "************** RemoteTCPInput::deserialize" << m_settings.m_dataAddress; + MsgConfigureRemoteTCPInput* message = MsgConfigureRemoteTCPInput::create(m_settings, QList(), true); m_inputMessageQueue.push(message); @@ -196,6 +230,7 @@ bool RemoteTCPInput::handleMessage(const Message& message) { qDebug() << "RemoteTCPInput::handleMessage:" << message.getIdentifier(); MsgConfigureRemoteTCPInput& conf = (MsgConfigureRemoteTCPInput&) message; + qDebug() << "*********** RemoteTCPInput::handleMessage MsgConfigureRemoteTCPInput" << m_settings.m_dataAddress; applySettings(conf.getSettings(), conf.getSettingsKeys(), conf.getForce()); return true; } @@ -210,6 +245,42 @@ bool RemoteTCPInput::handleMessage(const Message& message) } return true; } + else if (MsgSaveReplay::match(message)) + { + MsgSaveReplay& cmd = (MsgSaveReplay&) message; + m_replayBuffer.save(cmd.getFilename(), m_settings.m_devSampleRate, getCenterFrequency()); + return true; + } + else if (MsgSendMessage::match(message)) + { + MsgSendMessage& msg = (MsgSendMessage&) message; + m_remoteInputTCPPHandler->getInputMessageQueue()->push(MsgSendMessage::create(msg.getCallsign(), msg.getText(), msg.getBroadcast())); + return true; + } + else if (MsgReportPosition::match(message)) + { + MsgReportPosition& report = (MsgReportPosition&) message; + + m_latitude = report.getLatitude(); + m_longitude = report.getLongitude(); + m_altitude = report.getAltitude(); + + emit positionChanged(m_latitude, m_longitude, m_altitude); + + return true; + } + else if (MsgReportDirection::match(message)) + { + MsgReportDirection& report = (MsgReportDirection&) message; + + m_isotropic = report.getIsotropic(); + m_azimuth = report.getAzimuth(); + m_elevation = report.getElevation(); + + emit directionChanged(m_isotropic, m_azimuth, m_elevation); + + return true; + } else { return false; @@ -242,6 +313,12 @@ void RemoteTCPInput::applySettings(const RemoteTCPInputSettings& settings, const forwardChange = true; } + if ((settingsKeys.contains("channelSampleRate") || force) + && (settings.m_devSampleRate != m_settings.m_devSampleRate)) + { + m_replayBuffer.clear(); + } + mutexLocker.unlock(); if (settings.m_useReverseAPI) @@ -265,6 +342,18 @@ void RemoteTCPInput::applySettings(const RemoteTCPInputSettings& settings, const m_settings.applySettings(settingsKeys, settings); } + if (settingsKeys.contains("replayLength") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setSize(m_settings.m_replayLength, m_settings.m_devSampleRate); + } + + if (settingsKeys.contains("replayOffset") || settingsKeys.contains("devSampleRate") || force) { + m_replayBuffer.setReadOffset(((unsigned)(m_settings.m_replayOffset * m_settings.m_devSampleRate)) * 2); + } + + if (settingsKeys.contains("replayLoop") || force) { + m_replayBuffer.setLoop(m_settings.m_replayLoop); + } + m_remoteInputTCPPHandler->getInputMessageQueue()->push(RemoteTCPInputTCPHandler::MsgConfigureTcpHandler::create(m_settings, settingsKeys, force)); } @@ -459,6 +548,9 @@ int RemoteTCPInput::webapiReportGet( void RemoteTCPInput::webapiFormatDeviceReport(SWGSDRangel::SWGDeviceReport& response) { response.getRemoteTcpInputReport()->setSampleRate(m_settings.m_channelSampleRate); + response.getRemoteTcpInputReport()->setLatitude(m_latitude); + response.getRemoteTcpInputReport()->setLongitude(m_longitude); + response.getRemoteTcpInputReport()->setAltitude(m_altitude); } void RemoteTCPInput::webapiReverseSendSettings(const QList& deviceSettingsKeys, const RemoteTCPInputSettings& settings, bool force) diff --git a/plugins/samplesource/remotetcpinput/remotetcpinput.h b/plugins/samplesource/remotetcpinput/remotetcpinput.h index 56a1e4f37..449109a31 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinput.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinput.h @@ -31,13 +31,14 @@ #include #include "dsp/devicesamplesource.h" +#include "dsp/replaybuffer.h" #include "remotetcpinputsettings.h" +#include "remotetcpinputtcphandler.h" class QNetworkAccessManager; class QNetworkReply; class DeviceAPI; -class RemoteTCPInputTCPHandler; class RemoteTCPInput : public DeviceSampleSource { Q_OBJECT @@ -124,6 +125,100 @@ public: { } }; + class MsgSaveReplay : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QString getFilename() const { return m_filename; } + + static MsgSaveReplay* create(const QString& filename) { + return new MsgSaveReplay(filename); + } + + protected: + QString m_filename; + + MsgSaveReplay(const QString& filename) : + Message(), + m_filename(filename) + { } + }; + + class MsgSendMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const QString& getCallsign() const { return m_callsign; } + const QString& getText() const { return m_text; } + bool getBroadcast() const { return m_broadcast; } + + static MsgSendMessage* create(const QString& callsign, const QString& text, bool broadcast) { + return new MsgSendMessage(callsign, text, broadcast); + } + + protected: + QString m_callsign; + QString m_text; + bool m_broadcast; + + MsgSendMessage(const QString& callsign, const QString& text, bool broadcast) : + Message(), + m_callsign(callsign), + m_text(text), + m_broadcast(broadcast) + { } + }; + + class MsgReportPosition : public Message { + MESSAGE_CLASS_DECLARATION + + public: + float getLatitude() const { return m_latitude; } + float getLongitude() const { return m_longitude; } + float getAltitude() const { return m_altitude; } + + static MsgReportPosition* create(float latitude, float longitude, float altitude) { + return new MsgReportPosition(latitude, longitude, altitude); + } + + private: + float m_latitude; + float m_longitude; + float m_altitude; + + MsgReportPosition(float latitude, float longitude, float altitude) : + Message(), + m_latitude(latitude), + m_longitude(longitude), + m_altitude(altitude) + { } + }; + + class MsgReportDirection : public Message { + MESSAGE_CLASS_DECLARATION + + public: + bool getIsotropic() const { return m_isotropic; } + float getAzimuth() const { return m_azimuth; } + float getElevation() const { return m_elevation; } + + static MsgReportDirection* create(bool isotropic, float azimuth, float elevation) { + return new MsgReportDirection(isotropic, azimuth, elevation); + } + + private: + bool m_isotropic; + float m_azimuth; + float m_elevation; + + MsgReportDirection(bool isotropic, float azimuth, float elevation) : + Message(), + m_isotropic(isotropic), + m_azimuth(azimuth), + m_elevation(elevation) + { } + }; + RemoteTCPInput(DeviceAPI *deviceAPI); virtual ~RemoteTCPInput(); virtual void destroy(); @@ -177,6 +272,15 @@ public: const QStringList& deviceSettingsKeys, SWGSDRangel::SWGDeviceSettings& response); + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_remoteInputTCPPHandler) { + m_remoteInputTCPPHandler->getMagSqLevels(avg, peak, nbSamples); + } else { + avg = 0.0; peak = 0.0; nbSamples = 1; + } + } + private: DeviceAPI *m_deviceAPI; QRecursiveMutex m_mutex; @@ -185,7 +289,15 @@ private: QString m_deviceDescription; QNetworkAccessManager *m_networkManager; QNetworkRequest m_networkRequest; + ReplayBuffer m_replayBuffer; QThread m_thread; + bool m_running; + float m_latitude; // Position of remote device (antenna) + float m_longitude; + float m_altitude; + bool m_isotropic; // Direction of remote anntenna + float m_azimuth; + float m_elevation; void applySettings(const RemoteTCPInputSettings& settings, const QList& settingsKeys, bool force = false); void webapiFormatDeviceReport(SWGSDRangel::SWGDeviceReport& response); diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp index b1eec42ea..27fed839d 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "ui_remotetcpinputgui.h" #include "gui/colormapper.h" @@ -30,17 +31,19 @@ #include "dsp/dspcommands.h" #include "device/deviceapi.h" #include "device/deviceuiset.h" +#include "util/db.h" #include "remotetcpinputgui.h" #include "remotetcpinputtcphandler.h" +#include "maincore.h" RemoteTCPInputGui::RemoteTCPInputGui(DeviceUISet *deviceUISet, QWidget* parent) : DeviceGUI(parent), ui(new Ui::RemoteTCPInputGui), m_settings(), - m_sampleSource(0), - m_lastEngineState(DeviceAPI::StNotStarted), + m_sampleSource(nullptr), m_sampleRate(0), m_centerFrequency(0), + m_tickCount(0), m_doApplySettings(true), m_forceSettings(true), m_deviceGains(nullptr), @@ -71,12 +74,15 @@ RemoteTCPInputGui::RemoteTCPInputGui(DeviceUISet *deviceUISet, QWidget* parent) ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(openDeviceSettingsDialog(const QPoint &))); displaySettings(); - connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); - m_statusTimer.start(500); + connect(deviceUISet->m_deviceAPI, &DeviceAPI::stateChanged, this, &RemoteTCPInputGui::updateStatus); + updateStatus(); + connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updateHardware())); m_sampleSource = (RemoteTCPInput*) m_deviceUISet->m_deviceAPI->getSampleSource(); @@ -89,11 +95,12 @@ RemoteTCPInputGui::RemoteTCPInputGui(DeviceUISet *deviceUISet, QWidget* parent) makeUIConnections(); DialPopup::addPopupsToChildDials(this); m_resizer.enableChildMouseTracking(); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); } RemoteTCPInputGui::~RemoteTCPInputGui() { - m_statusTimer.stop(); m_updateTimer.stop(); delete ui; } @@ -110,6 +117,7 @@ void RemoteTCPInputGui::destroy() void RemoteTCPInputGui::resetToDefaults() { + qDebug() << "*************** RemoteTCPInputGui::resetToDefaults"; m_settings.resetToDefaults(); displaySettings(); m_forceSettings = true; @@ -150,6 +158,7 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) } else { m_settings.applySettings(cfg.getSettingsKeys(), cfg.getSettings()); } + qDebug() << "********* RemoteTCPInputGui::handleMessage MsgConfigureRemoteTCPInput" << m_settings.m_dataAddress; blockApplySettings(true); displaySettings(); @@ -224,24 +233,27 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) ui->detectedProtocol->setText(QString("Protocol: %1").arg(report.getProtocol())); // Update GUI so we only show widgets available for the protocol in use - bool sdra = report.getProtocol() == "SDRA"; - bool spyServer = report.getProtocol() == "Spy Server"; - if (spyServer) { + m_sdra = report.getProtocol() == "SDRA"; + m_spyServer = report.getProtocol() == "Spy Server"; + m_remoteControl = report.getRemoteControl(); + m_iqOnly = report.getIQOnly(); + + if (m_spyServer) { m_spyServerGains.m_gains[0].m_max = report.getMaxGain(); } - if ((sdra || spyServer) && (ui->sampleBits->count() < 4)) + if ((m_sdra || m_spyServer) && (ui->sampleBits->count() < 4)) { ui->sampleBits->addItem("16"); ui->sampleBits->addItem("24"); ui->sampleBits->addItem("32"); } - else if (!(sdra || spyServer) && (ui->sampleBits->count() != 1)) + else if (!(m_sdra || m_spyServer) && (ui->sampleBits->count() != 1)) { while (ui->sampleBits->count() > 1) { ui->sampleBits->removeItem(ui->sampleBits->count() - 1); } } - if ((sdra || spyServer) && (ui->decim->count() != 7)) + if ((m_sdra || m_spyServer) && (ui->decim->count() != 7)) { ui->decim->addItem("2"); ui->decim->addItem("4"); @@ -250,26 +262,19 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) ui->decim->addItem("32"); ui->decim->addItem("64"); } - else if (!(sdra || spyServer) && (ui->decim->count() != 1)) + else if (!(m_sdra || m_spyServer) && (ui->decim->count() != 1)) { while (ui->decim->count() > 1) { ui->decim->removeItem(ui->decim->count() - 1); } } - if (!sdra) + if (!m_sdra) { ui->deltaFrequency->setValue(0); ui->channelGain->setValue(0); ui->decimation->setChecked(true); } - ui->deltaFrequencyLabel->setEnabled(sdra); - ui->deltaFrequency->setEnabled(sdra); - ui->deltaUnits->setEnabled(sdra); - ui->channelGainLabel->setEnabled(sdra); - ui->channelGain->setEnabled(sdra); - ui->channelGainText->setEnabled(sdra); - ui->decimation->setEnabled(sdra); - if (sdra) { + if (m_sdra) { ui->centerFrequency->setValueRange(9, 0, 999999999); // Should add transverter control to protocol in the future } else { ui->centerFrequency->setValueRange(7, 0, 9999999); @@ -291,19 +296,7 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) { ui->devSampleRate->setValueRange(8, 0, 99999999); } - ui->devSampleRateLabel->setEnabled(!spyServer); - ui->devSampleRate->setEnabled(!spyServer); - ui->devSampleRateUnits->setEnabled(!spyServer); - ui->agc->setEnabled(!spyServer); - ui->rfBWLabel->setEnabled(!spyServer); - ui->rfBW->setEnabled(!spyServer); - ui->rfBWUnits->setEnabled(!spyServer); - ui->dcOffset->setEnabled(!spyServer); - ui->iqImbalance->setEnabled(!spyServer); - ui->ppm->setEnabled(!spyServer); - ui->ppmLabel->setEnabled(!spyServer); - ui->ppmText->setEnabled(!spyServer); - + displayEnabled(); displayGains(); return true; } @@ -314,13 +307,33 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) if (report.getConnected()) { m_connectionError = false; - ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); + //ui->startStop->setStyleSheet("QToolButton { background-color : green; }"); } else { m_connectionError = true; - ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + //ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); } + updateStatus(); + return true; + } + else if (RemoteTCPInput::MsgSendMessage::match(message)) + { + const RemoteTCPInput::MsgSendMessage& msg = (const RemoteTCPInput::MsgSendMessage&) message; + + ui->messages->addItem(QString("%1> %2").arg(msg.getCallsign()).arg(msg.getText())); + ui->messages->scrollToBottom(); + + return true; + } + else if (RemoteTCPInput::MsgReportPosition::match(message)) + { + // Could display in future + return true; + } + else if (RemoteTCPInput::MsgReportDirection::match(message)) + { + // Could display in future return true; } else @@ -329,6 +342,87 @@ bool RemoteTCPInputGui::handleMessage(const Message& message) } } +void RemoteTCPInputGui::displayEnabled() +{ + int state = m_deviceUISet->m_deviceAPI->state(); + bool remoteControl; + bool enableMessages; + bool enableSquelchEnable; + bool enableSquelch; + bool sdra; + + if (state == DeviceAPI::StRunning) + { + sdra = m_sdra; + remoteControl = m_remoteControl; + enableMessages = !m_iqOnly; + enableSquelchEnable = !m_iqOnly; + enableSquelch = !m_iqOnly && m_settings.m_squelchEnabled; + } + else + { + sdra = m_settings.m_protocol == "SDRangel"; + remoteControl = m_settings.m_overrideRemoteSettings; + enableMessages = false; + enableSquelchEnable = m_settings.m_overrideRemoteSettings; + enableSquelch = m_settings.m_overrideRemoteSettings && m_settings.m_squelchEnabled; + } + + ui->deltaFrequencyLabel->setEnabled(sdra && remoteControl); + ui->deltaFrequency->setEnabled(sdra && remoteControl); + ui->deltaUnits->setEnabled(sdra && remoteControl); + ui->channelGainLabel->setEnabled(sdra && remoteControl); + ui->channelGain->setEnabled(sdra && remoteControl); + ui->channelGainText->setEnabled(sdra && remoteControl); + ui->decimation->setEnabled(sdra && remoteControl); + + ui->channelSampleRate->setEnabled(m_settings.m_channelDecimation && sdra && remoteControl); + ui->channelSampleRateLabel->setEnabled(m_settings.m_channelDecimation && sdra && remoteControl); + ui->channelSampleRateUnit->setEnabled(m_settings.m_channelDecimation && sdra && remoteControl); + + ui->devSampleRateLabel->setEnabled(!m_spyServer && remoteControl); + ui->devSampleRate->setEnabled(!m_spyServer && remoteControl); + ui->devSampleRateUnits->setEnabled(!m_spyServer && remoteControl); + ui->agc->setEnabled(!m_spyServer && remoteControl); + ui->rfBWLabel->setEnabled(!m_spyServer && remoteControl); + ui->rfBW->setEnabled(!m_spyServer && remoteControl); + ui->rfBWUnits->setEnabled(!m_spyServer && remoteControl); + ui->dcOffset->setEnabled(!m_spyServer && remoteControl); + ui->iqImbalance->setEnabled(!m_spyServer && remoteControl); + ui->ppm->setEnabled(!m_spyServer && remoteControl); + ui->ppmLabel->setEnabled(!m_spyServer && remoteControl); + ui->ppmText->setEnabled(!m_spyServer && remoteControl); + + ui->centerFrequency->setEnabled(remoteControl); + ui->biasTee->setEnabled(remoteControl); + ui->directSampling->setEnabled(remoteControl); + ui->decimLabel->setEnabled(remoteControl); + ui->decim->setEnabled(remoteControl); + ui->gain1Label->setEnabled(remoteControl); + ui->gain1->setEnabled(remoteControl); + ui->gain1Text->setEnabled(remoteControl); + ui->gain2Label->setEnabled(remoteControl); + ui->gain2->setEnabled(remoteControl); + ui->gain2Text->setEnabled(remoteControl); + ui->gain3Label->setEnabled(remoteControl); + ui->gain3->setEnabled(remoteControl); + ui->gain3Text->setEnabled(remoteControl); + ui->sampleBitsLabel->setEnabled(remoteControl); + ui->sampleBits->setEnabled(remoteControl); + ui->sampleBitsUnits->setEnabled(remoteControl); + + ui->squelchEnabled->setEnabled(enableSquelchEnable); + ui->squelch->setEnabled(enableSquelch); + ui->squelchText->setEnabled(enableSquelch); + ui->squelchUnits->setEnabled(enableSquelch); + ui->squelchGate->setEnabled(enableSquelch); + + ui->sendMessage->setEnabled(enableMessages); + ui->txAddress->setEnabled(enableMessages); + ui->txMessage->setEnabled(enableMessages); + ui->messages->setEnabled(enableMessages); +} + void RemoteTCPInputGui::handleInputMessages() { Message* message; @@ -386,12 +480,14 @@ void RemoteTCPInputGui::displaySettings() ui->channelSampleRate->setValue(m_settings.m_channelSampleRate); ui->deviceRateText->setText(tr("%1k").arg(m_settings.m_channelSampleRate / 1000.0)); ui->decimation->setChecked(!m_settings.m_channelDecimation); - ui->channelSampleRate->setEnabled(m_settings.m_channelDecimation); - ui->channelSampleRateLabel->setEnabled(m_settings.m_channelDecimation); - ui->channelSampleRateUnit->setEnabled(m_settings.m_channelDecimation); ui->sampleBits->setCurrentText(QString::number(m_settings.m_sampleBits)); - ui->dataPort->setText(tr("%1").arg(m_settings.m_dataPort)); + ui->squelchEnabled->setChecked(m_settings.m_squelchEnabled); + ui->squelch->setValue(m_settings.m_squelch); + ui->squelchText->setText(QString::number(m_settings.m_squelch)); + ui->squelchGate->setValue(m_settings.m_squelchGate); + + ui->dataPort->setValue(m_settings.m_dataPort); ui->dataAddress->blockSignals(true); ui->dataAddress->clear(); for (const auto& address : m_settings.m_addressList) { @@ -412,6 +508,11 @@ void RemoteTCPInputGui::displaySettings() } displayGains(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); + ui->replayLoop->setChecked(m_settings.m_replayLoop); + displayEnabled(); blockApplySettings(false); } @@ -517,6 +618,7 @@ const QHash R { {RemoteTCPProtocol::RTLSDR_E4000, &m_rtlSDRe4kGains}, {RemoteTCPProtocol::RTLSDR_R820T, &m_rtlSDRR820Gains}, + {RemoteTCPProtocol::RTLSDR_R828D, &m_rtlSDRR820Gains}, {RemoteTCPProtocol::AIRSPY, &m_airspyGains}, {RemoteTCPProtocol::AIRSPY_HF, &m_airspyHFGains}, {RemoteTCPProtocol::BLADE_RF1, &m_baldeRF1Gains}, @@ -609,7 +711,12 @@ void RemoteTCPInputGui::on_startStop_toggled(bool checked) { if (m_doApplySettings) { - m_connectionError = false; + if (m_connectionError) + { + // Clear previous error + m_connectionError = false; + updateStatus(); + } RemoteTCPInput::MsgStartStop *message = RemoteTCPInput::MsgStartStop::create(checked); m_sampleSource->getInputMessageQueue()->push(message); } @@ -789,6 +896,29 @@ void RemoteTCPInputGui::on_sampleBits_currentIndexChanged(int index) sendSettings(); } +void RemoteTCPInputGui::on_squelchEnabled_toggled(bool checked) +{ + m_settings.m_squelchEnabled = checked; + m_settingsKeys.append("squelchEnabled"); + displayEnabled(); + sendSettings(); +} + +void RemoteTCPInputGui::on_squelch_valueChanged(int value) +{ + m_settings.m_squelch = value; + ui->squelchText->setText(QString::number(m_settings.m_squelch)); + m_settingsKeys.append("squelch"); + sendSettings(); +} + +void RemoteTCPInputGui::on_squelchGate_valueChanged(double value) +{ + m_settings.m_squelchGate = value; + m_settingsKeys.append("squelchGate"); + sendSettings(); +} + void RemoteTCPInputGui::on_dataAddress_editingFinished() { m_settings.m_dataAddress = ui->dataAddress->currentText(); @@ -810,17 +940,9 @@ void RemoteTCPInputGui::on_dataAddress_currentIndexChanged(int index) sendSettings(); } -void RemoteTCPInputGui::on_dataPort_editingFinished() +void RemoteTCPInputGui::on_dataPort_valueChanged(int value) { - bool ok; - quint16 udpPort = ui->dataPort->text().toInt(&ok); - - if ((!ok) || (udpPort < 1024)) { - udpPort = 9998; - } - - m_settings.m_dataPort = udpPort; - ui->dataPort->setText(tr("%1").arg(m_settings.m_dataPort)); + m_settings.m_dataPort = value; m_settingsKeys.append("dataPort"); sendSettings(); @@ -831,6 +953,7 @@ void RemoteTCPInputGui::on_overrideRemoteSettings_toggled(bool checked) m_settings.m_overrideRemoteSettings = checked; m_settingsKeys.append("overrideRemoteSettings"); sendSettings(); + displayEnabled(); } void RemoteTCPInputGui::on_preFill_valueChanged(int value) @@ -867,10 +990,14 @@ void RemoteTCPInputGui::updateHardware() void RemoteTCPInputGui::updateStatus() { - int state = m_deviceUISet->m_deviceAPI->state(); - - if (!m_connectionError && (m_lastEngineState != state)) + if (m_connectionError) { + ui->startStop->setStyleSheet("QToolButton { background-color : red; }"); + } + else + { + int state = m_deviceUISet->m_deviceAPI->state(); + switch(state) { case DeviceAPI::StNotStarted: @@ -889,9 +1016,28 @@ void RemoteTCPInputGui::updateStatus() default: break; } - - m_lastEngineState = state; } + displayEnabled(); +} + +void RemoteTCPInputGui::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_sampleSource->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(tr("%1").arg(powDbAvg, 0, 'f', 1)); + } + + m_tickCount++; } void RemoteTCPInputGui::openDeviceSettingsDialog(const QPoint& p) @@ -899,6 +1045,9 @@ void RemoteTCPInputGui::openDeviceSettingsDialog(const QPoint& p) if (m_contextMenuType == ContextMenuDeviceSettings) { BasicDeviceSettingsDialog dialog(this); + dialog.setReplayBytesPerSecond(m_settings.m_devSampleRate * 2 * sizeof(FixReal)); + dialog.setReplayLength(m_settings.m_replayLength); + dialog.setReplayStep(m_settings.m_replayStep); dialog.setUseReverseAPI(m_settings.m_useReverseAPI); dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); @@ -908,6 +1057,11 @@ void RemoteTCPInputGui::openDeviceSettingsDialog(const QPoint& p) new DialogPositioner(&dialog, false); dialog.exec(); + m_settings.m_replayLength = dialog.getReplayLength(); + m_settings.m_replayStep = dialog.getReplayStep(); + displayReplayLength(); + displayReplayOffset(); + displayReplayStep(); m_settings.m_useReverseAPI = dialog.useReverseAPI(); m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); @@ -919,6 +1073,110 @@ void RemoteTCPInputGui::openDeviceSettingsDialog(const QPoint& p) resetContextMenuType(); } +void RemoteTCPInputGui::displayReplayLength() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + if (!replayEnabled) { + ui->replayOffset->setMaximum(0); + } else { + ui->replayOffset->setMaximum(m_settings.m_replayLength * 10 - 1); + } + ui->replayLabel->setEnabled(replayEnabled); + ui->replayOffset->setEnabled(replayEnabled); + ui->replayOffsetText->setEnabled(replayEnabled); + ui->replaySave->setEnabled(replayEnabled); +} + +void RemoteTCPInputGui::displayReplayOffset() +{ + bool replayEnabled = m_settings.m_replayLength > 0.0f; + ui->replayOffset->setValue(m_settings.m_replayOffset * 10); + ui->replayOffsetText->setText(QString("%1s").arg(m_settings.m_replayOffset, 0, 'f', 1)); + ui->replayNow->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); + ui->replayPlus->setEnabled(replayEnabled && (std::round(m_settings.m_replayOffset * 10) < ui->replayOffset->maximum())); + ui->replayMinus->setEnabled(replayEnabled && (m_settings.m_replayOffset > 0.0f)); +} + +void RemoteTCPInputGui::displayReplayStep() +{ + QString step; + float intpart; + float frac = modf(m_settings.m_replayStep, &intpart); + if (frac == 0.0f) { + step = QString::number((int)intpart); + } else { + step = QString::number(m_settings.m_replayStep, 'f', 1); + } + ui->replayPlus->setText(QString("+%1s").arg(step)); + ui->replayPlus->setToolTip(QString("Add %1 seconds to time delay").arg(step)); + ui->replayMinus->setText(QString("-%1s").arg(step)); + ui->replayMinus->setToolTip(QString("Remove %1 seconds from time delay").arg(step)); +} + +void RemoteTCPInputGui::on_replayOffset_valueChanged(int value) +{ + m_settings.m_replayOffset = value / 10.0f; + displayReplayOffset(); + m_settingsKeys.append("replayOffset"); + sendSettings(); +} + +void RemoteTCPInputGui::on_replayNow_clicked() +{ + ui->replayOffset->setValue(0); +} + +void RemoteTCPInputGui::on_replayPlus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() + m_settings.m_replayStep * 10); +} + +void RemoteTCPInputGui::on_replayMinus_clicked() +{ + ui->replayOffset->setValue(ui->replayOffset->value() - m_settings.m_replayStep * 10); +} + +void RemoteTCPInputGui::on_replaySave_clicked() +{ + QFileDialog fileDialog(nullptr, "Select file to save IQ data to", "", "*.wav"); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + if (fileDialog.exec()) + { + QStringList fileNames = fileDialog.selectedFiles(); + if (fileNames.size() > 0) + { + RemoteTCPInput::MsgSaveReplay *message = RemoteTCPInput ::MsgSaveReplay::create(fileNames[0]); + m_sampleSource->getInputMessageQueue()->push(message); + } + } +} + +void RemoteTCPInputGui::on_replayLoop_toggled(bool checked) +{ + m_settings.m_replayLoop = checked; + m_settingsKeys.append("replayLoop"); + sendSettings(); +} + +void RemoteTCPInputGui::on_sendMessage_clicked() +{ + QString message = ui->txMessage->text().trimmed(); + if (!message.isEmpty()) + { + ui->messages->addItem(QString("< %1").arg(message)); + ui->messages->scrollToBottom(); + bool broadcast = ui->txAddress->currentText() == "All"; + QString callsign = MainCore::instance()->getSettings().getStationName(); + m_sampleSource->getInputMessageQueue()->push(RemoteTCPInput::MsgSendMessage::create(callsign, message, broadcast)); + } +} + +void RemoteTCPInputGui::on_txMessage_returnPressed() +{ + on_sendMessage_clicked(); + ui->txMessage->selectAll(); +} + void RemoteTCPInputGui::makeUIConnections() { QObject::connect(ui->startStop, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_startStop_toggled); @@ -940,10 +1198,21 @@ void RemoteTCPInputGui::makeUIConnections() QObject::connect(ui->channelSampleRate, &ValueDial::changed, this, &RemoteTCPInputGui::on_channelSampleRate_changed); QObject::connect(ui->decimation, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_decimation_toggled); QObject::connect(ui->sampleBits, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPInputGui::on_sampleBits_currentIndexChanged); + QObject::connect(ui->squelchEnabled, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_squelchEnabled_toggled); + QObject::connect(ui->squelch, &QDial::valueChanged, this, &RemoteTCPInputGui::on_squelch_valueChanged); + QObject::connect(ui->squelchGate, &PeriodDial::valueChanged, this, &RemoteTCPInputGui::on_squelchGate_valueChanged); QObject::connect(ui->dataAddress->lineEdit(), &QLineEdit::editingFinished, this, &RemoteTCPInputGui::on_dataAddress_editingFinished); QObject::connect(ui->dataAddress, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPInputGui::on_dataAddress_currentIndexChanged); - QObject::connect(ui->dataPort, &QLineEdit::editingFinished, this, &RemoteTCPInputGui::on_dataPort_editingFinished); + QObject::connect(ui->dataPort, QOverload::of(&QSpinBox::valueChanged), this, &RemoteTCPInputGui::on_dataPort_valueChanged); QObject::connect(ui->overrideRemoteSettings, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_overrideRemoteSettings_toggled); QObject::connect(ui->preFill, &QDial::valueChanged, this, &RemoteTCPInputGui::on_preFill_valueChanged); QObject::connect(ui->protocol, QOverload::of(&QComboBox::currentIndexChanged), this, &RemoteTCPInputGui::on_protocol_currentIndexChanged); + QObject::connect(ui->replayOffset, &QSlider::valueChanged, this, &RemoteTCPInputGui::on_replayOffset_valueChanged); + QObject::connect(ui->replayNow, &QToolButton::clicked, this, &RemoteTCPInputGui::on_replayNow_clicked); + QObject::connect(ui->replayPlus, &QToolButton::clicked, this, &RemoteTCPInputGui::on_replayPlus_clicked); + QObject::connect(ui->replayMinus, &QToolButton::clicked, this, &RemoteTCPInputGui::on_replayMinus_clicked); + QObject::connect(ui->replaySave, &QToolButton::clicked, this, &RemoteTCPInputGui::on_replaySave_clicked); + QObject::connect(ui->replayLoop, &ButtonSwitch::toggled, this, &RemoteTCPInputGui::on_replayLoop_toggled); + QObject::connect(ui->sendMessage, &QToolButton::clicked, this, &RemoteTCPInputGui::on_sendMessage_clicked); + QObject::connect(ui->txMessage, &QLineEdit::returnPressed, this, &RemoteTCPInputGui::on_txMessage_returnPressed); } diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.h b/plugins/samplesource/remotetcpinput/remotetcpinputgui.h index 9afcec66a..b252c45c8 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.h @@ -108,12 +108,11 @@ private: QList m_settingsKeys; RemoteTCPInput* m_sampleSource; QTimer m_updateTimer; - QTimer m_statusTimer; - int m_lastEngineState; MessageQueue m_inputMessageQueue; int m_sampleRate; quint64 m_centerFrequency; + uint32_t m_tickCount; bool m_doApplySettings; bool m_forceSettings; @@ -125,6 +124,11 @@ private: DeviceGains::GainRange m_spyServerGainRange; DeviceGains m_spyServerGains; + bool m_sdra; + bool m_spyServer; + bool m_remoteControl; + bool m_iqOnly; + static const DeviceGains::GainRange m_rtlSDR34kGainRange; static const DeviceGains m_rtlSDRe4kGains; static const DeviceGains::GainRange m_rtlSDRR820GainRange; @@ -175,9 +179,13 @@ private: void blockApplySettings(bool block); void displaySettings(); QString gainText(int stage); + void displayEnabled(); void displayGains(); void displayRemoteSettings(); void displayRemoteShift(); + void displayReplayLength(); + void displayReplayOffset(); + void displayReplayStep(); void sendSettings(); void updateSampleRateAndFrequency(); void applyDecimation(); @@ -206,15 +214,27 @@ private slots: void on_channelSampleRate_changed(quint64 value); void on_decimation_toggled(bool checked); void on_sampleBits_currentIndexChanged(int index); + void on_squelchEnabled_toggled(bool checked); + void on_squelch_valueChanged(int value); + void on_squelchGate_valueChanged(double value); void on_dataAddress_editingFinished(); void on_dataAddress_currentIndexChanged(int index); - void on_dataPort_editingFinished(); + void on_dataPort_valueChanged(int value); void on_overrideRemoteSettings_toggled(bool checked); void on_preFill_valueChanged(int value); void on_protocol_currentIndexChanged(int index); + void on_replayOffset_valueChanged(int value); + void on_replayNow_clicked(); + void on_replayPlus_clicked(); + void on_replayMinus_clicked(); + void on_replaySave_clicked(); + void on_replayLoop_toggled(bool checked); + void on_sendMessage_clicked(); + void on_txMessage_returnPressed(); void updateHardware(); void updateStatus(); void openDeviceSettingsDialog(const QPoint& p); + void tick(); }; #endif // INCLUDE_REMOTETCPINPUTGUI_H diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui b/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui index ec526650d..c90b15ced 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui +++ b/plugins/samplesource/remotetcpinput/remotetcpinputgui.ui @@ -7,7 +7,7 @@ 0 0 360 - 360 + 586 @@ -19,13 +19,13 @@ 360 - 360 + 500 - 491 - 360 + 533 + 610 @@ -863,6 +863,184 @@ Use to ensure full dynamic range of 8-bit data is used.
+ + + + Qt::Horizontal + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + + + Check to enable IQ squelch + + + SQ + + + + + + + Qt::Vertical + + + + + + + + 24 + 24 + + + + IQ squelch power level in dB + + + -150 + + + 0 + + + 1 + + + + + + + + 32 + 0 + + + + -150 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + + + + + Qt::Vertical + + + + + + + + 40 + 0 + + + + IQ squelch gate time + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 30 + 0 + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + dB + + + + + @@ -914,33 +1092,18 @@ Use to ensure full dynamic range of 8-bit data is used. - - - true - - - - 60 - 0 - - - - - 60 - 16777215 - - + Remote data port (rtl_tcp defaults to 1234) - - 00000 + + 1024 - - 0 + + 65535 - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + 1234 @@ -961,7 +1124,7 @@ Use to ensure full dynamic range of 8-bit data is used. - 75 + 92 0 @@ -1162,6 +1325,182 @@ This should typically be empty. If full, your CPU cannot keep up and data will b + + + + Qt::Horizontal + + + + + + + + + false + + + Click to send message + + + TX + + + + + + + false + + + Who to send message to + + + + Host + + + + + All + + + + + + + + false + + + Message to transmit + + + + + + + + + + + false + + + Messages + + + QListView::Static + + + + + + + + + Qt::Horizontal + + + + + + + + + + 65 + 0 + + + + Time Delay + + + + + + + Replay time delay in seconds + + + 500 + + + Qt::Horizontal + + + + + + + Replay time delay in seconds + + + 0.0s + + + + + + + Set time delay to 0 seconds + + + Now + + + + + + + Add displayed number of seconds to time delay + + + +5s + + + + + + + Remove displayed number of seconds from time delay + + + -5s + + + + + + + Repeatedly replay data in replay buffer + + + + + + + :/playloop.png:/playloop.png + + + + + + + Save replay buffer to a file + + + + + + + :/save.png:/save.png + + + + + @@ -1214,6 +1553,18 @@ This should typically be empty. If full, your CPU cannot keep up and data will b
gui/valuedial.h
1 + + LevelMeterSignalDB + QWidget +
gui/levelmeter.h
+ 1 +
+ + PeriodDial + QWidget +
gui/perioddial.h
+ 1 +
startStop @@ -1232,7 +1583,8 @@ This should typically be empty. If full, your CPU cannot keep up and data will b channelGain decimation sampleBits - dataPort + dataAddress + protocol overrideRemoteSettings preFill diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp index ad5291794..b374b9fc1 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.cpp @@ -53,6 +53,13 @@ void RemoteTCPInputSettings::resetToDefaults() m_reverseAPIAddress = "127.0.0.1"; m_reverseAPIPort = 8888; m_reverseAPIDeviceIndex = 0; + m_replayOffset = 0.0f; + m_replayLength = 20.0f; + m_replayStep = 5.0f; + m_replayLoop = false; + m_squelchEnabled = false; + m_squelch = -100.0f; + m_squelchGate = 0.001f; } QByteArray RemoteTCPInputSettings::serialize() const @@ -83,11 +90,19 @@ QByteArray RemoteTCPInputSettings::serialize() const s.writeU32(23, m_reverseAPIDeviceIndex); s.writeList(24, m_addressList); s.writeString(25, m_protocol); + s.writeFloat(26, m_replayOffset); + s.writeFloat(27, m_replayLength); + s.writeFloat(28, m_replayStep); + s.writeBool(29, m_replayLoop); for (int i = 0; i < m_maxGains; i++) { s.writeS32(30+i, m_gain[i]); } + s.writeBool(40, m_squelchEnabled); + s.writeFloat(41, m_squelch); + s.writeFloat(42, m_squelchGate); + return s.final(); } @@ -140,10 +155,19 @@ bool RemoteTCPInputSettings::deserialize(const QByteArray& data) d.readList(24, &m_addressList); d.readString(25, &m_protocol, "SDRangel"); + d.readFloat(26, &m_replayOffset, 0.0f); + d.readFloat(27, &m_replayLength, 20.0f); + d.readFloat(28, &m_replayStep, 5.0f); + d.readBool(29, &m_replayLoop, false); + for (int i = 0; i < m_maxGains; i++) { d.readS32(30+i, &m_gain[i], 0); } + d.readBool(40, &m_squelchEnabled, false); + d.readFloat(41, &m_squelch, -100.0f); + d.readFloat(42, &m_squelchGate, 0.001f); + return true; } else @@ -212,7 +236,7 @@ void RemoteTCPInputSettings::applySettings(const QStringList& settingsKeys, cons if (settingsKeys.contains("preFill")) { m_preFill = settings.m_preFill; } - if (settingsKeys.contains("_useReverseAPI")) { + if (settingsKeys.contains("useReverseAPI")) { m_useReverseAPI = settings.m_useReverseAPI; } if (settingsKeys.contains("reverseAPIAddress")) { @@ -230,6 +254,27 @@ void RemoteTCPInputSettings::applySettings(const QStringList& settingsKeys, cons if (settingsKeys.contains("protocol")) { m_protocol = settings.m_protocol; } + if (settingsKeys.contains("replayOffset")) { + m_replayOffset = settings.m_replayOffset; + } + if (settingsKeys.contains("replayLength")) { + m_replayLength = settings.m_replayLength; + } + if (settingsKeys.contains("replayStep")) { + m_replayStep = settings.m_replayStep; + } + if (settingsKeys.contains("replayLoop")) { + m_replayLoop = settings.m_replayLoop; + } + if (settingsKeys.contains("squelchEnabled")) { + m_squelchEnabled = settings.m_squelchEnabled; + } + if (settingsKeys.contains("squelch")) { + m_squelch = settings.m_squelch; + } + if (settingsKeys.contains("squelchGate")) { + m_squelchGate = settings.m_squelchGate; + } for (int i = 0; i < m_maxGains; i++) { @@ -318,6 +363,27 @@ QString RemoteTCPInputSettings::getDebugString(const QStringList& settingsKeys, if (settingsKeys.contains("protocol") || force) { ostr << " m_protocol: " << m_protocol.toStdString(); } + if (settingsKeys.contains("replayOffset") || force) { + ostr << " m_replayOffset: " << m_replayOffset; + } + if (settingsKeys.contains("replayLength") || force) { + ostr << " m_replayLength: " << m_replayLength; + } + if (settingsKeys.contains("replayStep") || force) { + ostr << " m_replayStep: " << m_replayStep; + } + if (settingsKeys.contains("replayLoop") || force) { + ostr << " m_replayLoop: " << m_replayLoop; + } + if (settingsKeys.contains("squelchEnabled") || force) { + ostr << " m_squelchEnabled: " << m_squelchEnabled; + } + if (settingsKeys.contains("squelch") || force) { + ostr << " m_squelch: " << m_squelch; + } + if (settingsKeys.contains("squelchGate") || force) { + ostr << " m_squelchGate: " << m_squelchGate; + } for (int i = 0; i < m_maxGains; i++) { diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h index a95dc9773..c209532c0 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputsettings.h @@ -54,6 +54,13 @@ struct RemoteTCPInputSettings uint16_t m_reverseAPIDeviceIndex; QStringList m_addressList; // List of dataAddresses that have been used in the past QString m_protocol; // "SDRangel" or "Spy Server" + float m_replayOffset; //!< Replay offset in seconds + float m_replayLength; //!< Replay buffer size in seconds + float m_replayStep; //!< Replay forward/back step size in seconds + bool m_replayLoop; //!< Replay buffer repeatedly without recording new data + bool m_squelchEnabled; + float m_squelch; + float m_squelchGate; RemoteTCPInputSettings(); void resetToDefaults(); diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp index 9eaf92038..1481f2c45 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp +++ b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.cpp @@ -17,11 +17,11 @@ // along with this program. If not, see . // /////////////////////////////////////////////////////////////////////////////////// -#include #include #include "device/deviceapi.h" #include "util/message.h" +#include "maincore.h" #include "remotetcpinputtcphandler.h" #include "remotetcpinput.h" @@ -31,41 +31,66 @@ MESSAGE_CLASS_DEFINITION(RemoteTCPInputTCPHandler::MsgReportRemoteDevice, Messag MESSAGE_CLASS_DEFINITION(RemoteTCPInputTCPHandler::MsgReportConnection, Message) MESSAGE_CLASS_DEFINITION(RemoteTCPInputTCPHandler::MsgConfigureTcpHandler, Message) -RemoteTCPInputTCPHandler::RemoteTCPInputTCPHandler(SampleSinkFifo *sampleFifo, DeviceAPI *deviceAPI) : +RemoteTCPInputTCPHandler::RemoteTCPInputTCPHandler(SampleSinkFifo *sampleFifo, DeviceAPI *deviceAPI, ReplayBuffer *replayBuffer) : m_deviceAPI(deviceAPI), m_running(false), m_dataSocket(nullptr), m_tcpBuf(nullptr), m_sampleFifo(sampleFifo), - m_messageQueueToGUI(0), + m_replayBuffer(replayBuffer), + m_messageQueueToInput(nullptr), + m_messageQueueToGUI(nullptr), m_fillBuffer(true), m_timer(this), m_reconnectTimer(this), m_sdra(false), m_converterBuffer(nullptr), m_converterBufferNbSamples(0), - m_settings() + m_settings(), + m_remoteControl(true), + m_iqOnly(false), + m_decoder(nullptr), + m_zOutBuf(m_zBufSize, '\0'), + m_blacklisted(false), + m_magsq(0.0f), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0) { m_sampleFifo->setSize(5000000); // Start with large FIFO, to avoid having to resize m_tcpBuf = new char[m_sampleFifo->size()*2*4]; m_timer.setInterval(50); // Previously 125, but this results in an obviously slow spectrum refresh rate connect(&m_reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect())); m_reconnectTimer.setSingleShot(true); + + // Initialise zlib decompressor + m_zStream.zalloc = Z_NULL; + m_zStream.zfree = Z_NULL; + m_zStream.opaque = Z_NULL; + m_zStream.avail_in = 0; + m_zStream.next_in = Z_NULL; + if (Z_OK != inflateInit(&m_zStream)) { + qDebug() << "RemoteTCPInputTCPHandler::RemoteTCPInputTCPHandler: inflateInit failed."; + } } RemoteTCPInputTCPHandler::~RemoteTCPInputTCPHandler() { + qDebug() << "RemoteTCPInputTCPHandler::~RemoteTCPInputTCPHandler"; delete[] m_tcpBuf; if (m_converterBuffer) { delete[] m_converterBuffer; } + qDebug() << "RemoteTCPInputTCPHandler::~RemoteTCPInputTCPHandler cleanup"; cleanup(); + qDebug() << "RemoteTCPInputTCPHandler::~RemoteTCPInputTCPHandler done"; } void RemoteTCPInputTCPHandler::reset() { QMutexLocker mutexLocker(&m_mutex); m_inputMessageQueue.clear(); + m_blacklisted = false; } // start() is called from DSPDeviceSourceEngine thread @@ -88,6 +113,7 @@ void RemoteTCPInputTCPHandler::start() void RemoteTCPInputTCPHandler::stop() { + qDebug("RemoteTCPInputTCPHandler::stop locking"); QMutexLocker mutexLocker(&m_mutex); qDebug("RemoteTCPInputTCPHandler::stop"); @@ -101,19 +127,21 @@ void RemoteTCPInputTCPHandler::started() // Don't connectToHost until we get settings connect(&m_timer, SIGNAL(timeout()), this, SLOT(processData())); - m_timer.start(); disconnect(thread(), SIGNAL(started()), this, SLOT(started())); } void RemoteTCPInputTCPHandler::finished() { + qDebug("RemoteTCPInputTCPHandler::finished"); QMutexLocker mutexLocker(&m_mutex); m_timer.stop(); disconnect(&m_timer, SIGNAL(timeout()), this, SLOT(processData())); - disconnectFromHost(); + //disconnectFromHost(); + cleanup(); disconnect(thread(), SIGNAL(finished()), this, SLOT(finished())); m_running = false; + qDebug("RemoteTCPInputTCPHandler::finished done"); } void RemoteTCPInputTCPHandler::connectToHost(const QString& address, quint16 port) @@ -133,7 +161,7 @@ void RemoteTCPInputTCPHandler::connectToHost(const QString& address, quint16 por m_dataSocket->connectToHost(address, port); } -void RemoteTCPInputTCPHandler::disconnectFromHost() +/*void RemoteTCPInputTCPHandler::disconnectFromHost() { if (m_dataSocket) { @@ -146,15 +174,31 @@ void RemoteTCPInputTCPHandler::disconnectFromHost() #else disconnect(m_dataSocket, &QAbstractSocket::errorOccurred, this, &RemoteTCPInputTCPHandler::errorOccurred); #endif - m_dataSocket->disconnectFromHost(); + //m_dataSocket->disconnectFromHost(); cleanup(); } -} +}*/ void RemoteTCPInputTCPHandler::cleanup() { + if (m_decoder) + { + FLAC__stream_decoder_delete(m_decoder); + m_decoder = nullptr; + } if (m_dataSocket) { + qDebug() << "RemoteTCPInputTCPHandler::cleanup: Closing and deleting socket"; + // Disconnect disconnected, so don't get called recursively + disconnect(m_dataSocket, SIGNAL(readyRead()), this, SLOT(dataReadyRead())); + disconnect(m_dataSocket, SIGNAL(connected()), this, SLOT(connected())); + disconnect(m_dataSocket, SIGNAL(disconnected()), this, SLOT(disconnected())); +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + disconnect(m_dataSocket, QOverload::of(&QAbstractSocket::error), this, &RemoteTCPInputTCPHandler::errorOccurred); +#else + disconnect(m_dataSocket, &QAbstractSocket::errorOccurred, this, &RemoteTCPInputTCPHandler::errorOccurred); +#endif + m_dataSocket->close(); m_dataSocket->deleteLater(); m_dataSocket = nullptr; } @@ -176,225 +220,188 @@ void RemoteTCPInputTCPHandler::clearBuffer() else { m_dataSocket->flush(); - m_dataSocket->readAll(); - m_fillBuffer = true; + if (!m_decoder) { // Can't throw away FLAC header + m_dataSocket->readAll(); + m_fillBuffer = true; + } } } } +void RemoteTCPInputTCPHandler::sendCommand(RemoteTCPProtocol::Command cmd, quint32 value) +{ + QMutexLocker mutexLocker(&m_mutex); + quint8 request[5]; + + request[0] = (quint8) cmd; + RemoteTCPProtocol::encodeUInt32(&request[1], value); + if (m_dataSocket) + { + qint64 len = m_dataSocket->write((char*)request, sizeof(request)); + if (len != sizeof(request)) { + qDebug() << "RemoteTCPInputTCPHandler::sendCommand: Failed to write all of request:" << len; + } + } else { + qDebug() << "RemoteTCPInputTCPHandler::sendCommand: No socket"; + } +} + +void RemoteTCPInputTCPHandler::sendCommandFloat(RemoteTCPProtocol::Command cmd, float value) +{ + QMutexLocker mutexLocker(&m_mutex); + quint8 request[5]; + + request[0] = (quint8) cmd; + RemoteTCPProtocol::encodeFloat(&request[1], value); + if (m_dataSocket) + { + qint64 len = m_dataSocket->write((char*)request, sizeof(request)); + if (len != sizeof(request)) { + qDebug() << "RemoteTCPInputTCPHandler::sendCommand: Failed to write all of request:" << len; + } + } else { + qDebug() << "RemoteTCPInputTCPHandler::sendCommand: No socket"; + } +} + void RemoteTCPInputTCPHandler::setSampleRate(int sampleRate) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setSampleRate; - RemoteTCPProtocol::encodeUInt32(&request[1], sampleRate); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setSampleRate, sampleRate); } void RemoteTCPInputTCPHandler::setCenterFrequency(quint64 frequency) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setCenterFrequency; - RemoteTCPProtocol::encodeUInt32(&request[1], frequency); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setCenterFrequency, frequency); // FIXME: Can't support >4GHz } void RemoteTCPInputTCPHandler::setTunerAGC(bool agc) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setTunerGainMode; - RemoteTCPProtocol::encodeUInt32(&request[1], agc); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setTunerGainMode, agc); } void RemoteTCPInputTCPHandler::setTunerGain(int gain) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setTunerGain; - RemoteTCPProtocol::encodeUInt32(&request[1], gain); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setTunerGain, gain); } void RemoteTCPInputTCPHandler::setGainByIndex(int index) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setGainByIndex; - RemoteTCPProtocol::encodeUInt32(&request[1], index); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setGainByIndex, index); } void RemoteTCPInputTCPHandler::setFreqCorrection(int correction) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setFrequencyCorrection; - RemoteTCPProtocol::encodeUInt32(&request[1], correction); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setFrequencyCorrection, correction); } void RemoteTCPInputTCPHandler::setIFGain(quint16 stage, quint16 gain) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setTunerIFGain; - RemoteTCPProtocol::encodeUInt32(&request[1], (stage << 16) | gain); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setTunerIFGain, (stage << 16) | gain); } void RemoteTCPInputTCPHandler::setAGC(bool agc) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setAGCMode; - RemoteTCPProtocol::encodeUInt32(&request[1], agc); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setAGCMode, agc); } void RemoteTCPInputTCPHandler::setDirectSampling(bool enabled) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setDirectSampling; - RemoteTCPProtocol::encodeUInt32(&request[1], enabled ? 3 : 0); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setDirectSampling, enabled ? 3 : 0); } void RemoteTCPInputTCPHandler::setDCOffsetRemoval(bool enabled) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setDCOffsetRemoval; - RemoteTCPProtocol::encodeUInt32(&request[1], enabled); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setDCOffsetRemoval, enabled); } void RemoteTCPInputTCPHandler::setIQCorrection(bool enabled) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setIQCorrection; - RemoteTCPProtocol::encodeUInt32(&request[1], enabled); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setIQCorrection, enabled); } void RemoteTCPInputTCPHandler::setBiasTee(bool enabled) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setBiasTee; - RemoteTCPProtocol::encodeUInt32(&request[1], enabled); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setBiasTee, enabled); } void RemoteTCPInputTCPHandler::setBandwidth(int bandwidth) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setTunerBandwidth; - RemoteTCPProtocol::encodeUInt32(&request[1], bandwidth); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setTunerBandwidth, bandwidth); } void RemoteTCPInputTCPHandler::setDecimation(int dec) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setDecimation; - RemoteTCPProtocol::encodeUInt32(&request[1], dec); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setDecimation, dec); } void RemoteTCPInputTCPHandler::setChannelSampleRate(int sampleRate) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setChannelSampleRate; - RemoteTCPProtocol::encodeUInt32(&request[1], sampleRate); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setChannelSampleRate, sampleRate); } void RemoteTCPInputTCPHandler::setChannelFreqOffset(int offset) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setChannelFreqOffset; - RemoteTCPProtocol::encodeUInt32(&request[1], offset); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setChannelFreqOffset, offset); } void RemoteTCPInputTCPHandler::setChannelGain(int gain) { - QMutexLocker mutexLocker(&m_mutex); - - quint8 request[5]; - request[0] = RemoteTCPProtocol::setChannelGain; - RemoteTCPProtocol::encodeUInt32(&request[1], gain); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); - } + sendCommand(RemoteTCPProtocol::setChannelGain, gain); } void RemoteTCPInputTCPHandler::setSampleBitDepth(int sampleBits) +{ + sendCommand(RemoteTCPProtocol::setSampleBitDepth, sampleBits); +} + +void RemoteTCPInputTCPHandler::setSquelchEnabled(bool enabled) +{ + sendCommand(RemoteTCPProtocol::setIQSquelchEnabled, (quint32) enabled); +} + +void RemoteTCPInputTCPHandler::setSquelch(float squelch) +{ + sendCommandFloat(RemoteTCPProtocol::setIQSquelch, squelch); +} + +void RemoteTCPInputTCPHandler::setSquelchGate(float squelchGate) +{ + sendCommandFloat(RemoteTCPProtocol::setIQSquelchGate, squelchGate); +} + +void RemoteTCPInputTCPHandler::sendMessage(const QString& callsign, const QString& text, bool broadcast) { QMutexLocker mutexLocker(&m_mutex); - quint8 request[5]; - request[0] = RemoteTCPProtocol::setSampleBitDepth; - RemoteTCPProtocol::encodeUInt32(&request[1], sampleBits); - if (m_dataSocket) { - m_dataSocket->write((char*)request, sizeof(request)); + if (m_dataSocket) + { + qint64 len; + char cmd[1+4+1]; + QByteArray callsignBytes = callsign.toUtf8(); + QByteArray textBytes = text.toUtf8(); + QByteArray bytes; + + bytes.append(callsignBytes); + bytes.append('\0'); + bytes.append(textBytes); + bytes.append('\0'); + + cmd[0] = (char) RemoteTCPProtocol::sendMessage; + RemoteTCPProtocol::encodeUInt32((quint8*) &cmd[1], bytes.size() + 1); + cmd[5] = (char) broadcast; + + len = m_dataSocket->write(&cmd[0], sizeof(cmd)); + if (len != sizeof(cmd)) { + qDebug() << "RemoteTCPInputTCPHandler::set: Failed to write all of message header:" << len; + } + len = m_dataSocket->write(bytes.data(), bytes.size()); + if (len != bytes.size()) { + qDebug() << "RemoteTCPInputTCPHandler::set: Failed to write all of message:" << len; + } + m_dataSocket->flush(); + qDebug() << "sendMessage" << text; + } else { + qDebug() << "RemoteTCPInputTCPHandler::sendMessage: No socket"; } } @@ -407,8 +414,10 @@ void RemoteTCPInputTCPHandler::spyServerConnect() SpyServerProtocol::encodeUInt32(&request[4], 4+9); SpyServerProtocol::encodeUInt32(&request[8], SpyServerProtocol::ProtocolID); memcpy(&request[8+4], "SDRangel", 9); - if (m_dataSocket) { + if (m_dataSocket) + { m_dataSocket->write((char*)request, sizeof(request)); + m_dataSocket->flush(); } } @@ -421,8 +430,10 @@ void RemoteTCPInputTCPHandler::spyServerSet(int setting, int value) SpyServerProtocol::encodeUInt32(&request[4], 8); SpyServerProtocol::encodeUInt32(&request[8], setting); SpyServerProtocol::encodeUInt32(&request[12], value); - if (m_dataSocket) { + if (m_dataSocket) + { m_dataSocket->write((char*)request, sizeof(request)); + m_dataSocket->flush(); } } @@ -577,12 +588,37 @@ void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& setti } clearBuffer(); } + if (settingsKeys.contains("squelchEnabled") || force) + { + if (m_sdra) { + setSquelchEnabled(settings.m_squelchEnabled); + } + } + if (settingsKeys.contains("squelch") || force) + { + if (m_sdra) { + setSquelch(settings.m_squelch); + } + } + if (settingsKeys.contains("squelchGate") || force) + { + if (m_sdra) { + setSquelchGate(settings.m_squelchGate); + } + } + } + + if (m_dataSocket) { + m_dataSocket->flush(); // Apparently needed for WebAssembly with proxy } // Don't use force, as disconnect can cause rtl_tcp to quit - if (settingsKeys.contains("dataAddress") || settingsKeys.contains("dataPort") || (m_dataSocket == nullptr)) + if (settingsKeys.contains("dataAddress") + || settingsKeys.contains("dataPort") + || (m_dataSocket == nullptr) && !m_blacklisted) { - disconnectFromHost(); + //disconnectFromHost(); + cleanup(); connectToHost(settings.m_dataAddress, settings.m_dataPort); } @@ -593,6 +629,362 @@ void RemoteTCPInputTCPHandler::applySettings(const RemoteTCPInputSettings& setti } } +static FLAC__StreamDecoderReadStatus flacReadCallback(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *clientData) +{ + RemoteTCPInputTCPHandler *handler = (RemoteTCPInputTCPHandler *) clientData; + + return handler->flacRead(decoder, buffer, bytes); +} + +static FLAC__StreamDecoderWriteStatus flacWriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *clientData) +{ + RemoteTCPInputTCPHandler *handler = (RemoteTCPInputTCPHandler *) clientData; + + return handler->flacWrite(decoder, frame, buffer); +} + +static void flacErrorCallback(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *clientData) +{ + RemoteTCPInputTCPHandler *handler = (RemoteTCPInputTCPHandler *) clientData; + + return handler->flacError(decoder, status); +} + +/*FLAC__StreamDecoderReadStatus RemoteTCPInputTCPHandler::flacRead(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes) +{ + if (m_dataSocket) + { + qint64 bytesRequested = *bytes; + qint64 bytesRead = std::min(bytesRequested, m_compressedData.size()); + + //bytesRead = m_dataSocket->read((char *) buffer, bytesRequested); + + memcpy(buffer, m_compressedData.constData(), bytesRead); + + qDebug() << "flacRead" << bytesRequested << bytesRead; + + if (bytesRead != -1) + { + *bytes = (size_t) bytesRead; + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } + else + { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } + } + else + { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } +}*/ + +FLAC__StreamDecoderReadStatus RemoteTCPInputTCPHandler::flacRead(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes) +{ + qsizetype bytesRequested = *bytes; + qsizetype bytesRead = std::min(bytesRequested, (qsizetype) m_compressedData.size()); + + memcpy(buffer, m_compressedData.constData(), bytesRead); + m_compressedData.remove(0, bytesRead); + + //qDebug() << "RemoteTCPInputTCPHandler::flacRead bytesRequested" << bytesRequested << "bytesRead" << bytesRead; + if (bytesRead == 0) + { + qDebug() << "RemoteTCPInputTCPHandler::flacRead: Decoder will hang if we can't return data"; + abort(); + } + + *bytes = (size_t) bytesRead; + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; +} + +FIFO::FIFO(qsizetype elements) +{ + m_data.resize(elements); + clear(); +} + +qsizetype FIFO::write(quint8 *data, qsizetype elements) +{ + qsizetype writeCount = std::min(elements, m_data.size() - m_fill); + qsizetype remaining = m_data.size() - m_writePtr; + qsizetype len2 = writeCount - remaining; + + //qDebug() << "write" << write << remaining << len2; + + if (len2 < 0) + { + std::memcpy(&m_data.data()[m_writePtr], data, writeCount); + m_writePtr += writeCount; + } + else if (len2 == 0) + { + std::memcpy(&m_data.data()[m_writePtr], data, writeCount); + m_writePtr = 0; + } + else + { + std::memcpy(&m_data.data()[m_writePtr], data, remaining); + std::memcpy(&m_data.data()[0], &data[remaining], len2); + m_writePtr = len2; + } + + m_fill += writeCount; + + return writeCount; +} + +qsizetype FIFO::read(quint8 *data, qsizetype elements) +{ + qsizetype readCount = std::min(elements, m_fill); + qsizetype remaining = m_data.size() - m_readPtr; + qsizetype len2 = readCount - remaining; + + // qDebug() << "read" << read << remaining << len2; + + if (len2 < 0) + { + std::memcpy(data, &m_data.data()[m_readPtr], readCount); + m_readPtr += readCount; + } + else if (len2 == 0) + { + std::memcpy(data, &m_data.data()[m_readPtr], readCount); + m_readPtr = 0; + } + else + { + std::memcpy(&data[0], &m_data.data()[m_readPtr], remaining); + std::memcpy(&data[remaining], &m_data[0], len2); + m_readPtr = len2; + } + + m_fill -= readCount; + + return readCount; +} + +qsizetype FIFO::readPtr(quint8 **data, qsizetype elements) +{ + *data = (quint8 *) &m_data.data()[m_readPtr]; + + return std::min(elements, m_data.size() - m_readPtr); +} + +void FIFO::read(qsizetype elements) +{ + m_readPtr = (m_readPtr + elements) % m_data.size(); + m_fill -= elements; + if (m_fill < 0) + { + qDebug() << "FIFO::read: Underrun"; + m_fill = 0; + } +} + +void FIFO::resize(qsizetype elements) +{ + m_data.resize(elements); + m_data.squeeze(); +} + +void FIFO::clear() +{ + m_writePtr = 0; + m_readPtr = 0; + m_fill = 0; +} + +void RemoteTCPInputTCPHandler::calcPower(const Sample *iq, int nbSamples) +{ + for (int i = 0; i < nbSamples; i++) + { + Real re = iq[i].real();// SDR_RX_SCALED; + Real im = iq[i].imag();// SDR_RX_SCALED; + + Real magsq = (re*re + im*im) / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + m_magsqPeak = std::max(magsq, m_magsqPeak); + m_magsqCount++; + } +} + +FLAC__StreamDecoderWriteStatus RemoteTCPInputTCPHandler::flacWrite(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) +{ + m_uncompressedFrames++; + + int nbSamples = frame->header.blocksize; +//qDebug() << "RemoteTCPInputTCPHandler::flacWrite m_uncompressedFrames" << m_uncompressedFrames << "nbSamples" << nbSamples; + if (nbSamples > (int) m_converterBufferNbSamples) + { + if (m_converterBuffer) { + delete[] m_converterBuffer; + } + m_converterBuffer = new int32_t[nbSamples*2]; + } + + // Convert and interleave samples and output to FIFO + if ((frame->header.bits_per_sample == 8) && (SDR_RX_SAMP_SZ == 24) && (frame->header.channels == 2)) + { + qint32 *out = (qint32 *)m_converterBuffer; + const qint32 *inI = buffer[0]; + const qint32 *inQ = buffer[1]; + + for (int i = 0; i < nbSamples; i++) + { + *out++ = *inI++ << 16; + *out++ = *inQ++ << 16; + } + m_uncompressedData.write(reinterpret_cast(m_converterBuffer), nbSamples*sizeof(Sample)); + } + else if ((frame->header.bits_per_sample == 16) && (SDR_RX_SAMP_SZ == 24) && (frame->header.channels == 2)) + { + qint32 *out = (qint32 *)m_converterBuffer; + const qint32 *inI = buffer[0]; + const qint32 *inQ = buffer[1]; + + for (int i = 0; i < nbSamples; i++) + { + *out++ = *inI++ << 8; + *out++ = *inQ++ << 8; + } + m_uncompressedData.write(reinterpret_cast(m_converterBuffer), nbSamples*sizeof(Sample)); + } + else if ((frame->header.bits_per_sample == 24) && (SDR_RX_SAMP_SZ == 24) && (frame->header.channels == 2)) + { + qint32 *out = (qint32 *)m_converterBuffer; + const qint32 *inI = buffer[0]; + const qint32 *inQ = buffer[1]; + + for (int i = 0; i < nbSamples; i++) + { + *out++ = *inI++; + *out++ = *inQ++; + } + m_uncompressedData.write(reinterpret_cast(m_converterBuffer), nbSamples*sizeof(Sample)); + } + else if ((frame->header.bits_per_sample == 32) && (SDR_RX_SAMP_SZ == 24) && (frame->header.channels == 2)) + { + qint32 *out = (qint32 *)m_converterBuffer; + const qint32 *inI = buffer[0]; + const qint32 *inQ = buffer[1]; + + for (int i = 0; i < nbSamples; i++) + { + *out++ = *inI++; + *out++ = *inQ++; + } + m_uncompressedData.write(reinterpret_cast(m_converterBuffer), nbSamples*sizeof(Sample)); + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::flacWrite: Unsupported format"; + } + + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} + + +// Convert from zlib uncompressed network format to Samples, to uncompressed data FIFO +void RemoteTCPInputTCPHandler::processDecompressedZlibData(const char *inBuf, int nbSamples) +{ + // Ensure conversion buffer is large enough - FIXME: Don't use this buffer - just write in to FIFO + if (nbSamples > (int) m_converterBufferNbSamples) + { + if (m_converterBuffer) { + delete[] m_converterBuffer; + } + m_converterBuffer = new int32_t[nbSamples*2]; + } + + // Convert from network format to Sample + if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 16)) + { + const quint8 *in = (const quint8 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (((qint16)in[is]) - 128) << 8; + } + } + else if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 24)) + { + const quint8 *in = (const quint8 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (((qint32)in[is]) - 128) << 16; + } + } + else if ((m_settings.m_sampleBits == 16) && (SDR_RX_SAMP_SZ == 16)) + { + const qint16 *in = (const qint16 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = in[is]; + } + } + else if ((m_settings.m_sampleBits == 16) && (SDR_RX_SAMP_SZ == 24)) + { + const qint16 *in = (const qint16 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = in[is] << 8; + } + } + else if ((m_settings.m_sampleBits == 24) && (SDR_RX_SAMP_SZ == 24)) + { + const quint8 *in = (const quint8 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (((in[3*is+2] << 16) | (in[3*is+1] << 8) | in[3*is]) << 8) >> 8; + } + } + else if ((m_settings.m_sampleBits == 24) && (SDR_RX_SAMP_SZ == 16)) + { + const quint8 *in = (const quint8 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = (in[3*is+2] << 8) | in[3*is+1]; + } + } + else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 16)) + { + const qint32 *in = (const qint32 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = in[is] >> 8; + } + } + else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24)) + { + const qint32 *in = (const qint32 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; + + for (int is = 0; is < nbSamples*2; is++) { + out[is] = in[is]; + } + } + else // invalid size + { + qWarning("RemoteTCPInputTCPHandler::convert: unexpected sample size in stream: %d bits", (int) m_settings.m_sampleBits); + } + + m_uncompressedData.write(reinterpret_cast(m_converterBuffer), nbSamples*sizeof(Sample)); +} + +void RemoteTCPInputTCPHandler::flacError(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status) +{ + qDebug() << "RemoteTCPInputTCPHandler::flacError: Error:" << status; +} + void RemoteTCPInputTCPHandler::connected() { QMutexLocker mutexLocker(&m_mutex); @@ -605,9 +997,20 @@ void RemoteTCPInputTCPHandler::connected() m_spyServer = m_settings.m_protocol == "Spy Server"; m_state = HEADER; m_sdra = false; + m_remoteControl = true; + m_iqOnly = true; if (m_spyServer) { spyServerConnect(); } + // Start calls to processData + m_timer.start(); + + /*if (m_dataSocket->bytesAvailable()) { + qDebug() << "Data is already available"; + dataReadyRead(); + } else { + qDebug() << "No data available"; + }*/ } void RemoteTCPInputTCPHandler::reconnect() @@ -628,35 +1031,52 @@ void RemoteTCPInputTCPHandler::disconnected() MsgReportConnection *msg = MsgReportConnection::create(false); m_messageQueueToGUI->push(msg); } - // Try to reconnect - m_reconnectTimer.start(500); + if (!m_blacklisted) + { + // Try to reconnect immediately - it may just be server settings changed + m_reconnectTimer.start(1); + } + else + { + // Stop device so we don't try to reconnect + RemoteTCPInput::MsgStartStop *msg = RemoteTCPInput::MsgStartStop::create(false); + m_messageQueueToInput->push(msg); + } } void RemoteTCPInputTCPHandler::errorOccurred(QAbstractSocket::SocketError socketError) { + QMutexLocker mutexLocker(&m_mutex); qDebug() << "RemoteTCPInputTCPHandler::errorOccurred: " << socketError; - cleanup(); - if (m_messageQueueToGUI) + + // For RemoteHostClosedError, disconnected() will be called afterwards, so don't try to reconnect here + // We try to reconnect here, for errors such as ConnectionRefusedError + if (socketError != QAbstractSocket::RemoteHostClosedError) { - MsgReportConnection *msg = MsgReportConnection::create(false); - m_messageQueueToGUI->push(msg); + cleanup(); + if (m_messageQueueToGUI) + { + MsgReportConnection *msg = MsgReportConnection::create(false); + m_messageQueueToGUI->push(msg); + } + // Try to reconnect + m_reconnectTimer.start(500); } - // Try to reconnect - m_reconnectTimer.start(500); } void RemoteTCPInputTCPHandler::dataReadyRead() { QMutexLocker mutexLocker(&m_mutex); - if (!m_readMetaData && !m_spyServer) - { + if (!m_readMetaData && !m_spyServer) { processMetaData(); - } - else if (!m_readMetaData && m_spyServer) - { + } else if (!m_readMetaData && m_spyServer) { processSpyServerMetaData(); } + + if (m_readMetaData && !m_iqOnly) { + processCommands(); + } } void RemoteTCPInputTCPHandler::processMetaData() @@ -682,19 +1102,13 @@ void RemoteTCPInputTCPHandler::processMetaData() m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol)); + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol, false, true)); } if (m_settings.m_sampleBits != 8) { RemoteTCPInputSettings& settings = m_settings; settings.m_sampleBits = 8; - QList settingsKeys{"sampleBits"}; - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } + sendSettings(settings, {"sampleBits"}); } } else if (protocol == "SDRA") @@ -704,10 +1118,22 @@ void RemoteTCPInputTCPHandler::processMetaData() bytesRead = m_dataSocket->read((char *)&metaData[4], RemoteTCPProtocol::m_sdraMetaDataSize-4); m_device = (RemoteTCPProtocol::Device)RemoteTCPProtocol::extractUInt32(&metaData[4]); - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol)); + quint32 protocolRevision = RemoteTCPProtocol::extractUInt32(&metaData[60]); + quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]); + if (protocolRevision >= 1) + { + m_iqOnly = !(bool) ((flags >> 7) & 1); + m_remoteControl = (bool) ((flags >> 6) & 1); } - if (!m_settings.m_overrideRemoteSettings) + else + { + m_iqOnly = true; + m_remoteControl = true; + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, protocol, m_iqOnly, m_remoteControl)); + } + if (!m_settings.m_overrideRemoteSettings || !m_remoteControl) { // Update local settings to match remote RemoteTCPInputSettings& settings = m_settings; @@ -716,7 +1142,6 @@ void RemoteTCPInputTCPHandler::processMetaData() settingsKeys.append("centerFrequency"); settings.m_loPpmCorrection = RemoteTCPProtocol::extractUInt32(&metaData[16]); settingsKeys.append("loPpmCorrection"); - quint32 flags = RemoteTCPProtocol::extractUInt32(&metaData[20]); settings.m_biasTee = flags & 1; settingsKeys.append("biasTee"); settings.m_directSampling = (flags >> 1) & 1; @@ -752,19 +1177,57 @@ void RemoteTCPInputTCPHandler::processMetaData() settings.m_channelDecimation = true; settingsKeys.append("channelDecimation"); } - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + if (protocolRevision >= 1) + { + settings.m_squelchEnabled = (flags >> 5) & 1; + settingsKeys.append("squelchEnabled"); + settings.m_squelch = RemoteTCPProtocol::extractFloat(&metaData[64]); + settingsKeys.append("squelch"); + settings.m_squelchGate = RemoteTCPProtocol::extractFloat(&metaData[68]); + settingsKeys.append("squelchGate"); } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + sendSettings(settings, settingsKeys); + } + + if (!m_iqOnly) + { + qDebug() << "RemoteTCPInputTCPHandler: Compression enabled"; + // Create FLAC decoder for IQ decompression + m_decoder = FLAC__stream_decoder_new(); + m_remainingSamples = 0; + m_compressedFrames = 0; + m_uncompressedFrames = 0; + + int bytesPerSecond = m_settings.m_channelSampleRate * 2 * sizeof(Sample); + int fifoSize = 2 * m_settings.m_preFill * bytesPerSecond; + m_uncompressedData.resize(fifoSize); + m_uncompressedData.clear(); + + if (m_decoder) + { + FLAC__StreamDecoderInitStatus initStatus; + initStatus = FLAC__stream_decoder_init_stream(m_decoder, flacReadCallback, nullptr, nullptr, nullptr, nullptr, flacWriteCallback, nullptr, flacErrorCallback, this); + if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) + { + qDebug() << "RemoteTCPInputTCPHandler: Failed to init FLAC decoder: " << initStatus; + } } + else + { + qDebug() << "RemoteTCPInputTCPHandler: Failed to allocate FLAC decoder"; + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler: Compression disabled"; } } else { qDebug() << "RemoteTCPInputTCPHandler::dataReadyRead: Unknown protocol: " << protocol; + m_dataSocket->close(); } - if (m_settings.m_overrideRemoteSettings) + if (m_settings.m_overrideRemoteSettings && m_remoteControl) { // Force settings to be sent to remote device (this needs to be after m_sdra is determined above) applySettings(m_settings, QList(), true); @@ -868,7 +1331,7 @@ void RemoteTCPInputTCPHandler::processSpyServerDevice(const SpyServerProtocol::D break; } if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, "Spy Server", ssDevice->m_maxGainIndex)); + m_messageQueueToGUI->push(MsgReportRemoteDevice::create(m_device, "Spy Server", false, true, ssDevice->m_maxGainIndex)); } RemoteTCPInputSettings& settings = m_settings; @@ -882,12 +1345,7 @@ void RemoteTCPInputTCPHandler::processSpyServerDevice(const SpyServerProtocol::D m_settings.m_log2Decim = settings.m_log2Decim = ssDevice->m_minDecimation; settingsKeys.append("log2Decim"); } - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } + sendSettings(settings, settingsKeys); } void RemoteTCPInputTCPHandler::processSpyServerState(const SpyServerProtocol::State* ssState, bool initial) @@ -922,12 +1380,7 @@ void RemoteTCPInputTCPHandler::processSpyServerState(const SpyServerProtocol::St } if (settingsKeys.size() > 0) { - if (m_messageQueueToInput) { - m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } - if (m_messageQueueToGUI) { - m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); - } + sendSettings(settings, settingsKeys); } } } @@ -978,7 +1431,7 @@ void RemoteTCPInputTCPHandler::processSpyServerData(int requiredBytes, bool clea if (!clear) { const int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8; - convert(bytesRead / bytesPerIQPair); + processUncompressedData(&m_tcpBuf[0], bytesRead / bytesPerIQPair); } m_spyServerHeader.m_size -= bytesRead; requiredBytes -= bytesRead; @@ -1013,19 +1466,484 @@ void RemoteTCPInputTCPHandler::processSpyServerData(int requiredBytes, bool clea } } +void RemoteTCPInputTCPHandler::sendSettings(const RemoteTCPInputSettings& settings, const QStringList& settingsKeys) +{ + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgConfigureRemoteTCPInput::create(settings, settingsKeys)); + } +} + +void RemoteTCPInputTCPHandler::processCommands() +{ + bool done = false; + + while (!done) + { + if (m_state == HEADER) + { + if (m_dataSocket->bytesAvailable() >= 5) + { + quint8 buf[5]; + qint64 bytesRead = m_dataSocket->read((char *) buf, sizeof(buf)); + + if (bytesRead == sizeof(buf)) + { + m_command = (RemoteTCPProtocol::Command) buf[0]; + + switch (m_command) + { + case RemoteTCPProtocol::setCenterFrequency: + { + quint32 centerFrequency = (quint32) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (centerFrequency != m_settings.m_centerFrequency) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_centerFrequency = centerFrequency; + sendSettings(settings, {"centerFrequency"}); + } + break; + } + case RemoteTCPProtocol::setSampleRate: + { + int devSampleRate = RemoteTCPProtocol::extractInt32(&buf[1]); + if (devSampleRate != m_settings.m_devSampleRate) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_devSampleRate = devSampleRate; + sendSettings(settings, {"devSampleRate"}); + } + break; + } + case RemoteTCPProtocol::setTunerGainMode: + { + // Currently fixed as 1 + } + case RemoteTCPProtocol::setTunerGain: + { + int gain = RemoteTCPProtocol::extractUInt32(&buf[1]); + if (gain != m_settings.m_gain[0]) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_gain[0] = gain; + sendSettings(settings, {"gain[0]"}); + } + break; + } + case RemoteTCPProtocol::setFrequencyCorrection: + { + qint32 loPpmCorrection = (qint32) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (loPpmCorrection != m_settings.m_loPpmCorrection) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_loPpmCorrection = loPpmCorrection; + sendSettings(settings, {"loPpmCorrection"}); + } + break; + } + case RemoteTCPProtocol::setTunerIFGain: + { + int v = RemoteTCPProtocol::extractUInt32(&buf[1]); + int gain = (int)(qint16)(v & 0xffff); + int stage = (v >> 16) & 0xffff; + if ((stage < RemoteTCPInputSettings::m_maxGains) && (gain != m_settings.m_gain[stage])) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_gain[stage] = gain; + sendSettings(settings, {QString("gain[%1]").arg(stage)}); + } + break; + } + case RemoteTCPProtocol::setAGCMode: + { + bool agc = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (agc != m_settings.m_agc) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_agc = agc; + sendSettings(settings, {"agc"}); + } + break; + } + case RemoteTCPProtocol::setDirectSampling: + { + bool directSampling = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (directSampling != m_settings.m_directSampling) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_directSampling = directSampling; + sendSettings(settings, {"directSampling"}); + } + break; + } + case RemoteTCPProtocol::setBiasTee: + { + bool biasTee = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (biasTee != m_settings.m_biasTee) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_biasTee = biasTee; + sendSettings(settings, {"biasTee"}); + } + break; + } + case RemoteTCPProtocol::setTunerBandwidth: + { + int rfBW = RemoteTCPProtocol::extractInt32(&buf[1]); + if (rfBW != m_settings.m_rfBW) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_rfBW = rfBW; + sendSettings(settings, {"rfBW"}); + } + break; + } + case RemoteTCPProtocol::setDecimation: + { + int log2Decim = RemoteTCPProtocol::extractInt32(&buf[1]); + if (log2Decim != m_settings.m_log2Decim) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_log2Decim = log2Decim; + sendSettings(settings, {"log2Decim"}); + } + break; + } + case RemoteTCPProtocol::setDCOffsetRemoval: + { + bool dcBlock = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (dcBlock != m_settings.m_dcBlock) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_dcBlock = dcBlock; + sendSettings(settings, {"dcBlock"}); + } + break; + } + case RemoteTCPProtocol::setIQCorrection: + { + bool iqCorrection = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (iqCorrection != m_settings.m_iqCorrection) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_iqCorrection = iqCorrection; + sendSettings(settings, {"iqCorrection"}); + } + break; + } + case RemoteTCPProtocol::setChannelSampleRate: + { + qint32 channelSampleRate = RemoteTCPProtocol::extractInt32(&buf[1]); + if (channelSampleRate != m_settings.m_channelSampleRate) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_channelSampleRate = channelSampleRate; + sendSettings(settings, {"channelSampleRate"}); + } + break; + } + case RemoteTCPProtocol::setChannelFreqOffset: + { + qint32 inputFrequencyOffset = (qint32) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (inputFrequencyOffset != m_settings.m_inputFrequencyOffset) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_inputFrequencyOffset = inputFrequencyOffset; + sendSettings(settings, {"inputFrequencyOffset"}); + } + break; + } + case RemoteTCPProtocol::setChannelGain: + { + qint32 channelGain = RemoteTCPProtocol::extractInt32(&buf[1]); + if (channelGain != m_settings.m_channelGain) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_channelGain = channelGain; + sendSettings(settings, {"channelGain"}); + } + break; + } + case RemoteTCPProtocol::setSampleBitDepth: + { + qint32 sampleBits = RemoteTCPProtocol::extractInt32(&buf[1]); + if (sampleBits != m_settings.m_sampleBits) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_sampleBits = sampleBits; + sendSettings(settings, {"sampleBits"}); + } + break; + } + case RemoteTCPProtocol::setIQSquelchEnabled: + { + bool squelchEnabled = (bool) RemoteTCPProtocol::extractUInt32(&buf[1]); + if (squelchEnabled != m_settings.m_squelchEnabled) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_squelchEnabled = squelchEnabled; + sendSettings(settings, {"squelchEnabled"}); + } + break; + } + case RemoteTCPProtocol::setIQSquelch: + { + float squelch = RemoteTCPProtocol::extractFloat(&buf[1]); + if (squelch != m_settings.m_squelch) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_squelch = squelch; + sendSettings(settings, {"squelch"}); + } + break; + } + case RemoteTCPProtocol::setIQSquelchGate: + { + float squelchGate = RemoteTCPProtocol::extractFloat(&buf[1]); + if (squelchGate != m_settings.m_squelchGate) + { + RemoteTCPInputSettings settings = m_settings; + settings.m_squelchGate = squelchGate; + sendSettings(settings, {"squelchGate"}); + } + break; + } + default: + m_commandLength = RemoteTCPProtocol::extractUInt32(&buf[1]); + m_state = DATA; + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << sizeof(buf); + } + } + else + { + done = true; + } + } + if (m_state == DATA) + { + if (m_dataSocket->bytesAvailable() >= m_commandLength) + { + try + { + switch (m_command) + { + + + case RemoteTCPProtocol::dataIQ: + { + break; + } + + case RemoteTCPProtocol::dataIQFLAC: + { + qsizetype s = m_compressedData.size(); + m_compressedData.resize(s + m_commandLength); + qint64 bytesRead = m_dataSocket->read(&m_compressedData.data()[s], m_commandLength); + m_compressedFrames++; + //qDebug() << "*************************** RemoteTCPProtocol::dataIQFLAC m_compressedData.size()" << m_compressedData.size() << "m_compressedFrames" << m_compressedFrames << "m_uncompressedFrames" << m_uncompressedFrames; + if (bytesRead == m_commandLength) + { + // FLAC encoder writes out 4 (fLaC), 38 (STREAMINFO), 51 (?) byte headers, that are transmitted as one command block, + // then each command block will be a complete audio block (first two bytes will be 0xfff8) + // FLAC__stream_decoder_process_single will keep calling the read callback until it's decoded one metadata or audio block + // so we need to make sure there's enough data that it will be able to return + + bool decodeDone = false; + + while (!decodeDone) + { + //qDebug() << "m_compressedFrames" << m_compressedFrames << "m_uncompressedFrames" << m_uncompressedFrames; + if (m_compressedFrames - 1 > m_uncompressedFrames) + { + if (!FLAC__stream_decoder_process_single(m_decoder)) + { + qDebug() << "FLAC decode failed"; + decodeDone = true; + } + } + else + { + decodeDone = true; + } + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << m_commandLength; + } + break; + } + + case RemoteTCPProtocol::dataIQzlib: + { + if (m_commandLength > m_compressedData.size()) { + m_compressedData.resize(m_commandLength); + } + qint64 bytesRead = m_dataSocket->read(m_compressedData.data(), m_commandLength); + if (bytesRead == m_commandLength) + { + // Decompressing using zlib + m_zStream.next_in = (Bytef *) m_compressedData.data(); + m_zStream.avail_in = m_commandLength; + m_zStream.next_out = (Bytef *) m_zOutBuf.data(); + m_zStream.avail_out = m_zOutBuf.size(); + + int ret = inflate(&m_zStream, Z_NO_FLUSH); + + if (ret == Z_STREAM_END) { + inflateReset(&m_zStream); + // Convert and write to uncompressed data FIFO + int uncompressedBytes = m_zOutBuf.size() - m_zStream.avail_out; + int nbSamples = uncompressedBytes / 2 / (m_settings.m_sampleBits / 8); + processDecompressedZlibData(m_zOutBuf.data(), nbSamples); + } else if (ret == Z_NEED_DICT) { + qDebug() << "zlib needs dict to inflate"; + } else if (ret == Z_DATA_ERROR) { + qDebug() << "zlib data error"; + } else if (ret == Z_MEM_ERROR) { + qDebug() << "zlib mem error"; + } else { + qDebug() << "Unexpected zlib return value" << ret; + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << m_commandLength; + } + break; + } + + case RemoteTCPProtocol::dataPosition: + { + char pos[4+4+4]; + qint64 bytesRead = m_dataSocket->read(pos, m_commandLength); + if (bytesRead == m_commandLength) + { + float latitude = RemoteTCPProtocol::extractFloat((const quint8 *) &pos[0]); + float longitude = RemoteTCPProtocol::extractFloat((const quint8 *) &pos[4]); + float altitude = RemoteTCPProtocol::extractFloat((const quint8 *) &pos[8]); + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Position " << latitude << longitude << altitude; + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgReportPosition::create(latitude, longitude, altitude)); + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << m_commandLength; + } + break; + } + + case RemoteTCPProtocol::dataDirection: + { + char dir[4+4+4]; + qint64 bytesRead = m_dataSocket->read(dir, m_commandLength); + if (bytesRead == m_commandLength) + { + float isotropic = RemoteTCPProtocol::extractUInt32((const quint8 *) &dir[0]); + float azimuth = RemoteTCPProtocol::extractFloat((const quint8 *) &dir[4]); + float elevation = RemoteTCPProtocol::extractFloat((const quint8 *) &dir[8]); + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Direction " << isotropic << azimuth << elevation; + if (m_messageQueueToInput) { + m_messageQueueToInput->push(RemoteTCPInput::MsgReportDirection::create(isotropic, azimuth, elevation)); + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << m_commandLength; + } + break; + } + + case RemoteTCPProtocol::sendMessage: + { + char *buf = new char[m_commandLength]; + qint64 bytesRead = m_dataSocket->read(buf, m_commandLength); + + if (bytesRead == m_commandLength) + { + bool broadcast = (bool) buf[0]; + int i; + for (i = 1; i < (int) m_commandLength; i++) + { + if (buf[i] == '\0') { + break; + } + } + QString callsign = QString::fromUtf8(&buf[1]); + QString text = QString::fromUtf8(&buf[i+1]); + + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Message " << m_dataSocket->peerAddress() << m_dataSocket->peerPort() << callsign << broadcast << text; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgSendMessage::create(callsign, text, broadcast)); + } + } + else + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to read:" << bytesRead << "/" << m_commandLength; + } + delete[] buf; + break; + } + + case RemoteTCPProtocol::sendBlacklistedMessage: + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Disconnecting as blacklisted"; + if (m_messageQueueToGUI) { + m_messageQueueToGUI->push(RemoteTCPInput::MsgSendMessage::create("", "Disconnecting as IP address is blacklisted", false)); + } + m_blacklisted = true; + qDebug() << "set m_blacklisted" << m_blacklisted; + break; + } + + default: + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Unknown command" << m_command; + char *buf = new char[m_commandLength]; + m_dataSocket->read(buf, m_commandLength); + delete[] buf; + break; + } + } + } + catch(std::bad_alloc&) + { + qDebug() << "RemoteTCPInputTCPHandler::processCommands: Failed to allocate memory"; + done = true; + } + m_state = HEADER; + } + else + { + done = true; + } + } + } +} + // QTimer::timeout isn't guaranteed to be called on every timeout, so we need to look at the system clock void RemoteTCPInputTCPHandler::processData() { QMutexLocker mutexLocker(&m_mutex); + if (m_dataSocket && (m_dataSocket->state() == QAbstractSocket::ConnectedState)) { int sampleRate = m_settings.m_channelSampleRate; - int bytesPerIQPair = 2 * m_settings.m_sampleBits / 8; + int bytesPerIQPair = m_iqOnly ? (2 * m_settings.m_sampleBits / 8) : (2 * sizeof(Sample)); int bytesPerSecond = sampleRate * bytesPerIQPair; - if (m_dataSocket->bytesAvailable() < (0.1f * m_settings.m_preFill * bytesPerSecond)) + qint64 bytesAvailable = m_iqOnly ? m_dataSocket->bytesAvailable() : m_uncompressedData.fill(); + + if ((bytesAvailable < (0.1f * m_settings.m_preFill * bytesPerSecond)) && !m_fillBuffer) { - qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering - bytesAvailable:" << m_dataSocket->bytesAvailable(); + qDebug() << "RemoteTCPInputTCPHandler::processData: Buffering - bytesAvailable:" << bytesAvailable; m_fillBuffer = true; } @@ -1033,9 +1951,9 @@ void RemoteTCPInputTCPHandler::processData() // QTcpSockets buffer size should be unlimited - we pretend here it's twice as big as the point we start reading from it if (m_messageQueueToGUI) { - qint64 size = std::max(m_dataSocket->bytesAvailable(), (qint64)(m_settings.m_preFill * bytesPerSecond)); + qint64 size = std::max(bytesAvailable, (qint64)(m_settings.m_preFill * bytesPerSecond)); RemoteTCPInput::MsgReportTCPBuffer *report = RemoteTCPInput::MsgReportTCPBuffer::create( - m_dataSocket->bytesAvailable(), size, m_dataSocket->bytesAvailable() / (float)bytesPerSecond, + bytesAvailable, size, bytesAvailable / (float)bytesPerSecond, m_sampleFifo->fill(), m_sampleFifo->size(), m_sampleFifo->fill() / (float)bytesPerSecond ); m_messageQueueToGUI->push(report); @@ -1045,9 +1963,9 @@ void RemoteTCPInputTCPHandler::processData() // Prime buffer, before we start reading if (m_fillBuffer) { - if (m_dataSocket->bytesAvailable() >= m_settings.m_preFill * bytesPerSecond) + if (bytesAvailable >= m_settings.m_preFill * bytesPerSecond) { - qDebug() << "RemoteTCPInputTCPHandler::processData: Buffer primed - bytesAvailable:" << m_dataSocket->bytesAvailable(); + qDebug() << "RemoteTCPInputTCPHandler::processData: Buffer primed - bytesAvailable:" << bytesAvailable; m_fillBuffer = false; m_prevDateTime = QDateTime::currentDateTime(); factor = 1.0f / 4.0f; // If this is too high, samples can just be dropped downstream @@ -1056,22 +1974,31 @@ void RemoteTCPInputTCPHandler::processData() else { QDateTime currentDateTime = QDateTime::currentDateTime(); - factor = m_prevDateTime.msecsTo(currentDateTime) / 1000.0f; + factor = m_prevDateTime.msecsTo(currentDateTime) / 1000.0f; // FIXME: Close skew.. Actual sample rate may differ m_prevDateTime = currentDateTime; } unsigned int remaining = m_sampleFifo->size() - m_sampleFifo->fill(); - int requiredSamples = (int)std::min((unsigned int)(factor * sampleRate), remaining); + unsigned int maxRequired = (unsigned int) (factor * sampleRate); + int requiredSamples = (int)std::min(maxRequired, remaining); + int overflow = maxRequired - requiredSamples; + if (overflow > 0) { + qDebug() << "Not enough space in FIFO:" << overflow << maxRequired; + } if (!m_fillBuffer) { - if (!m_spyServer) + if (!m_iqOnly) + { + processDecompressedData(requiredSamples); + } + else if (!m_spyServer) { - // rtl_tcp/SDRA stream is just IQ samples if (m_dataSocket->bytesAvailable() >= requiredSamples*bytesPerIQPair) { + // rtl_tcp stream is just IQ samples m_dataSocket->read(&m_tcpBuf[0], requiredSamples*bytesPerIQPair); - convert(requiredSamples); + processUncompressedData(&m_tcpBuf[0], requiredSamples); } } else @@ -1084,9 +2011,57 @@ void RemoteTCPInputTCPHandler::processData() } } -// The following code assumes host is little endian -void RemoteTCPInputTCPHandler::convert(int nbSamples) +// Copy from decompressed FIFO to replay buffer and sample FIFO +void RemoteTCPInputTCPHandler::processDecompressedData(int requiredSamples) { + qint64 requiredBytes = requiredSamples * sizeof(Sample); + + m_replayBuffer->lock(); + + while ((requiredBytes > 0) && !m_uncompressedData.empty()) + { + quint8 *uncompressedPtr; + qsizetype uncompressedBytes = m_uncompressedData.readPtr(&uncompressedPtr, requiredSamples * sizeof(Sample)); + qsizetype uncompressedSamples = 2 * uncompressedBytes / sizeof(Sample); + + // Save data to replay buffer + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write((FixReal *) uncompressedPtr, (unsigned int) uncompressedSamples); + } + + const FixReal *buf = (FixReal *) uncompressedPtr; + qint32 remaining = uncompressedSamples; + + while (remaining > 0) + { + qint32 len; + + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + calcPower(reinterpret_cast(buf), len / 2); + + m_sampleFifo->write((quint8 *) buf, len * sizeof(FixReal)); + } + + m_uncompressedData.read(uncompressedBytes); + requiredBytes -= uncompressedBytes; + } + + m_replayBuffer->unlock(); +} + +// Convert from uncompressed network format to Samples, then copy to replay buffer and sample FIFO +// The following code assumes host is little endian +void RemoteTCPInputTCPHandler::processUncompressedData(const char *inBuf, int nbSamples) +{ + // Ensure conversion buffer is large enough if (nbSamples > (int) m_converterBufferNbSamples) { if (m_converterBuffer) { @@ -1095,102 +2070,121 @@ void RemoteTCPInputTCPHandler::convert(int nbSamples) m_converterBuffer = new int32_t[nbSamples*2]; } - if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && !m_spyServer) + // Convert from network format to Sample + if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && m_spyServer) { - m_sampleFifo->write(reinterpret_cast(m_tcpBuf), nbSamples*sizeof(Sample)); - } - else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24) && m_spyServer) - { - float *in = (float *)m_tcpBuf; - qint32 *out = (qint32 *)m_converterBuffer; + const float *in = (const float *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (qint32)(in[is] * SDR_RX_SCALEF); } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 16) && m_spyServer) { - float *in = (float *)m_tcpBuf; - qint16 *out = (qint16 *)m_converterBuffer; + const float *in = (const float *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (qint16)(in[is] * SDR_RX_SCALEF); } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 16)) { - quint8 *in = (quint8 *)m_tcpBuf; - qint16 *out = (qint16 *)m_converterBuffer; + const quint8 *in = (const quint8 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (((qint16)in[is]) - 128) << 8; } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 8) && (SDR_RX_SAMP_SZ == 24)) { - quint8 *in = (quint8 *)m_tcpBuf; - qint32 *out = (qint32 *)m_converterBuffer; + const quint8 *in = (const quint8 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (((qint32)in[is]) - 128) << 16; } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 24) && (SDR_RX_SAMP_SZ == 24)) { - quint8 *in = (quint8 *)m_tcpBuf; - qint32 *out = (qint32 *)m_converterBuffer; + const quint8 *in = (const quint8 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (((in[3*is+2] << 16) | (in[3*is+1] << 8) | in[3*is]) << 8) >> 8; } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 24) && (SDR_RX_SAMP_SZ == 16)) { - quint8 *in = (quint8 *)m_tcpBuf; - qint16 *out = (qint16 *)m_converterBuffer; + const quint8 *in = (const quint8 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = (in[3*is+2] << 8) | in[3*is+1]; } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 16) && (SDR_RX_SAMP_SZ == 24)) { - qint16 *in = (qint16 *)m_tcpBuf; - qint32 *out = (qint32 *)m_converterBuffer; + const qint16 *in = (const qint16 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = in[is] << 8; } - - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); } else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 16)) { - qint32 *in = (qint32 *)m_tcpBuf; - qint16 *out = (qint16 *)m_converterBuffer; + const qint32 *in = (const qint32 *) inBuf; + qint16 *out = (qint16 *) m_converterBuffer; for (int is = 0; is < nbSamples*2; is++) { out[is] = in[is] >> 8; } + } + else if ((m_settings.m_sampleBits == 32) && (SDR_RX_SAMP_SZ == 24)) + { + const qint32 *in = (const qint32 *) inBuf; + qint32 *out = (qint32 *) m_converterBuffer; - m_sampleFifo->write(reinterpret_cast(out), nbSamples*sizeof(Sample)); + for (int is = 0; is < nbSamples*2; is++) { + out[is] = in[is]; + } } else // invalid size { qWarning("RemoteTCPInputTCPHandler::convert: unexpected sample size in stream: %d bits", (int) m_settings.m_sampleBits); } + + qint32 len = nbSamples*2; + + // Save data to replay buffer + m_replayBuffer->lock(); + bool replayEnabled = m_replayBuffer->getSize() > 0; + if (replayEnabled) { + m_replayBuffer->write((const FixReal *) m_converterBuffer, len); + } + + const FixReal *buf = (const FixReal *) m_converterBuffer; + qint32 remaining = len; + + while (remaining > 0) + { + // Choose between live data or replayed data + if (replayEnabled && m_replayBuffer->useReplay()) { + len = m_replayBuffer->read(remaining, buf); + } else { + len = remaining; + } + remaining -= len; + + calcPower(reinterpret_cast(buf), len / 2); + + m_sampleFifo->write(reinterpret_cast(buf), len * sizeof(FixReal)); + } + + m_replayBuffer->unlock(); } void RemoteTCPInputTCPHandler::handleInputMessages() @@ -1214,6 +2208,14 @@ bool RemoteTCPInputTCPHandler::handleMessage(const Message& cmd) applySettings(notif.getSettings(), notif.getSettingsKeys(), notif.getForce()); return true; } + else if (RemoteTCPInput::MsgSendMessage::match(cmd)) + { + RemoteTCPInput::MsgSendMessage& msg = (RemoteTCPInput::MsgSendMessage&) cmd; + + sendMessage(MainCore::instance()->getSettings().getStationName(), msg.getText(), msg.getBroadcast()); + + return true; + } else { return false; diff --git a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h index a829c9370..f1b990ca2 100644 --- a/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h +++ b/plugins/samplesource/remotetcpinput/remotetcpinputtcphandler.h @@ -26,7 +26,12 @@ #include #include +#include +#include + #include "util/messagequeue.h" +#include "util/movingaverage.h" +#include "dsp/replaybuffer.h" #include "remotetcpinputsettings.h" #include "../../channelrx/remotetcpsink/remotetcpprotocol.h" #include "spyserver.h" @@ -35,6 +40,31 @@ class SampleSinkFifo; class MessageQueue; class DeviceAPI; +class FIFO { +public: + + FIFO(qsizetype elements = 10); + + qsizetype write(quint8 *data, qsizetype elements); + qsizetype read(quint8 *data, qsizetype elements); + qsizetype readPtr(quint8 **data, qsizetype elements); + void read(qsizetype elements); + void resize(qsizetype elements); // Sets capacity + void clear(); + qsizetype fill() const { return m_fill; } // Number of elements in use + bool empty() const { return m_fill == 0; } + bool full() const { return m_fill == m_data.size(); } + +private: + + qsizetype m_readPtr; + qsizetype m_writePtr; + qsizetype m_fill; + + QByteArray m_data; + +}; + class RemoteTCPInputTCPHandler : public QObject { Q_OBJECT @@ -71,22 +101,28 @@ public: public: RemoteTCPProtocol::Device getDevice() const { return m_device; } QString getProtocol() const { return m_protocol; } + bool getIQOnly() const { return m_iqOnly; } + bool getRemoteControl() const { return m_remoteControl; } int getMaxGain() const { return m_maxGain; } - static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain = 0) + static MsgReportRemoteDevice* create(RemoteTCPProtocol::Device device, const QString& protocol, bool iqOnly, bool remoteControl, int maxGain = 0) { - return new MsgReportRemoteDevice(device, protocol, maxGain); + return new MsgReportRemoteDevice(device, protocol, iqOnly, remoteControl, maxGain); } protected: RemoteTCPProtocol::Device m_device; QString m_protocol; + bool m_iqOnly; + bool m_remoteControl; int m_maxGain; - MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol, int maxGain) : + MsgReportRemoteDevice(RemoteTCPProtocol::Device device, const QString& protocol, bool iqOnly, bool remoteControl, int maxGain) : Message(), m_device(device), m_protocol(protocol), + m_iqOnly(iqOnly), + m_remoteControl(remoteControl), m_maxGain(maxGain) { } }; @@ -111,7 +147,7 @@ public: { } }; - RemoteTCPInputTCPHandler(SampleSinkFifo* sampleFifo, DeviceAPI *deviceAPI); + RemoteTCPInputTCPHandler(SampleSinkFifo* sampleFifo, DeviceAPI *deviceAPI, ReplayBuffer *replayBuffer); ~RemoteTCPInputTCPHandler(); MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } void setMessageQueueToInput(MessageQueue *queue) { m_messageQueueToInput = queue; } @@ -120,6 +156,29 @@ public: void start(); void stop(); int getBufferGauge() const { return 0; } + void processCommands(); + + FLAC__StreamDecoderReadStatus flacRead(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes); + FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); + void flacError(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status); + + 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; + } public slots: void dataReadyRead(); @@ -129,11 +188,22 @@ public slots: private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + DeviceAPI *m_deviceAPI; bool m_running; QTcpSocket *m_dataSocket; char *m_tcpBuf; SampleSinkFifo *m_sampleFifo; + ReplayBuffer *m_replayBuffer; MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication MessageQueue *m_messageQueueToInput; MessageQueue *m_messageQueueToGUI; @@ -148,6 +218,8 @@ private: SpyServerProtocol::Header m_spyServerHeader; enum {HEADER, DATA} m_state; //!< FSM for reading Spy Server packets + RemoteTCPProtocol::Command m_command; + quint32 m_commandLength; int32_t *m_converterBuffer; uint32_t m_converterBufferNbSamples; @@ -155,13 +227,38 @@ private: QRecursiveMutex m_mutex; RemoteTCPInputSettings m_settings; - void applyTCPLink(const QString& address, quint16 port); + bool m_remoteControl; + bool m_iqOnly; + QByteArray m_compressedData; + + // FLAC decompression + qint64 m_compressedFrames; + qint64 m_uncompressedFrames; + FIFO m_uncompressedData; + FLAC__StreamDecoder *m_decoder; + int m_remainingSamples; + + // Zlib decompression + z_stream m_zStream; + QByteArray m_zOutBuf; + static const int m_zBufSize = 32768+128; // + + bool m_blacklisted; + + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + MovingAverageUtil m_movingAverage; + bool handleMessage(const Message& message); - void convert(int nbSamples); void connectToHost(const QString& address, quint16 port); - void disconnectFromHost(); + //void disconnectFromHost(); void cleanup(); void clearBuffer(); + void sendCommand(RemoteTCPProtocol::Command cmd, quint32 value); + void sendCommandFloat(RemoteTCPProtocol::Command cmd, float value); void setSampleRate(int sampleRate); void setCenterFrequency(quint64 frequency); void setTunerAGC(bool agc); @@ -180,6 +277,10 @@ private: void setChannelFreqOffset(int offset); void setChannelGain(int gain); void setSampleBitDepth(int sampleBits); + void setSquelchEnabled(bool enabled); + void setSquelch(float squelch); + void setSquelchGate(float squelchGate); + void sendMessage(const QString& callsign, const QString& text, bool broadcast); void applySettings(const RemoteTCPInputSettings& settings, const QList& settingsKeys, bool force = false); void processMetaData(); void spyServerConnect(); @@ -190,6 +291,11 @@ private: void processSpyServerDevice(const SpyServerProtocol::Device* ssDevice); void processSpyServerState(const SpyServerProtocol::State* ssState, bool initial); void processSpyServerData(int requiredBytes, bool clear); + void processDecompressedData(int requiredSamples); + void processUncompressedData(const char *inBuf, int nbSamples); + void processDecompressedZlibData(const char *inBuf, int nbSamples); + void calcPower(const Sample *iq, int nbSamples); + void sendSettings(const RemoteTCPInputSettings& settings, const QStringList& settingsKeys); private slots: void started(); diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 9255a4974..bb6989995 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -266,6 +266,7 @@ set(sdrbase_SOURCES util/rtpsink.cpp util/syncmessenger.cpp util/samplesourceserializer.cpp + util/sdrangelserverlist.cpp util/simpleserializer.cpp util/serialutil.cpp util/solardynamicsobservatory.cpp @@ -526,6 +527,7 @@ set(sdrbase_HEADERS util/rtty.h util/syncmessenger.h util/samplesourceserializer.h + util/sdrangelserverlist.h util/simpleserializer.h util/serialutil.h util/solardynamicsobservatory.h diff --git a/sdrbase/util/sdrangelserverlist.cpp b/sdrbase/util/sdrangelserverlist.cpp new file mode 100644 index 000000000..7a3bf8933 --- /dev/null +++ b/sdrbase/util/sdrangelserverlist.cpp @@ -0,0 +1,183 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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 "sdrangelserverlist.h" + +#include +#include +#include +#include +#include +#include + +SDRangelServerList::SDRangelServerList() +{ + m_networkManager = new QNetworkAccessManager(); + QObject::connect(m_networkManager, &QNetworkAccessManager::finished, this, &SDRangelServerList::handleReply); + + QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); + QDir writeableDir(locations[0]); + if (!writeableDir.mkpath(QStringLiteral("cache") + QDir::separator() + QStringLiteral("sdrangelserver"))) { + qDebug() << "Failed to create cache/sdrangelserver"; + } + + m_cache = new QNetworkDiskCache(); + m_cache->setCacheDirectory(locations[0] + QDir::separator() + QStringLiteral("cache") + QDir::separator() + QStringLiteral("sdrangelserver")); + m_cache->setMaximumCacheSize(100000000); + m_networkManager->setCache(m_cache); + + connect(&m_timer, &QTimer::timeout, this, &SDRangelServerList::update); +} + +SDRangelServerList::~SDRangelServerList() +{ + QObject::disconnect(m_networkManager, &QNetworkAccessManager::finished, this, &SDRangelServerList::handleReply); + delete m_networkManager; +} + +void SDRangelServerList::getData() +{ + QUrl url = QUrl("https://sdrangel.org/websdr/websdrs.json"); + m_networkManager->get(QNetworkRequest(url)); +} + +void SDRangelServerList::getDataPeriodically(int periodInMins) +{ + m_timer.setInterval(periodInMins*60*1000); + m_timer.start(); + update(); +} + +void SDRangelServerList::update() +{ + getData(); +} + +void SDRangelServerList::handleReply(QNetworkReply* reply) +{ + if (reply) + { + if (!reply->error()) + { + QString url = reply->url().toEncoded().constData(); + QByteArray bytes = reply->readAll(); + + handleJSON(url, bytes); + } + else + { + qDebug() << "SDRangelServerList::handleReply: error: " << reply->error(); + } + reply->deleteLater(); + } + else + { + qDebug() << "SDRangelServerList::handleReply: reply is null"; + } +} + +void SDRangelServerList::handleJSON(const QString& url, const QByteArray& bytes) +{ + (void) url; + + QList sdrs; + QJsonDocument document = QJsonDocument::fromJson(bytes); + + if (document.isArray()) + { + QJsonArray servers = document.array(); + + for (auto valRef : servers) + { + if (valRef.isObject()) + { + QJsonObject serverObj = valRef.toObject(); + SDRangelServer sdr; + + if (serverObj.contains(QStringLiteral("address"))) { + sdr.m_address = serverObj.value(QStringLiteral("address")).toString(); + } + if (serverObj.contains(QStringLiteral("port"))) { + sdr.m_port = serverObj.value(QStringLiteral("port")).toInt(); + } + if (serverObj.contains(QStringLiteral("minFrequency"))) { + sdr.m_minFrequency = serverObj.value(QStringLiteral("minFrequency")).toInt(); + } + if (serverObj.contains(QStringLiteral("maxFrequency"))) { + sdr.m_maxFrequency = serverObj.value(QStringLiteral("maxFrequency")).toInt(); + } + if (serverObj.contains(QStringLiteral("maxSampleRate"))) { + sdr.m_maxSampleRate = serverObj.value(QStringLiteral("maxSampleRate")).toInt(); + } + if (serverObj.contains(QStringLiteral("device"))) { + sdr.m_device = serverObj.value(QStringLiteral("device")).toString(); + } + if (serverObj.contains(QStringLiteral("antenna"))) { + sdr.m_antenna = serverObj.value(QStringLiteral("antenna")).toString(); + } + if (serverObj.contains(QStringLiteral("remoteControl"))) { + sdr.m_remoteControl = serverObj.value(QStringLiteral("remoteControl")).toInt() == 1; + } + if (serverObj.contains(QStringLiteral("stationName"))) { + sdr.m_stationName = serverObj.value(QStringLiteral("stationName")).toString(); + } + if (serverObj.contains(QStringLiteral("location"))) { + sdr.m_location = serverObj.value(QStringLiteral("location")).toString(); + } + if (serverObj.contains(QStringLiteral("latitude"))) { + sdr.m_latitude = serverObj.value(QStringLiteral("latitude")).toDouble(); + } + if (serverObj.contains(QStringLiteral("longitude"))) { + sdr.m_longitude = serverObj.value(QStringLiteral("longitude")).toDouble(); + } + if (serverObj.contains(QStringLiteral("altitude"))) { + sdr.m_altitude = serverObj.value(QStringLiteral("altitude")).toDouble(); + } + if (serverObj.contains(QStringLiteral("isotropic"))) { + sdr.m_isotropic = serverObj.value(QStringLiteral("isotropic")).toInt() == 1; + } + if (serverObj.contains(QStringLiteral("azimuth"))) { + sdr.m_azimuth = serverObj.value(QStringLiteral("azimuth")).toDouble(); + } + if (serverObj.contains(QStringLiteral("elevation"))) { + sdr.m_elevation = serverObj.value(QStringLiteral("elevation")).toDouble(); + } + if (serverObj.contains(QStringLiteral("clients"))) { + sdr.m_clients = serverObj.value(QStringLiteral("clients")).toInt(); + } + if (serverObj.contains(QStringLiteral("maxClients"))) { + sdr.m_maxClients = serverObj.value(QStringLiteral("maxClients")).toInt(); + } + if (serverObj.contains(QStringLiteral("timeLimit"))) { + sdr.m_timeLimit = serverObj.value(QStringLiteral("timeLimit")).toInt(); + } + + sdrs.append(sdr); + } + else + { + qDebug() << "SDRangelServerList::handleJSON: Element not an object:\n" << valRef; + } + } + } + else + { + qDebug() << "SDRangelServerList::handleJSON: Doc doesn't contain an array:\n" << document; + } + + emit dataUpdated(sdrs); +} diff --git a/sdrbase/util/sdrangelserverlist.h b/sdrbase/util/sdrangelserverlist.h new file mode 100644 index 000000000..e563e1338 --- /dev/null +++ b/sdrbase/util/sdrangelserverlist.h @@ -0,0 +1,81 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2024 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_SDRANGELSERVERLIST_H +#define INCLUDE_SDRANGELSERVERLIST_H + +#include +#include + +#include "export.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QNetworkDiskCache; + +// Gets a list of public SDRangel Servers from https://sdrangel.org/websdr/ +class SDRBASE_API SDRangelServerList : public QObject +{ + Q_OBJECT + +public: + + struct SDRangelServer { + QString m_address; + quint16 m_port; + qint64 m_minFrequency; + qint64 m_maxFrequency; + int m_maxSampleRate; + QString m_device; + QString m_antenna; + bool m_remoteControl; + QString m_stationName; + QString m_location; + float m_latitude; + float m_longitude; + float m_altitude; + bool m_isotropic; + float m_azimuth; + float m_elevation; + int m_clients; + int m_maxClients; + int m_timeLimit; // In minutes + }; + + SDRangelServerList(); + ~SDRangelServerList(); + + void getData(); + void getDataPeriodically(int periodInMins=1); + +public slots: + void handleReply(QNetworkReply* reply); + void update(); + +signals: + void dataUpdated(const QList& sdrs); // Emitted when data are available. + +private: + QNetworkAccessManager *m_networkManager; + QNetworkDiskCache *m_cache; + QTimer m_timer; // Timer for periodic updates + + void handleJSON(const QString& url, const QByteArray& bytes); + +}; + +#endif /* INCLUDE_SDRANGELSERVERLIST_H */ diff --git a/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml b/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml index 09cc073e6..c27b4e712 100644 --- a/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml +++ b/swagger/sdrangel/api/swagger/include/RemoteTCPInput.yaml @@ -61,3 +61,12 @@ RemoteTCPInputReport: properties: sampleRate: type: integer + latitude: + type: number + format: float + longitude: + type: number + format: float + altitude: + type: number + format: float diff --git a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.cpp b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.cpp index 74c4c5abf..eb50dd12a 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.cpp @@ -1,6 +1,6 @@ /** * 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 --- + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- * * OpenAPI spec version: 7.0.0 * Contact: f4exb06@gmail.com @@ -59,7 +59,7 @@ SWGRemoteTCPInputReport::fromJson(QString &json) { void SWGRemoteTCPInputReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&sample_rate, pJson["sampleRate"], "qint32", ""); - + } QString @@ -79,6 +79,9 @@ SWGRemoteTCPInputReport::asJsonObject() { if(m_sample_rate_isSet){ obj->insert("sampleRate", QJsonValue(sample_rate)); } + obj->insert("latitude", QJsonValue(latitude)); + obj->insert("longitude", QJsonValue(longitude)); + obj->insert("altitude", QJsonValue(altitude)); return obj; } diff --git a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.h b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.h index 1dd6df6a8..92ea400e0 100644 --- a/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGRemoteTCPInputReport.h @@ -1,6 +1,6 @@ /** * 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 --- + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- * * OpenAPI spec version: 7.0.0 * Contact: f4exb06@gmail.com @@ -43,13 +43,21 @@ public: qint32 getSampleRate(); void setSampleRate(qint32 sample_rate); - + float getLatitude() { return latitude; } + float getLongitude() { return longitude; } + float getAltitude() { return altitude; } + void setLatitude(float latitude) { this->latitude = latitude; } + void setLongitude(float longitude) { this->longitude = longitude; } + void setAltitude(float altitude) { this->altitude = altitude; } virtual bool isSet() override; private: qint32 sample_rate; bool m_sample_rate_isSet; + float latitude; + float longitude; + float altitude; };