diff --git a/CMakeLists.txt b/CMakeLists.txt index a9c67193..a038a103 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,12 +43,14 @@ 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) # 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) @@ -234,6 +236,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) @@ -252,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) @@ -302,7 +312,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/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/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..b1436431 --- /dev/null +++ b/decoder_modules/pager_decoder/src/decoder.h @@ -0,0 +1,11 @@ +#pragma once +#include + +class Decoder { +public: + 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/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 diff --git a/decoder_modules/pager_decoder/src/main.cpp b/decoder_modules/pager_decoder/src/main.cpp new file mode 100644 index 00000000..aacd76cb --- /dev/null +++ b/decoder_modules/pager_decoder/src/main.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "decoder.h" +#include "pocsag/decoder.h" +#include "flex/decoder.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 +}; + +ConfigManager config; + +enum Protocol { + PROTOCOL_INVALID = -1, + PROTOCOL_POCSAG, + PROTOCOL_FLEX +}; + +class PagerDecoderModule : public ModuleManager::Instance { +public: + PagerDecoderModule(std::string name) { + this->name = name; + + // Define protocols + protocols.define("POCSAG", PROTOCOL_POCSAG); + protocols.define("FLEX", PROTOCOL_FLEX); + + // Initialize VFO with default values + vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 12500, 24000, 12500, 12500, true); + vfo->setSnapInterval(1); + + // Select the protocol + selectProtocol(PROTOCOL_POCSAG); + + gui::menu.registerEntry(name, menuHandler, this, this); + } + + ~PagerDecoderModule() { + gui::menu.removeEntry(name); + // Stop DSP + if (enabled) { + decoder->stop(); + decoder.reset(); + 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), 12500, 24000, 12500, 12500, true); + vfo->setSnapInterval(1); + + decoder->setVFO(vfo); + decoder->start(); + + enabled = true; + } + + void disable() { + decoder->stop(); + sigpath::vfoManager.deleteVFO(vfo); + enabled = false; + } + + bool isEnabled() { + 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; + + 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)) { + _this->selectProtocol(_this->protocols.value(_this->protoId)); + } + + 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(); } + } + + std::string name; + bool enabled = true; + + Protocol proto = PROTOCOL_INVALID; + int protoId = 0; + + OptionList protocols; + + // DSP Chain + VFOManager::VFO* vfo; + std::unique_ptr decoder; + + 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..54923755 --- /dev/null +++ b/decoder_modules/pager_decoder/src/pocsag/decoder.h @@ -0,0 +1,111 @@ +#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(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_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; + + 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..3faca285 --- /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 : public 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/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]; 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..a4b6db67 --- /dev/null +++ b/misc_modules/iq_exporter/src/main.cpp @@ -0,0 +1,589 @@ +#include +#include +#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_NONE = -1, + MODE_BASEBAND, + MODE_VFO +}; + +enum Protocol { + PROTOCOL_TCP_SERVER, + PROTOCOL_TCP_CLIENT, + 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 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 (Server)", PROTOCOL_TCP_SERVER); + protocols.define("TCP (Client)", PROTOCOL_TCP_CLIENT); + 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); + + // 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; + config.acquire(); + if (config.conf[name].contains("mode")) { + std::string modeStr = config.conf[name]["mode"]; + 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"]; + 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("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()); + } + 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(nMode); + 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 + reshape.init(&iqStream, packetSize/sampleSize(), 0); + handler.init(&reshape.out, dataHandler, this); + + // Set operating mode + setMode(nMode); + + // Start if needed + if (autoStart) { start(); } + + // Register menu entry + gui::menu.registerEntry(name, menuHandler, this, this); + } + + ~IQExporterModule() { + // Un-register menu entry + gui::menu.removeEntry(name); + + // Stop networking + stop(); + + // Stop DSP + setMode(MODE_NONE); + + // Free buffer + dsp::buffer::free(buffer); + } + + 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; + } + + bool isEnabled() { + return enabled; + } + + void start() { + if (running) { return; } + + // Acquire lock on the socket + std::lock_guard lck1(sockMtx); + + // Start listening or open UDP socket + try { + 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); + } + } + catch (const std::exception& e) { + flog::error("[IQExporter] Could not start socket: {}", e.what()); + errorStr = e.what(); + showError = true; + return; + } + + running = true; + } + + void stop() { + if (!running) { return; } + + // Acquire lock on the socket + std::lock_guard lck1(sockMtx); + + // Stop listening or close UDP socket + if (proto == PROTOCOL_TCP_SERVER) { + // 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 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; + + // 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(); } + + 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); + } + + // 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); + } + + // Sample type selector + 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); + _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(); + 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 || (!_this->enabled && _this->wasRunning)) { + 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); + } + } + + // 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 (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, bool fromDisabled = false) { + // If there is no mode to change, do nothing + if (!fromDisabled && mode == newMode) { return; } + + // Stop the DSP + reshape.stop(); + handler.stop(); + + // Delete VFO or unbind IQ stream + if (vfo) { + sigpath::vfoManager.deleteVFO(vfo); + vfo = NULL; + } + if (mode == MODE_BASEBAND && !fromDisabled) { + sigpath::iqFrontEnd.unbindIQStream(&iqStream); + } + + // If the mode was none, we're done + if (newMode == MODE_NONE) { + return; + } + + // Create VFO or bind IQ stream + if (newMode == MODE_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 + reshape.setInput(vfo->output); + } + else { + // Bind IQ stream + sigpath::iqFrontEnd.bindIQStream(&iqStream); + + // Set its output as the input to the DSP + reshape.setInput(&iqStream); + } + + // Start DSP + reshape.start(); + handler.start(); + + // Update 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; + } + } + } + + 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; + + // 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()) { + // Unlock socket mutex + _this->sockMtx.unlock(); + 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_32f_s32f_convert_16i((int16_t*)_this->buffer, (float*)data, 32768.0f, count*2); + 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: + // 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; + bool enabled = true; + + Mode mode = MODE_NONE; + int modeId; + int samplerate = 1000000.0; + int srId; + 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; + + std::thread listenWorkerThread; + + std::mutex sockMtx; + std::shared_ptr sock; + std::shared_ptr listener; +}; + +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 833512c7..ae681271 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 | ⛔ | ⛔ | ⛔ | @@ -376,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 | ✅ | ✅ | ✅ |