From 4b6835141e5bf0336c6f672ff8f5621f08fb4735 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Tue, 30 Jan 2024 22:18:18 +0100 Subject: [PATCH 1/9] fix low PI RDS callsign decoding --- decoder_modules/radio/src/rds.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/decoder_modules/radio/src/rds.cpp b/decoder_modules/radio/src/rds.cpp index 577af2c5..2e315bbc 100644 --- a/decoder_modules/radio/src/rds.cpp +++ b/decoder_modules/radio/src/rds.cpp @@ -408,6 +408,11 @@ namespace rds { rest /= 26; } + // Pad with As + while (restStr.size() < 3) { + restStr += 'A'; + } + // Reorder chars for (int i = restStr.size() - 1; i >= 0; i--) { callsign += restStr[i]; From 3fc893568a1a302640c59000d12c801072195327 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Wed, 31 Jan 2024 23:34:40 +0100 Subject: [PATCH 2/9] beginning of pager decoder --- CMakeLists.txt | 8 +- decoder_modules/pager_decoder/CMakeLists.txt | 8 + decoder_modules/pager_decoder/src/decoder.h | 8 + decoder_modules/pager_decoder/src/main.cpp | 255 ++++++++++++++++++ .../pager_decoder/src/pocsag/decoder.h | 32 +++ .../pager_decoder/src/pocsag/dsp.h | 71 +++++ .../pager_decoder/src/pocsag/pocsag.cpp | 140 ++++++++++ .../pager_decoder/src/pocsag/pocsag.h | 48 ++++ readme.md | 3 +- 9 files changed, 571 insertions(+), 2 deletions(-) create mode 100644 decoder_modules/pager_decoder/CMakeLists.txt create mode 100644 decoder_modules/pager_decoder/src/decoder.h create mode 100644 decoder_modules/pager_decoder/src/main.cpp create mode 100644 decoder_modules/pager_decoder/src/pocsag/decoder.h create mode 100644 decoder_modules/pager_decoder/src/pocsag/dsp.h create mode 100644 decoder_modules/pager_decoder/src/pocsag/pocsag.cpp create mode 100644 decoder_modules/pager_decoder/src/pocsag/pocsag.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a9c67193..45234670 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ option(OPT_BUILD_FALCON9_DECODER "Build the falcon9 live decoder (Dependencies: option(OPT_BUILD_KG_SSTV_DECODER "Build the KG SSTV (KG-STV) decoder module (no dependencies required)" OFF) option(OPT_BUILD_M17_DECODER "Build the M17 decoder module (Dependencies: codec2)" OFF) option(OPT_BUILD_METEOR_DEMODULATOR "Build the meteor demodulator module (no dependencies required)" ON) +option(OPT_BUILD_PAGER_DECODER "Build the pager decoder module (no dependencies required)" OFF) option(OPT_BUILD_RADIO "Main audio modulation decoder (AM, FM, SSB, etc...)" ON) option(OPT_BUILD_WEATHER_SAT_DECODER "Build the HRPT decoder module (no dependencies required)" OFF) @@ -234,6 +235,10 @@ if (OPT_BUILD_METEOR_DEMODULATOR) add_subdirectory("decoder_modules/meteor_demodulator") endif (OPT_BUILD_METEOR_DEMODULATOR) +if (OPT_BUILD_PAGER_DECODER) +add_subdirectory("decoder_modules/pager_decoder") +endif (OPT_BUILD_PAGER_DECODER) + if (OPT_BUILD_RADIO) add_subdirectory("decoder_modules/radio") endif (OPT_BUILD_RADIO) @@ -242,6 +247,7 @@ if (OPT_BUILD_WEATHER_SAT_DECODER) add_subdirectory("decoder_modules/weather_sat_decoder") endif (OPT_BUILD_WEATHER_SAT_DECODER) +add_subdirectory("decoder_modules/pager_decoder") # Misc if (OPT_BUILD_DISCORD_PRESENCE) @@ -302,7 +308,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") add_custom_target(do_always ALL cp \"$/libsdrpp_core.dylib\" \"$\") endif () -# cmake .. "-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake" -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_M17_DECODER=ON -DOPT_BUILD_SCANNER=ON -DOPT_BUILD_SCHEDULER=ON -DOPT_BUILD_USRP_SOURCE=ON +# cmake .. "-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake" -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_M17_DECODER=ON -DOPT_BUILD_SCANNER=ON -DOPT_BUILD_SCHEDULER=ON -DOPT_BUILD_USRP_SOURCE=ON -DOPT_BUILD_PAGER_DECODER=ON # Create module cmake file configure_file(${CMAKE_SOURCE_DIR}/sdrpp_module.cmake ${CMAKE_CURRENT_BINARY_DIR}/sdrpp_module.cmake @ONLY) diff --git a/decoder_modules/pager_decoder/CMakeLists.txt b/decoder_modules/pager_decoder/CMakeLists.txt new file mode 100644 index 00000000..4d834963 --- /dev/null +++ b/decoder_modules/pager_decoder/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.13) +project(pager_decoder) + +file(GLOB_RECURSE SRC "src/*.cpp" "src/*.c") + +include(${SDRPP_MODULE_CMAKE}) + +target_include_directories(pager_decoder PRIVATE "src/") \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/decoder.h b/decoder_modules/pager_decoder/src/decoder.h new file mode 100644 index 00000000..dd2328e8 --- /dev/null +++ b/decoder_modules/pager_decoder/src/decoder.h @@ -0,0 +1,8 @@ +#pragma once + +class Decoder { +public: + + virtual void showMenu(); + +}; \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/main.cpp b/decoder_modules/pager_decoder/src/main.cpp new file mode 100644 index 00000000..482dca43 --- /dev/null +++ b/decoder_modules/pager_decoder/src/main.cpp @@ -0,0 +1,255 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pocsag/pocsag.h" + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +SDRPP_MOD_INFO{ + /* Name: */ "pager_decoder", + /* Description: */ "POCSAG and Flex Pager Decoder" + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ -1 +}; + +const char* msgTypes[] = { + "Numeric", + "Unknown (0b01)", + "Unknown (0b10)", + "Alphanumeric", +}; + +ConfigManager config; + +#define INPUT_SAMPLE_RATE 24000.0 +#define INPUT_BANDWIDTH 12500.0 +#define INPUT_BAUD_RATE 2400.0 + +enum Protocol { + PROTOCOL_POCSAG, + PROTOCOL_FLEX +}; + +class PagerDecoderModule : public ModuleManager::Instance { +public: + PagerDecoderModule(std::string name) : diag(0.6, 2400) { + this->name = name; + + // Define protocols + protocols.define("POCSAG", PROTOCOL_POCSAG); + protocols.define("FLEX", PROTOCOL_FLEX); + + // Load config + config.acquire(); + if (!config.conf.contains(name)) { + config.conf[name]["showLines"] = false; + } + showLines = config.conf[name]["showLines"]; + if (showLines) { + diag.lines.push_back(-1.0); + diag.lines.push_back(-1.0/3.0); + diag.lines.push_back(1.0/3.0); + diag.lines.push_back(1.0); + } + config.release(true); + + // Initialize VFO + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, INPUT_BANDWIDTH, INPUT_SAMPLE_RATE, INPUT_BANDWIDTH, INPUT_BANDWIDTH, true); + vfo->setSnapInterval(1); + + // Initialize DSP here (negative dev to invert signal) + demod.init(vfo->output, -4500.0, INPUT_SAMPLE_RATE); + dcBlock.init(&demod.out, 0.001); + float taps[] = { 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f }; + shape = dsp::taps::fromArray(10, taps); + fir.init(&dcBlock.out, shape); + recov.init(&fir.out, INPUT_SAMPLE_RATE/INPUT_BAUD_RATE, 1e5, 0.1, 0.05); + doubler.init(&recov.out); + slicer.init(&doubler.outB); + dataHandler.init(&slicer.out, _dataHandler, this); + reshape.init(&doubler.outA, 2400.0, (INPUT_BAUD_RATE / 30.0) - 2400.0); + diagHandler.init(&reshape.out, _diagHandler, this); + + // Initialize decode + decoder.onMessage.bind(&PagerDecoderModule::messageHandler, this); + + // Start DSP Here + demod.start(); + dcBlock.start(); + fir.start(); + recov.start(); + doubler.start(); + slicer.start(); + dataHandler.start(); + reshape.start(); + diagHandler.start(); + + gui::menu.registerEntry(name, menuHandler, this, this); + } + + ~PagerDecoderModule() { + gui::menu.removeEntry(name); + // Stop DSP + if (enabled) { + demod.stop(); + dcBlock.stop(); + fir.stop(); + recov.stop(); + doubler.stop(); + slicer.stop(); + dataHandler.stop(); + reshape.stop(); + diagHandler.stop(); + sigpath::vfoManager.deleteVFO(vfo); + } + + sigpath::sinkManager.unregisterStream(name); + } + + void postInit() {} + + void enable() { + double bw = gui::waterfall.getBandwidth(); + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, std::clamp(0, -bw / 2.0, bw / 2.0), INPUT_BANDWIDTH, INPUT_SAMPLE_RATE, INPUT_BANDWIDTH, INPUT_BANDWIDTH, true); + vfo->setSnapInterval(250); + + // Start DSP + demod.start(); + dcBlock.start(); + fir.start(); + recov.start(); + doubler.start(); + slicer.start(); + dataHandler.start(); + reshape.start(); + diagHandler.start(); + + enabled = true; + } + + void disable() { + demod.stop(); + dcBlock.stop(); + fir.stop(); + recov.stop(); + doubler.stop(); + slicer.stop(); + dataHandler.stop(); + reshape.stop(); + diagHandler.stop(); + reshape.stop(); + diagHandler.stop(); + + sigpath::vfoManager.deleteVFO(vfo); + enabled = false; + } + + bool isEnabled() { + return enabled; + } + +private: + static void menuHandler(void* ctx) { + PagerDecoderModule* _this = (PagerDecoderModule*)ctx; + + float menuWidth = ImGui::GetContentRegionAvail().x; + + if (!_this->enabled) { style::beginDisabled(); } + + ImGui::LeftLabel("Protocol"); + ImGui::FillWidth(); + if (ImGui::Combo(("##pager_decoder_proto_" + _this->name).c_str(), &_this->protoId, _this->protocols.txt)) { + // TODO + } + + ImGui::SetNextItemWidth(menuWidth); + _this->diag.draw(); + + if (!_this->enabled) { style::endDisabled(); } + } + + static void _dataHandler(uint8_t* data, int count, void* ctx) { + PagerDecoderModule* _this = (PagerDecoderModule*)ctx; + _this->decoder.process(data, count); + } + + static void _diagHandler(float* data, int count, void* ctx) { + PagerDecoderModule* _this = (PagerDecoderModule*)ctx; + float* buf = _this->diag.acquireBuffer(); + memcpy(buf, data, count * sizeof(float)); + _this->diag.releaseBuffer(); + } + + void messageHandler(pocsag::Address addr, pocsag::MessageType type, const std::string& msg) { + flog::debug("[{}]: '{}'", (uint32_t)addr, msg); + } + + std::string name; + bool enabled = true; + + int protoId = 0; + + OptionList protocols; + + pocsag::Decoder decoder; + + // DSP Chain + VFOManager::VFO* vfo; + dsp::demod::Quadrature demod; + dsp::correction::DCBlocker dcBlock; + dsp::tap shape; + dsp::filter::FIR fir; + dsp::clock_recovery::MM recov; + dsp::routing::Doubler doubler; + dsp::digital::BinarySlicer slicer; + dsp::buffer::Reshaper reshape; + dsp::sink::Handler dataHandler; + dsp::sink::Handler diagHandler; + + ImGui::SymbolDiagram diag; + + bool showLines = false; +}; + +MOD_EXPORT void _INIT_() { + // Create default recording directory + json def = json({}); + config.setPath(core::args["root"].s() + "/pager_decoder_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new PagerDecoderModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (PagerDecoderModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} diff --git a/decoder_modules/pager_decoder/src/pocsag/decoder.h b/decoder_modules/pager_decoder/src/pocsag/decoder.h new file mode 100644 index 00000000..93a03844 --- /dev/null +++ b/decoder_modules/pager_decoder/src/pocsag/decoder.h @@ -0,0 +1,32 @@ +#pragma once +#include "../decoder.h" +#include +#include +#include + +class POCSAGDecoder : public Decoder { +public: + POCSAGDecoder() : diag(0.6, 2400) { + // Define baudrate options + baudrates.define(512, "512 Baud", 512); + baudrates.define(1200, "1200 Baud", 1200); + baudrates.define(2400, "2400 Baud", 2400); + } + + void showMenu() { + ImGui::LeftLabel("Baudrate"); + ImGui::FillWidth(); + if (ImGui::Combo(("##pager_decoder_proto_" + name).c_str(), &brId, baudrates.txt)) { + // TODO + } + } + +private: + std::string name; + + ImGui::SymbolDiagram diag; + + int brId = 2; + + OptionList baudrates; +}; \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/pocsag/dsp.h b/decoder_modules/pager_decoder/src/pocsag/dsp.h new file mode 100644 index 00000000..0dd241aa --- /dev/null +++ b/decoder_modules/pager_decoder/src/pocsag/dsp.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class POCSAGDSP : dsp::Processor { + using base_type = dsp::Processor; +public: + POCSAGDSP() {} + POCSAGDSP(dsp::stream* in, double samplerate, double baudrate) { init(in, samplerate, baudrate); } + + void init(dsp::stream* in, double samplerate, double baudrate) { + // Save settings + // TODO + + // Configure blocks + demod.init(NULL, -4500.0, samplerate); + dcBlock.init(NULL, 0.001); + float taps[] = { 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f }; + shape = dsp::taps::fromArray(10, taps); + fir.init(NULL, shape); + recov.init(NULL, samplerate/baudrate, 1e5, 0.1, 0.05); + + // Free useless buffers + dcBlock.out.free(); + fir.out.free(); + recov.out.free(); + + // Init base + base_type::init(in); + } + + int process(int count, dsp::complex_t* in, float* softOut, uint8_t* out) { + count = demod.process(count, in, demod.out.readBuf); + count = dcBlock.process(count, demod.out.readBuf, demod.out.readBuf); + count = fir.process(count, demod.out.readBuf, demod.out.readBuf); + count = recov.process(count, demod.out.readBuf, softOut); + dsp::digital::BinarySlicer::process(count, softOut, out); + return count; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + + count = process(count, base_type::_in->readBuf, soft.writeBuf, base_type::out.writeBuf); + + base_type::_in->flush(); + if (!base_type::out.swap(count)) { return -1; } + if (!soft.swap(count)) { return -1; } + return count; + } + + dsp::stream soft; + +private: + dsp::demod::Quadrature demod; + dsp::correction::DCBlocker dcBlock; + dsp::tap shape; + dsp::filter::FIR fir; + dsp::clock_recovery::MM recov; + +}; \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/pocsag/pocsag.cpp b/decoder_modules/pager_decoder/src/pocsag/pocsag.cpp new file mode 100644 index 00000000..572f69e7 --- /dev/null +++ b/decoder_modules/pager_decoder/src/pocsag/pocsag.cpp @@ -0,0 +1,140 @@ +#include "pocsag.h" +#include +#include + +#define POCSAG_FRAME_SYNC_CODEWORD ((uint32_t)(0b01111100110100100001010111011000)) +#define POCSAG_IDLE_CODEWORD_DATA ((uint32_t)(0b011110101100100111000)) +#define POCSAG_BATCH_BIT_COUNT (POCSAG_BATCH_CODEWORD_COUNT*32) + +#define POCSAG_GEN_POLY ((uint32_t)(0b11101101001)) + +namespace pocsag { + const char NUMERIC_CHARSET[] = { + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '*', + 'U', + ' ', + '-', + ']', + '[' + }; + + void Decoder::process(uint8_t* symbols, int count) { + for (int i = 0; i < count; i++) { + // Get symbol + uint32_t s = symbols[i]; + + // If not sync, try to acquire sync (TODO: sync confidence) + if (!synced) { + // Append new symbol to sync shift register + syncSR = (syncSR << 1) | s; + + // Test for sync + synced = (distance(syncSR, POCSAG_FRAME_SYNC_CODEWORD) <= POCSAG_SYNC_DIST); + + // Go to next symbol + continue; + } + + // TODO: Flush message on desync + + // Append bit to batch + batch[batchOffset >> 5] |= (s << (31 - (batchOffset & 0b11111))); + batchOffset++; + + // On end of batch, decode and reset + if (batchOffset >= POCSAG_BATCH_BIT_COUNT) { + decodeBatch(); + batchOffset = 0; + synced = false; + memset(batch, 0, sizeof(batch)); + } + } + } + + int Decoder::distance(uint32_t a, uint32_t b) { + uint32_t diff = a ^ b; + int dist = 0; + for (int i = 0; i < 32; i++) { + dist += (diff >> i ) & 1; + } + return dist; + } + + bool Decoder::correctCodeword(Codeword in, Codeword& out) { + + + return true; // TODO + } + + void Decoder::flushMessage() { + if (!msg.empty()) { + onMessage(addr, msgType, msg); + msg.clear(); + } + } + + void Decoder::decodeBatch() { + for (int i = 0; i < POCSAG_BATCH_CODEWORD_COUNT; i++) { + // Get codeword + Codeword cw = batch[i]; + + // Correct errors. If corrupted, skip + if (!correctCodeword(cw, cw)) { continue; } + // TODO: End message if two consecutive are corrupt + + // Get codeword type + CodewordType type = (CodewordType)((cw >> 31) & 1); + if (type == CODEWORD_TYPE_ADDRESS && (cw >> 11) == POCSAG_IDLE_CODEWORD_DATA) { + type = CODEWORD_TYPE_IDLE; + } + + // Decode codeword + if (type == CODEWORD_TYPE_IDLE) { + // If a non-empty message is available, send it out and clear + flushMessage(); + flog::debug("[{}:{}]: IDLE", (i >> 1), i&1); + } + else if (type == CODEWORD_TYPE_ADDRESS) { + // If a non-empty message is available, send it out and clear + flushMessage(); + + // Decode message type + msgType = (MessageType)((cw >> 11) & 0b11); + + // Decode address and append lower 8 bits from position + addr = ((cw >> 13) & 0b111111111111111111) << 3; + addr |= (i >> 1); + } + else if (type == CODEWORD_TYPE_MESSAGE) { + // Extract the 20 data bits + uint32_t data = (cw >> 11) & 0b11111111111111111111; + + // Decode data depending on message type + if (msgType == MESSAGE_TYPE_NUMERIC) { + // Numeric messages pack 5 characters per message codeword + msg += NUMERIC_CHARSET[(data >> 16) & 0b1111]; + msg += NUMERIC_CHARSET[(data >> 12) & 0b1111]; + msg += NUMERIC_CHARSET[(data >> 8) & 0b1111]; + msg += NUMERIC_CHARSET[(data >> 4) & 0b1111]; + msg += NUMERIC_CHARSET[data & 0b1111]; + } + else if (msgType == MESSAGE_TYPE_ALPHANUMERIC) { + + } + + // Save last data + lastMsgData = data; + } + } + } +} \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/pocsag/pocsag.h b/decoder_modules/pager_decoder/src/pocsag/pocsag.h new file mode 100644 index 00000000..b0e78cfa --- /dev/null +++ b/decoder_modules/pager_decoder/src/pocsag/pocsag.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include +#include + +#define POCSAG_SYNC_DIST 4 +#define POCSAG_BATCH_CODEWORD_COUNT 16 + +namespace pocsag { + enum CodewordType { + CODEWORD_TYPE_IDLE = -1, + CODEWORD_TYPE_ADDRESS = 0, + CODEWORD_TYPE_MESSAGE = 1 + }; + + enum MessageType { + MESSAGE_TYPE_NUMERIC = 0b00, + MESSAGE_TYPE_ALPHANUMERIC = 0b11 + }; + + using Codeword = uint32_t; + using Address = uint32_t; + + class Decoder { + public: + void process(uint8_t* symbols, int count); + + NewEvent onMessage; + + private: + static int distance(uint32_t a, uint32_t b); + bool correctCodeword(Codeword in, Codeword& out); + void flushMessage(); + void decodeBatch(); + + uint32_t syncSR = 0; + bool synced = false; + int batchOffset = 0; + + Codeword batch[POCSAG_BATCH_CODEWORD_COUNT]; + + Address addr; + MessageType msgType; + std::string msg; + + uint32_t lastMsgData; + }; +} \ No newline at end of file diff --git a/readme.md b/readme.md index 833512c7..4fb3f293 100644 --- a/readme.md +++ b/readme.md @@ -334,7 +334,7 @@ Modules in beta are still included in releases for the most part but not enabled | hackrf_source | Working | libhackrf | OPT_BUILD_HACKRF_SOURCE | ✅ | ✅ | ✅ | | hermes_source | Beta | - | OPT_BUILD_HERMES_SOURCE | ✅ | ✅ | ✅ | | limesdr_source | Working | liblimesuite | OPT_BUILD_LIMESDR_SOURCE | ⛔ | ✅ | ✅ | -| perseus_source | Beta | libperseus-sdr | OPT_BUILD_PERSEUS_SOURCE | ⛔ | ⛔ | ⛔ | +| perseus_source | Beta | libperseus-sdr | OPT_BUILD_PERSEUS_SOURCE | ⛔ | ✅ | ✅ | | plutosdr_source | Working | libiio, libad9361 | OPT_BUILD_PLUTOSDR_SOURCE | ✅ | ✅ | ✅ | | rfspace_source | Working | - | OPT_BUILD_RFSPACE_SOURCE | ✅ | ✅ | ✅ | | rtl_sdr_source | Working | librtlsdr | OPT_BUILD_RTL_SDR_SOURCE | ✅ | ✅ | ✅ | @@ -367,6 +367,7 @@ Modules in beta are still included in releases for the most part but not enabled | kgsstv_decoder | Unfinished | - | OPT_BUILD_KGSSTV_DECODER | ⛔ | ⛔ | ⛔ | | m17_decoder | Beta | - | OPT_BUILD_M17_DECODER | ⛔ | ✅ | ⛔ | | meteor_demodulator | Working | - | OPT_BUILD_METEOR_DEMODULATOR | ✅ | ✅ | ⛔ | +| pager_decoder | Unfinished | - | OPT_BUILD_PAGER_DECODER | ⛔ | ⛔ | ⛔ | | radio | Working | - | OPT_BUILD_RADIO | ✅ | ✅ | ✅ | | weather_sat_decoder | Unfinished | - | OPT_BUILD_WEATHER_SAT_DECODER | ⛔ | ⛔ | ⛔ | From ef42ea01d8e2b85a06ff335506577629e7b3ffc2 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 00:55:17 +0100 Subject: [PATCH 3/9] add flex decoder menu entry and fix pocsag decoding --- decoder_modules/pager_decoder/src/decoder.h | 9 +- decoder_modules/pager_decoder/src/main.cpp | 189 +++++------------- .../pager_decoder/src/pocsag/decoder.h | 83 +++++++- .../pager_decoder/src/pocsag/dsp.h | 2 +- 4 files changed, 141 insertions(+), 142 deletions(-) diff --git a/decoder_modules/pager_decoder/src/decoder.h b/decoder_modules/pager_decoder/src/decoder.h index dd2328e8..b1436431 100644 --- a/decoder_modules/pager_decoder/src/decoder.h +++ b/decoder_modules/pager_decoder/src/decoder.h @@ -1,8 +1,11 @@ #pragma once +#include class Decoder { public: - - virtual void showMenu(); - + virtual ~Decoder() {} + virtual void showMenu() {}; + virtual void setVFO(VFOManager::VFO* vfo) = 0; + virtual void start() = 0; + virtual void stop() = 0; }; \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/main.cpp b/decoder_modules/pager_decoder/src/main.cpp index 482dca43..aacd76cb 100644 --- a/decoder_modules/pager_decoder/src/main.cpp +++ b/decoder_modules/pager_decoder/src/main.cpp @@ -5,24 +5,11 @@ #include #include #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include "pocsag/pocsag.h" +#include "decoder.h" +#include "pocsag/decoder.h" +#include "flex/decoder.h" #define CONCAT(a, b) ((std::string(a) + b).c_str()) @@ -34,77 +21,29 @@ SDRPP_MOD_INFO{ /* Max instances */ -1 }; -const char* msgTypes[] = { - "Numeric", - "Unknown (0b01)", - "Unknown (0b10)", - "Alphanumeric", -}; - ConfigManager config; -#define INPUT_SAMPLE_RATE 24000.0 -#define INPUT_BANDWIDTH 12500.0 -#define INPUT_BAUD_RATE 2400.0 - enum Protocol { + PROTOCOL_INVALID = -1, PROTOCOL_POCSAG, PROTOCOL_FLEX }; class PagerDecoderModule : public ModuleManager::Instance { public: - PagerDecoderModule(std::string name) : diag(0.6, 2400) { + PagerDecoderModule(std::string name) { this->name = name; // Define protocols protocols.define("POCSAG", PROTOCOL_POCSAG); protocols.define("FLEX", PROTOCOL_FLEX); - // Load config - config.acquire(); - if (!config.conf.contains(name)) { - config.conf[name]["showLines"] = false; - } - showLines = config.conf[name]["showLines"]; - if (showLines) { - diag.lines.push_back(-1.0); - diag.lines.push_back(-1.0/3.0); - diag.lines.push_back(1.0/3.0); - diag.lines.push_back(1.0); - } - config.release(true); - - // Initialize VFO - vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, INPUT_BANDWIDTH, INPUT_SAMPLE_RATE, INPUT_BANDWIDTH, INPUT_BANDWIDTH, true); + // Initialize VFO with default values + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 12500, 24000, 12500, 12500, true); vfo->setSnapInterval(1); - // Initialize DSP here (negative dev to invert signal) - demod.init(vfo->output, -4500.0, INPUT_SAMPLE_RATE); - dcBlock.init(&demod.out, 0.001); - float taps[] = { 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f }; - shape = dsp::taps::fromArray(10, taps); - fir.init(&dcBlock.out, shape); - recov.init(&fir.out, INPUT_SAMPLE_RATE/INPUT_BAUD_RATE, 1e5, 0.1, 0.05); - doubler.init(&recov.out); - slicer.init(&doubler.outB); - dataHandler.init(&slicer.out, _dataHandler, this); - reshape.init(&doubler.outA, 2400.0, (INPUT_BAUD_RATE / 30.0) - 2400.0); - diagHandler.init(&reshape.out, _diagHandler, this); - - // Initialize decode - decoder.onMessage.bind(&PagerDecoderModule::messageHandler, this); - - // Start DSP Here - demod.start(); - dcBlock.start(); - fir.start(); - recov.start(); - doubler.start(); - slicer.start(); - dataHandler.start(); - reshape.start(); - diagHandler.start(); + // Select the protocol + selectProtocol(PROTOCOL_POCSAG); gui::menu.registerEntry(name, menuHandler, this, this); } @@ -113,15 +52,8 @@ public: gui::menu.removeEntry(name); // Stop DSP if (enabled) { - demod.stop(); - dcBlock.stop(); - fir.stop(); - recov.stop(); - doubler.stop(); - slicer.stop(); - dataHandler.stop(); - reshape.stop(); - diagHandler.stop(); + decoder->stop(); + decoder.reset(); sigpath::vfoManager.deleteVFO(vfo); } @@ -132,36 +64,17 @@ public: void enable() { double bw = gui::waterfall.getBandwidth(); - vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, std::clamp(0, -bw / 2.0, bw / 2.0), INPUT_BANDWIDTH, INPUT_SAMPLE_RATE, INPUT_BANDWIDTH, INPUT_BANDWIDTH, true); - vfo->setSnapInterval(250); + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, std::clamp(0, -bw / 2.0, bw / 2.0), 12500, 24000, 12500, 12500, true); + vfo->setSnapInterval(1); - // Start DSP - demod.start(); - dcBlock.start(); - fir.start(); - recov.start(); - doubler.start(); - slicer.start(); - dataHandler.start(); - reshape.start(); - diagHandler.start(); + decoder->setVFO(vfo); + decoder->start(); enabled = true; } void disable() { - demod.stop(); - dcBlock.stop(); - fir.stop(); - recov.stop(); - doubler.stop(); - slicer.stop(); - dataHandler.stop(); - reshape.stop(); - diagHandler.stop(); - reshape.stop(); - diagHandler.stop(); - + decoder->stop(); sigpath::vfoManager.deleteVFO(vfo); enabled = false; } @@ -170,6 +83,36 @@ public: return enabled; } + void selectProtocol(Protocol newProto) { + // Cannot change while disabled + if (!enabled) { return; } + + // If the protocol hasn't changed, no need to do anything + if (newProto == proto) { return; } + + // Delete current decoder + decoder.reset(); + + // Create a new decoder + switch (newProto) { + case PROTOCOL_POCSAG: + decoder = std::make_unique(name, vfo); + break; + case PROTOCOL_FLEX: + decoder = std::make_unique(name, vfo); + break; + default: + flog::error("Tried to select unknown pager protocol"); + return; + } + + // Start the new decoder + decoder->start(); + + // Save selected protocol + proto = newProto; + } + private: static void menuHandler(void* ctx) { PagerDecoderModule* _this = (PagerDecoderModule*)ctx; @@ -181,54 +124,28 @@ private: ImGui::LeftLabel("Protocol"); ImGui::FillWidth(); if (ImGui::Combo(("##pager_decoder_proto_" + _this->name).c_str(), &_this->protoId, _this->protocols.txt)) { - // TODO + _this->selectProtocol(_this->protocols.value(_this->protoId)); } - ImGui::SetNextItemWidth(menuWidth); - _this->diag.draw(); + if (_this->decoder) { _this->decoder->showMenu(); } + + ImGui::Button(("Record##pager_decoder_show_" + _this->name).c_str(), ImVec2(menuWidth, 0)); + ImGui::Button(("Show Messages##pager_decoder_show_" + _this->name).c_str(), ImVec2(menuWidth, 0)); if (!_this->enabled) { style::endDisabled(); } } - static void _dataHandler(uint8_t* data, int count, void* ctx) { - PagerDecoderModule* _this = (PagerDecoderModule*)ctx; - _this->decoder.process(data, count); - } - - static void _diagHandler(float* data, int count, void* ctx) { - PagerDecoderModule* _this = (PagerDecoderModule*)ctx; - float* buf = _this->diag.acquireBuffer(); - memcpy(buf, data, count * sizeof(float)); - _this->diag.releaseBuffer(); - } - - void messageHandler(pocsag::Address addr, pocsag::MessageType type, const std::string& msg) { - flog::debug("[{}]: '{}'", (uint32_t)addr, msg); - } - std::string name; bool enabled = true; + Protocol proto = PROTOCOL_INVALID; int protoId = 0; OptionList protocols; - pocsag::Decoder decoder; - // DSP Chain VFOManager::VFO* vfo; - dsp::demod::Quadrature demod; - dsp::correction::DCBlocker dcBlock; - dsp::tap shape; - dsp::filter::FIR fir; - dsp::clock_recovery::MM recov; - dsp::routing::Doubler doubler; - dsp::digital::BinarySlicer slicer; - dsp::buffer::Reshaper reshape; - dsp::sink::Handler dataHandler; - dsp::sink::Handler diagHandler; - - ImGui::SymbolDiagram diag; + std::unique_ptr decoder; bool showLines = false; }; diff --git a/decoder_modules/pager_decoder/src/pocsag/decoder.h b/decoder_modules/pager_decoder/src/pocsag/decoder.h index 93a03844..54923755 100644 --- a/decoder_modules/pager_decoder/src/pocsag/decoder.h +++ b/decoder_modules/pager_decoder/src/pocsag/decoder.h @@ -1,28 +1,107 @@ #pragma once #include "../decoder.h" +#include #include #include #include +#include +#include "dsp.h" +#include "pocsag.h" + +const char* msgTypes[] = { + "Numeric", + "Unknown (0b01)", + "Unknown (0b10)", + "Alphanumeric", +}; class POCSAGDecoder : public Decoder { public: - POCSAGDecoder() : diag(0.6, 2400) { + POCSAGDecoder(const std::string& name, VFOManager::VFO* vfo) : diag(0.6, 2400) { + this->name = name; + this->vfo = vfo; + // Define baudrate options baudrates.define(512, "512 Baud", 512); baudrates.define(1200, "1200 Baud", 1200); baudrates.define(2400, "2400 Baud", 2400); + + // Init DSP + vfo->setBandwidthLimits(12500, 12500, true); + vfo->setSampleRate(24000, 12500); + dsp.init(vfo->output, 24000, 2400); + reshape.init(&dsp.soft, 2400.0, (2400 / 30.0) - 2400.0); + dataHandler.init(&dsp.out, _dataHandler, this); + diagHandler.init(&reshape.out, _diagHandler, this); + + // Init decoder + decoder.onMessage.bind(&POCSAGDecoder::messageHandler, this); + } + + ~POCSAGDecoder() { + stop(); } void showMenu() { ImGui::LeftLabel("Baudrate"); ImGui::FillWidth(); - if (ImGui::Combo(("##pager_decoder_proto_" + name).c_str(), &brId, baudrates.txt)) { + if (ImGui::Combo(("##pager_decoder_pocsag_br_" + name).c_str(), &brId, baudrates.txt)) { // TODO } + + ImGui::FillWidth(); + diag.draw(); + } + + void setVFO(VFOManager::VFO* vfo) { + this->vfo = vfo; + vfo->setBandwidthLimits(12500, 12500, true); + vfo->setSampleRate(24000, 12500); + dsp.setInput(vfo->output); + } + + void start() { + flog::debug("POCSAG start"); + dsp.start(); + reshape.start(); + dataHandler.start(); + diagHandler.start(); + } + + void stop() { + flog::debug("POCSAG stop"); + dsp.stop(); + reshape.stop(); + dataHandler.stop(); + diagHandler.stop(); } private: + static void _dataHandler(uint8_t* data, int count, void* ctx) { + POCSAGDecoder* _this = (POCSAGDecoder*)ctx; + _this->decoder.process(data, count); + } + + static void _diagHandler(float* data, int count, void* ctx) { + POCSAGDecoder* _this = (POCSAGDecoder*)ctx; + float* buf = _this->diag.acquireBuffer(); + memcpy(buf, data, count * sizeof(float)); + _this->diag.releaseBuffer(); + } + + void messageHandler(pocsag::Address addr, pocsag::MessageType type, const std::string& msg) { + flog::debug("[{}]: '{}'", (uint32_t)addr, msg); + } + std::string name; + VFOManager::VFO* vfo; + + POCSAGDSP dsp; + dsp::buffer::Reshaper reshape; + dsp::sink::Handler dataHandler; + dsp::sink::Handler diagHandler; + + pocsag::Decoder decoder; ImGui::SymbolDiagram diag; diff --git a/decoder_modules/pager_decoder/src/pocsag/dsp.h b/decoder_modules/pager_decoder/src/pocsag/dsp.h index 0dd241aa..3faca285 100644 --- a/decoder_modules/pager_decoder/src/pocsag/dsp.h +++ b/decoder_modules/pager_decoder/src/pocsag/dsp.h @@ -11,7 +11,7 @@ #include #include -class POCSAGDSP : dsp::Processor { +class POCSAGDSP : public dsp::Processor { using base_type = dsp::Processor; public: POCSAGDSP() {} From f1f04d59fe2f710dbdafa1ceb2a2c3edd5e0ea85 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 00:55:36 +0100 Subject: [PATCH 4/9] add missing files --- .../pager_decoder/src/flex/decoder.h | 96 +++++++++++++++++++ .../pager_decoder/src/flex/flex.cpp | 5 + decoder_modules/pager_decoder/src/flex/flex.h | 11 +++ 3 files changed, 112 insertions(+) create mode 100644 decoder_modules/pager_decoder/src/flex/decoder.h create mode 100644 decoder_modules/pager_decoder/src/flex/flex.cpp create mode 100644 decoder_modules/pager_decoder/src/flex/flex.h diff --git a/decoder_modules/pager_decoder/src/flex/decoder.h b/decoder_modules/pager_decoder/src/flex/decoder.h new file mode 100644 index 00000000..6f54e264 --- /dev/null +++ b/decoder_modules/pager_decoder/src/flex/decoder.h @@ -0,0 +1,96 @@ +#pragma once +#include "../decoder.h" +#include +#include +#include +#include +#include +#include "flex.h" + +class FLEXDecoder : public Decoder { + dsp::stream dummy1; + dsp::stream dummy2; +public: + FLEXDecoder(const std::string& name, VFOManager::VFO* vfo) : diag(0.6, 1600) { + this->name = name; + this->vfo = vfo; + + // Define baudrate options + baudrates.define(1600, "1600 Baud", 1600); + baudrates.define(3200, "3200 Baud", 3200); + baudrates.define(6400, "6400 Baud", 6400); + + // Init DSP + vfo->setBandwidthLimits(12500, 12500, true); + vfo->setSampleRate(16000, 12500); + reshape.init(&dummy1, 1600.0, (1600 / 30.0) - 1600.0); + dataHandler.init(&dummy2, _dataHandler, this); + diagHandler.init(&reshape.out, _diagHandler, this); + } + + ~FLEXDecoder() { + stop(); + } + + void showMenu() { + ImGui::LeftLabel("Baudrate"); + ImGui::FillWidth(); + if (ImGui::Combo(("##pager_decoder_flex_br_" + name).c_str(), &brId, baudrates.txt)) { + // TODO + } + + ImGui::FillWidth(); + diag.draw(); + } + + void setVFO(VFOManager::VFO* vfo) { + this->vfo = vfo; + vfo->setBandwidthLimits(12500, 12500, true); + vfo->setSampleRate(24000, 12500); + // dsp.setInput(vfo->output); + } + + void start() { + flog::debug("FLEX start"); + // dsp.start(); + reshape.start(); + dataHandler.start(); + diagHandler.start(); + } + + void stop() { + flog::debug("FLEX stop"); + // dsp.stop(); + reshape.stop(); + dataHandler.stop(); + diagHandler.stop(); + } + +private: + static void _dataHandler(uint8_t* data, int count, void* ctx) { + FLEXDecoder* _this = (FLEXDecoder*)ctx; + // _this->decoder.process(data, count); + } + + static void _diagHandler(float* data, int count, void* ctx) { + FLEXDecoder* _this = (FLEXDecoder*)ctx; + float* buf = _this->diag.acquireBuffer(); + memcpy(buf, data, count * sizeof(float)); + _this->diag.releaseBuffer(); + } + + std::string name; + + VFOManager::VFO* vfo; + dsp::buffer::Reshaper reshape; + dsp::sink::Handler dataHandler; + dsp::sink::Handler diagHandler; + + flex::Decoder decoder; + + ImGui::SymbolDiagram diag; + + int brId = 0; + + OptionList baudrates; +}; \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/flex/flex.cpp b/decoder_modules/pager_decoder/src/flex/flex.cpp new file mode 100644 index 00000000..d21f723e --- /dev/null +++ b/decoder_modules/pager_decoder/src/flex/flex.cpp @@ -0,0 +1,5 @@ +#include "flex.h" + +namespace flex { + // TODO +} \ No newline at end of file diff --git a/decoder_modules/pager_decoder/src/flex/flex.h b/decoder_modules/pager_decoder/src/flex/flex.h new file mode 100644 index 00000000..2c37f171 --- /dev/null +++ b/decoder_modules/pager_decoder/src/flex/flex.h @@ -0,0 +1,11 @@ +#pragma once + +namespace flex { + class Decoder { + public: + // TODO + + private: + // TODO + }; +} \ No newline at end of file From f486c657c15b99034ed04829fd19550896008fd0 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 01:12:51 +0100 Subject: [PATCH 5/9] fix cmake to prevent always enabling the pager decoder --- CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 45234670..3de4e252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,7 +247,6 @@ if (OPT_BUILD_WEATHER_SAT_DECODER) add_subdirectory("decoder_modules/weather_sat_decoder") endif (OPT_BUILD_WEATHER_SAT_DECODER) -add_subdirectory("decoder_modules/pager_decoder") # Misc if (OPT_BUILD_DISCORD_PRESENCE) From 1f2b50c9bbfbc6ff6b7be6dac323cd49d2360dab Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 18:36:25 +0100 Subject: [PATCH 6/9] add beginning of IQ exporter module --- CMakeLists.txt | 5 + make_windows_package.ps1 | 2 + misc_modules/iq_exporter/CMakeLists.txt | 6 + misc_modules/iq_exporter/src/main.cpp | 298 ++++++++++++++++++++++++ readme.md | 1 + 5 files changed, 312 insertions(+) create mode 100644 misc_modules/iq_exporter/CMakeLists.txt create mode 100644 misc_modules/iq_exporter/src/main.cpp 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 | ✅ | ✅ | ✅ | From fbeb2195da43c97382e6c239046c959cfdc0deb2 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 18:54:18 +0100 Subject: [PATCH 7/9] fix make_windows_package.ps1 issue --- make_windows_package.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/make_windows_package.ps1 b/make_windows_package.ps1 index 64b73466..91a75320 100644 --- a/make_windows_package.ps1 +++ b/make_windows_package.ps1 @@ -79,8 +79,6 @@ 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/ From 122e67ef65337209da2400494e61937a0ff8e815 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Thu, 1 Feb 2024 21:38:13 +0100 Subject: [PATCH 8/9] finished VFO mode of the iq exporter --- misc_modules/iq_exporter/src/main.cpp | 215 +++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 7 deletions(-) diff --git a/misc_modules/iq_exporter/src/main.cpp b/misc_modules/iq_exporter/src/main.cpp index 9b636116..e206e109 100644 --- a/misc_modules/iq_exporter/src/main.cpp +++ b/misc_modules/iq_exporter/src/main.cpp @@ -21,6 +21,7 @@ SDRPP_MOD_INFO{ ConfigManager config; enum Mode { + MODE_NONE = -1, MODE_BASEBAND, MODE_VFO }; @@ -46,6 +47,20 @@ public: modes.define("Baseband", MODE_BASEBAND); modes.define("VFO", MODE_VFO); + // Define VFO samplerates + for (int i = 3000; i <= 192000; i <<= 1) { + samplerates.define(i, getSrScaled(i), i); + } + for (int i = 250000; i < 1000000; i += 250000) { + samplerates.define(i, getSrScaled(i), i); + } + for (int i = 1000000; i < 10000000; i += 500000) { + samplerates.define(i, getSrScaled(i), i); + } + for (int i = 10000000; i <= 100000000; i += 5000000) { + samplerates.define(i, getSrScaled(i), i); + } + // Define protocols protocols.define("TCP", PROTOCOL_TCP); protocols.define("UDP", PROTOCOL_UDP); @@ -58,10 +73,15 @@ public: // Load config bool autoStart = false; + Mode nMode = MODE_BASEBAND; 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 (modes.keyExists(modeStr)) { nMode = modes.value(modes.keyId(modeStr)); } + } + if (config.conf[name].contains("samplerate")) { + int sr = config.conf[name]["samplerate"]; + if (samplerates.keyExists(sr)) { samplerate = samplerates.value(samplerates.keyId(sr)); } } if (config.conf[name].contains("protocol")) { std::string protoStr = config.conf[name]["protocol"]; @@ -85,7 +105,8 @@ public: config.release(); // Set menu IDs - modeId = modes.valueId(mode); + modeId = modes.valueId(nMode); + srId = samplerates.valueId(samplerate); protoId = protocols.valueId(proto); sampTypeId = sampleTypes.valueId(sampType); @@ -93,7 +114,13 @@ public: buffer = dsp::buffer::alloc(STREAM_BUFFER_SIZE * sizeof(dsp::complex_t)); // Init DSP - handler.init(NULL, dataHandler, this); + handler.init(&iqStream, dataHandler, this); + + // Set operating mode + setMode(nMode); + + // Start if needed + if (autoStart) { start(); } // Register menu entry gui::menu.registerEntry(name, menuHandler, this, this); @@ -103,6 +130,12 @@ public: // Un-register menu entry gui::menu.removeEntry(name); + // Stop networking + stop(); + + // Stop DSP + setMode(MODE_NONE); + // Free buffer dsp::buffer::free(buffer); } @@ -124,7 +157,26 @@ public: void start() { if (running) { return; } - // TODO + // Acquire lock on the socket + std::lock_guard lck1(sockMtx); + + // Start listening or open UDP socket + try { + if (proto == PROTOCOL_TCP) { + // Create listener + listener = net::listen(hostname, port); + + // Start listen worker + listenWorkerThread = std::thread(&IQExporterModule::listenWorker, this); + } + else { + // Open UDP socket + sock = net::openudp(hostname, port, "0.0.0.0", 0, true); + } + } + catch (const std::exception& e) { + flog::error("[IQExporter] Could not start socket: {}", e.what()); + } running = true; } @@ -132,12 +184,54 @@ public: void stop() { if (!running) { return; } - // TODO + // Acquire lock on the socket + std::lock_guard lck1(sockMtx); + + // Stop listening or close UDP socket + if (proto == PROTOCOL_TCP) { + // Stop listener + if (listener) { + listener->stop(); + } + + // Wait for worker to stop + if (listenWorkerThread.joinable()) { listenWorkerThread.join(); } + + // Free listener + listener.reset(); + + // Close socket and free it + if (sock) { + sock->close(); + sock.reset(); + } + } + else { + // Close UDP socket and free it + if (sock) { + sock->close(); + sock.reset(); + } + } running = false; } private: + std::string getSrScaled(double sr) { + char buf[1024]; + if (sr >= 1000000.0) { + sprintf(buf, "%.1lf MS/s", sr / 1000000.0); + } + else if (sr >= 1000.0) { + sprintf(buf, "%.1lf KS/s", sr / 1000.0); + } + else { + sprintf(buf, "%.1lf S/s", sr); + } + return std::string(buf); + } + static void menuHandler(void* ctx) { IQExporterModule* _this = (IQExporterModule*)ctx; float menuWidth = ImGui::GetContentRegionAvail().x; @@ -156,10 +250,27 @@ private: config.release(true); } + // In VFO mode, show samplerate selector + if (_this->mode == MODE_VFO) { + ImGui::LeftLabel("Samplerate"); + ImGui::FillWidth(); + if (ImGui::Combo(("##iq_exporter_sr_" + _this->name).c_str(), &_this->srId, _this->samplerates.txt)) { + _this->samplerate = _this->samplerates.value(_this->srId); + if (_this->vfo) { + _this->vfo->setBandwidthLimits(_this->samplerate, _this->samplerate, true); + _this->vfo->setSampleRate(_this->samplerate, _this->samplerate); + } + config.acquire(); + config.conf[_this->name]["samplerate"] = _this->samplerates.key(_this->srId); + 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)) { + _this->proto = _this->protocols.value(_this->protoId); config.acquire(); config.conf[_this->name]["protocol"] = _this->protocols.key(_this->protoId); config.release(true); @@ -169,6 +280,7 @@ private: ImGui::LeftLabel("Sample type"); ImGui::FillWidth(); if (ImGui::Combo(("##iq_exporter_samp_" + _this->name).c_str(), &_this->sampTypeId, _this->sampleTypes.txt)) { + _this->sampType = _this->sampleTypes.value(_this->sampTypeId); config.acquire(); config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampTypeId); config.release(true); @@ -209,12 +321,94 @@ private: } } + // Status text + ImGui::TextUnformatted("Status:"); + ImGui::SameLine(); + if (_this->sock && _this->sock->isOpen()) { + ImGui::TextColored(ImVec4(0.0, 1.0, 0.0, 1.0), (_this->proto == PROTOCOL_TCP) ? "Connected" : "Sending"); + } + else if (_this->listener && _this->listener->listening()) { + ImGui::TextColored(ImVec4(1.0, 1.0, 0.0, 1.0), "Listening"); + } + else { + ImGui::TextUnformatted("Idle"); + } + if (!_this->enabled) { ImGui::EndDisabled(); } } void setMode(Mode newMode) { - // Delete VFO or unbind IQ stream + // If there is no mode to change, do nothing + flog::debug("Mode change"); + if (mode == newMode) { + flog::debug("New mode same as existing mode, doing nothing"); + return; + } + // Stop the DSP + flog::debug("Stopping DSP"); + handler.stop(); + + // Delete VFO or unbind IQ stream + if (vfo) { + flog::debug("Deleting old VFO"); + sigpath::vfoManager.deleteVFO(vfo); + vfo = NULL; + } + if (mode == MODE_BASEBAND) { + flog::debug("Unbinding old stream"); + sigpath::iqFrontEnd.unbindIQStream(&iqStream); + } + + // If the mode was none, we're done + if (newMode == MODE_NONE) { + flog::debug("Exiting, new mode is NONE"); + return; + } + + // Create VFO or bind IQ stream + if (newMode == MODE_VFO) { + flog::debug("Creating new VFO"); + // Create VFO + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, samplerate, samplerate, samplerate, samplerate, true); + + // Set its output as the input to the DSP + handler.setInput(vfo->output); + } + else { + flog::debug("Binding IQ stream"); + // Bind IQ stream + sigpath::iqFrontEnd.bindIQStream(&iqStream); + + // Set its output as the input to the DSP + handler.setInput(&iqStream); + } + + // Start DSP + flog::debug("Starting DSP"); + handler.start(); + + // Update mode + flog::debug("Updating mode"); + mode = newMode; + modeId = modes.valueId(newMode); + } + + void listenWorker() { + while (true) { + // Accept a client + auto newSock = listener->accept(); + if (!newSock) { break; } + + // Update socket + { + std::lock_guard lck(sockMtx); + sock = newSock; + } + + // Wait until disconnection + // TODO + } } static void dataHandler(dsp::complex_t* data, int count, void* ctx) { @@ -254,8 +448,10 @@ private: std::string name; bool enabled = true; - Mode mode = MODE_BASEBAND; + Mode mode = MODE_NONE; int modeId; + int samplerate = 1000000.0; + int srId; Protocol proto = PROTOCOL_TCP; int protoId; SampleType sampType = SAMPLE_TYPE_INT16; @@ -265,15 +461,20 @@ private: bool running = false; OptionList modes; + OptionList samplerates; OptionList protocols; OptionList sampleTypes; VFOManager::VFO* vfo = NULL; + dsp::stream iqStream; dsp::sink::Handler handler; uint8_t* buffer = NULL; + std::thread listenWorkerThread; + std::mutex sockMtx; std::shared_ptr sock; + std::shared_ptr listener; }; MOD_EXPORT void _INIT_() { From 7ab743d05b3b56d47f2a04d317885a29e61fb03e Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Fri, 2 Feb 2024 04:11:29 +0100 Subject: [PATCH 9/9] finish iq exporter and fix network lib send not closing socket on error --- core/src/utils/net.cpp | 11 +- misc_modules/iq_exporter/src/main.cpp | 158 ++++++++++++++++++++------ 2 files changed, 134 insertions(+), 35 deletions(-) diff --git a/core/src/utils/net.cpp b/core/src/utils/net.cpp index 99ff8390..2abd6239 100644 --- a/core/src/utils/net.cpp +++ b/core/src/utils/net.cpp @@ -138,7 +138,16 @@ namespace net { } int Socket::send(const uint8_t* data, size_t len, const Address* dest) { - return sendto(sock, (const char*)data, len, 0, (sockaddr*)(dest ? &dest->addr : (raddr ? &raddr->addr : NULL)), sizeof(sockaddr_in)); + // Send data + int err = sendto(sock, (const char*)data, len, 0, (sockaddr*)(dest ? &dest->addr : (raddr ? &raddr->addr : NULL)), sizeof(sockaddr_in)); + + // On error, close socket + if (err <= 0 && !WOULD_BLOCK) { + close(); + return err; + } + + return err; } int Socket::sendstr(const std::string& str, const Address* dest) { diff --git a/misc_modules/iq_exporter/src/main.cpp b/misc_modules/iq_exporter/src/main.cpp index e206e109..a4b6db67 100644 --- a/misc_modules/iq_exporter/src/main.cpp +++ b/misc_modules/iq_exporter/src/main.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include SDRPP_MOD_INFO{ @@ -27,7 +29,8 @@ enum Mode { }; enum Protocol { - PROTOCOL_TCP, + PROTOCOL_TCP_SERVER, + PROTOCOL_TCP_CLIENT, PROTOCOL_UDP }; @@ -62,7 +65,8 @@ public: } // Define protocols - protocols.define("TCP", PROTOCOL_TCP); + protocols.define("TCP (Server)", PROTOCOL_TCP_SERVER); + protocols.define("TCP (Client)", PROTOCOL_TCP_CLIENT); protocols.define("UDP", PROTOCOL_UDP); // Define sample types @@ -71,6 +75,13 @@ public: sampleTypes.define("Int32", SAMPLE_TYPE_INT32); sampleTypes.define("Float32", SAMPLE_TYPE_FLOAT32); + // Define packet sizes + for (int i = 8; i <= 32768; i <<= 1) { + char buf[16]; + sprintf(buf, "%d Bytes", i); + packetSizes.define(i, buf, i); + } + // Load config bool autoStart = false; Mode nMode = MODE_BASEBAND; @@ -91,6 +102,10 @@ public: std::string sampTypeStr = config.conf[name]["sampleType"]; if (sampleTypes.keyExists(sampTypeStr)) { sampType = sampleTypes.value(sampleTypes.keyId(sampTypeStr)); } } + if (config.conf[name].contains("packetSize")) { + int size = config.conf[name]["packetSize"]; + if (packetSizes.keyExists(size)) { packetSize = packetSizes.value(packetSizes.keyId(size)); } + } if (config.conf[name].contains("host")) { std::string hostStr = config.conf[name]["host"]; strcpy(hostname, hostStr.c_str()); @@ -109,12 +124,14 @@ public: srId = samplerates.valueId(samplerate); protoId = protocols.valueId(proto); sampTypeId = sampleTypes.valueId(sampType); + packetSizeId = packetSizes.valueId(packetSize); // Allocate buffer buffer = dsp::buffer::alloc(STREAM_BUFFER_SIZE * sizeof(dsp::complex_t)); // Init DSP - handler.init(&iqStream, dataHandler, this); + reshape.init(&iqStream, packetSize/sampleSize(), 0); + handler.init(&reshape.out, dataHandler, this); // Set operating mode setMode(nMode); @@ -143,10 +160,27 @@ public: void postInit() {} void enable() { + // Rebind streams and start DSP + setMode(mode, true); + + // Restart networking if it was running + if (wasRunning) { start(); } + + // Mark as running enabled = true; } void disable() { + // Save running state + wasRunning = running; + + // Stop networking + stop(); + + // Stop the DSP and unbind streams + setMode(MODE_NONE); + + // Mark as disabled enabled = false; } @@ -162,13 +196,17 @@ public: // Start listening or open UDP socket try { - if (proto == PROTOCOL_TCP) { + if (proto == PROTOCOL_TCP_SERVER) { // Create listener listener = net::listen(hostname, port); // Start listen worker listenWorkerThread = std::thread(&IQExporterModule::listenWorker, this); } + else if (proto == PROTOCOL_TCP_CLIENT) { + // Connect to TCP server + sock = net::connect(hostname, port); + } else { // Open UDP socket sock = net::openudp(hostname, port, "0.0.0.0", 0, true); @@ -176,6 +214,9 @@ public: } catch (const std::exception& e) { flog::error("[IQExporter] Could not start socket: {}", e.what()); + errorStr = e.what(); + showError = true; + return; } running = true; @@ -188,7 +229,7 @@ public: std::lock_guard lck1(sockMtx); // Stop listening or close UDP socket - if (proto == PROTOCOL_TCP) { + if (proto == PROTOCOL_TCP_SERVER) { // Stop listener if (listener) { listener->stop(); @@ -207,7 +248,7 @@ public: } } else { - // Close UDP socket and free it + // Close socket and free it if (sock) { sock->close(); sock.reset(); @@ -235,6 +276,11 @@ private: static void menuHandler(void* ctx) { IQExporterModule* _this = (IQExporterModule*)ctx; float menuWidth = ImGui::GetContentRegionAvail().x; + + // Error message box + ImGui::GenericDialog("##iq_exporter_err_", _this->showError, GENERIC_DIALOG_BUTTONS_OK, [=](){ + ImGui::Text("Error: %s", _this->errorStr.c_str()); + }); if (!_this->enabled) { ImGui::BeginDisabled(); } @@ -281,11 +327,23 @@ private: ImGui::FillWidth(); if (ImGui::Combo(("##iq_exporter_samp_" + _this->name).c_str(), &_this->sampTypeId, _this->sampleTypes.txt)) { _this->sampType = _this->sampleTypes.value(_this->sampTypeId); + _this->reshape.setKeep(_this->packetSize/_this->sampleSize()); config.acquire(); config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampTypeId); config.release(true); } + // Packet size selector + ImGui::LeftLabel("Packet size"); + ImGui::FillWidth(); + if (ImGui::Combo(("##iq_exporter_pkt_sz_" + _this->name).c_str(), &_this->packetSizeId, _this->packetSizes.txt)) { + _this->packetSize = _this->packetSizes.value(_this->packetSizeId); + _this->reshape.setKeep(_this->packetSize/_this->sampleSize()); + config.acquire(); + config.conf[_this->name]["packetSize"] = _this->packetSizes.key(_this->packetSizeId); + config.release(true); + } + // Hostname and port field if (ImGui::InputText(("##iq_exporter_host_" + _this->name).c_str(), _this->hostname, sizeof(_this->hostname))) { config.acquire(); @@ -304,7 +362,7 @@ private: if (_this->running) { ImGui::EndDisabled(); } // Start/Stop buttons - if (_this->running) { + if (_this->running || (!_this->enabled && _this->wasRunning)) { if (ImGui::Button(("Stop##iq_exporter_stop_" + _this->name).c_str(), ImVec2(menuWidth, 0))) { _this->stop(); config.acquire(); @@ -321,75 +379,78 @@ private: } } + // Check if the socket is open by attempting a read + bool sockOpen; + { + uint8_t dummy; + sockOpen = !(!_this->sock || !_this->sock->isOpen() || (_this->proto != PROTOCOL_UDP && _this->sock->recv(&dummy, 1, false, net::NONBLOCKING) == 0)); + } + // Status text ImGui::TextUnformatted("Status:"); ImGui::SameLine(); - if (_this->sock && _this->sock->isOpen()) { - ImGui::TextColored(ImVec4(0.0, 1.0, 0.0, 1.0), (_this->proto == PROTOCOL_TCP) ? "Connected" : "Sending"); + if (sockOpen) { + ImGui::TextColored(ImVec4(0.0, 1.0, 0.0, 1.0), (_this->proto == PROTOCOL_TCP_SERVER || _this->proto == PROTOCOL_TCP_CLIENT) ? "Connected" : "Sending"); } else if (_this->listener && _this->listener->listening()) { ImGui::TextColored(ImVec4(1.0, 1.0, 0.0, 1.0), "Listening"); } + else if (!_this->enabled) { + ImGui::TextUnformatted("Disabled"); + } else { + // If we're idle and still supposed to be running, the server has closed the connection (TODO: kinda jank...) + if (_this->running) { _this->stop(); } + ImGui::TextUnformatted("Idle"); } if (!_this->enabled) { ImGui::EndDisabled(); } } - void setMode(Mode newMode) { + void setMode(Mode newMode, bool fromDisabled = false) { // If there is no mode to change, do nothing - flog::debug("Mode change"); - if (mode == newMode) { - flog::debug("New mode same as existing mode, doing nothing"); - return; - } + if (!fromDisabled && mode == newMode) { return; } // Stop the DSP - flog::debug("Stopping DSP"); + reshape.stop(); handler.stop(); // Delete VFO or unbind IQ stream if (vfo) { - flog::debug("Deleting old VFO"); sigpath::vfoManager.deleteVFO(vfo); vfo = NULL; } - if (mode == MODE_BASEBAND) { - flog::debug("Unbinding old stream"); + if (mode == MODE_BASEBAND && !fromDisabled) { sigpath::iqFrontEnd.unbindIQStream(&iqStream); } // If the mode was none, we're done if (newMode == MODE_NONE) { - flog::debug("Exiting, new mode is NONE"); return; } // Create VFO or bind IQ stream if (newMode == MODE_VFO) { - flog::debug("Creating new VFO"); // Create VFO vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, samplerate, samplerate, samplerate, samplerate, true); // Set its output as the input to the DSP - handler.setInput(vfo->output); + reshape.setInput(vfo->output); } else { - flog::debug("Binding IQ stream"); // Bind IQ stream sigpath::iqFrontEnd.bindIQStream(&iqStream); // Set its output as the input to the DSP - handler.setInput(&iqStream); + reshape.setInput(&iqStream); } // Start DSP - flog::debug("Starting DSP"); + reshape.start(); handler.start(); // Update mode - flog::debug("Updating mode"); mode = newMode; modeId = modes.valueId(newMode); } @@ -405,21 +466,37 @@ private: std::lock_guard lck(sockMtx); sock = newSock; } + } + } - // Wait until disconnection - // TODO + int sampleSize() { + switch (sampType) { + case SAMPLE_TYPE_INT8: + return sizeof(int8_t)*2; + case SAMPLE_TYPE_INT16: + return sizeof(int16_t)*2; + case SAMPLE_TYPE_INT32: + return sizeof(int32_t)*2; + case SAMPLE_TYPE_FLOAT32: + return sizeof(dsp::complex_t); + default: + return -1; } } 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); + // Try to cquire lock on socket + if (!_this->sockMtx.try_lock()) { return; } // If not valid or open, give uo - if (!_this->sock || !_this->sock->isOpen()) { return; } - + if (!_this->sock || !_this->sock->isOpen()) { + // Unlock socket mutex + _this->sockMtx.unlock(); + return; + } + // Convert the samples or send directory for float32 int size; switch (_this->sampType) { @@ -428,7 +505,7 @@ private: size = sizeof(int8_t)*2; break; case SAMPLE_TYPE_INT16: - volk_32fc_convert_16ic((lv_16sc_t*)_this->buffer, (lv_32fc_t*)data, count); + volk_32f_s32f_convert_16i((int16_t*)_this->buffer, (float*)data, 32768.0f, count*2); size = sizeof(int16_t)*2; break; case SAMPLE_TYPE_INT32: @@ -438,11 +515,16 @@ private: case SAMPLE_TYPE_FLOAT32: _this->sock->send((uint8_t*)data, count*sizeof(dsp::complex_t)); default: + // Unlock socket mutex + _this->sockMtx.unlock(); return; } // Send converted samples _this->sock->send(_this->buffer, count*size); + + // Unlock socket mutex + _this->sockMtx.unlock(); } std::string name; @@ -452,21 +534,29 @@ private: int modeId; int samplerate = 1000000.0; int srId; - Protocol proto = PROTOCOL_TCP; + Protocol proto = PROTOCOL_TCP_SERVER; int protoId; SampleType sampType = SAMPLE_TYPE_INT16; int sampTypeId; + int packetSize = 1024; + int packetSizeId; char hostname[1024] = "localhost"; int port = 1234; bool running = false; + bool wasRunning = false; + + bool showError = false; + std::string errorStr = ""; OptionList modes; OptionList samplerates; OptionList protocols; OptionList sampleTypes; + OptionList packetSizes; VFOManager::VFO* vfo = NULL; dsp::stream iqStream; + dsp::buffer::Reshaper reshape; dsp::sink::Handler handler; uint8_t* buffer = NULL;