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/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