From 3fc893568a1a302640c59000d12c801072195327 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Wed, 31 Jan 2024 23:34:40 +0100 Subject: [PATCH] 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 | ⛔ | ⛔ | ⛔ |