Skip to content

Module System Overview

Lunae Mons Research edited this page Sep 1, 2025 · 1 revision

Module System Overview

SDR++CE's modular architecture allows extending functionality through dynamically loaded plugins. This system provides clean separation between core infrastructure and feature implementations.

Module Types

Source Modules

Purpose: Provide IQ data streams from hardware or files

Examples: RTL-SDR, HackRF, PlutoSDR, File Source, Network Source

Pattern: Handler-based registration with SourceManager

// Handler structure
SourceManager::SourceHandler handler;
handler.ctx = this;
handler.menuHandler = drawMenu;        // UI callback
handler.startHandler = startAcquisition;  // Begin streaming
handler.stopHandler = stopAcquisition;    // Stop streaming  
handler.tuneHandler = tuneFrequency;      // Frequency change
handler.stream = &outputStream;           // IQ output stream

sigpath::sourceManager.registerSource("My SDR", &handler);

Decoder Modules

Purpose: Process VFO signals to extract audio or data

Examples: AM/FM demodulators, Digital protocols (DMR, P25, etc.)

Pattern: Factory pattern providing dsp::hier_block instances

// Register decoder factory
sigpath::vfoManager.registerDemodulator("My Protocol", this);

// Factory method
dsp::hier_block<dsp::complex_t, dsp::stereo_t>* createDemodulator(
    VFOManager::VFO* vfo, std::string name, ConfigManager* config,
    dsp::stream<dsp::complex_t>* input, double bandwidth) {
    
    return new MyDemodulator(input, bandwidth);
}

Sink Modules

Purpose: Output audio to devices, files, or network

Examples: Audio devices (ALSA, WASAPI), Network streaming, File recording

Pattern: Provider pattern with SinkManager

// Provider structure  
SinkManager::SinkProvider provider;
provider.create = createSink;
provider.ctx = this;

sigpath::sinkManager.registerSinkProvider("My Sink", provider);

// Factory function
static SinkManager::Sink* createSink(SinkManager::Stream* stream, 
                                   std::string name, void* ctx) {
    return new MySink(stream, name, (MyModule*)ctx);
}

Misc Modules

Purpose: Utilities and tools that don't fit other categories

Examples: Scanner, Frequency Manager, Recorder, Protocol analyzers

Pattern: Direct UI integration and optional ModuleCom interfaces

Module Interface

Required Exports

Every module must export these C functions:

extern "C" {
    // Module metadata
    SDRPP_EXPORT ModuleInfo_t* _INFO_();
    
    // Module lifecycle
    SDRPP_EXPORT void _INIT_();
    SDRPP_EXPORT void _END_();
    
    // Instance management
    SDRPP_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name);
    SDRPP_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance);
}

Module Information

// Define module metadata
SDRPP_MOD_INFO{
    /* Name:        */ "my_module",
    /* Description: */ "My awesome module",  
    /* Author:      */ "Your Name",
    /* Version:     */ "1.0.0",
    /* Max instances */ 1  // -1 for unlimited
};

Base Class

All module instances inherit from ModuleManager::Instance:

class MyModule : public ModuleManager::Instance {
public:
    MyModule(std::string name);
    ~MyModule();
    
    // Lifecycle methods
    void postInit() override;  // Called after all modules loaded
    void enable() override;    // Module enabled in config
    void disable() override;   // Module disabled
    bool isEnabled() override; // Query enabled state
    
private:
    std::string _name;
    bool _enabled = false;
};

Module Development Workflow

1. Project Structure

my_module/
├── CMakeLists.txt
├── src/
│ ├── main.cpp # Module exports and main class
│ ├── main.h # Class declarations
│ └── dsp/ # Custom DSP blocks (optional)
└── README.md

2. CMake Configuration

cmake_minimum_required(VERSION 3.13)
project(my_module)

# Find SDR++ core
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDRPP_CORE REQUIRED sdrpp_core)

# Build module as shared library
file(GLOB_RECURSE SRC "src/*.cpp")
add_library(my_module SHARED ${SRC})

target_link_libraries(my_module PRIVATE sdrpp_core)
target_include_directories(my_module PRIVATE 
    "src/" 
    ${SDRPP_CORE_INCLUDE_DIRS}
)

# Install to modules directory
install(TARGETS my_module DESTINATION lib/sdrpp/plugins)

3. Implementation Template

#include <module.h>
#include <gui/gui.h>
#include <signal_path/signal_path.h>
#include <core.h>

// Module information
SDRPP_MOD_INFO{
    /* Name:        */ "my_module",
    /* Description: */ "Example module template",
    /* Author:      */ "SDR++ Community", 
    /* Version:     */ "1.0.0",
    /* Max instances */ -1
};

class MyModule : public ModuleManager::Instance {
public:
    MyModule(std::string name) : _name(name) {
        // Module initialization
    }
    
    void postInit() override {
        // Register UI after all modules loaded
        gui::menu.registerEntry(_name, menuHandler, this);
    }
    
    void enable() override {
        _enabled = true;
    }
    
    void disable() override {
        _enabled = false;
    }
    
    bool isEnabled() override {
        return _enabled;
    }
    
private:
    static void menuHandler(void* ctx) {
        ((MyModule*)ctx)->drawMenu();
    }
    
    void drawMenu() {
        if (SmGui::BeginMenu(_name.c_str())) {
            SmGui::Text("Module is %s", _enabled ? "enabled" : "disabled");
            SmGui::EndMenu();
        }
    }
    
    std::string _name;
    bool _enabled = false;
};

// Required exports
MOD_EXPORT void _INIT_() {
    // One-time initialization
}

MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
    return new MyModule(name);
}

MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) {
    delete (MyModule*)instance;
}

MOD_EXPORT void _END_() {
    // Cleanup
}

Inter-Module Communication

ModuleComManager

Modules can expose services to other modules:

// Define command codes
#define CMD_GET_STATUS    0
#define CMD_SET_FREQUENCY 1

// Interface handler
int interfaceHandler(int command, void* in, void* out, void* ctx) {
    MyModule* module = (MyModule*)ctx;
    
    switch (command) {
        case CMD_GET_STATUS:
            *(bool*)out = module->isRunning();
            return 0;
        case CMD_SET_FREQUENCY:
            module->setFrequency(*(double*)in);
            return 0;
        default:
            return -1; // Unknown command
    }
}

// Register interface (in constructor)
core::modComManager.registerInterface(_name, interfaceHandler, this);

// Call interface from other modules
bool running;
if (core::modComManager.callInterface("module_name", CMD_GET_STATUS, nullptr, &running)) {
    // Command succeeded
}

Configuration Management

Module Configuration Pattern

void MyModule::loadConfig() {
    core::configManager.acquire();
    
    // Always check if section exists
    if (core::configManager.conf.contains(_name)) {
        auto& cfg = core::configManager.conf[_name];
        
        if (cfg.contains("enabled")) {
            _enabled = cfg["enabled"];
        }
        if (cfg.contains("frequency")) {
            _frequency = cfg["frequency"];
        }
    }
    
    core::configManager.release();
}

void MyModule::saveConfig() {
    core::configManager.acquire();
    core::configManager.conf[_name]["enabled"] = _enabled;
    core::configManager.conf[_name]["frequency"] = _frequency;
    core::configManager.release(true); // Mark as dirty
}

Default Schema (Critical)

All module configuration keys must be defined in core/src/core.cpp:

// In defConfig creation
defConfig["myModule"]["enabled"] = false;
defConfig["myModule"]["frequency"] = 100000000.0;
defConfig["myModule"]["gain"] = 20.0;

Without default schema entries, settings will be lost during configuration repair.

Threading Considerations

DSP Thread Safety

  • Use atomic variables for parameter updates
  • Never allocate memory in DSP processing
  • Avoid blocking operations
  • Pre-allocate all buffers

UI Thread Safety

  • Only modify UI state from UI thread
  • Use proper locking for shared data
  • Batch configuration updates
  • Handle missing modules gracefully

Common Patterns

Error Handling

// Always validate inputs
if (!inputStream || !outputStream) {
    flog::error("[{}] Invalid stream configuration", _name);
    return false;
}

// Handle missing dependencies gracefully
if (!core::modComManager.interfaceExists("required_module")) {
    flog::warn("[{}] Required module not available, disabling feature", _name);
    _featureEnabled = false;
}

Performance Optimization

// Pre-allocate buffers
MyModule::MyModule(std::string name) {
    _buffer = new float[MAX_BUFFER_SIZE];
    _tempBuffer = new dsp::complex_t[MAX_SAMPLES];
}

// Use SIMD when possible
#ifdef VOLK_AVAILABLE
    volk_32fc_magnitude_32f(_magnitudes, _samples, count);
#else
    // Fallback implementation
    for (int i = 0; i < count; i++) {
        _magnitudes[i] = abs(_samples[i]);
    }
#endif

Next: Module Development Guide for step-by-step module creation

Clone this wiki locally