diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cf394af..104370e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ option(OPT_BUILD_AIRSPYHF_SOURCE "Build Airspy HF+ Source Module (Dependencies: option(OPT_BUILD_BLADERF_SOURCE "Build BladeRF Source Module (Dependencies: libbladeRF)" OFF) 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_RFSPACE_SOURCE "Build RFspace Source Module (no dependencies required)" ON) @@ -92,6 +93,10 @@ if (OPT_BUILD_HACKRF_SOURCE) add_subdirectory("source_modules/hackrf_source") endif (OPT_BUILD_HACKRF_SOURCE) +if (OPT_BUILD_HERMES_SOURCE) +add_subdirectory("source_modules/hermes_source") +endif (OPT_BUILD_HERMES_SOURCE) + if (OPT_BUILD_LIMESDR_SOURCE) add_subdirectory("source_modules/limesdr_source") endif (OPT_BUILD_LIMESDR_SOURCE) diff --git a/core/src/utils/new_networking.cpp b/core/src/utils/new_networking.cpp deleted file mode 100644 index 4d04844d..00000000 --- a/core/src/utils/new_networking.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#include "new_networking.h" - diff --git a/core/src/utils/new_networking.h b/core/src/utils/new_networking.h deleted file mode 100644 index 1e6d651a..00000000 --- a/core/src/utils/new_networking.h +++ /dev/null @@ -1,96 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -/* - Ryzerth's Epic Networking Functions -*/ - -namespace net { - enum SocketType { - SOCK_TYPE_TCP, - SOCK_TYPE_UDP - }; - - struct ReadHandler { - int count; - void* buf; - void (*handle)(int count, void* buf, void* ctx); - void* ctx; - }; - - class SocketClass { - public: - SocketClass(struct addrinfo* localAddr, struct addrinfo* remoteAddr, SocketType sockType); - ~SocketClass(); - - int read(int count, void* buf, int timeout); - int write(int count, void* buf); - - void readAsync(int count, void* buf, void (*handle)(int count, void* buf, void* ctx), void* ctx); - - bool isOpen(); - void close(); - - private: - void readWorker(); - - bool open = false; - - struct addrinfo* laddr; - struct addrinfo* raddr; - SocketType type; - - std::queue readQueue; - std::thread readThread; - std::mutex readMtx; - std::condition_variable readCnd; - - }; - - typedef std::shared_ptr Socket; - - namespace tcp { - struct AcceptHandler { - void (*handle)(Socket client, void* ctx); - void* ctx; - }; - - class ListenerClass { - public: - ListenerClass(struct addrinfo* addr); - ~ListenerClass(); - - Socket accept(int count, void* buf, int timeout); - void acceptAsync(void (*handle)(int count, void* buf, void* ctx), void* ctx); - - bool isOpen(); - void close(); - - private: - void acceptWorker(); - - bool open = false; - - struct addrinfo* addr; - - std::queue acceptQueue; - std::thread acceptThread; - std::mutex acceptMtx; - std::condition_variable acceptCnd; - - }; - - typedef std::shared_ptr Listener; - - Socket connect(std::string host, int port); - Listener listen(std::string host, int port); - } - - namespace udp { - Socket open(std::string remoteHost, int remotePort, std::string localHost = "", int localPort = -1); - } -} \ No newline at end of file diff --git a/readme.md b/readme.md index aa102089..48b3fb32 100644 --- a/readme.md +++ b/readme.md @@ -274,34 +274,41 @@ Modules in beta are still included in releases for the most part but not enabled ## Sources -| Name | Stage | Dependencies | Option | Built by default| Built in Release | Enabled in SDR++ by default | -|------------------|------------|-------------------|----------------------------|:---------------:|:-----------------------:|:---------------------------:| -| 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) | ✅ | -| file_source | Working | - | OPT_BUILD_FILE_SOURCE | ✅ | ✅ | ✅ | -| hackrf_source | Working | libhackrf | OPT_BUILD_HACKRF_SOURCE | ✅ | ✅ | ✅ | -| limesdr_source | Working | liblimesuite | OPT_BUILD_LIMESDR_SOURCE | ⛔ | ✅ | ✅ | -| rtl_sdr_source | Working | librtlsdr | OPT_BUILD_RTL_SDR_SOURCE | ✅ | ✅ | ✅ | -| rtl_tcp_source | Working | - | OPT_BUILD_RTL_TCP_SOURCE | ✅ | ✅ | ✅ | -| sdrplay_source | Working | SDRplay API | OPT_BUILD_SDRPLAY_SOURCE | ⛔ | ✅ | ✅ | -| soapy_source | Working | soapysdr | OPT_BUILD_SOAPY_SOURCE | ✅ | ✅ | ✅ | -| spyserver_source | Working | - | OPT_BUILD_SPYSERVER_SOURCE | ✅ | ✅ | ✅ | -| plutosdr_source | Working | libiio, libad9361 | OPT_BUILD_PLUTOSDR_SOURCE | ✅ | ✅ | ✅ | +| Name | Stage | Dependencies | Option | Built by default| Built in Release | Enabled in SDR++ by default | +|---------------------|------------|-------------------|----------------------------|:---------------:|:-----------------------:|:---------------------------:| +| 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) | ✅ | +| file_source | Working | - | OPT_BUILD_FILE_SOURCE | ✅ | ✅ | ✅ | +| hackrf_source | Working | libhackrf | OPT_BUILD_HACKRF_SOURCE | ✅ | ✅ | ✅ | +| hermes_source | Unfinished | - | OPT_BUILD_HERMES_SOURCE | ✅ | ✅ | ✅ | +| limesdr_source | Working | liblimesuite | OPT_BUILD_LIMESDR_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 | ✅ | ✅ | ✅ | +| rtl_tcp_source | Working | - | OPT_BUILD_RTL_TCP_SOURCE | ✅ | ✅ | ✅ | +| sdrplay_source | Working | SDRplay API | OPT_BUILD_SDRPLAY_SOURCE | ⛔ | ✅ | ✅ | +| sdrpp_server_source | Working | - | OPT_BUILD_SPYSERVER_SOURCE | ✅ | ✅ | ✅ | +| soapy_source | Working | soapysdr | OPT_BUILD_SOAPY_SOURCE | ✅ | ✅ | ✅ | +| spyserver_source | Working | - | OPT_BUILD_SPYSERVER_SOURCE | ✅ | ✅ | ✅ | ## Sinks | Name | Stage | Dependencies | Option | Built by default| Built in Release | Enabled in SDR++ by default | |--------------------|------------|--------------|------------------------------|:---------------:|:----------------:|:---------------------------:| +| android_audio_sink | Working | - | OPT_BUILD_ANDROID_AUDIO_SINK | ⛔ | ✅ | ⛔ | | 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 | ⛔ | ✅ | ⛔ | +| portaudio_sink | Beta | portaudio | OPT_BUILD_PORTAUDIO_SINK | ⛔ | ✅ | ⛔ | ## Decoders | Name | Stage | Dependencies | Option | Built by default| Built in Release | Enabled in SDR++ by default | |---------------------|------------|--------------|-------------------------------|:---------------:|:----------------:|:---------------------------:| +| dmr_decoder | Unfinished | - | OPT_BUILD_DMR_DECODER | ⛔ | ⛔ | ⛔ | | falcon9_decoder | Unfinished | ffplay | OPT_BUILD_FALCON9_DECODER | ⛔ | ⛔ | ⛔ | +| kgsstv_decoder | Unfinished | - | OPT_BUILD_KGSSTV_DECODER | ⛔ | ⛔ | ⛔ | | m17_decoder | Beta | - | OPT_BUILD_M17_DECODER | ⛔ | ✅ | ⛔ | | meteor_demodulator | Working | - | OPT_BUILD_METEOR_DEMODULATOR | ✅ | ✅ | ⛔ | | radio | Working | - | OPT_BUILD_RADIO | ✅ | ✅ | ✅ | @@ -314,8 +321,10 @@ 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_server | Working | - | OPT_BUILD_RIGCTL_SERVER | ✅ | ✅ | ⛔ | +| rigctl_client | Unfinished | - | OPT_BUILD_RIGCTL_CLIENT | ⛔ | ⛔ | ⛔ | +| rigctl_server | Working | - | OPT_BUILD_RIGCTL_SERVER | ✅ | ✅ | ✅ | | scanner | Beta | - | OPT_BUILD_SCANNER | ✅ | ✅ | ✅ | +| scheduler | Unfinished | - | OPT_BUILD_SCHEDULER | ⛔ | ⛔ | ⛔ | # Troubleshooting diff --git a/source_modules/hermes_source/CMakeLists.txt b/source_modules/hermes_source/CMakeLists.txt new file mode 100644 index 00000000..f1fca269 --- /dev/null +++ b/source_modules/hermes_source/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.13) +project(hermes_source) + +file(GLOB SRC "src/*.cpp") + +add_library(hermes_source SHARED ${SRC}) +target_link_libraries(hermes_source PRIVATE sdrpp_core) +set_target_properties(hermes_source PROPERTIES PREFIX "") + +target_include_directories(hermes_source PRIVATE "src/") + +if (MSVC) + target_compile_options(hermes_source PRIVATE /O2 /Ob2 /std:c++17 /EHsc) +elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(hermes_source PRIVATE -O3 -std=c++17 -Wno-unused-command-line-argument -undefined dynamic_lookup) +else () + target_compile_options(hermes_source PRIVATE -O3 -std=c++17) +endif () + +if(WIN32) + target_link_libraries(hermes_source PRIVATE wsock32 ws2_32) +endif() + +# Install directives +install(TARGETS hermes_source DESTINATION lib/sdrpp/plugins) \ No newline at end of file diff --git a/source_modules/hermes_source/src/hermes.cpp b/source_modules/hermes_source/src/hermes.cpp new file mode 100644 index 00000000..4d32f30a --- /dev/null +++ b/source_modules/hermes_source/src/hermes.cpp @@ -0,0 +1,203 @@ +#include "hermes.h" +#include + +namespace hermes { + Client::Client(std::shared_ptr sock) { + this->sock = sock; + + // Start worker + workerThread = std::thread(&Client::worker, this); + } + + void Client::close() { + if (!open) { return; } + sock->close(); + + // Wait for worker to exit + if (workerThread.joinable()) { workerThread.join(); } + + open = false; + } + + void Client::start() { + sendMetisControl((MetisControl)(METIS_CTRL_IQ | METIS_CTRL_NO_WD)); + } + + void Client::stop() { + sendMetisControl(METIS_CTRL_NONE); + } + + void Client::setFrequency(double freq) { + writeReg(HL_REG_TX1_NCO_FREQ, freq); + } + + void Client::setGain(int gain) { + writeReg(HL_REG_RX_LNA, gain | (1 << 6)); + } + + void Client::sendMetisUSB(uint8_t endpoint, void* frame0, void* frame1) { + // Build packet + uint32_t seq = usbSeq++; + MetisUSBPacket pkt; + pkt.hdr.signature = htons(HERMES_METIS_SIGNATURE); + pkt.hdr.type = METIS_PKT_USB; + pkt.endpoint = endpoint; + pkt.seq = htonl(seq); + if (frame0) { memcpy(pkt.frame[0], frame0, 512); } + else { memset(pkt.frame[0], 0, 512); } + if (frame1) { memcpy(pkt.frame[1], frame1, 512); } + else { memset(pkt.frame[1], 0, 512); } + + // Send packet + sock->send((uint8_t*)&pkt, sizeof(pkt)); + } + + void Client::sendMetisControl(MetisControl ctrl) { + // Build packet + MetisControlPacket pkt; + pkt.hdr.signature = htons(HERMES_METIS_SIGNATURE); + pkt.hdr.type = METIS_PKT_CONTROL; + pkt.ctrl = ctrl; + + // Send packet + sock->send((uint8_t*)&pkt, sizeof(pkt)); + } + + uint32_t Client::readReg(uint8_t addr) { + uint8_t frame[512]; + memset(frame, 0, sizeof(frame)); + + HPSDRUSBHeader* hdr = (HPSDRUSBHeader*)frame; + hdr->sync[0] = HERMES_HPSDR_USB_SYNC; + hdr->sync[1] = HERMES_HPSDR_USB_SYNC; + hdr->sync[2] = HERMES_HPSDR_USB_SYNC; + hdr->c0 = (addr << 1) | (1 << 7); + + sendMetisUSB(2, frame); + + return 0; + } + + void Client::writeReg(uint8_t addr, uint32_t val) { + uint8_t frame[512]; + memset(frame, 0, sizeof(frame)); + + HPSDRUSBHeader* hdr = (HPSDRUSBHeader*)frame; + hdr->sync[0] = HERMES_HPSDR_USB_SYNC; + hdr->sync[1] = HERMES_HPSDR_USB_SYNC; + hdr->sync[2] = HERMES_HPSDR_USB_SYNC; + hdr->c0 = addr << 1; + *(uint32_t*)hdr->c = htonl(val); + + sendMetisUSB(2, frame); + } + + void Client::worker() { + uint8_t rbuf[2048]; + MetisUSBPacket* pkt = (MetisUSBPacket*)rbuf; + while (true) { + // Wait for a packet or exit if connection closed + int len = sock->recv(rbuf, 2048); + if (len <= 0) { break; } + + // Ignore anything that's not a USB packet + if (htons(pkt->hdr.signature) != HERMES_METIS_SIGNATURE || pkt->hdr.type != METIS_PKT_USB) { + continue; + } + + // Parse frames + for (int frn = 0; frn < 2; frn++) { + uint8_t* frame = pkt->frame[frn]; + HPSDRUSBHeader* hdr = (HPSDRUSBHeader*)frame; + + // Make sure this is a valid frame by checking the sync + if (hdr->sync[0] != 0x7F || hdr->sync[1] != 0x7F || hdr->sync[2] != 0x7F) { + continue; + } + + // Check if this is a response + if (hdr->c0 & (1 << 7)) { + uint8_t reg = (hdr->c0 >> 1) & 0x3F; + spdlog::warn("Got response! Reg={0}, Seq={1}", reg, htonl(pkt->seq)); + } + + // Decode and send IQ to stream + // TODO: More efficient way? + uint8_t* iq = &frame[8]; + for (int i = 0; i < 63; i++) { + // Convert to 32bit + int32_t si = ((uint32_t)iq[(i*8) + 0] << 16) | ((uint32_t)iq[(i*8) + 1] << 8) | (uint32_t)iq[(i*8) + 2]; + int32_t sq = ((uint32_t)iq[(i*8) + 3] << 16) | ((uint32_t)iq[(i*8) + 4] << 8) | (uint32_t)iq[(i*8) + 5]; + + // Sign extend + si = (si << 8) >> 8; + sq = (sq << 8) >> 8; + + // Convert to float (IQ swapper for some reason... I means in-phase :facepalm:) + out.writeBuf[i].im = (float)si / (float)0x1000000; + out.writeBuf[i].re = (float)sq / (float)0x1000000; + } + out.swap(63); + } + } + } + + std::vector discover() { + auto sock = net::openudp("192.168.0.255", 1024); + + // Build discovery packet + uint8_t discoveryPkt[64]; + memset(discoveryPkt, 0, sizeof(discoveryPkt)); + *(uint16_t*)&discoveryPkt[0] = htons(HERMES_METIS_SIGNATURE); + discoveryPkt[2] = METIS_PKT_DISCOVER; + + // Send the packet 5 times to make sure it's received + for (int i = 0; i < HERMES_DISCOVER_REPEAT; i++) { + sock->send(discoveryPkt, sizeof(discoveryPkt)); + } + + std::vector devices; + + while (true) { + // Wait for a response + uint8_t resp[1024]; + int len = sock->recv(resp, sizeof(resp), false, HERMES_DISCOVER_TIMEOUT); + + // Give up if timeout or error + if (len <= 0) { break; } + + // Verify that it is a valid response + if (len < 60) { continue; } + if (resp[0] != 0xEF || resp[1] != 0xFE) { continue; } + + // Analyze + Info info; + memcpy(info.mac, &resp[3], 6); + info.gatewareVerMaj = resp[0x09]; + info.gatewareVerMin = resp[0x15]; + + // Check if the device is already in the list + bool found = false; + for (const auto& d : devices) { + if (!memcmp(info.mac, d.mac, 6)) { + found = true; + break; + } + } + if (found) { continue; } + + devices.push_back(info); + } + + + return devices; + } + + std::shared_ptr open(std::string host, int port) { + // Open UDP socket + auto sock = net::openudp(host, port); + + // TODO: Check if open successful + return std::make_shared(sock); + } +} \ No newline at end of file diff --git a/source_modules/hermes_source/src/hermes.h b/source_modules/hermes_source/src/hermes.h new file mode 100644 index 00000000..60caf6ff --- /dev/null +++ b/source_modules/hermes_source/src/hermes.h @@ -0,0 +1,148 @@ +#pragma once +#include "net.h" +#include +#include +#include +#include +#include + +#define HERMES_DISCOVER_REPEAT 5 +#define HERMES_DISCOVER_TIMEOUT 1000 +#define HERMES_METIS_SIGNATURE 0xEFFE +#define HERMES_HPSDR_USB_SYNC 0x7F + +namespace hermes { + enum MetisPacketType { + METIS_PKT_USB = 0x01, + METIS_PKT_DISCOVER = 0x02, + METIS_PKT_CONTROL = 0x04 + }; + + enum MetisControl { + METIS_CTRL_NONE = 0, + METIS_CTRL_IQ = (1 << 0), + METIS_CTRL_WIDEBAND = (1 << 1), + METIS_CTRL_NO_WD = (1 << 7) + }; + + enum BoardID { + BOARD_ID_HERMES_EMUL = 1, + BOARD_ID_HL2 = 6 + }; + + struct Info { + uint8_t mac[6]; + uint8_t gatewareVerMaj; + uint8_t gatewareVerMin; + BoardID boardId; + }; + + enum HermesLiteReg { + HL_REG_TX1_NCO_FREQ = 0x01, + HL_REG_RX1_NCO_FREQ = 0x02, + HL_REG_RX2_NCO_FREQ = 0x03, + HL_REG_RX3_NCO_FREQ = 0x04, + HL_REG_RX4_NCO_FREQ = 0x05, + HL_REG_RX5_NCO_FREQ = 0x06, + HL_REG_RX6_NCO_FREQ = 0x07, + HL_REG_RX7_NCO_FREQ = 0x08, + + HL_REG_RX_LNA = 0x0A, + HL_REG_TX_LNA = 0x0E, + HL_REG_CWX = 0x0F, + HL_REG_CW_HANG_TIME = 0x10, + + HL_REG_RX8_NCO_FREQ = 0x12, + HL_REG_RX9_NCO_FREQ = 0x13, + HL_REG_RX10_NCO_FREQ = 0x14, + HL_REG_RX11_NCO_FREQ = 0x15, + HL_REG_RX12_NCO_FREQ = 0x16, + + HL_REG_PREDISTORTION = 0x2B, + + HL_REG_RESET_ON_DCNT = 0x3A, + HL_REG_AD9866_SPI = 0x3B, + HL_REG_I2C_1 = 0x3C, + HL_REG_I2C_2 = 0x3D, + HL_REG_ERROR = 0x3F, + }; + + enum HermesLiteSamplerate { + HL_SAMP_RATE_48KHZ = 0, + HL_SAMP_RATE_96KHZ = 1, + HL_SAMP_RATE_192KHZ = 2, + HL_SAMP_RATE_384KHZ = 3 + }; + +#pragma pack(push, 1) + struct HPSDRUSBHeader { + uint8_t sync[3]; + uint8_t c0; + uint8_t c[4]; + }; + + struct MetisPacketHeader { + uint16_t signature; + uint8_t type; + }; + + struct MetisUSBPacket { + MetisPacketHeader hdr; + uint8_t endpoint; + uint32_t seq; + uint8_t frame[2][512]; + }; + + struct MetisControlPacket { + MetisPacketHeader hdr; + uint8_t ctrl; + uint8_t rsvd[60]; + }; + + // struct MetisDiscoverPacket { + // MetisPacketHeader hdr; + // union { + // { + // uint16_t mac[6]; + // uint8_t gatewareMajVer; + // uint8_t boardId; + // }; + // uint8_t rsvd[60]; + // }; + // }; +#pragma pack(pop) + + class Client { + public: + Client(std::shared_ptr sock); + + void close(); + + void start(); + void stop(); + + void setFrequency(double freq); + void setGain(int gain); + + dsp::stream out; + + //private: + void sendMetisUSB(uint8_t endpoint, void* frame0, void* frame1 = NULL); + void sendMetisControl(MetisControl ctrl); + + uint32_t readReg(uint8_t addr); + void writeReg(uint8_t addr, uint32_t val); + + void worker(); + + bool open = true; + + std::thread workerThread; + std::shared_ptr sock; + uint32_t usbSeq = 0; + + }; + + std::vector discover(); + std::shared_ptr open(std::string host, int port); +} \ No newline at end of file diff --git a/source_modules/hermes_source/src/main.cpp b/source_modules/hermes_source/src/main.cpp new file mode 100644 index 00000000..d221333f --- /dev/null +++ b/source_modules/hermes_source/src/main.cpp @@ -0,0 +1,201 @@ +#include "hermes.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +SDRPP_MOD_INFO{ + /* Name: */ "hermes_source", + /* Description: */ "Hermes Lite 2 source module for SDR++", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +ConfigManager config; + +class HermesSourceModule : public ModuleManager::Instance { +public: + HermesSourceModule(std::string name) { + this->name = name; + + lnk.init(NULL, &stream); + + sampleRate = 384000.0; + + handler.ctx = this; + handler.selectHandler = menuSelected; + handler.deselectHandler = menuDeselected; + handler.menuHandler = menuHandler; + handler.startHandler = start; + handler.stopHandler = stop; + handler.tuneHandler = tune; + handler.stream = &stream; + + refresh(); + + // TODO: Select device + + sigpath::sourceManager.registerSource("Hermes", &handler); + } + + ~HermesSourceModule() { + stop(this); + sigpath::sourceManager.unregisterSource("Hermes"); + } + + void postInit() {} + + enum AGCMode { + AGC_MODE_OFF, + AGC_MODE_LOW, + AGC_MODE_HIGG + }; + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + + void refresh() { + devList.clear(); + devListTxt = ""; + + // TOOD: Update dev list + } + + // TODO: Implement select functions + +private: + static void menuSelected(void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + core::setInputSampleRate(_this->sampleRate); + spdlog::info("HermesSourceModule '{0}': Menu Select!", _this->name); + } + + static void menuDeselected(void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + spdlog::info("HermesSourceModule '{0}': Menu Deselect!", _this->name); + } + + static void start(void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + if (_this->running) { return; } + + // TODO: Implement start + _this->dev = hermes::open("192.168.0.144", 1024); + + // TODO: STOP USING A LINK, FIND A BETTER WAY + _this->lnk.setInput(&_this->dev->out); + _this->lnk.start(); + _this->dev->start(); + + // TODO: Check if the USB commands are accepted before start + _this->dev->setFrequency(_this->freq); + _this->dev->setGain(_this->gain); + _this->dev->writeReg(0, 3 << 24); + + _this->running = true; + spdlog::info("HermesSourceModule '{0}': Start!", _this->name); + } + + static void stop(void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + if (!_this->running) { return; } + _this->running = false; + + // TODO: Implement stop + _this->dev->stop(); + _this->dev->close(); + _this->lnk.stop(); + + spdlog::info("HermesSourceModule '{0}': Stop!", _this->name); + } + + static void tune(double freq, void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + if (_this->running) { + // TODO: Check if dev exists + _this->dev->setFrequency(_this->freq); + } + _this->freq = freq; + spdlog::info("HermesSourceModule '{0}': Tune: {1}!", _this->name, freq); + } + + static void menuHandler(void* ctx) { + HermesSourceModule* _this = (HermesSourceModule*)ctx; + + if (_this->running) { SmGui::BeginDisabled(); } + + // TODO: Device selection + if (SmGui::Button("Discover")) { + auto disc = hermes::discover(); + spdlog::warn("Found {0} devices!", disc.size()); + } + + if (_this->running) { SmGui::EndDisabled(); } + + // TODO: Device parameters + + if (SmGui::SliderInt("Gain##hermes_source", &_this->gain, 0, 60)) { + _this->dev->setGain(_this->gain); + } + + if (SmGui::Button("Hermes Test")) { + _this->dev->readReg(hermes::HL_REG_RX1_NCO_FREQ); + } + } + + std::string name; + bool enabled = true; + dsp::stream stream; + dsp::routing::StreamLink lnk; + double sampleRate; + SourceManager::SourceHandler handler; + bool running = false; + double freq; + + int gain = 0; + + std::shared_ptr dev; + + std::vector devList; + std::string devListTxt; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + def["devices"] = json({}); + def["device"] = ""; + config.setPath(core::args["root"].s() + "/hermes_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new HermesSourceModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(ModuleManager::Instance* instance) { + delete (HermesSourceModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file diff --git a/source_modules/hermes_source/src/net.cpp b/source_modules/hermes_source/src/net.cpp new file mode 100644 index 00000000..c26a1986 --- /dev/null +++ b/source_modules/hermes_source/src/net.cpp @@ -0,0 +1,294 @@ +#include "net.h" +#include + +#ifdef _WIN32 +#define WOULD_BLOCK (WSAGetLastError() == WSAEWOULDBLOCK) +#else +#define WOULD_BLOCK (errno == EWOULDBLOCK) +#endif + +namespace net { + bool _init = false; + + // === Private functions === + + void init() { + if (_init) { return; } +#ifdef _WIN32 + // Initialize WinSock2 + WSADATA wsa; + if (WSAStartup(MAKEWORD(2, 2), &wsa)) { + throw std::runtime_error("Could not initialize WinSock2"); + return; + } +#else + // Disable SIGPIPE to avoid closing when the remote host disconnects + signal(SIGPIPE, SIG_IGN); +#endif + _init = true; + } + + bool queryHost(uint32_t* addr, std::string host) { + hostent* ent = gethostbyname(host.c_str()); + if (!ent || !ent->h_addr_list[0]) { return false; } + *addr = *(uint32_t*)ent->h_addr_list[0]; + return true; + } + + void closeSocket(SockHandle_t sock) { +#ifdef _WIN32 + closesocket(sock); +#else + shutdown(sock, SHUT_RDWR); + close(sock); +#endif + } + + // === Socket functions === + + Socket::Socket(SockHandle_t sock, struct sockaddr_in* raddr) { + this->sock = sock; + if (raddr) { + this->raddr = (struct sockaddr_in*)malloc(sizeof(sockaddr_in)); + memcpy(this->raddr, raddr, sizeof(sockaddr_in)); + } + } + + Socket::~Socket() { + close(); + if (raddr) { free(raddr); } + } + + void Socket::close() { + if (!open) { return; } + open = false; + closeSocket(sock); + } + + bool Socket::isOpen() { + return open; + } + + SocketType Socket::type() { + return raddr ? SOCKET_TYPE_UDP : SOCKET_TYPE_TCP; + } + + int Socket::send(const uint8_t* data, size_t len) { + return sendto(sock, (const char*)data, len, 0, (sockaddr*)raddr, sizeof(sockaddr_in)); + } + + int Socket::sendstr(const std::string& str) { + return send((const uint8_t*)str.c_str(), str.length()); + } + + int Socket::recv(uint8_t* data, size_t maxLen, bool forceLen, int timeout) { + // Create FD set + fd_set set; + FD_ZERO(&set); + FD_SET(sock, &set); + + // Define timeout + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = timeout * 1000; + + int read = 0; + bool blocking = (timeout != NONBLOCKING); + do { + // Wait for data or error if + if (blocking) { + int err = select(sock+1, &set, NULL, &set, (timeout > 0) ? &tv : NULL); + if (err <= 0) { return err; } + } + + // Receive + int err = ::recv(sock, (char*)&data[read], maxLen - read, 0); + if (err <= 0 && !WOULD_BLOCK) { + close(); + return err; + } + read += err; + } + while (blocking && forceLen && read < maxLen); + return read; + } + + int Socket::recvline(std::string& str, int maxLen, int timeout) { + // Disallow nonblocking mode + if (timeout < 0) { return -1; } + + str.clear(); + int read = 0; + while (true) { + char c; + int err = recv((uint8_t*)&c, 1, timeout); + if (err <= 0) { return err; } + if (c == '\n') { break; } + str += c; + read++; + if (maxLen && read >= maxLen) { break; } + } + return read; + } + + // === Listener functions === + + Listener::Listener(SockHandle_t sock) { + this->sock = sock; + } + + Listener::~Listener() { + stop(); + } + + void Listener::stop() { + closeSocket(sock); + open = false; + } + + bool Listener::listening() { + return open; + } + + std::shared_ptr Listener::accept(int timeout) { + // Create FD set + fd_set set; + FD_ZERO(&set); + FD_SET(sock, &set); + + // Define timeout + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = timeout * 1000; + + // Wait for data or error + // TODO: Support non-blockign mode + int err = select(sock+1, &set, NULL, &set, (timeout > 0) ? &tv : NULL); + if (err <= 0) { return NULL; } + + // Accept + SockHandle_t s = ::accept(sock, NULL, 0); + if (!s) { + stop(); + return NULL; + } + + // Enable nonblocking mode +#ifdef _WIN32 + u_long enabled = 1; + ioctlsocket(s, FIONBIO, &enabled); +#else + fcntl(s, F_SETFL, O_NONBLOCK); +#endif + + return std::make_shared(s); + } + + // === Creation functions === + + std::shared_ptr listen(std::string host, int port) { + // Init library if needed + init(); + + // Get host address + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (!queryHost((uint32_t*)&addr.sin_addr.s_addr, host)) { return NULL; } + + // Create socket + SockHandle_t s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + // TODO: Support non-blockign mode + +#ifndef _WIN32 + // Allow port reusing if the app was killed or crashed + // and the socket is stuck in TIME_WAIT state. + // This option has a different meaning on Windows, + // so we use it only for non-Windows systems + int enable = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { + closeSocket(s); + throw std::runtime_error("Could not configure socket"); + return NULL; + } +#endif + + // Bind socket to the port + if (bind(s, (sockaddr*)&addr, sizeof(addr))) { + closeSocket(s); + throw std::runtime_error("Could not bind socket"); + return NULL; + } + + // Enable listening + if (::listen(s, SOMAXCONN) != 0) { + throw std::runtime_error("Could start listening for connections"); + return NULL; + } + + // Return listener class + return std::make_shared(s); + } + + std::shared_ptr connect(std::string host, int port) { + // Init library if needed + init(); + + // Get host address + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (!queryHost((uint32_t*)&addr.sin_addr.s_addr, host)) { return NULL; } + + // Create socket + SockHandle_t s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + // Connect to server + if (::connect(s, (sockaddr*)&addr, sizeof(addr))) { + closeSocket(s); + throw std::runtime_error("Could not connect"); + return NULL; + } + + // Enable nonblocking mode +#ifdef _WIN32 + u_long enabled = 1; + ioctlsocket(s, FIONBIO, &enabled); +#else + fcntl(s, F_SETFL, O_NONBLOCK); +#endif + + // Return socket class + return std::make_shared(s); + } + + std::shared_ptr openudp(std::string rhost, int rport, std::string lhost, int lport) { + // Init library if needed + init(); + + // Get local address + struct sockaddr_in laddr; + laddr.sin_family = AF_INET; + laddr.sin_port = htons(lport); + if (!queryHost((uint32_t*)&laddr.sin_addr.s_addr, lhost)) { return NULL; } + + // Get remote address + struct sockaddr_in raddr; + raddr.sin_family = AF_INET; + raddr.sin_port = htons(rport); + if (!queryHost((uint32_t*)&raddr.sin_addr.s_addr, rhost)) { return NULL; } + + // Create socket + SockHandle_t s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + + // Bind socket to local port + if (bind(s, (sockaddr*)&laddr, sizeof(laddr))) { + closeSocket(s); + throw std::runtime_error("Could not bind socket"); + return NULL; + } + + // Return socket class + return std::make_shared(s, &raddr); + } +} \ No newline at end of file diff --git a/source_modules/hermes_source/src/net.h b/source_modules/hermes_source/src/net.h new file mode 100644 index 00000000..4a0cde05 --- /dev/null +++ b/source_modules/hermes_source/src/net.h @@ -0,0 +1,155 @@ +#pragma once +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace net { +#ifdef _WIN32 + typedef SOCKET SockHandle_t; +#else + typedef int SockHandle_t; +#endif + + enum { + NO_TIMEOUT = -1, + NONBLOCKING = 0 + }; + + enum SocketType { + SOCKET_TYPE_TCP, + SOCKET_TYPE_UDP + }; + + class Socket { + public: + Socket(SockHandle_t sock, struct sockaddr_in* raddr = NULL); + ~Socket(); + + /** + * Close socket. The socket can no longer be used after this. + */ + void close(); + + /** + * Check if the socket is open. + * @return True if open, false if closed. + */ + bool isOpen(); + + /** + * Get socket type. Either TCP or UDP. + * @return Socket type. + */ + SocketType type(); + + /** + * Send data on socket. + * @param data Data to be sent. + * @param len Number of bytes to be sent. + * @return Number of bytes sent. + */ + int send(const uint8_t* data, size_t len); + + /** + * Send string on socket. Terminating null byte is not sent, include one in the string if you need it. + * @param str String to be sent. + * @return Number of bytes sent. + */ + int sendstr(const std::string& str); + + /** + * Receive data from socket. + * @param data Buffer to read the data into. + * @param maxLen Maximum number of bytes to read. + * @param forceLen Read the maximum number of bytes even if it requires multiple receive operations. + * @param timeout Timeout in milliseconds. Use NO_TIMEOUT or NONBLOCKING here if needed. + * @return Number of bytes read. 0 means timed out or closed. -1 means would block or error. + */ + int recv(uint8_t* data, size_t maxLen, bool forceLen = false, int timeout = NO_TIMEOUT); + + /** + * Receive line from socket. + * @param str String to read the data into. + * @param maxLen Maximum line length allowed, 0 for no limit. + * @param timeout Timeout in milliseconds. Use NO_TIMEOUT or NONBLOCKING here if needed. + * @return Length of the returned string. 0 means timed out or closed. -1 means would block or error. + */ + int recvline(std::string& str, int maxLen = 0, int timeout = NO_TIMEOUT); + + private: + struct sockaddr_in* raddr = NULL; + SockHandle_t sock; + bool open = true; + + }; + + class Listener { + public: + Listener(SockHandle_t sock); + ~Listener(); + + /** + * Stop listening. The listener can no longer be used after this. + */ + void stop(); + + /** + * CHeck if the listener is still listening. + * @return True if listening, false if not. + */ + bool listening(); + + /** + * Accept connection. + * @param timeout Timeout in milliseconds. 0 means no timeout. + * @return Socket of the connection. NULL means timed out or closed. + */ + std::shared_ptr accept(int timeout = NO_TIMEOUT); + + private: + SockHandle_t sock; + bool open = true; + + }; + + /** + * Create TCP listener. + * @param host Hostname or IP to listen on ("0.0.0.0" for Any). + * @param port Port to listen on. + * @return Listener instance on success, null otherwise. + */ + std::shared_ptr listen(std::string host, int port); + + /** + * Create TCP connection. + * @param host Remote hostname or IP address. + * @param port Remote port. + * @return Socket instance on success, null otherwise. + */ + std::shared_ptr connect(std::string host, int port); + + /** + * Create UDP socket. + * @param rhost Remote hostname or IP address. + * @param rport Remote port. + * @param lhost Local hostname or IP used to bind the socket (optional, "0.0.0.0" for Any). + * @param lpost Local port used to bind the socket (optional, 0 to allocate automatically). + * @return Socket instance on success, null otherwise. + */ + std::shared_ptr openudp(std::string rhost, int rport, std::string lhost = "0.0.0.0", int lport = 0); +} \ No newline at end of file