diff --git a/CMakeLists.txt b/CMakeLists.txt index 3de4e252..a038a103 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ option(OPT_BUILD_WEATHER_SAT_DECODER "Build the HRPT decoder module (no dependen # Misc option(OPT_BUILD_DISCORD_PRESENCE "Build the Discord Rich Presence module" ON) option(OPT_BUILD_FREQUENCY_MANAGER "Build the Frequency Manager module" ON) +option(OPT_BUILD_IQ_EXPORTER "Build the IQ Exporter module" OFF) option(OPT_BUILD_RECORDER "Audio and baseband recorder" ON) option(OPT_BUILD_RIGCTL_CLIENT "Rigctl client to make SDR++ act as a panadapter" ON) option(OPT_BUILD_RIGCTL_SERVER "Rigctl backend for controlling SDR++ with software like gpredict" ON) @@ -257,6 +258,10 @@ if (OPT_BUILD_FREQUENCY_MANAGER) add_subdirectory("misc_modules/frequency_manager") endif (OPT_BUILD_FREQUENCY_MANAGER) +if (OPT_BUILD_IQ_EXPORTER) +add_subdirectory("misc_modules/iq_exporter") +endif (OPT_BUILD_IQ_EXPORTER) + if (OPT_BUILD_RECORDER) add_subdirectory("misc_modules/recorder") endif (OPT_BUILD_RECORDER) diff --git a/make_windows_package.ps1 b/make_windows_package.ps1 index 91a75320..64b73466 100644 --- a/make_windows_package.ps1 +++ b/make_windows_package.ps1 @@ -79,6 +79,8 @@ cp $build_dir/misc_modules/discord_integration/Release/discord_integration.dll s cp $build_dir/misc_modules/frequency_manager/Release/frequency_manager.dll sdrpp_windows_x64/modules/ +cp $build_dir/misc_modules/iq_exporter/Release/iq_exporter.dll sdrpp_windows_x64/modules/ + cp $build_dir/misc_modules/recorder/Release/recorder.dll sdrpp_windows_x64/modules/ cp $build_dir/misc_modules/rigctl_client/Release/rigctl_client.dll sdrpp_windows_x64/modules/ diff --git a/misc_modules/iq_exporter/CMakeLists.txt b/misc_modules/iq_exporter/CMakeLists.txt new file mode 100644 index 00000000..5ffece18 --- /dev/null +++ b/misc_modules/iq_exporter/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.13) +project(iq_exporter) + +file(GLOB SRC "src/*.cpp") + +include(${SDRPP_MODULE_CMAKE}) \ No newline at end of file diff --git a/misc_modules/iq_exporter/src/main.cpp b/misc_modules/iq_exporter/src/main.cpp new file mode 100644 index 00000000..9b636116 --- /dev/null +++ b/misc_modules/iq_exporter/src/main.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SDRPP_MOD_INFO{ + /* Name: */ "iq_exporter", + /* Description: */ "Export raw IQ through TCP or UDP", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ -1 +}; + +ConfigManager config; + +enum Mode { + MODE_BASEBAND, + MODE_VFO +}; + +enum Protocol { + PROTOCOL_TCP, + PROTOCOL_UDP +}; + +enum SampleType { + SAMPLE_TYPE_INT8, + SAMPLE_TYPE_INT16, + SAMPLE_TYPE_INT32, + SAMPLE_TYPE_FLOAT32 +}; + +class IQExporterModule : public ModuleManager::Instance { +public: + IQExporterModule(std::string name) { + this->name = name; + + // Define operating modes + modes.define("Baseband", MODE_BASEBAND); + modes.define("VFO", MODE_VFO); + + // Define protocols + protocols.define("TCP", PROTOCOL_TCP); + protocols.define("UDP", PROTOCOL_UDP); + + // Define sample types + sampleTypes.define("Int8", SAMPLE_TYPE_INT8); + sampleTypes.define("Int16", SAMPLE_TYPE_INT16); + sampleTypes.define("Int32", SAMPLE_TYPE_INT32); + sampleTypes.define("Float32", SAMPLE_TYPE_FLOAT32); + + // Load config + bool autoStart = false; + config.acquire(); + if (config.conf[name].contains("mode")) { + std::string modeStr = config.conf[name]["mode"]; + if (modes.keyExists(modeStr)) { mode = modes.value(modes.keyId(modeStr)); } + } + if (config.conf[name].contains("protocol")) { + std::string protoStr = config.conf[name]["protocol"]; + if (protocols.keyExists(protoStr)) { proto = protocols.value(protocols.keyId(protoStr)); } + } + if (config.conf[name].contains("sampleType")) { + std::string sampTypeStr = config.conf[name]["sampleType"]; + if (sampleTypes.keyExists(sampTypeStr)) { sampType = sampleTypes.value(sampleTypes.keyId(sampTypeStr)); } + } + if (config.conf[name].contains("host")) { + std::string hostStr = config.conf[name]["host"]; + strcpy(hostname, hostStr.c_str()); + } + if (config.conf[name].contains("port")) { + port = config.conf[name]["port"]; + port = std::clamp(port, 1, 65535); + } + if (config.conf[name].contains("running")) { + autoStart = config.conf[name]["running"]; + } + config.release(); + + // Set menu IDs + modeId = modes.valueId(mode); + protoId = protocols.valueId(proto); + sampTypeId = sampleTypes.valueId(sampType); + + // Allocate buffer + buffer = dsp::buffer::alloc(STREAM_BUFFER_SIZE * sizeof(dsp::complex_t)); + + // Init DSP + handler.init(NULL, dataHandler, this); + + // Register menu entry + gui::menu.registerEntry(name, menuHandler, this, this); + } + + ~IQExporterModule() { + // Un-register menu entry + gui::menu.removeEntry(name); + + // Free buffer + dsp::buffer::free(buffer); + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + + void start() { + if (running) { return; } + + // TODO + + running = true; + } + + void stop() { + if (!running) { return; } + + // TODO + + running = false; + } + +private: + static void menuHandler(void* ctx) { + IQExporterModule* _this = (IQExporterModule*)ctx; + float menuWidth = ImGui::GetContentRegionAvail().x; + + if (!_this->enabled) { ImGui::BeginDisabled(); } + + if (_this->running) { ImGui::BeginDisabled(); } + + // Mode selector + ImGui::LeftLabel("Mode"); + ImGui::FillWidth(); + if (ImGui::Combo(("##iq_exporter_mode_" + _this->name).c_str(), &_this->modeId, _this->modes.txt)) { + _this->setMode(_this->modes.value(_this->modeId)); + config.acquire(); + config.conf[_this->name]["mode"] = _this->modes.key(_this->modeId); + config.release(true); + } + + // Mode protocol selector + ImGui::LeftLabel("Protocol"); + ImGui::FillWidth(); + if (ImGui::Combo(("##iq_exporter_proto_" + _this->name).c_str(), &_this->protoId, _this->protocols.txt)) { + config.acquire(); + config.conf[_this->name]["protocol"] = _this->protocols.key(_this->protoId); + config.release(true); + } + + // Sample type selector + ImGui::LeftLabel("Sample type"); + ImGui::FillWidth(); + if (ImGui::Combo(("##iq_exporter_samp_" + _this->name).c_str(), &_this->sampTypeId, _this->sampleTypes.txt)) { + config.acquire(); + config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampTypeId); + config.release(true); + } + + // Hostname and port field + if (ImGui::InputText(("##iq_exporter_host_" + _this->name).c_str(), _this->hostname, sizeof(_this->hostname))) { + config.acquire(); + config.conf[_this->name]["host"] = _this->hostname; + config.release(true); + } + ImGui::SameLine(); + ImGui::FillWidth(); + if (ImGui::InputInt(("##iq_exporter_port_" + _this->name).c_str(), &_this->port, 0, 0)) { + _this->port = std::clamp(_this->port, 1, 65535); + config.acquire(); + config.conf[_this->name]["port"] = _this->port; + config.release(true); + } + + if (_this->running) { ImGui::EndDisabled(); } + + // Start/Stop buttons + if (_this->running) { + if (ImGui::Button(("Stop##iq_exporter_stop_" + _this->name).c_str(), ImVec2(menuWidth, 0))) { + _this->stop(); + config.acquire(); + config.conf[_this->name]["running"] = false; + config.release(true); + } + } + else { + if (ImGui::Button(("Start##iq_exporter_start_" + _this->name).c_str(), ImVec2(menuWidth, 0))) { + _this->start(); + config.acquire(); + config.conf[_this->name]["running"] = true; + config.release(true); + } + } + + if (!_this->enabled) { ImGui::EndDisabled(); } + } + + void setMode(Mode newMode) { + // Delete VFO or unbind IQ stream + + } + + static void dataHandler(dsp::complex_t* data, int count, void* ctx) { + IQExporterModule* _this = (IQExporterModule*)ctx; + + // Acquire lock on socket + std::lock_guard lck(_this->sockMtx); + + // If not valid or open, give uo + if (!_this->sock || !_this->sock->isOpen()) { return; } + + // Convert the samples or send directory for float32 + int size; + switch (_this->sampType) { + case SAMPLE_TYPE_INT8: + volk_32f_s32f_convert_8i((int8_t*)_this->buffer, (float*)data, 128.0f, count*2); + size = sizeof(int8_t)*2; + break; + case SAMPLE_TYPE_INT16: + volk_32fc_convert_16ic((lv_16sc_t*)_this->buffer, (lv_32fc_t*)data, count); + size = sizeof(int16_t)*2; + break; + case SAMPLE_TYPE_INT32: + volk_32f_s32f_convert_32i((int32_t*)_this->buffer, (float*)data, (float)2147483647.0f, count*2); + size = sizeof(int32_t)*2; + break; + case SAMPLE_TYPE_FLOAT32: + _this->sock->send((uint8_t*)data, count*sizeof(dsp::complex_t)); + default: + return; + } + + // Send converted samples + _this->sock->send(_this->buffer, count*size); + } + + std::string name; + bool enabled = true; + + Mode mode = MODE_BASEBAND; + int modeId; + Protocol proto = PROTOCOL_TCP; + int protoId; + SampleType sampType = SAMPLE_TYPE_INT16; + int sampTypeId; + char hostname[1024] = "localhost"; + int port = 1234; + bool running = false; + + OptionList modes; + OptionList protocols; + OptionList sampleTypes; + + VFOManager::VFO* vfo = NULL; + dsp::sink::Handler handler; + uint8_t* buffer = NULL; + + std::mutex sockMtx; + std::shared_ptr sock; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + std::string root = (std::string)core::args["root"]; + config.setPath(root + "/iq_exporter_config_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new IQExporterModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (IQExporterModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file diff --git a/readme.md b/readme.md index 4fb3f293..ae681271 100644 --- a/readme.md +++ b/readme.md @@ -377,6 +377,7 @@ Modules in beta are still included in releases for the most part but not enabled |---------------------|------------|--------------|-----------------------------|:----------------:|:----------------:|:---------------------------:| | discord_integration | Working | - | OPT_BUILD_DISCORD_PRESENCE | ✅ | ✅ | ⛔ | | frequency_manager | Working | - | OPT_BUILD_FREQUENCY_MANAGER | ✅ | ✅ | ✅ | +| iq_exporter | Unfinished | - | OPT_BUILD_IQ_EXPORTER | ⛔ | ⛔ | ⛔ | | recorder | Working | - | OPT_BUILD_RECORDER | ✅ | ✅ | ✅ | | rigctl_client | Unfinished | - | OPT_BUILD_RIGCTL_CLIENT | ✅ | ✅ | ⛔ | | rigctl_server | Working | - | OPT_BUILD_RIGCTL_SERVER | ✅ | ✅ | ✅ |