diff --git a/app/main.cpp b/app/main.cpp index f8211904a..cb6a2f310 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -36,6 +36,7 @@ #include "loggerwithfile.h" #include "mainwindow.h" +#include "remotetcpsinkstarter.h" #include "dsp/dsptypes.h" static int runQtApplication(int argc, char* argv[], qtwebapp::LoggerWithFile *logger) @@ -74,7 +75,6 @@ static int runQtApplication(int argc, char* argv[], qtwebapp::LoggerWithFile *lo if (settings.contains(uiScaleFactor)) { QString scaleFactor = settings.value(uiScaleFactor).toString(); - qDebug() << "Setting QT_SCALE_FACTOR to" << scaleFactor; qputenv("QT_SCALE_FACTOR", scaleFactor.toLatin1()); } @@ -178,7 +178,27 @@ static int runQtApplication(int argc, char* argv[], qtwebapp::LoggerWithFile *lo applicationPid); #endif + if (parser.getListDevices()) + { + // Disable log on console, so we can more easily see device list + logger->setConsoleMinMessageLevel(QtFatalMsg); + // Don't pass logger to MainWindow, otherwise it can reenable log output + logger = nullptr; + } + MainWindow w(logger, parser); + + if (parser.getListDevices()) + { + // List available physical devices and exit + RemoteTCPSinkStarter::listAvailableDevices(); + exit (EXIT_SUCCESS); + } + + if (parser.getRemoteTCPSink()) { + RemoteTCPSinkStarter::start(parser); + } + w.show(); return a.exec(); diff --git a/appsrv/main.cpp b/appsrv/main.cpp index d94df1c3f..46ce93378 100644 --- a/appsrv/main.cpp +++ b/appsrv/main.cpp @@ -1,8 +1,6 @@ /////////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2017 Edouard Griffiths, F4EXB. // // // -// Swagger server adapter interface // -// // // 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 // @@ -26,6 +24,7 @@ #include "loggerwithfile.h" #include "mainparser.h" #include "mainserver.h" +#include "remotetcpsinkstarter.h" #include "dsp/dsptypes.h" void handler(int sig) { @@ -92,20 +91,39 @@ static int runQtApplication(int argc, char* argv[], qtwebapp::LoggerWithFile *lo QCoreApplication::applicationPid()); #endif - MainServer m(logger, parser, &a); + if (parser.getListDevices()) + { + // Disable log on console, so we can more easily see device list + logger->setConsoleMinMessageLevel(QtFatalMsg); + // Don't pass logger to MainWindow, otherwise it can reenable log output + logger = nullptr; + } - // This will cause the application to exit when the main core is finished - QObject::connect(&m, SIGNAL(finished()), &a, SLOT(quit())); + MainServer m(logger, parser, &a); - return a.exec(); - } + // This will cause the application to exit when the main core is finished + QObject::connect(&m, SIGNAL(finished()), &a, SLOT(quit())); - int main(int argc, char* argv[]) - { - qtwebapp::LoggerWithFile *logger = new qtwebapp::LoggerWithFile(qApp); - logger->installMsgHandler(); - int res = runQtApplication(argc, argv, logger); - delete logger; - qWarning("SDRangel quit."); - return res; - } + if (parser.getListDevices()) + { + // List available physical devices and exit + RemoteTCPSinkStarter::listAvailableDevices(); + exit (EXIT_SUCCESS); + } + + if (parser.getRemoteTCPSink()) { + RemoteTCPSinkStarter::start(parser); + } + + return a.exec(); +} + +int main(int argc, char* argv[]) +{ + qtwebapp::LoggerWithFile *logger = new qtwebapp::LoggerWithFile(qApp); + logger->installMsgHandler(); + int res = runQtApplication(argc, argv, logger); + delete logger; + qWarning("SDRangel quit."); + return res; +} diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 5b640f7bb..63f0ed392 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -285,6 +285,7 @@ set(sdrbase_SOURCES mainparser.cpp maincore.cpp + remotetcpsinkstarter.cpp resources/webapi.qrc ) @@ -519,6 +520,7 @@ set(sdrbase_HEADERS mainparser.h maincore.h + remotetcpsinkstarter.h ) include_directories( diff --git a/sdrbase/mainparser.cpp b/sdrbase/mainparser.cpp index 6ab22f2ce..dfb0116bd 100644 --- a/sdrbase/mainparser.cpp +++ b/sdrbase/mainparser.cpp @@ -36,7 +36,13 @@ MainParser::MainParser() : "file", ""), m_scratchOption("scratch", "Start from scratch (no current config)."), - m_soapyOption("soapy", "Activate Soapy SDR support.") + m_soapyOption("soapy", "Activate Soapy SDR support."), + m_remoteTCPSinkOption("remote-tcp", "Start Remote TCP Sink"), + m_remoteTCPSinkAddressOption("remote-tcp-address", "Remote TCP Sink interface IP address (Default 127.0.0.1).", "address", "127.0.0.1"), + m_remoteTCPSinkPortOption("remote-tcp-port", "Remote TCP Sink port (Default 1234).", "port", "1234"), + m_remoteTCPSinkHWTypeOption("remote-tcp-hwtype", "Remote TCP Sink device hardware type (Optional. E.g. RTLSDR/SDRplayV3/AirspyHF).", "hwtype"), + m_remoteTCPSinkSerialOption("remote-tcp-serial", "Remote TCP Sink device serial (Optional).", "serial"), + m_listDevicesOption("list-devices", "List available physical devices.") { m_serverAddress = ""; // Bind to any address @@ -44,6 +50,12 @@ MainParser::MainParser() : m_scratch = false; m_soapy = false; m_fftwfWindowFileName = ""; + m_remoteTCPSink = false; + m_remoteTCPSinkAddress = "127.0.0.1"; + m_remoteTCPSinkPort = 1234; + m_remoteTCPSinkHWType = ""; + m_remoteTCPSinkSerial = ""; + m_listDevices = false; m_parser.setApplicationDescription("Software Defined Radio application"); m_parser.addHelpOption(); @@ -54,6 +66,12 @@ MainParser::MainParser() : m_parser.addOption(m_fftwfWisdomOption); m_parser.addOption(m_scratchOption); m_parser.addOption(m_soapyOption); + m_parser.addOption(m_remoteTCPSinkOption); + m_parser.addOption(m_remoteTCPSinkAddressOption); + m_parser.addOption(m_remoteTCPSinkPortOption); + m_parser.addOption(m_remoteTCPSinkHWTypeOption); + m_parser.addOption(m_remoteTCPSinkSerialOption); + m_parser.addOption(m_listDevicesOption); } MainParser::~MainParser() @@ -65,19 +83,18 @@ void MainParser::parse(const QCoreApplication& app) int pos; bool ok; + QString ipRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; + QRegularExpression ipRegex ("^" + ipRange + + "\\." + ipRange + + "\\." + ipRange + + "\\." + ipRange + "$"); + QRegularExpressionValidator ipValidator(ipRegex); // server address QString serverAddress = m_parser.value(m_serverAddressOption); if (!serverAddress.isEmpty()) { - QString ipRange = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; - QRegularExpression ipRegex ("^" + ipRange - + "\\." + ipRange - + "\\." + ipRange - + "\\." + ipRange + "$"); - QRegularExpressionValidator ipValidator(ipRegex); - if (ipValidator.validate(serverAddress, pos) == QValidator::Acceptable) { m_serverAddress = serverAddress; } else { @@ -104,4 +121,37 @@ void MainParser::parse(const QCoreApplication& app) // Soapy SDR support m_soapy = m_parser.isSet(m_soapyOption); + + // Remote TCP Sink options + + m_remoteTCPSink = m_parser.isSet(m_remoteTCPSinkOption); + + QString remoteTCPSinkAddress = m_parser.value(m_remoteTCPSinkAddressOption); + if (!remoteTCPSinkAddress.isEmpty()) + { + if (ipValidator.validate(remoteTCPSinkAddress, pos) == QValidator::Acceptable) { + m_remoteTCPSinkAddress = remoteTCPSinkAddress; + } else { + qWarning() << "MainParser::parse: remote TCP Sink address invalid. Defaulting to " << m_remoteTCPSinkAddress; + } + } + + QString remoteTCPSinkPortStr = m_parser.value(m_remoteTCPSinkPortOption); + int remoteTCPSinkPort = remoteTCPSinkPortStr.toInt(&ok); + + if (ok && (remoteTCPSinkPort > 1023) && (remoteTCPSinkPort < 65536)) { + m_remoteTCPSinkPort = remoteTCPSinkPort; + } else { + qWarning() << "MainParser::parse: remote TCP Sink port invalid. Defaulting to " << m_serverPort; + } + + m_remoteTCPSinkHWType = m_parser.value(m_remoteTCPSinkHWTypeOption); + m_remoteTCPSinkSerial = m_parser.value(m_remoteTCPSinkSerialOption); + m_listDevices = m_parser.isSet(m_listDevicesOption); + + if (m_remoteTCPSink && m_remoteTCPSinkHWType.isEmpty() && m_remoteTCPSinkSerial.isEmpty()) + { + qCritical() << "You must specify a device with either -remote-tcp-hwtype or -remote-tcp-serial"; + exit (EXIT_FAILURE); + } } diff --git a/sdrbase/mainparser.h b/sdrbase/mainparser.h index f472d2733..fb3705434 100644 --- a/sdrbase/mainparser.h +++ b/sdrbase/mainparser.h @@ -37,6 +37,12 @@ public: bool getScratch() const { return m_scratch; } bool getSoapy() const { return m_soapy; } const QString& getFFTWFWisdomFileName() const { return m_fftwfWindowFileName; } + bool getRemoteTCPSink() const { return m_remoteTCPSink; } + const QString& getRemoteTCPSinkAddressOption() const { return m_remoteTCPSinkAddress; } + const int getRemoteTCPSinkPortOption() const { return m_remoteTCPSinkPort; } + const QString& getRemoteTCPSinkHWType() const { return m_remoteTCPSinkHWType; } + const QString& getRemoteTCPSinkSerial() const { return m_remoteTCPSinkSerial; } + const bool getListDevices() const { return m_listDevices; } private: QString m_serverAddress; @@ -44,6 +50,12 @@ private: QString m_fftwfWindowFileName; bool m_scratch; bool m_soapy; + bool m_remoteTCPSink; + QString m_remoteTCPSinkAddress; + int m_remoteTCPSinkPort; + QString m_remoteTCPSinkHWType; + QString m_remoteTCPSinkSerial; + bool m_listDevices; QCommandLineParser m_parser; QCommandLineOption m_serverAddressOption; @@ -51,6 +63,12 @@ private: QCommandLineOption m_fftwfWisdomOption; QCommandLineOption m_scratchOption; QCommandLineOption m_soapyOption; + QCommandLineOption m_remoteTCPSinkOption; + QCommandLineOption m_remoteTCPSinkAddressOption; + QCommandLineOption m_remoteTCPSinkPortOption; + QCommandLineOption m_remoteTCPSinkHWTypeOption; + QCommandLineOption m_remoteTCPSinkSerialOption; + QCommandLineOption m_listDevicesOption; }; diff --git a/sdrbase/remotetcpsinkstarter.cpp b/sdrbase/remotetcpsinkstarter.cpp new file mode 100644 index 000000000..0dd99e1b8 --- /dev/null +++ b/sdrbase/remotetcpsinkstarter.cpp @@ -0,0 +1,200 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "remotetcpsinkstarter.h" + +#include "maincore.h" +#include "device/deviceset.h" +#include "device/deviceapi.h" +#include "device/deviceenumerator.h" +#include "dsp/devicesamplesource.h" +#include "channel/channelapi.h" +#include "SWGChannelSettings.h" +#include "SWGRemoteTCPSinkSettings.h" +#include "SWGDeviceState.h" + +// Lists available physical devices to stdout +void RemoteTCPSinkStarter::listAvailableDevices() +{ + int nbSamplingDevices = DeviceEnumerator::instance()->getNbRxSamplingDevices(); + + printf("Available devices:\n"); + for (int i = 0; i < nbSamplingDevices; i++) + { + const PluginInterface::SamplingDevice *samplingDevice; + + samplingDevice = DeviceEnumerator::instance()->getRxSamplingDevice(i); + if (samplingDevice->type == PluginInterface::SamplingDevice::PhysicalDevice) + { + printf(" HWType: %s", qPrintable(samplingDevice->hardwareId)); + if (!samplingDevice->serial.isEmpty()) { + printf(" Serial: %s", qPrintable(samplingDevice->serial)); + } + printf("\n"); + } + } +} + +// Instantiate specified sampling source device and create a RemoteTCPSink channel +// on the specified address and port and start the device +static void startRemoteTCPSink(const QString& address, int port, const QString& hwType, const QString& serial) +{ + MainCore *mainCore = MainCore::instance(); + + // Delete any existing device sets, in case requested device is already in use + int initialDeviceSets = mainCore->getDeviceSets().size(); + for (int i = 0; i < initialDeviceSets; i++) + { + MainCore::MsgRemoveLastDeviceSet *msg = MainCore::MsgRemoveLastDeviceSet::create(); + mainCore->getMainMessageQueue()->push(msg); + } + + // Wait until they've been deleted + if (initialDeviceSets > 0) + { + do + { + QTime dieTime = QTime::currentTime().addMSecs(100); + while (QTime::currentTime() < dieTime) { + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + } + } + while (mainCore->getDeviceSets().size() > 0); + } + + // Create DeviceSet + int deviceSetIndex = mainCore->getDeviceSets().size(); + MainCore::MsgAddDeviceSet *msg = MainCore::MsgAddDeviceSet::create(0); + mainCore->getMainMessageQueue()->push(msg); + + // Switch to requested device type + int nbSamplingDevices = DeviceEnumerator::instance()->getNbRxSamplingDevices(); + bool found = false; + for (int i = 0; i < nbSamplingDevices; i++) + { + const PluginInterface::SamplingDevice *samplingDevice; + + samplingDevice = DeviceEnumerator::instance()->getRxSamplingDevice(i); + + if (!hwType.isEmpty() && (hwType != samplingDevice->hardwareId)) { + continue; + } + if (!serial.isEmpty() && (serial != samplingDevice->serial)) { + continue; + } + + int direction = 0; + MainCore::MsgSetDevice *msg = MainCore::MsgSetDevice::create(deviceSetIndex, i, direction); + mainCore->getMainMessageQueue()->push(msg); + found = true; + break; + } + if (!found) + { + qCritical() << "startRemoteTCPSink: Failed to find device"; + return; + } + + // Add RemoteTCPSink channel + PluginAPI::ChannelRegistrations *channelRegistrations = mainCore->getPluginManager()->getRxChannelRegistrations(); + int nbRegistrations = channelRegistrations->size(); + int index = 0; + for (; index < nbRegistrations; index++) + { + if (channelRegistrations->at(index).m_channelId == "RemoteTCPSink") { + break; + } + } + + if (index < nbRegistrations) + { + MainCore::MsgAddChannel *msg = MainCore::MsgAddChannel::create(deviceSetIndex, index, 0); + mainCore->getMainMessageQueue()->push(msg); + } + else + { + qCritical() << "startRemoteTCPSink: RemoteTCPSink is not available"; + return; + } + int channelIndex = 0; + + // Wait until device & channel are created - is there a better way? + DeviceSet *deviceSet = nullptr; + ChannelAPI *channelAPI = 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]; + if (deviceSet) { + channelAPI = deviceSet->m_deviceAPI->getChanelSinkAPIAt(channelIndex); + } + } + } + while (channelAPI == nullptr); + + // Set TCP settings + QStringList channelSettingsKeys = {"dataAddress", "dataPort"}; + SWGSDRangel::SWGChannelSettings response; + response.init(); + SWGSDRangel::SWGRemoteTCPSinkSettings *sinkSettings = response.getRemoteTcpSinkSettings(); + sinkSettings->setDataAddress(new QString(address)); + sinkSettings->setDataPort(port); + QString errorMessage; + channelAPI->webapiSettingsPutPatch(false, channelSettingsKeys, response, errorMessage); + + // Wait some time for settings to be applied + QCoreApplication::processEvents(QEventLoop::AllEvents, 100); + + // Start the device (use WebAPI so GUI is updated) + DeviceSampleSource *source = deviceSet->m_deviceAPI->getSampleSource(); + QStringList deviceActionsKeys; + SWGSDRangel::SWGDeviceState state; + state.init(); + int res = source->webapiRun(true, state, errorMessage); + if (res != 200) { + qCritical() << "startRemoteTCPSink: Failed to start device: " << res; + } else { + qInfo().nospace().noquote() << "Remote TCP Sink started on " << address << ":" << port; + } +} + +// Start Remote TCP Sink on specified device, with specified address and port +void RemoteTCPSinkStarter::start(const MainParser& parser) +{ + QString remoteTCPSinkAddress = parser.getRemoteTCPSinkAddressOption(); + int remoteTCPSinkPort = parser.getRemoteTCPSinkPortOption(); + QString remoteTCPSinkHWType = parser.getRemoteTCPSinkHWType(); + QString remoteTCPSinkSerial = parser.getRemoteTCPSinkSerial(); + + QTimer::singleShot(250, [=] { + startRemoteTCPSink( + remoteTCPSinkAddress, + remoteTCPSinkPort, + remoteTCPSinkHWType, + remoteTCPSinkSerial); + }); +} diff --git a/sdrbase/remotetcpsinkstarter.h b/sdrbase/remotetcpsinkstarter.h new file mode 100644 index 000000000..8b723b6ea --- /dev/null +++ b/sdrbase/remotetcpsinkstarter.h @@ -0,0 +1,32 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2023 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef REMOTETCPSINKSTARTER_H +#define REMOTETCPSINKSTARTER_H + +#include "mainparser.h" +#include "export.h" + +class SDRBASE_API RemoteTCPSinkStarter { + +public: + static void listAvailableDevices(); + static void start(const MainParser& parser); + +}; + +#endif /* REMOTETCPSINKSTARTER_H */