diff --git a/CMakeLists.txt b/CMakeLists.txt index b2d6662b..a9c67193 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,24 +17,25 @@ option(OPT_BUILD_FILE_SOURCE "Wav file source" ON) option(OPT_BUILD_HACKRF_SOURCE "Build HackRF Source Module (Dependencies: libhackrf)" ON) option(OPT_BUILD_HERMES_SOURCE "Build Hermes Source Module (no dependencies required)" ON) option(OPT_BUILD_LIMESDR_SOURCE "Build LimeSDR Source Module (Dependencies: liblimesuite)" OFF) -option(OPT_BUILD_SDRPP_SERVER_SOURCE "Build SDR++ Server Source Module (no dependencies required)" ON) +option(OPT_BUILD_PERSEUS_SOURCE "Build Perseus Source Module (Dependencies: libperseus-sdr)" OFF) +option(OPT_BUILD_PLUTOSDR_SOURCE "Build PlutoSDR Source Module (Dependencies: libiio, libad9361)" ON) option(OPT_BUILD_RFSPACE_SOURCE "Build RFspace Source Module (no dependencies required)" ON) option(OPT_BUILD_RTL_SDR_SOURCE "Build RTL-SDR Source Module (Dependencies: librtlsdr)" ON) option(OPT_BUILD_RTL_TCP_SOURCE "Build RTL-TCP Source Module (no dependencies required)" ON) +option(OPT_BUILD_SDRPP_SERVER_SOURCE "Build SDR++ Server Source Module (no dependencies required)" ON) option(OPT_BUILD_SDRPLAY_SOURCE "Build SDRplay Source Module (Dependencies: libsdrplay)" OFF) option(OPT_BUILD_SOAPY_SOURCE "Build SoapySDR Source Module (Dependencies: soapysdr)" ON) option(OPT_BUILD_SPECTRAN_SOURCE "Build Spectran Source Module (Dependencies: Aaronia RTSA Suite)" OFF) option(OPT_BUILD_SPECTRAN_HTTP_SOURCE "Build Spectran HTTP Source Module (no dependencies required)" ON) option(OPT_BUILD_SPYSERVER_SOURCE "Build SpyServer Source Module (no dependencies required)" ON) -option(OPT_BUILD_PLUTOSDR_SOURCE "Build PlutoSDR Source Module (Dependencies: libiio, libad9361)" ON) option(OPT_BUILD_USRP_SOURCE "Build USRP Source Module (libuhd)" OFF) # Sinks option(OPT_BUILD_ANDROID_AUDIO_SINK "Build Android Audio Sink Module (Dependencies: AAudio, only for android)" OFF) option(OPT_BUILD_AUDIO_SINK "Build Audio Sink Module (Dependencies: rtaudio)" ON) -option(OPT_BUILD_PORTAUDIO_SINK "Build PortAudio Sink Module (Dependencies: portaudio)" OFF) option(OPT_BUILD_NETWORK_SINK "Build Audio Sink Module (no dependencies required)" ON) option(OPT_BUILD_NEW_PORTAUDIO_SINK "Build the new PortAudio Sink Module (Dependencies: portaudio)" OFF) +option(OPT_BUILD_PORTAUDIO_SINK "Build PortAudio Sink Module (Dependencies: portaudio)" OFF) # Decoders option(OPT_BUILD_ATV_DECODER "Build ATV decoder (no dependencies required)" OFF) @@ -141,9 +142,13 @@ if (OPT_BUILD_LIMESDR_SOURCE) add_subdirectory("source_modules/limesdr_source") endif (OPT_BUILD_LIMESDR_SOURCE) -if (OPT_BUILD_SDRPP_SERVER_SOURCE) -add_subdirectory("source_modules/sdrpp_server_source") -endif (OPT_BUILD_SDRPP_SERVER_SOURCE) +if (OPT_BUILD_PERSEUS_SOURCE) +add_subdirectory("source_modules/perseus_source") +endif (OPT_BUILD_PERSEUS_SOURCE) + +if (OPT_BUILD_PLUTOSDR_SOURCE) +add_subdirectory("source_modules/plutosdr_source") +endif (OPT_BUILD_PLUTOSDR_SOURCE) if (OPT_BUILD_RFSPACE_SOURCE) add_subdirectory("source_modules/rfspace_source") @@ -157,6 +162,10 @@ if (OPT_BUILD_RTL_TCP_SOURCE) add_subdirectory("source_modules/rtl_tcp_source") endif (OPT_BUILD_RTL_TCP_SOURCE) +if (OPT_BUILD_SDRPP_SERVER_SOURCE) +add_subdirectory("source_modules/sdrpp_server_source") +endif (OPT_BUILD_SDRPP_SERVER_SOURCE) + if (OPT_BUILD_SDRPLAY_SOURCE) add_subdirectory("source_modules/sdrplay_source") endif (OPT_BUILD_SDRPLAY_SOURCE) @@ -177,10 +186,6 @@ if (OPT_BUILD_SPYSERVER_SOURCE) add_subdirectory("source_modules/spyserver_source") endif (OPT_BUILD_SPYSERVER_SOURCE) -if (OPT_BUILD_PLUTOSDR_SOURCE) -add_subdirectory("source_modules/plutosdr_source") -endif (OPT_BUILD_PLUTOSDR_SOURCE) - if (OPT_BUILD_USRP_SOURCE) add_subdirectory("source_modules/usrp_source") endif (OPT_BUILD_USRP_SOURCE) diff --git a/core/src/utils/new_event.h b/core/src/utils/new_event.h new file mode 100644 index 00000000..940765a7 --- /dev/null +++ b/core/src/utils/new_event.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include +#include + +typedef int HandlerID; + +template +class NewEvent { +public: + using Handler = std::function; + + HandlerID bind(const Handler& handler) { + std::lock_guard lck(mtx); + HandlerID id = genID(); + handlers[id] = handler; + return id; + } + + template + HandlerID bind(MHandler handler, T* ctx) { + return bind([=](Args... args){ + (ctx->*handler)(args...); + }); + } + + void unbind(HandlerID id) { + std::lock_guard lck(mtx); + if (handlers.find(id) == handlers.end()) { + throw std::runtime_error("Could not unbind handler, unknown ID"); + } + handlers.erase(id); + } + + void operator()(Args... args) { + std::lock_guard lck(mtx); + for (const auto& [desc, handler] : handlers) { + handler(args...); + } + } + +private: + HandlerID genID() { + int id; + for (id = 1; handlers.find(id) != handlers.end(); id++); + return id; + } + + std::map handlers; + std::mutex mtx; +}; \ No newline at end of file diff --git a/make_macos_bundle.sh b/make_macos_bundle.sh index 0acc0053..703bf107 100644 --- a/make_macos_bundle.sh +++ b/make_macos_bundle.sh @@ -62,6 +62,7 @@ bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/decoder_module bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/discord_integration/discord_integration.dylib bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/frequency_manager/frequency_manager.dylib bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/recorder/recorder.dylib +bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/rigctl_client/rigctl_client.dylib bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/rigctl_server/rigctl_server.dylib bundle_install_binary $BUNDLE $BUNDLE/Contents/Plugins $BUILD_DIR/misc_modules/scanner/scanner.dylib diff --git a/readme.md b/readme.md index 902d3b80..65257a3d 100644 --- a/readme.md +++ b/readme.md @@ -329,11 +329,12 @@ Modules in beta are still included in releases for the most part but not enabled |----------------------|------------|-------------------|--------------------------------|:---------------:|:-----------------------:|:---------------------------:| | airspy_source | Working | libairspy | OPT_BUILD_AIRSPY_SOURCE | ✅ | ✅ | ✅ | | airspyhf_source | Working | libairspyhf | OPT_BUILD_AIRSPYHF_SOURCE | ✅ | ✅ | ✅ | -| bladerf_source | Working | libbladeRF | OPT_BUILD_BLADERF_SOURCE | ⛔ | ⚠️ (not Debian Buster) | ✅ | +| bladerf_source | Working | libbladeRF | OPT_BUILD_BLADERF_SOURCE | ⛔ | ✅ (not Debian Buster) | ✅ | | file_source | Working | - | OPT_BUILD_FILE_SOURCE | ✅ | ✅ | ✅ | | hackrf_source | Working | libhackrf | OPT_BUILD_HACKRF_SOURCE | ✅ | ✅ | ✅ | -| hermes_source | Beta | - | OPT_BUILD_HERMES_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 | ⛔ | ⛔ | ⛔ | | 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 | ✅ | ✅ | ✅ | @@ -350,7 +351,7 @@ Modules in beta are still included in releases for the most part but not enabled | Name | Stage | Dependencies | Option | Built by default| Built in Release | Enabled in SDR++ by default | |--------------------|------------|--------------|------------------------------|:---------------:|:----------------:|:---------------------------:| -| android_audio_sink | Working | - | OPT_BUILD_ANDROID_AUDIO_SINK | ⛔ | ✅ | ⛔ | +| android_audio_sink | Working | - | OPT_BUILD_ANDROID_AUDIO_SINK | ⛔ | ✅ | ✅ (Android only) | | audio_sink | Working | rtaudio | OPT_BUILD_AUDIO_SINK | ✅ | ✅ | ✅ | | network_sink | Working | - | OPT_BUILD_NETWORK_SINK | ✅ | ✅ | ✅ | | new_portaudio_sink | Beta | portaudio | OPT_BUILD_NEW_PORTAUDIO_SINK | ⛔ | ✅ | ⛔ | @@ -376,9 +377,9 @@ 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 | ✅ | ✅ | ✅ | | recorder | Working | - | OPT_BUILD_RECORDER | ✅ | ✅ | ✅ | -| rigctl_client | Unfinished | - | OPT_BUILD_RIGCTL_CLIENT | ⛔ | ⛔ | ⛔ | +| rigctl_client | Unfinished | - | OPT_BUILD_RIGCTL_CLIENT | ✅ | ✅ | ⛔ | | rigctl_server | Working | - | OPT_BUILD_RIGCTL_SERVER | ✅ | ✅ | ✅ | -| scanner | Beta | - | OPT_BUILD_SCANNER | ✅ | ✅ | ✅ | +| scanner | Beta | - | OPT_BUILD_SCANNER | ✅ | ✅ | ⛔ | | scheduler | Unfinished | - | OPT_BUILD_SCHEDULER | ⛔ | ⛔ | ⛔ | # Troubleshooting diff --git a/source_modules/perseus_source/CMakeLists.txt b/source_modules/perseus_source/CMakeLists.txt new file mode 100644 index 00000000..1b273f3d --- /dev/null +++ b/source_modules/perseus_source/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.13) +project(perseus_source) + +file(GLOB SRC "src/*.cpp") + +include(${SDRPP_MODULE_CMAKE}) + +if (MSVC) + # Lib path + target_link_directories(perseus_source PRIVATE "C:/Users/ryzerth/Documents/Code/libperseus-sdr/build/Debug") + + target_include_directories(perseus_source PUBLIC "C:/Users/ryzerth/Documents/Code/libperseus-sdr/src") + + target_link_libraries(perseus_source PRIVATE perseus-sdr) +else (MSVC) + find_package(PkgConfig) + + pkg_check_modules(LIBPERSEUSSDR REQUIRED libperseus-sdr) + + target_include_directories(perseus_source PRIVATE ${LIBPERSEUSSDR_INCLUDE_DIRS}) + target_link_directories(perseus_source PRIVATE ${LIBPERSEUSSDR_LIBRARY_DIRS}) + target_link_libraries(perseus_source PRIVATE ${LIBPERSEUSSDR_LIBRARIES}) + + # Include it because for some reason pkgconfig doesn't look here? + if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + target_include_directories(perseus_source PRIVATE "/usr/local/include") + endif() + +endif () \ No newline at end of file diff --git a/source_modules/perseus_source/src/main.cpp b/source_modules/perseus_source/src/main.cpp new file mode 100644 index 00000000..fa1b8869 --- /dev/null +++ b/source_modules/perseus_source/src/main.cpp @@ -0,0 +1,468 @@ +#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: */ "perseus_source", + /* Description: */ "Perseus SDR source module for SDR++", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +#define MAX_SAMPLERATE_COUNT 128 + +ConfigManager config; + +class PerseusSourceModule : public ModuleManager::Instance { +public: + PerseusSourceModule(std::string name) { + this->name = name; + + sampleRate = 768000; + + handler.ctx = this; + handler.selectHandler = menuSelected; + handler.deselectHandler = menuDeselected; + handler.menuHandler = menuHandler; + handler.startHandler = start; + handler.stopHandler = stop; + handler.tuneHandler = tune; + handler.stream = &stream; + + perseus_set_debug(9); + + refresh(); + + config.acquire(); + std::string serial = config.conf["device"]; + config.release(); + select(serial); + + sigpath::sourceManager.registerSource("Perseus", &handler); + } + + ~PerseusSourceModule() { + stop(this); + sigpath::sourceManager.unregisterSource("Perseus"); + if (libInit) { perseus_exit(); } + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + + void refresh() { + // Re-initialize driver + if (libInit) { perseus_exit(); } + int devCount = perseus_init(); + if (devCount < 0) { + libInit = false; + flog::error("Could not initialize libperseus: {}", perseus_errorstr()); + return; + } + libInit = true; + + // Open each device to get the serial number + for (int i = 0; i < devCount; i++) { + // Open device + perseus_descr* dev = perseus_open(i); + if (!dev) { + flog::error("Failed to open Perseus device with ID {}: {}", i, perseus_errorstr()); + continue; + } + + // Load firmware + int err = perseus_firmware_download(dev, NULL); + if (err) { + flog::error("Could not upload firmware to device {}: {}", i, perseus_errorstr()); + perseus_close(dev); + continue; + } + + // Get info + eeprom_prodid prodId; + err = perseus_get_product_id(dev, &prodId); + if (err) { + flog::error("Could not getproduct info from device {}: {}", i, perseus_errorstr()); + perseus_close(dev); + continue; + } + + // Create entry + char serial[128]; + char buf[128]; + sprintf(serial, "%05d", (int)prodId.sn); + sprintf(buf, "Perseus %d.%d [%s]", (int)prodId.hwver, (int)prodId.hwrel, serial); + devList.define(serial, buf, i); + + // Close device + perseus_close(dev); + } + } + + void select(const std::string& serial) { + // If there are no devices, give up + if (devList.empty()) { + selectedSerial.clear(); + return; + } + + // If the serial number is not available, select first instead + if (!devList.keyExists(serial)) { + select(devList.key(0)); + return; + } + + // Open device + selectedSerial = serial; + selectedPerseusId = devList.value(devList.keyId(serial)); + perseus_descr* dev = perseus_open(selectedPerseusId); + if (!dev) { + flog::error("Failed to open device {}: {}", selectedPerseusId, perseus_errorstr()); + selectedSerial.clear(); + return; + } + + // Load firmware + int err = perseus_firmware_download(dev, NULL); + if (err) { + flog::error("Could not upload firmware to device: {}", perseus_errorstr()); + perseus_close(dev); + selectedSerial.clear(); + return; + } + + // Get info + eeprom_prodid prodId; + err = perseus_get_product_id(dev, &prodId); + if (err) { + flog::error("Could not getproduct info from device: {}", perseus_errorstr()); + perseus_close(dev); + selectedSerial.clear(); + return; + } + + // List samplerates + srList.clear(); + int samplerates[MAX_SAMPLERATE_COUNT]; + memset(samplerates, 0, sizeof(int)*MAX_SAMPLERATE_COUNT); + err = perseus_get_sampling_rates(dev, samplerates, MAX_SAMPLERATE_COUNT); + if (err) { + flog::error("Could not get samplerate list: {}", perseus_errorstr()); + perseus_close(dev); + selectedSerial.clear(); + return; + } + for (int i = 0; i < MAX_SAMPLERATE_COUNT; i++) { + if (!samplerates[i]) { break; } + srList.define(samplerates[i], getBandwdithScaled(samplerates[i]), samplerates[i]); + } + + // TODO: List attenuator values + + // Load options + srId = 0; + dithering = false; + preamp = false; + preselector = true; + atten = 0; + config.acquire(); + if (config.conf["devices"][selectedSerial].contains("samplerate")) { + int sr = config.conf["devices"][selectedSerial]["samplerate"]; + if (srList.keyExists(sr)) { + srId = srList.keyId(sr); + } + } + if (config.conf["devices"][selectedSerial].contains("dithering")) { + dithering = config.conf["devices"][selectedSerial]["dithering"]; + } + if (config.conf["devices"][selectedSerial].contains("preamp")) { + preamp = config.conf["devices"][selectedSerial]["preamp"]; + } + if (config.conf["devices"][selectedSerial].contains("preselector")) { + preselector = config.conf["devices"][selectedSerial]["preselector"]; + } + if (config.conf["devices"][selectedSerial].contains("attenuation")) { + atten = config.conf["devices"][selectedSerial]["attenuation"]; + } + config.release(); + + // Update samplerate + sampleRate = srList[srId]; + core::setInputSampleRate(sampleRate); + + // Close device + perseus_close(dev); + } + +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); + } + + static void menuSelected(void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + core::setInputSampleRate(_this->sampleRate); + flog::info("PerseusSourceModule '{0}': Menu Select!", _this->name); + } + + static void menuDeselected(void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + flog::info("PerseusSourceModule '{0}': Menu Deselect!", _this->name); + } + + static void start(void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + if (_this->running) { return; } + if (_this->selectedSerial.empty()) { + flog::error("No device is selected"); + return; + } + + // Open device + _this->openDev = perseus_open(_this->selectedPerseusId); + if (!_this->openDev) { + flog::error("Failed to open device {}: {}", _this->selectedPerseusId, perseus_errorstr()); + return; + } + + // Load firmware + int err = perseus_firmware_download(_this->openDev, NULL); + if (err) { + flog::error("Could not upload firmware to device: {}", perseus_errorstr()); + perseus_close(_this->openDev); + return; + } + + // Set samplerate + err = perseus_set_sampling_rate(_this->openDev, _this->sampleRate); + if (err) { + flog::error("Could not set samplerate: {}", perseus_errorstr()); + perseus_close(_this->openDev); + return; + } + + // Set options + perseus_set_adc(_this->openDev, _this->dithering, _this->preamp); + perseus_set_attenuator_in_db(_this->openDev, _this->atten); + perseus_set_ddc_center_freq(_this->openDev, _this->freq, _this->preselector); + + // Start stream + int idealBufferSize = _this->sampleRate / 200; + int multipleOf1024 = std::clamp(idealBufferSize / 1024, 1, 2); + int bufferSize = multipleOf1024 * 1024; + int bufferBytes = bufferSize*6; + err = perseus_start_async_input(_this->openDev, bufferBytes, callback, _this); + if (err) { + flog::error("Could not start stream: {}", perseus_errorstr()); + perseus_close(_this->openDev); + return; + } + + _this->running = true; + flog::info("PerseusSourceModule '{0}': Start!", _this->name); + } + + static void stop(void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + if (!_this->running) { return; } + _this->running = false; + + // Stop stream + _this->stream.stopWriter(); + perseus_stop_async_input(_this->openDev); + _this->stream.clearWriteStop(); + + // Close device + perseus_close(_this->openDev); + + flog::info("PerseusSourceModule '{0}': Stop!", _this->name); + } + + static void tune(double freq, void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + if (_this->running) { + perseus_set_ddc_center_freq(_this->openDev, freq, _this->preselector); + } + _this->freq = freq; + flog::info("PerseusSourceModule '{0}': Tune: {1}!", _this->name, freq); + } + + static void menuHandler(void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + + if (_this->running) { SmGui::BeginDisabled(); } + + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Combo(CONCAT("##_airspyhf_dev_sel_", _this->name), &_this->devId, _this->devList.txt)) { + std::string serial = _this->devList.key(_this->devId); + _this->select(serial); + config.acquire(); + config.conf["device"] = serial; + config.release(true); + } + + if (SmGui::Combo(CONCAT("##_airspyhf_sr_sel_", _this->name), &_this->srId, _this->srList.txt)) { + _this->sampleRate = _this->srList[_this->srId]; + core::setInputSampleRate(_this->sampleRate); + if (!_this->selectedSerial.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedSerial]["samplerate"] = _this->sampleRate; + config.release(true); + } + } + + SmGui::SameLine(); + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Button(CONCAT("Refresh##_airspyhf_refr_", _this->name))) { + _this->refresh(); + _this->select(_this->selectedSerial); + core::setInputSampleRate(_this->sampleRate); + } + + if (_this->running) { SmGui::EndDisabled(); } + + SmGui::LeftLabel("Attenuation"); + SmGui::FillWidth(); + if (SmGui::SliderFloatWithSteps(CONCAT("##_airspyhf_atten_", _this->name), &_this->atten, 0, 30, 10, SmGui::FMT_STR_FLOAT_DB_NO_DECIMAL)) { + if (_this->running) { + perseus_set_attenuator_in_db(_this->openDev, _this->atten); + } + if (!_this->selectedSerial.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedSerial]["attenuation"] = _this->atten; + config.release(true); + } + } + + if (SmGui::Checkbox(CONCAT("Preamp##_airspyhf_preamp_", _this->name), &_this->preamp)) { + if (_this->running) { + perseus_set_adc(_this->openDev, _this->dithering, _this->preamp); + } + if (!_this->selectedSerial.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedSerial]["preamp"] = _this->preamp; + config.release(true); + } + } + + if (SmGui::Checkbox(CONCAT("Dithering##_airspyhf_dither_", _this->name), &_this->dithering)) { + if (_this->running) { + perseus_set_adc(_this->openDev, _this->dithering, _this->preamp); + } + if (!_this->selectedSerial.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedSerial]["dithering"] = _this->dithering; + config.release(true); + } + } + + if (SmGui::Checkbox(CONCAT("Preselector##_airspyhf_presel_", _this->name), &_this->preselector)) { + if (_this->running) { + perseus_set_ddc_center_freq(_this->openDev, _this->freq, _this->preselector); + } + if (!_this->selectedSerial.empty()) { + config.acquire(); + config.conf["devices"][_this->selectedSerial]["preselector"] = _this->preselector; + config.release(true); + } + } + } + + static int callback(void* buf, int bufferSize, void* ctx) { + PerseusSourceModule* _this = (PerseusSourceModule*)ctx; + uint8_t* samples = (uint8_t*)buf; + int sampleCount = bufferSize / 6; + for (int i = 0; i < sampleCount; i++) { + int32_t re, im; + re = *(samples++); + re |= *(samples++) << 8; + re |= *(samples++) << 16; + re |= (re >> 23) * (0xFF << 24); // Sign extend + im = *(samples++); + im |= *(samples++) << 8; + im |= *(samples++) << 16; + im |= (im >> 23) * (0xFF << 24); // Sign extend + _this->stream.writeBuf[i].re = ((float)re / (float)0x7FFFFF); + _this->stream.writeBuf[i].im = ((float)im / (float)0x7FFFFF); + } + _this->stream.swap(sampleCount); + return 0; + } + + std::string name; + bool enabled = true; + dsp::stream stream; + int sampleRate; + SourceManager::SourceHandler handler; + bool running = false; + double freq; + int devId = 0; + int srId = 0; + bool libInit = false; + perseus_descr* openDev; + std::string selectedSerial = ""; + int selectedPerseusId; + float atten = 0; + bool preamp = false; + bool dithering = false; + bool preselector = true; + + OptionList devList; + OptionList srList; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + def["devices"] = json({}); + def["device"] = ""; + config.setPath(core::args["root"].s() + "/perseus_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new PerseusSourceModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) { + delete (PerseusSourceModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file diff --git a/source_modules/spectran_http_source/src/main.cpp b/source_modules/spectran_http_source/src/main.cpp index 14e49109..8ec62131 100644 --- a/source_modules/spectran_http_source/src/main.cpp +++ b/source_modules/spectran_http_source/src/main.cpp @@ -28,7 +28,7 @@ public: this->name = name; strcpy(hostname, "localhost"); - sampleRate = 41000000.0; + sampleRate = 5750000.0; handler.ctx = this; handler.selectHandler = menuSelected; @@ -103,8 +103,14 @@ private: static void tune(double freq, void* ctx) { SpectranHTTPSourceModule* _this = (SpectranHTTPSourceModule*)ctx; - if (_this->running) { - // TODO + bool connected = (_this->client && _this->client->isOpen()); + if (connected) { + int64_t newfreq = round(freq); + if (newfreq != _this->lastReportedFreq && _this->gotReport) { + flog::debug("Sending tuning command"); + _this->lastReportedFreq = newfreq; + _this->client->setCenterFrequency(newfreq); + } } _this->freq = freq; flog::info("SpectranHTTPSourceModule '{0}': Tune: {1}!", _this->name, freq); @@ -138,6 +144,8 @@ private: _this->tryConnect(); } else if (connected && SmGui::Button("Disconnect##spectran_http_source")) { + _this->client->onCenterFrequencyChanged.unbind(_this->onFreqChangedId); + _this->client->onCenterFrequencyChanged.unbind(_this->onSamplerateChangedId); _this->client->close(); } if (_this->running) { style::endDisabled(); } @@ -154,13 +162,28 @@ private: void tryConnect() { try { + gotReport = false; client = std::make_shared(hostname, port, &stream); + onFreqChangedId = client->onCenterFrequencyChanged.bind(&SpectranHTTPSourceModule::onFreqChanged, this); + onSamplerateChangedId = client->onSamplerateChanged.bind(&SpectranHTTPSourceModule::onSamplerateChanged, this); + client->startWorker(); } catch (std::runtime_error e) { flog::error("Could not connect: {0}", e.what()); } } + void onFreqChanged(double newFreq) { + if (lastReportedFreq == newFreq) { return; } + lastReportedFreq = newFreq; + tuner::tune(tuner::TUNER_MODE_IQ_ONLY, "", newFreq); + gotReport = true; + } + + void onSamplerateChanged(double newSr) { + core::setInputSampleRate(newSr); + } + std::string name; bool enabled = true; double sampleRate; @@ -168,11 +191,16 @@ private: bool running = false; std::shared_ptr client; + HandlerID onFreqChangedId; + HandlerID onSamplerateChangedId; double freq; + int64_t lastReportedFreq = 0; + bool gotReport; + char hostname[1024]; - int port = 80; + int port = 54664; dsp::stream stream; }; diff --git a/source_modules/spectran_http_source/src/spectran_http_client.cpp b/source_modules/spectran_http_source/src/spectran_http_client.cpp index f5959a96..8b886b75 100644 --- a/source_modules/spectran_http_source/src/spectran_http_client.cpp +++ b/source_modules/spectran_http_source/src/spectran_http_client.cpp @@ -5,6 +5,8 @@ SpectranHTTPClient::SpectranHTTPClient(std::string host, int port, dsp::streamstream = stream; // Connect to server + this->host = host; + this->port = port; sock = net::connect(host, port); http = net::http::Client(sock); @@ -14,6 +16,13 @@ SpectranHTTPClient::SpectranHTTPClient(std::string host, int port, dsp::streamclearWriteStop(); } +void SpectranHTTPClient::setCenterFrequency(uint64_t freq) { + // Connect to control endpoint (TODO: Switch to an always connected endpoint) + auto controlSock = net::connect(host, port); + auto controlHttp = net::http::Client(controlSock); + + // Make request + net::http::RequestHeader rqhdr(net::http::METHOD_PUT, "/control", host); + char buf[1024]; + sprintf(buf, "{\"frequencyCenter\":%d,\"frequencySpan\":%d,\"type\":\"capture\"}", freq, _samplerate); + std::string data = buf; + char lenBuf[16]; + sprintf(lenBuf, "%d", data.size()); + rqhdr.setField("Content-Length", lenBuf); + controlHttp.sendRequestHeader(rqhdr); + controlSock->sendstr(data); + net::http::ResponseHeader rshdr; + controlHttp.recvResponseHeader(rshdr, 5000); + + flog::debug("Response: {}", rshdr.getStatusString()); +} + void SpectranHTTPClient::worker() { while (sock->isOpen()) { // Get chunk header @@ -52,6 +82,40 @@ void SpectranHTTPClient::worker() { return; } + // Decode JSON (yes, this is hacky, but it must be extremely fast) + auto startFreqBegin = jsonData.find("\"startFrequency\":"); + auto startFreqEnd = jsonData.find(',', startFreqBegin); + std::string startFreqStr = jsonData.substr(startFreqBegin + 17, startFreqEnd - startFreqBegin - 17); + int64_t startFreq = std::stoll(startFreqStr); + + auto endFreqBegin = jsonData.find("\"endFrequency\":"); + auto endFreqEnd = jsonData.find(',', endFreqBegin); + std::string endFreqStr = jsonData.substr(endFreqBegin + 15, endFreqEnd - endFreqBegin - 15); + int64_t endFreq = std::stoll(endFreqStr); + + auto sampleFreqBegin = jsonData.find("\"sampleFrequency\":"); + bool sampleFreqReceived = (sampleFreqBegin != -1); + int64_t sampleFreq; + if (sampleFreqReceived) { + auto sampleFreqEnd = jsonData.find(',', sampleFreqBegin); + std::string sampleFreqStr = jsonData.substr(sampleFreqBegin + 18, sampleFreqEnd - sampleFreqBegin - 18); + sampleFreq = std::stoll(sampleFreqStr); + } + + // Calculate and update center freq + int64_t samplerate = sampleFreqReceived ? sampleFreq : (endFreq - startFreq); + int64_t centerFreq = round(((double)endFreq + (double)startFreq) / 2.0); + if (centerFreq != _centerFreq) { + flog::debug("New center freq: {}", centerFreq); + _centerFreq = centerFreq; + onCenterFrequencyChanged(centerFreq); + } + if (samplerate != _samplerate) { + flog::debug("New samplerate: {}", samplerate); + _samplerate = samplerate; + onSamplerateChanged(samplerate); + } + // Read (and check for) record separator uint8_t rs; int rslen = sock->recv(&rs, 1, true, 5000); @@ -72,10 +136,11 @@ void SpectranHTTPClient::worker() { i += read; sampLen += read; } + int sampCount = sampLen / 8; // Swap to stream if (streamingEnabled) { - if (!stream->swap(sampLen / 8)) { return; } + if (!stream->swap(sampCount)) { return; } } // Read trailing CRLF diff --git a/source_modules/spectran_http_source/src/spectran_http_client.h b/source_modules/spectran_http_source/src/spectran_http_client.h index 96e5cbae..c2fb83d6 100644 --- a/source_modules/spectran_http_source/src/spectran_http_client.h +++ b/source_modules/spectran_http_source/src/spectran_http_client.h @@ -4,22 +4,35 @@ #include #include #include +#include +#include class SpectranHTTPClient { public: SpectranHTTPClient(std::string host, int port, dsp::stream* stream); + void startWorker(); void streaming(bool enabled); bool isOpen(); void close(); + void setCenterFrequency(uint64_t freq); + + NewEvent onCenterFrequencyChanged; + NewEvent onSamplerateChanged; + private: void worker(); + std::string host; + int port; + std::shared_ptr sock; net::http::Client http; dsp::stream* stream; std::thread workerThread; bool streamingEnabled = false; + int64_t _centerFreq = 0; + uint64_t _samplerate = 0; }; \ No newline at end of file