#include #include #include #include #include #include #include #include #include #include #include #define CONCAT(a, b) ((std::string(a) + b).c_str()) SDRPP_MOD_INFO{ /* Name: */ "plutosdr_source", /* Description: */ "PlutoSDR source module for SDR++", /* Author: */ "Ryzerth", /* Version: */ 0, 2, 0, /* Max instances */ 1 }; ConfigManager config; class PlutoSDRSourceModule : public ModuleManager::Instance { public: PlutoSDRSourceModule(std::string name) { this->name = name; // Define valid samplerates for (int sr = 1000000; sr <= 61440000; sr += 500000) { samplerates.define(sr, getBandwdithScaled(sr), sr); } samplerates.define(61440000, getBandwdithScaled(61440000.0), 61440000.0); // Define valid bandwidths bandwidths.define(0, "Auto", 0); for (int bw = 1000000.0; bw <= 52000000; bw += 500000) { bandwidths.define(bw, getBandwdithScaled(bw), bw); } // Define gain modes gainModes.define("manual", "Manual", "manual"); gainModes.define("fast_attack", "Fast Attack", "fast_attack"); gainModes.define("slow_attack", "Slow Attack", "slow_attack"); gainModes.define("hybrid", "Hybrid", "hybrid"); // Enumerate devices refresh(); // Select device config.acquire(); uri = config.conf["device"]; config.release(); select(uri); // Register source handler.ctx = this; handler.selectHandler = menuSelected; handler.deselectHandler = menuDeselected; handler.menuHandler = menuHandler; handler.startHandler = start; handler.stopHandler = stop; handler.tuneHandler = tune; handler.stream = &stream; sigpath::sourceManager.registerSource("PlutoSDR", &handler); } ~PlutoSDRSourceModule() { stop(this); sigpath::sourceManager.unregisterSource("PlutoSDR"); } void postInit() {} void enable() { enabled = true; } void disable() { enabled = true; } bool isEnabled() { return enabled; } private: std::string getBandwdithScaled(double bw) { char buf[1024]; if (bw >= 1000000.0) { sprintf(buf, "%.1lfMHz", bw / 1000000.0); } else if (bw >= 1000.0) { sprintf(buf, "%.1lfKHz", bw / 1000.0); } else { sprintf(buf, "%.1lfHz", bw); } return std::string(buf); } void refresh() { // Clear device list devices.clear(); // Create scan context iio_scan_context* sctx = iio_create_scan_context(NULL, 0); if (!sctx) { flog::error("Failed get scan context"); return; } // Enumerate devices iio_context_info** ctxInfoList; ssize_t count = iio_scan_context_get_info_list(sctx, &ctxInfoList); if (count < 0) { flog::error("Failed to enumerate contexts"); return; } for (ssize_t i = 0; i < count; i++) { iio_context_info* info = ctxInfoList[i]; std::string desc = iio_context_info_get_description(info); std::string uri = iio_context_info_get_uri(info); devices.define(uri, desc, uri); } iio_context_info_list_free(ctxInfoList); // Destroy scan context iio_scan_context_destroy(sctx); #ifdef __ANDROID__ // On Android, a default IP entry must be made (TODO: This is not ideal since the IP cannot be changed) const char* androidURI = "ip:192.168.2.1"; devices.define(androidURI, "Default (192.168.2.1)", androidURI); #endif } void select(const std::string& nuri) { // If no device is available, give up if (devices.empty()) { uri.clear(); return; } // If the device is not available, select the first one if (!devices.keyExists(nuri)) { select(devices.key(0)); } // Update URI uri = nuri; // TODO: Enumerate capabilities // Load device config config.acquire(); if (config.conf["devices"][uri.c_str()].contains("samplerate")) { // Select given samplerate or default if invalid samplerate = config.conf["devices"][uri]["samplerate"]; if (samplerates.keyExists(samplerate)) { srId = samplerates.keyId(samplerate); } else { srId = 0; samplerate = samplerates.value(srId); } } if (config.conf["devices"][uri].contains("bandwidth")) { // Select given bandwidth or default if invalid bandwidth = config.conf["devices"][uri]["bandwidth"]; if (bandwidths.keyExists(bandwidth)) { bwId = bandwidths.keyId(bandwidth); } else { bwId = 0; bandwidth = bandwidths.value(bwId); } } if (config.conf["devices"][uri].contains("gainMode")) { // Select given gain mode or default if invalid std::string gm = config.conf["devices"][uri]["gainMode"]; if (gainModes.keyExists(gm)) { gmId = gainModes.keyId(gm); } else { gmId = 0; } } if (config.conf["devices"][uri].contains("gain")) { gain = config.conf["devices"][uri]["gain"]; gain = std::clamp(gain, -1.0f, 73.0f); } config.release(); } static void menuSelected(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; core::setInputSampleRate(_this->samplerate); flog::info("PlutoSDRSourceModule '{0}': Menu Select!", _this->name); } static void menuDeselected(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; flog::info("PlutoSDRSourceModule '{0}': Menu Deselect!", _this->name); } static void start(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; if (_this->running) { return; } // Open context _this->ctx = iio_create_context_from_uri(_this->uri.c_str()); if (_this->ctx == NULL) { flog::error("Could not open pluto ({})", _this->uri); return; } // Get phy and device handle _this->phy = iio_context_find_device(_this->ctx, "ad9361-phy"); if (_this->phy == NULL) { flog::error("Could not connect to pluto phy"); iio_context_destroy(_this->ctx); return; } _this->dev = iio_context_find_device(_this->ctx, "cf-ad9361-lpc"); if (_this->dev == NULL) { flog::error("Could not connect to pluto dev"); iio_context_destroy(_this->ctx); return; } // Get RX channels _this->rxChan = iio_device_find_channel(_this->phy, "voltage0", false); _this->rxLO = iio_device_find_channel(_this->phy, "altvoltage0", true); // Enable RX LO and disable TX iio_channel_attr_write_bool(iio_device_find_channel(_this->phy, "altvoltage1", true), "powerdown", true); iio_channel_attr_write_bool(_this->rxLO, "powerdown", false); // Configure RX channel iio_channel_attr_write(_this->rxChan, "rf_port_select", "A_BALANCED"); iio_channel_attr_write_longlong(_this->rxLO, "frequency", round(_this->freq)); // Freq iio_channel_attr_write_bool(_this->rxChan, "filter_fir_en", true); // Digital filter iio_channel_attr_write_longlong(_this->rxChan, "sampling_frequency", round(_this->samplerate)); // Sample rate iio_channel_attr_write_double(_this->rxChan, "hardwaregain", _this->gain); // Gain iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gmId).c_str()); // Gain mode _this->setBandwidth(_this->bandwidth); // Configure the ADC filters ad9361_set_bb_rate(_this->phy, round(_this->samplerate)); // Start worker thread _this->running = true; _this->workerThread = std::thread(worker, _this); flog::info("PlutoSDRSourceModule '{0}': Start!", _this->name); } static void stop(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; if (!_this->running) { return; } // Stop worker thread _this->running = false; _this->stream.stopWriter(); _this->workerThread.join(); _this->stream.clearWriteStop(); // Close device if (_this->ctx != NULL) { iio_context_destroy(_this->ctx); _this->ctx = NULL; } flog::info("PlutoSDRSourceModule '{0}': Stop!", _this->name); } static void tune(double freq, void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; _this->freq = freq; if (_this->running) { // Tune device iio_channel_attr_write_longlong(_this->rxLO, "frequency", round(freq)); } flog::info("PlutoSDRSourceModule '{0}': Tune: {1}!", _this->name, freq); } static void menuHandler(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; if (_this->running) { SmGui::BeginDisabled(); } SmGui::FillWidth(); SmGui::ForceSync(); if (SmGui::Combo("##plutosdr_dev_sel", &_this->devId, _this->devices.txt)) { _this->select(_this->devices.value(_this->devId)); config.acquire(); config.conf["device"] = _this->devices.key(_this->devId); config.release(true); } if (SmGui::Combo(CONCAT("##_pluto_sr_", _this->name), &_this->srId, _this->samplerates.txt)) { _this->samplerate = _this->samplerates.value(_this->srId); core::setInputSampleRate(_this->samplerate); if (!_this->uri.empty()) { config.acquire(); config.conf["devices"][_this->uri]["samplerate"] = _this->samplerate; config.release(true); } } // Refresh button SmGui::SameLine(); SmGui::FillWidth(); SmGui::ForceSync(); if (SmGui::Button(CONCAT("Refresh##_pluto_refr_", _this->name))) { _this->refresh(); _this->select(_this->uri); } if (_this->running) { SmGui::EndDisabled(); } SmGui::LeftLabel("Bandwidth"); SmGui::FillWidth(); if (SmGui::Combo(CONCAT("##_pluto_bw_", _this->name), &_this->bwId, _this->bandwidths.txt)) { _this->bandwidth = _this->bandwidths.value(_this->bwId); if (_this->running) { _this->setBandwidth(_this->bandwidth); } if (!_this->uri.empty()) { config.acquire(); config.conf["devices"][_this->uri]["bandwidth"] = _this->bandwidth; config.release(true); } } SmGui::LeftLabel("Gain Mode"); SmGui::FillWidth(); SmGui::ForceSync(); if (SmGui::Combo(CONCAT("##_pluto_gainmode_select_", _this->name), &_this->gmId, _this->gainModes.txt)) { if (_this->running) { iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gmId).c_str()); } if (!_this->uri.empty()) { config.acquire(); config.conf["devices"][_this->uri]["gainMode"] = _this->gainModes.key(_this->gmId); config.release(true); } } SmGui::LeftLabel("Gain"); if (_this->gmId) { SmGui::BeginDisabled(); } SmGui::FillWidth(); if (SmGui::SliderFloatWithSteps(CONCAT("##_pluto_gain__", _this->name), &_this->gain, -1.0f, 73.0f, 1.0f, SmGui::FMT_STR_FLOAT_DB_NO_DECIMAL)) { if (_this->running) { iio_channel_attr_write_double(_this->rxChan, "hardwaregain", _this->gain); } if (!_this->uri.empty()) { config.acquire(); config.conf["devices"][_this->uri]["gain"] = _this->gain; config.release(true); } } if (_this->gmId) { SmGui::EndDisabled(); } } void setBandwidth(int bw) { if (bw > 0) { iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", bw); } else { iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", std::min(samplerate, 52000000)); } } static void worker(void* ctx) { PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx; int blockSize = _this->samplerate / 200.0f; // Acquire channels iio_channel* rx0_i = iio_device_find_channel(_this->dev, "voltage0", 0); iio_channel* rx0_q = iio_device_find_channel(_this->dev, "voltage1", 0); if (!rx0_i || !rx0_q) { flog::error("Failed to acquire RX channels"); return; } // Start streaming iio_channel_enable(rx0_i); iio_channel_enable(rx0_q); // Allocate buffer iio_buffer* rxbuf = iio_device_create_buffer(_this->dev, blockSize, false); if (!rxbuf) { flog::error("Could not create RX buffer"); return; } // Receive loop while (true) { // Read samples iio_buffer_refill(rxbuf); // Get buffer pointer int16_t* buf = (int16_t*)iio_buffer_first(rxbuf, rx0_i); if (!buf) { break; } // Convert samples to CF32 volk_16i_s32f_convert_32f((float*)_this->stream.writeBuf, buf, 32768.0f, blockSize * 2); // Send out the samples if (!_this->stream.swap(blockSize)) { break; }; } // Stop streaming iio_channel_disable(rx0_i); iio_channel_disable(rx0_q); // Free buffer iio_buffer_destroy(rxbuf); } std::string name; bool enabled = true; dsp::stream stream; SourceManager::SourceHandler handler; std::thread workerThread; iio_context* ctx = NULL; iio_device* phy = NULL; iio_device* dev = NULL; iio_channel* rxLO = NULL; iio_channel* rxChan = NULL; bool running = false; std::string uri = ""; double freq; int samplerate = 4000000; int bandwidth = 0; float gain = -1; int devId = 0; int srId = 0; int bwId = 0; int gmId = 0; OptionList devices; OptionList samplerates; OptionList bandwidths; OptionList gainModes; }; MOD_EXPORT void _INIT_() { json defConf = {}; defConf["device"] = ""; defConf["devices"] = {}; config.setPath(core::args["root"].s() + "/plutosdr_source_config.json"); config.load(defConf); config.enableAutoSave(); // Reset the configuration if the old format is still used config.acquire(); if (!config.conf.contains("device") || !config.conf.contains("devices")) { config.conf = defConf; config.release(true); } else { config.release(); } } MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { return new PlutoSDRSourceModule(name); } MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) { delete (PlutoSDRSourceModule*)instance; } MOD_EXPORT void _END_() { config.disableAutoSave(); config.save(); }