diff --git a/.gitignore b/.gitignore index d28d7cf8..1386eb14 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ m17_decoder/libcorrect SDR++.app android/deps android/app/assets -source_modules/dragonlabs_source source_modules/badgesdr_source decoder_modules/mystery_decoder -decoder_modules/tetra_decoder \ No newline at end of file +decoder_modules/tetra_decoder +misc_modules/modulation_monitor \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 384fb59e..5e3163ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(OPT_BUILD_AIRSPYHF_SOURCE "Build Airspy HF+ Source Module (Dependencies: option(OPT_BUILD_AUDIO_SOURCE "Build Audio Source Module (Dependencies: rtaudio)" ON) option(OPT_BUILD_BADGESDR_SOURCE "Build BadgeSDR Source Module (Dependencies: libusb)" OFF) option(OPT_BUILD_BLADERF_SOURCE "Build BladeRF Source Module (Dependencies: libbladeRF)" OFF) +option(OPT_BUILD_DRAGONLABS_SOURCE "Build Dragon Labs Source Module (Dependencies: libdlcr)" OFF) option(OPT_BUILD_FILE_SOURCE "Wav file source" ON) option(OPT_BUILD_FOBOSSDR_SOURCE "Build FobosSDR Source Module (Dependencies: libfobos)" OFF) option(OPT_BUILD_HACKRF_SOURCE "Build HackRF Source Module (Dependencies: libhackrf)" ON) @@ -143,6 +144,10 @@ if (OPT_BUILD_BLADERF_SOURCE) add_subdirectory("source_modules/bladerf_source") endif (OPT_BUILD_BLADERF_SOURCE) +if (OPT_BUILD_DRAGONLABS_SOURCE) +add_subdirectory("source_modules/dragonlabs_source") +endif (OPT_BUILD_DRAGONLABS_SOURCE) + if (OPT_BUILD_FILE_SOURCE) add_subdirectory("source_modules/file_source") endif (OPT_BUILD_FILE_SOURCE) diff --git a/core/src/dsp/chain.h b/core/src/dsp/chain.h index a3f5dc04..c09d60bf 100644 --- a/core/src/dsp/chain.h +++ b/core/src/dsp/chain.h @@ -163,10 +163,10 @@ namespace dsp { private: Processor* blockBefore(Processor* block) { - // TODO: This is wrong and must be fixed when I get more time + Processor* prev = NULL; for (auto& ln : links) { - if (ln == block) { return NULL; } - if (states[ln]) { return ln; } + if (ln == block) { return prev; } + if (states[ln]) { prev = ln; } } return NULL; } diff --git a/core/src/dsp/demod/fm.h b/core/src/dsp/demod/fm.h index 96b025a8..d66bece7 100644 --- a/core/src/dsp/demod/fm.h +++ b/core/src/dsp/demod/fm.h @@ -22,18 +22,17 @@ namespace dsp::demod { dsp::taps::free(filterTaps); } - void init(dsp::stream* in, double samplerate, double bandwidth, bool lowPass, bool highPass) { + void init(dsp::stream* in, double samplerate, double bandwidth, bool lowPass) { _samplerate = samplerate; _bandwidth = bandwidth; _lowPass = lowPass; - _highPass = highPass; demod.init(NULL, bandwidth / 2.0, _samplerate); loadDummyTaps(); fir.init(NULL, filterTaps); // Initialize taps - updateFilter(lowPass, highPass); + updateFilter(lowPass); if constexpr (std::is_same_v) { demod.out.free(); @@ -59,19 +58,13 @@ namespace dsp::demod { if (bandwidth == _bandwidth) { return; } _bandwidth = bandwidth; demod.setDeviation(_bandwidth / 2.0, _samplerate); - updateFilter(_lowPass, _highPass); + updateFilter(_lowPass); } void setLowPass(bool lowPass) { assert(base_type::_block_init); std::lock_guard lck(base_type::ctrlMtx); - updateFilter(lowPass, _highPass); - } - - void setHighPass(bool highPass) { - assert(base_type::_block_init); - std::lock_guard lck(base_type::ctrlMtx); - updateFilter(_lowPass, highPass); + updateFilter(lowPass); } void reset() { @@ -86,14 +79,14 @@ namespace dsp::demod { inline int process(int count, dsp::complex_t* in, T* out) { if constexpr (std::is_same_v) { demod.process(count, in, out); - if (filtering) { + if (_lowPass) { std::lock_guard lck(filterMtx); fir.process(count, out, out); } } if constexpr (std::is_same_v) { demod.process(count, in, demod.out.writeBuf); - if (filtering) { + if (_lowPass) { std::lock_guard lck(filterMtx); fir.process(count, demod.out.writeBuf, demod.out.writeBuf); } @@ -114,25 +107,17 @@ namespace dsp::demod { } private: - void updateFilter(bool lowPass, bool highPass) { + void updateFilter(bool lowPass) { std::lock_guard lck(filterMtx); // Update values _lowPass = lowPass; - _highPass = highPass; - filtering = (lowPass || highPass); // Free filter taps dsp::taps::free(filterTaps); - // Generate filter depending on low and high pass settings - if (_lowPass && _highPass) { - filterTaps = dsp::taps::bandPass(300.0, _bandwidth / 2.0, 100.0, _samplerate); - } - else if (_highPass) { - filterTaps = dsp::taps::highPass(300.0, 100.0, _samplerate); - } - else if (_lowPass) { + // Generate filter depending on the low pass settings + if (_lowPass) { filterTaps = dsp::taps::lowPass(_bandwidth / 2.0, (_bandwidth / 2.0) * 0.1, _samplerate); } else { @@ -152,7 +137,6 @@ namespace dsp::demod { double _samplerate; double _bandwidth; bool _lowPass; - bool _highPass; bool filtering; Quadrature demod; diff --git a/core/src/dsp/noise_reduction/ctcss_squelch.h b/core/src/dsp/noise_reduction/ctcss_squelch.h new file mode 100644 index 00000000..4b0d9b39 --- /dev/null +++ b/core/src/dsp/noise_reduction/ctcss_squelch.h @@ -0,0 +1,303 @@ +#pragma once +#include "../channel/rx_vfo.h" +#include "../demod/quadrature.h" +#include "../filter/fir.h" +#include "../taps/high_pass.h" +#include +#include +#include + +#define CTCSS_DECODE_SAMPLERATE 500//250.0 +#define CTCSS_DECODE_BANDWIDTH 200.0 +#define CTCSS_DECODE_OFFSET 160.55 + +namespace dsp::noise_reduction { + enum CTCSSTone { + /** + * Indicates that any valid tone will let audio through. + */ + CTCSS_TONE_ANY = -2, + + /** + * Indicates that no tone is being received, or to act as a decoder only, letting audio through continuously. + */ + CTCSS_TONE_NONE = -1, + + /** + * CTCSS Tone Frequency. + */ + CTCSS_TONE_67Hz, + CTCSS_TONE_69_3Hz, + CTCSS_TONE_71_9Hz, + CTCSS_TONE_74_4Hz, + CTCSS_TONE_77Hz, + CTCSS_TONE_79_7Hz, + CTCSS_TONE_82_5Hz, + CTCSS_TONE_85_4Hz, + CTCSS_TONE_88_5Hz, + CTCSS_TONE_91_5Hz, + CTCSS_TONE_94_8Hz, + CTCSS_TONE_97_4Hz, + CTCSS_TONE_100Hz, + CTCSS_TONE_103_5Hz, + CTCSS_TONE_107_2Hz, + CTCSS_TONE_110_9Hz, + CTCSS_TONE_114_8Hz, + CTCSS_TONE_118_8Hz, + CTCSS_TONE_123Hz, + CTCSS_TONE_127_3Hz, + CTCSS_TONE_131_8Hz, + CTCSS_TONE_136_5Hz, + CTCSS_TONE_141_3Hz, + CTCSS_TONE_146_2Hz, + CTCSS_TONE_150Hz, + CTCSS_TONE_151_4Hz, + CTCSS_TONE_156_7Hz, + CTCSS_TONE_159_8Hz, + CTCSS_TONE_162_2Hz, + CTCSS_TONE_165_5Hz, + CTCSS_TONE_167_9Hz, + CTCSS_TONE_171_3Hz, + CTCSS_TONE_173_8Hz, + CTCSS_TONE_177_3Hz, + CTCSS_TONE_179_9Hz, + CTCSS_TONE_183_5Hz, + CTCSS_TONE_186_2Hz, + CTCSS_TONE_189_9Hz, + CTCSS_TONE_192_8Hz, + CTCSS_TONE_196_6Hz, + CTCSS_TONE_199_5Hz, + CTCSS_TONE_203_5Hz, + CTCSS_TONE_206_5Hz, + CTCSS_TONE_210_7Hz, + CTCSS_TONE_218_1Hz, + CTCSS_TONE_225_7Hz, + CTCSS_TONE_229_1Hz, + CTCSS_TONE_233_6Hz, + CTCSS_TONE_241_8Hz, + CTCSS_TONE_250_3Hz, + CTCSS_TONE_254_1Hz, + _CTCSS_TONE_COUNT + }; + + const float CTCSS_TONES[_CTCSS_TONE_COUNT] = { + 67.0f, + 69.3f, + 71.9f, + 74.4f, + 77.0f, + 79.7f, + 82.5f, + 85.4f, + 88.5f, + 91.5f, + 94.8f, + 97.4f, + 100.0f, + 103.5f, + 107.2f, + 110.9f, + 114.8f, + 118.8f, + 123.0f, + 127.3f, + 131.8f, + 136.5f, + 141.3f, + 146.2f, + 150.0f, + 151.4f, + 156.7f, + 159.8f, + 162.2f, + 165.5f, + 167.9f, + 171.3f, + 173.8f, + 177.3f, + 179.9f, + 183.5f, + 186.2f, + 189.9f, + 192.8f, + 196.6f, + 199.5f, + 203.5f, + 206.5f, + 210.7f, + 218.1f, + 225.7f, + 229.1f, + 233.6f, + 241.8f, + 250.3f, + 254.1f + }; + + class CTCSSSquelch : public Processor { + using base_type = Processor; + public: + CTCSSSquelch() {} + + CTCSSSquelch(stream* in, double samplerate) { init(in, samplerate); } + + ~CTCSSSquelch() { + // If not initialized, do nothing + if (!base_type::_block_init) { return; } + + // Stop the DSP thread + base_type::stop(); + } + + void init(stream* in, double samplerate) { + // Save settings + _samplerate = samplerate; + + // Create dummy taps just for initialization + float dummy[1] = { 1.0f }; + auto dummyTaps = dsp::taps::fromArray(1, dummy); + + // Initialize the DDC and FM demod + ddc.init(NULL, samplerate, CTCSS_DECODE_SAMPLERATE, CTCSS_DECODE_BANDWIDTH, CTCSS_DECODE_OFFSET); + fm.init(NULL, 1.0, CTCSS_DECODE_SAMPLERATE); + + // Initilize the base block class + base_type::init(in); + } + + void setSamplerate(double samplerate) { + assert(base_type::_block_init); + std::lock_guard lck(base_type::ctrlMtx); + base_type::tempStop(); + _samplerate = samplerate; + ddc.setInSamplerate(samplerate); + base_type::tempStart(); + } + + void setRequiredTone(CTCSSTone tone) { + assert(base_type::_block_init); + requiredTone = tone; + } + + CTCSSTone getCurrentTone() { + assert(base_type::_block_init); + return currentTone; + } + + inline int process(int count, const stereo_t* in, stereo_t* out) { + // Shift and resample to the correct samplerate + int ddcOutCount = ddc.process(count, (complex_t*)in, ddc.out.writeBuf); + + // FM Demod the CTCSS tone + fm.process(ddcOutCount, ddc.out.writeBuf, fm.out.writeBuf); + + // Get the required tone + const CTCSSTone rtone = requiredTone; + + // Detect the tone frequency + for (int i = 0; i < ddcOutCount; i++) { + // Compute the running mean + const float val = fm.out.writeBuf[i]; + mean = 0.95f*mean + 0.05f*val; + + // Compute the running variance + const float err = val - mean; + var = 0.95f*var + 0.05f*err*err; + + // Run a schmitt trigger on the variance + bool nvarOk = varOk ? (var < 1100.0f) : (var < 1000.0f); + + // Check if the tone has to be rematched + if (nvarOk && (!varOk || mean < minFreq || mean > maxFreq)) { + // Compute the absolute frequency + float freq = mean + CTCSS_DECODE_OFFSET; + + // Check it against the known tones + if (freq < CTCSS_TONES[0] - 2.5) { + currentTone = CTCSS_TONE_NONE; + } + else if (freq > CTCSS_TONES[_CTCSS_TONE_COUNT-1] + 2.5) { + currentTone = CTCSS_TONE_NONE; + } + else if (freq < CTCSS_TONES[0]) { + currentTone = (CTCSSTone)0; + } + else if (freq > CTCSS_TONES[_CTCSS_TONE_COUNT-1]) { + currentTone = (CTCSSTone)(_CTCSS_TONE_COUNT-1); + } + else { + int a = 0; + int b = _CTCSS_TONE_COUNT-1; + while (b - a > 1) { + int c = (a + b) >> 1; + ((CTCSS_TONES[c] < freq) ? a : b) = c; + } + currentTone = (CTCSSTone)((freq - CTCSS_TONES[a] < CTCSS_TONES[b] - freq) ? a : b); + } + + // Update the mute status + mute = !(currentTone == rtone || (currentTone != CTCSS_TONE_NONE && rtone == CTCSS_TONE_ANY)); + + // Unmuted the audio if needed + // TODO + + // Recompute min and max freq if a valid tone is detected + if (currentTone != CTCSS_TONE_NONE) { + float c = CTCSS_TONES[currentTone]; + float l = (currentTone > CTCSS_TONE_67Hz) ? CTCSS_TONES[currentTone - 1] : c - 2.5f; + float r = (currentTone < CTCSS_TONE_254_1Hz) ? CTCSS_TONES[currentTone + 1] : c + 2.5f; + minFreq = (l+c) / 2.0f; + maxFreq = (r+c) / 2.0f; + } + } + + // Check for a rising edge on the variance + if (!nvarOk && varOk) { + // Mute the audio + // TODO + mute = true; + currentTone = CTCSS_TONE_NONE; + } + + // Save the new variance state + varOk = nvarOk; + } + + // DEBUG ONLY + if ((rtone != CTCSS_TONE_NONE) && mute) { + memset(out, 0, count * sizeof(stereo_t)); + } + else { + memcpy(out, in, count * sizeof(stereo_t)); + } + + return count; + } + + int run() { + int count = base_type::_in->read(); + if (count < 0) { return -1; } + process(count, base_type::_in->readBuf, base_type::out.writeBuf); + base_type::_in->flush(); + if (!base_type::out.swap(count)) { return -1; } + return count; + } + + + + private: + double _samplerate; + std::atomic requiredTone = CTCSS_TONE_ANY; + + float mean = 0.0f; + float var = 0.0f; + bool varOk = false; + float minFreq = 0.0f; + float maxFreq = 0.0f; + bool mute = true; + std::atomic currentTone = CTCSS_TONE_NONE; + + channel::RxVFO ddc; + demod::Quadrature fm; + }; +} \ No newline at end of file diff --git a/core/src/dsp/noise_reduction/squelch.h b/core/src/dsp/noise_reduction/power_squelch.h similarity index 83% rename from core/src/dsp/noise_reduction/squelch.h rename to core/src/dsp/noise_reduction/power_squelch.h index c4ddbee9..f29b12fc 100644 --- a/core/src/dsp/noise_reduction/squelch.h +++ b/core/src/dsp/noise_reduction/power_squelch.h @@ -3,14 +3,14 @@ // TODO: Rewrite better!!!!! namespace dsp::noise_reduction { - class Squelch : public Processor { + class PowerSquelch : public Processor { using base_type = Processor; public: - Squelch() {} + PowerSquelch() {} - Squelch(stream* in, double level) {} + PowerSquelch(stream* in, double level) {} - ~Squelch() { + ~PowerSquelch() { if (!base_type::_block_init) { return; } base_type::stop(); buffer::free(normBuffer); @@ -31,8 +31,11 @@ namespace dsp::noise_reduction { } inline int process(int count, const complex_t* in, complex_t* out) { - float sum; + // Compute the amplitude of each sample volk_32fc_magnitude_32f(normBuffer, (lv_32fc_t*)in, count); + + // Compute the mean amplitude + float sum = 0.0f; volk_32f_accumulator_s32f(&sum, normBuffer, count); sum /= (float)count; @@ -46,8 +49,6 @@ namespace dsp::noise_reduction { return count; } - //DEFAULT_PROC_RUN(); - int run() { int count = base_type::_in->read(); if (count < 0) { return -1; } diff --git a/core/src/signal_path/source.cpp b/core/src/signal_path/source.cpp index a4b08afe..900fe775 100644 --- a/core/src/signal_path/source.cpp +++ b/core/src/signal_path/source.cpp @@ -85,8 +85,8 @@ void SourceManager::tune(double freq) { return; } // TODO: No need to always retune the hardware in Panadapter mode - selectedHandler->tuneHandler(abs(((tuneMode == TuningMode::NORMAL) ? freq : ifFreq) + tuneOffset), selectedHandler->ctx); - onRetune.emit(freq); + selectedHandler->tuneHandler(abs(((tuneMode == TuningMode::NORMAL) ? (freq + tuneOffset) : ifFreq)), selectedHandler->ctx); + onRetune.emit(freq + tuneOffset); currentFreq = freq; } diff --git a/core/src/version.h b/core/src/version.h index 2e14cb6d..fabe97af 100644 --- a/core/src/version.h +++ b/core/src/version.h @@ -1,3 +1,3 @@ #pragma once -#define VERSION_STR "1.2.1" \ No newline at end of file +#define VERSION_STR "1.3.0" \ No newline at end of file diff --git a/decoder_modules/radio/src/demod.h b/decoder_modules/radio/src/demod.h index b250aefa..f9955990 100644 --- a/decoder_modules/radio/src/demod.h +++ b/decoder_modules/radio/src/demod.h @@ -20,6 +20,16 @@ enum IFNRPreset { IFNR_PRESET_BROADCAST }; +enum SquelchMode { + SQUELCH_MODE_OFF, + SQUELCH_MODE_POWER, + SQUELCH_MODE_SNR, + SQUELCH_MODE_CTCSS_MUTE, + SQUELCH_MODE_CTCSS_DECODE, + SQUELCH_MODE_DCS_MUTE, + SQUELCH_MODE_DCS_DECODE, +}; + namespace demod { class Demodulator { public: @@ -45,6 +55,8 @@ namespace demod { virtual int getDefaultDeemphasisMode() = 0; virtual bool getFMIFNRAllowed() = 0; virtual bool getNBAllowed() = 0; + virtual bool getHighPassAllowed() = 0; + virtual bool getSquelchAllowed() = 0; virtual dsp::stream* getOutput() = 0; }; } diff --git a/decoder_modules/radio/src/demodulators/am.h b/decoder_modules/radio/src/demodulators/am.h index 81d71806..1b6e8113 100644 --- a/decoder_modules/radio/src/demodulators/am.h +++ b/decoder_modules/radio/src/demodulators/am.h @@ -40,6 +40,12 @@ namespace demod { void showMenu() { float menuWidth = ImGui::GetContentRegionAvail().x; + if (ImGui::Checkbox(("Carrier AGC##_radio_am_carrier_agc_" + name).c_str(), &carrierAgc)) { + demod.setAGCMode(carrierAgc ? dsp::demod::AM::AGCMode::CARRIER : dsp::demod::AM::AGCMode::AUDIO); + _config->acquire(); + _config->conf[name][getName()]["carrierAgc"] = carrierAgc; + _config->release(true); + } ImGui::LeftLabel("AGC Attack"); ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); if (ImGui::SliderFloat(("##_radio_am_agc_attack_" + name).c_str(), &agcAttack, 1.0f, 200.0f)) { @@ -56,12 +62,6 @@ namespace demod { _config->conf[name][getName()]["agcDecay"] = agcDecay; _config->release(true); } - if (ImGui::Checkbox(("Carrier AGC##_radio_am_carrier_agc_" + name).c_str(), &carrierAgc)) { - demod.setAGCMode(carrierAgc ? dsp::demod::AM::AGCMode::CARRIER : dsp::demod::AM::AGCMode::AUDIO); - _config->acquire(); - _config->conf[name][getName()]["carrierAgc"] = carrierAgc; - _config->release(true); - } } void setBandwidth(double bandwidth) { demod.setBandwidth(bandwidth); } @@ -86,6 +86,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return false; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } private: diff --git a/decoder_modules/radio/src/demodulators/cw.h b/decoder_modules/radio/src/demodulators/cw.h index df2bdcfb..7f93d615 100644 --- a/decoder_modules/radio/src/demodulators/cw.h +++ b/decoder_modules/radio/src/demodulators/cw.h @@ -92,6 +92,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return false; } + bool getHighPassAllowed() { return false; } + bool getSquelchAllowed() { return false; } dsp::stream* getOutput() { return &demod.out; } private: diff --git a/decoder_modules/radio/src/demodulators/dsb.h b/decoder_modules/radio/src/demodulators/dsb.h index 2c56565e..fd08e04c 100644 --- a/decoder_modules/radio/src/demodulators/dsb.h +++ b/decoder_modules/radio/src/demodulators/dsb.h @@ -79,6 +79,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return true; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } private: diff --git a/decoder_modules/radio/src/demodulators/lsb.h b/decoder_modules/radio/src/demodulators/lsb.h index e442a87f..3945b6f3 100644 --- a/decoder_modules/radio/src/demodulators/lsb.h +++ b/decoder_modules/radio/src/demodulators/lsb.h @@ -79,6 +79,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return true; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } private: diff --git a/decoder_modules/radio/src/demodulators/nfm.h b/decoder_modules/radio/src/demodulators/nfm.h index c631c6b1..3fa0c105 100644 --- a/decoder_modules/radio/src/demodulators/nfm.h +++ b/decoder_modules/radio/src/demodulators/nfm.h @@ -22,14 +22,11 @@ namespace demod { if (config->conf[name][getName()].contains("lowPass")) { _lowPass = config->conf[name][getName()]["lowPass"]; } - if (config->conf[name][getName()].contains("highPass")) { - _highPass = config->conf[name][getName()]["highPass"]; - } _config->release(); // Define structure - demod.init(input, getIFSampleRate(), bandwidth, _lowPass, _highPass); + demod.init(input, getIFSampleRate(), bandwidth, _lowPass); } void start() { demod.start(); } @@ -43,12 +40,6 @@ namespace demod { _config->conf[name][getName()]["lowPass"] = _lowPass; _config->release(true); } - if (ImGui::Checkbox(("High Pass##_radio_wfm_highpass_" + name).c_str(), &_highPass)) { - demod.setHighPass(_highPass); - _config->acquire(); - _config->conf[name][getName()]["highPass"] = _highPass; - _config->release(true); - } } void setBandwidth(double bandwidth) { @@ -75,6 +66,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return true; } bool getNBAllowed() { return false; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } private: @@ -83,7 +76,6 @@ namespace demod { ConfigManager* _config = NULL; bool _lowPass = true; - bool _highPass = false; std::string name; }; diff --git a/decoder_modules/radio/src/demodulators/raw.h b/decoder_modules/radio/src/demodulators/raw.h index 094494de..652eec5f 100644 --- a/decoder_modules/radio/src/demodulators/raw.h +++ b/decoder_modules/radio/src/demodulators/raw.h @@ -59,6 +59,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return true; } + bool getHighPassAllowed() { return false; } + bool getSquelchAllowed() { return false; } dsp::stream* getOutput() { return &c2s.out; } private: diff --git a/decoder_modules/radio/src/demodulators/usb.h b/decoder_modules/radio/src/demodulators/usb.h index 41195ba6..fe9fa8b8 100644 --- a/decoder_modules/radio/src/demodulators/usb.h +++ b/decoder_modules/radio/src/demodulators/usb.h @@ -80,6 +80,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_NONE; } bool getFMIFNRAllowed() { return false; } bool getNBAllowed() { return true; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } private: diff --git a/decoder_modules/radio/src/demodulators/wfm.h b/decoder_modules/radio/src/demodulators/wfm.h index e8289b38..f5014a3a 100644 --- a/decoder_modules/radio/src/demodulators/wfm.h +++ b/decoder_modules/radio/src/demodulators/wfm.h @@ -100,18 +100,18 @@ namespace demod { } void showMenu() { - if (ImGui::Checkbox(("Stereo##_radio_wfm_stereo_" + name).c_str(), &_stereo)) { - setStereo(_stereo); - _config->acquire(); - _config->conf[name][getName()]["stereo"] = _stereo; - _config->release(true); - } if (ImGui::Checkbox(("Low Pass##_radio_wfm_lowpass_" + name).c_str(), &_lowPass)) { demod.setLowPass(_lowPass); _config->acquire(); _config->conf[name][getName()]["lowPass"] = _lowPass; _config->release(true); } + if (ImGui::Checkbox(("Stereo##_radio_wfm_stereo_" + name).c_str(), &_stereo)) { + setStereo(_stereo); + _config->acquire(); + _config->conf[name][getName()]["stereo"] = _stereo; + _config->release(true); + } if (ImGui::Checkbox(("Decode RDS##_radio_wfm_rds_" + name).c_str(), &_rds)) { demod.setRDSOut(_rds); _config->acquire(); @@ -270,6 +270,8 @@ namespace demod { int getDefaultDeemphasisMode() { return DEEMP_MODE_50US; } bool getFMIFNRAllowed() { return true; } bool getNBAllowed() { return false; } + bool getHighPassAllowed() { return true; } + bool getSquelchAllowed() { return true; } dsp::stream* getOutput() { return &demod.out; } // ============= DEDICATED FUNCTIONS ============= diff --git a/decoder_modules/radio/src/radio_interface.h b/decoder_modules/radio/src/radio_interface.h index 0f33294d..7e7b436e 100644 --- a/decoder_modules/radio/src/radio_interface.h +++ b/decoder_modules/radio/src/radio_interface.h @@ -5,10 +5,14 @@ enum { RADIO_IFACE_CMD_SET_MODE, RADIO_IFACE_CMD_GET_BANDWIDTH, RADIO_IFACE_CMD_SET_BANDWIDTH, - RADIO_IFACE_CMD_GET_SQUELCH_ENABLED, - RADIO_IFACE_CMD_SET_SQUELCH_ENABLED, + RADIO_IFACE_CMD_GET_SQUELCH_MODE, + RADIO_IFACE_CMD_SET_SQUELCH_MODE, RADIO_IFACE_CMD_GET_SQUELCH_LEVEL, RADIO_IFACE_CMD_SET_SQUELCH_LEVEL, + RADIO_IFACE_CMD_GET_CTCSS_TONE, + RADIO_IFACE_CMD_SET_CTCSS_TONE, + RADIO_IFACE_CMD_GET_HIGHPASS, + RADIO_IFACE_CMD_SET_HIGHPASS }; enum { diff --git a/decoder_modules/radio/src/radio_module.h b/decoder_modules/radio/src/radio_module.h index f469a459..52016b46 100644 --- a/decoder_modules/radio/src/radio_module.h +++ b/decoder_modules/radio/src/radio_module.h @@ -8,7 +8,8 @@ #include #include #include -#include +#include +#include #include #include #include @@ -49,6 +50,22 @@ public: ifnrPresets.define("Voice", IFNR_PRESET_VOICE); ifnrPresets.define("Narrow Band", IFNR_PRESET_NARROW_BAND); + squelchModes.define("off", "Off", SQUELCH_MODE_OFF); + squelchModes.define("power", "Power", SQUELCH_MODE_POWER); + //squelchModes.define("snr", "SNR", SQUELCH_MODE_SNR); + squelchModes.define("ctcss_mute", "CTCSS (Mute)", SQUELCH_MODE_CTCSS_MUTE); + squelchModes.define("ctcss_decode", "CTCSS (Decode Only)", SQUELCH_MODE_CTCSS_DECODE); + //squelchModes.define("dcs_mute", "DCS (Mute)", SQUELCH_MODE_DCS_MUTE); + //squelchModes.define("dcs_decode", "DCS (Decode Only)", SQUELCH_MODE_DCS_DECODE); + + for (int i = 0; i < dsp::noise_reduction::_CTCSS_TONE_COUNT; i++) { + float tone = dsp::noise_reduction::CTCSS_TONES[i]; + char buf[64]; + sprintf(buf, "%.1fHz", tone); + ctcssTones.define((int)round(tone) * 10, buf, (dsp::noise_reduction::CTCSSTone)i); + } + ctcssTones.define(-1, "Any", dsp::noise_reduction::CTCSS_TONE_ANY); + // Initialize the config if it doesn't exist bool created = false; config.acquire(); @@ -72,19 +89,24 @@ public: nb.init(NULL, 500.0 / 24000.0, 10.0); fmnr.init(NULL, 32); - squelch.init(NULL, MIN_SQUELCH); + powerSquelch.init(NULL, MIN_SQUELCH); ifChain.addBlock(&nb, false); - ifChain.addBlock(&squelch, false); + ifChain.addBlock(&powerSquelch, false); ifChain.addBlock(&fmnr, false); // Initialize audio DSP chain afChain.init(&dummyAudioStream); + ctcss.init(NULL, 50000.0); resamp.init(NULL, 250000.0, 48000.0); + hpTaps = dsp::taps::highPass(300.0, 100.0, 48000.0); + hpf.init(NULL, hpTaps); deemp.init(NULL, 50e-6, 48000.0); + afChain.addBlock(&ctcss, false); afChain.addBlock(&resamp, true); + afChain.addBlock(&hpf, false); afChain.addBlock(&deemp, false); // Initialize the sink @@ -233,6 +255,33 @@ private: } } + // Squelch + if (_this->squelchAllowed) { + ImGui::LeftLabel("Squelch Mode"); + ImGui::FillWidth(); + if (ImGui::Combo(("##_radio_sqelch_mode_" + _this->name).c_str(), &_this->squelchModeId, _this->squelchModes.txt)) { + _this->setSquelchMode(_this->squelchModes[_this->squelchModeId]); + } + switch (_this->squelchModes[_this->squelchModeId]) { + case SQUELCH_MODE_POWER: + ImGui::LeftLabel("Squelch Level"); + ImGui::FillWidth(); + if (ImGui::SliderFloat(("##_radio_sqelch_lvl_" + _this->name).c_str(), &_this->squelchLevel, _this->MIN_SQUELCH, _this->MAX_SQUELCH, "%.3fdB")) { + _this->setSquelchLevel(_this->squelchLevel); + } + break; + + case SQUELCH_MODE_CTCSS_MUTE: + if (_this->squelchModes[_this->squelchModeId] == SQUELCH_MODE_CTCSS_MUTE) { + ImGui::LeftLabel("CTCSS Tone"); + ImGui::FillWidth(); + if (ImGui::Combo(("##_radio_ctcss_tone_" + _this->name).c_str(), &_this->ctcssToneId, _this->ctcssTones.txt)) { + _this->setCTCSSTone(_this->ctcssTones[_this->ctcssToneId]); + } + } + } + } + // Noise blanker if (_this->nbAllowed) { if (ImGui::Checkbox(("Noise blanker (W.I.P.)##_radio_nb_ena_" + _this->name).c_str(), &_this->nbEnabled)) { @@ -246,19 +295,6 @@ private: } if (!_this->nbEnabled && _this->enabled) { style::endDisabled(); } } - - - // Squelch - if (ImGui::Checkbox(("Squelch##_radio_sqelch_ena_" + _this->name).c_str(), &_this->squelchEnabled)) { - _this->setSquelchEnabled(_this->squelchEnabled); - } - if (!_this->squelchEnabled && _this->enabled) { style::beginDisabled(); } - ImGui::SameLine(); - ImGui::SetNextItemWidth(menuWidth - ImGui::GetCursorPosX()); - if (ImGui::SliderFloat(("##_radio_sqelch_lvl_" + _this->name).c_str(), &_this->squelchLevel, _this->MIN_SQUELCH, _this->MAX_SQUELCH, "%.3fdB")) { - _this->setSquelchLevel(_this->squelchLevel); - } - if (!_this->squelchEnabled && _this->enabled) { style::endDisabled(); } // FM IF Noise Reduction if (_this->FMIFNRAllowed) { @@ -276,9 +312,53 @@ private: } } + // High pass + if (_this->highPassAllowed) { + if (ImGui::Checkbox(("High Pass##_radio_hpf_" + _this->name).c_str(), &_this->highPass)) { + _this->setHighPass(_this->highPass); + } + } + // Demodulator specific menu _this->selectedDemod->showMenu(); + // Display the squelch diagnostics + switch (_this->squelchModes[_this->squelchModeId]) { + case SQUELCH_MODE_CTCSS_MUTE: + ImGui::TextUnformatted("Received Tone:"); + ImGui::SameLine(); + { + auto ctone = _this->ctcss.getCurrentTone(); + auto dtone = _this->ctcssTones[_this->ctcssToneId]; + if (ctone != dsp::noise_reduction::CTCSS_TONE_NONE) { + if (dtone == dsp::noise_reduction::CTCSS_TONE_ANY || ctone == dtone) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "%.1fHz", dsp::noise_reduction::CTCSS_TONES[_this->ctcss.getCurrentTone()]); + } + else { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "%.1fHz", dsp::noise_reduction::CTCSS_TONES[_this->ctcss.getCurrentTone()]); + } + } + else { + ImGui::TextUnformatted("None"); + } + } + break; + + case SQUELCH_MODE_CTCSS_DECODE: + ImGui::TextUnformatted("Received Tone:"); + ImGui::SameLine(); + { + auto ctone = _this->ctcss.getCurrentTone(); + if (ctone != dsp::noise_reduction::CTCSS_TONE_NONE) { + ImGui::TextColored(ImVec4(0, 1, 0, 1), "%.1fHz", dsp::noise_reduction::CTCSS_TONES[_this->ctcss.getCurrentTone()]); + } + else { + ImGui::TextUnformatted("None"); + } + } + break; + } + if (!_this->enabled) { style::endDisabled(); } } @@ -287,13 +367,13 @@ private: switch (id) { case DemodID::RADIO_DEMOD_NFM: demod = new demod::NFM(); break; case DemodID::RADIO_DEMOD_WFM: demod = new demod::WFM(); break; - case DemodID::RADIO_DEMOD_AM: demod = new demod::AM(); break; + case DemodID::RADIO_DEMOD_AM: demod = new demod::AM(); break; case DemodID::RADIO_DEMOD_DSB: demod = new demod::DSB(); break; case DemodID::RADIO_DEMOD_USB: demod = new demod::USB(); break; - case DemodID::RADIO_DEMOD_CW: demod = new demod::CW(); break; + case DemodID::RADIO_DEMOD_CW: demod = new demod::CW(); break; case DemodID::RADIO_DEMOD_LSB: demod = new demod::LSB(); break; case DemodID::RADIO_DEMOD_RAW: demod = new demod::RAW(); break; - default: demod = NULL; break; + default: demod = NULL; break; } if (!demod) { return NULL; } @@ -360,15 +440,20 @@ private: maxBandwidth = selectedDemod->getMaxBandwidth(); bandwidthLocked = selectedDemod->getBandwidthLocked(); snapInterval = selectedDemod->getDefaultSnapInterval(); - squelchLevel = MIN_SQUELCH; deempAllowed = selectedDemod->getDeempAllowed(); deempId = deempModes.valueId((DeemphasisMode)selectedDemod->getDefaultDeemphasisMode()); - squelchEnabled = false; + squelchModeId = squelchModes.valueId(SQUELCH_MODE_OFF); + squelchLevel = MIN_SQUELCH; + ctcssToneId = ctcssTones.valueId(dsp::noise_reduction::CTCSS_TONE_67Hz); + highPass = false; + postProcEnabled = selectedDemod->getPostProcEnabled(); FMIFNRAllowed = selectedDemod->getFMIFNRAllowed(); FMIFNREnabled = false; fmIFPresetId = ifnrPresets.valueId(IFNR_PRESET_VOICE); nbAllowed = selectedDemod->getNBAllowed(); + squelchAllowed = selectedDemod->getSquelchAllowed(); + highPassAllowed = selectedDemod->getHighPassAllowed(); nbEnabled = false; nbLevel = 0.0f; double ifSamplerate = selectedDemod->getIFSampleRate(); @@ -380,11 +465,24 @@ private: if (config.conf[name][selectedDemod->getName()].contains("snapInterval")) { snapInterval = config.conf[name][selectedDemod->getName()]["snapInterval"]; } + + if (config.conf[name][selectedDemod->getName()].contains("squelchMode")) { + std::string squelchModeStr = config.conf[name][selectedDemod->getName()]["squelchMode"]; + if (squelchModes.keyExists(squelchModeStr)) { + squelchModeId = squelchModes.keyId(squelchModeStr); + } + } if (config.conf[name][selectedDemod->getName()].contains("squelchLevel")) { squelchLevel = config.conf[name][selectedDemod->getName()]["squelchLevel"]; } - if (config.conf[name][selectedDemod->getName()].contains("squelchEnabled")) { - squelchEnabled = config.conf[name][selectedDemod->getName()]["squelchEnabled"]; + if (config.conf[name][selectedDemod->getName()].contains("ctcssTone")) { + int ctcssToneX10 = config.conf[name][selectedDemod->getName()]["ctcssTone"]; + if (ctcssTones.keyExists(ctcssToneX10)) { + ctcssToneId = ctcssTones.keyId(ctcssToneX10); + } + } + if (config.conf[name][selectedDemod->getName()].contains("highPass")) { + highPass = config.conf[name][selectedDemod->getName()]["highPass"]; } if (config.conf[name][selectedDemod->getName()].contains("deempMode")) { if (!config.conf[name][selectedDemod->getName()]["deempMode"].is_string()) { @@ -434,16 +532,22 @@ private: setFMIFNREnabled(FMIFNRAllowed ? FMIFNREnabled : false); // Configure squelch + setSquelchMode(squelchAllowed ? squelchModes[squelchModeId] : SQUELCH_MODE_OFF); setSquelchLevel(squelchLevel); - setSquelchEnabled(squelchEnabled); + setCTCSSTone(ctcssTones[ctcssToneId]); // Configure AF chain if (postProcEnabled) { // Configure resampler afChain.stop(); - resamp.setInSamplerate(selectedDemod->getAFSampleRate()); - setAudioSampleRate(audioSampleRate); + double afsr = selectedDemod->getAFSampleRate(); + ctcss.setSamplerate(afsr); + resamp.setInSamplerate(afsr); afChain.enableBlock(&resamp, [=](dsp::stream* out){ stream.setInput(out); }); + setAudioSampleRate(audioSampleRate); + + // Configure the HPF + setHighPass(highPass && highPassAllowed); // Configure deemphasis setDeemphasisMode(deempModes[deempId]); @@ -489,12 +593,32 @@ private: // Configure resampler resamp.setOutSamplerate(audioSampleRate); + // Configure the HPF sample rate + hpTaps = dsp::taps::highPass(300.0, 100.0, audioSampleRate); + hpf.setTaps(hpTaps); + // Configure deemphasis sample rate deemp.setSamplerate(audioSampleRate); afChain.start(); } + void setHighPass(bool enabled) { + // Update the state + highPass = enabled; + + // Check if post-processing is enabled and that a demodulator is selected + if (!postProcEnabled || !selectedDemod) { return; } + + // Set the state of the HPF in the AF chain + afChain.setBlockEnabled(&hpf, enabled, [=](dsp::stream* out){ stream.setInput(out); }); + + // Save config + config.acquire(); + config.conf[name][selectedDemod->getName()]["highPass"] = enabled; + config.release(true); + } + void setDeemphasisMode(DeemphasisMode mode) { deempId = deempModes.valueId(mode); if (!postProcEnabled || !selectedDemod) { return; } @@ -522,6 +646,7 @@ private: void setNBLevel(float level) { nbLevel = std::clamp(level, MIN_NB, MAX_NB); nb.setLevel(nbLevel); + if (!selectedDemod) { return; } // Save config config.acquire(); @@ -529,20 +654,59 @@ private: config.release(true); } - void setSquelchEnabled(bool enable) { - squelchEnabled = enable; + void setSquelchMode(SquelchMode mode) { + squelchModeId = squelchModes.valueId(mode); if (!selectedDemod) { return; } - ifChain.setBlockEnabled(&squelch, squelchEnabled, [=](dsp::stream* out){ selectedDemod->setInput(out); }); + + // Disable all squelch blocks + ifChain.disableBlock(&powerSquelch, [=](dsp::stream* out){ selectedDemod->setInput(out); }); + afChain.disableBlock(&ctcss, [=](dsp::stream* out){ stream.setInput(out); }); + + // Enable the block depending on the mode + switch (mode) { + case SQUELCH_MODE_OFF: + break; + + case SQUELCH_MODE_POWER: + // Enable the power squelch block + ifChain.enableBlock(&powerSquelch, [=](dsp::stream* out){ selectedDemod->setInput(out); }); + break; + + case SQUELCH_MODE_SNR: + // TODO + break; + + case SQUELCH_MODE_CTCSS_MUTE: + // Set the required tone and enable the CTCSS squelch block + ctcss.setRequiredTone(ctcssTones[ctcssToneId]); + afChain.enableBlock(&ctcss, [=](dsp::stream* out){ stream.setInput(out); }); + break; + + case SQUELCH_MODE_CTCSS_DECODE: + // Set the required tone to none and enable the CTCSS squelch block + ctcss.setRequiredTone(dsp::noise_reduction::CTCSS_TONE_NONE); + afChain.enableBlock(&ctcss, [=](dsp::stream* out){ stream.setInput(out); }); + break; + + case SQUELCH_MODE_DCS_MUTE: + // TODO + break; + + case SQUELCH_MODE_DCS_DECODE: + // TODO + break; + } // Save config config.acquire(); - config.conf[name][selectedDemod->getName()]["squelchEnabled"] = squelchEnabled; + config.conf[name][selectedDemod->getName()]["squelchMode"] = squelchModes.key(squelchModeId); config.release(true); } void setSquelchLevel(float level) { squelchLevel = std::clamp(level, MIN_SQUELCH, MAX_SQUELCH); - squelch.setLevel(squelchLevel); + powerSquelch.setLevel(squelchLevel); + if (!selectedDemod) { return; } // Save config config.acquire(); @@ -550,6 +714,24 @@ private: config.release(true); } + void setCTCSSTone(dsp::noise_reduction::CTCSSTone tone) { + // Check for an invalid value + if (tone == dsp::noise_reduction::CTCSS_TONE_NONE) { return; } + + // If not in CTCSS mute mode, do nothing + if (squelchModes[squelchModeId] != SQUELCH_MODE_CTCSS_MUTE) { return; } + + // Set the tone + ctcssToneId = ctcssTones.valueId(tone); + ctcss.setRequiredTone(tone); + if (!selectedDemod) { return; } + + // Save config + config.acquire(); + config.conf[name][selectedDemod->getName()]["ctcssTone"] = ctcssTones.key(ctcssToneId); + config.release(true); + } + void setFMIFNREnabled(bool enabled) { FMIFNREnabled = enabled; if (!selectedDemod) { return; } @@ -619,13 +801,13 @@ private: if (_this->bandwidthLocked) { return; } _this->setBandwidth(*_in); } - else if (code == RADIO_IFACE_CMD_GET_SQUELCH_ENABLED && out) { - bool* _out = (bool*)out; - *_out = _this->squelchEnabled; + else if (code == RADIO_IFACE_CMD_GET_SQUELCH_MODE && out) { + SquelchMode* _out = (SquelchMode*)out; + *_out = _this->squelchModes[_this->squelchModeId]; } - else if (code == RADIO_IFACE_CMD_SET_SQUELCH_ENABLED && in && _this->enabled) { - bool* _in = (bool*)in; - _this->setSquelchEnabled(*_in); + else if (code == RADIO_IFACE_CMD_SET_SQUELCH_MODE && in && _this->enabled) { + SquelchMode* _in = (SquelchMode*)in; + _this->setSquelchMode(*_in); } else if (code == RADIO_IFACE_CMD_GET_SQUELCH_LEVEL && out) { float* _out = (float*)out; @@ -635,6 +817,22 @@ private: float* _in = (float*)in; _this->setSquelchLevel(*_in); } + else if (code == RADIO_IFACE_CMD_GET_CTCSS_TONE && out) { + dsp::noise_reduction::CTCSSTone* _out = (dsp::noise_reduction::CTCSSTone*)out; + *_out = _this->ctcssTones[_this->ctcssToneId]; + } + else if (code == RADIO_IFACE_CMD_SET_CTCSS_TONE && in && _this->enabled) { + dsp::noise_reduction::CTCSSTone* _in = (dsp::noise_reduction::CTCSSTone*)in; + _this->setCTCSSTone(*_in); + } + else if (code == RADIO_IFACE_CMD_GET_HIGHPASS && out) { + bool* _out = (bool*)out; + *_out = _this->highPass; + } + else if (code == RADIO_IFACE_CMD_SET_HIGHPASS && in && _this->enabled) { + bool* _in = (bool*)in; + _this->setHighPass(*_in); + } else { return; } @@ -655,12 +853,15 @@ private: dsp::chain ifChain; dsp::noise_reduction::NoiseBlanker nb; dsp::noise_reduction::FMIF fmnr; - dsp::noise_reduction::Squelch squelch; + dsp::noise_reduction::PowerSquelch powerSquelch; // Audio chain dsp::stream dummyAudioStream; dsp::chain afChain; + dsp::noise_reduction::CTCSSSquelch ctcss; dsp::multirate::RationalResampler resamp; + dsp::tap hpTaps; + dsp::filter::FIR hpf; dsp::filter::Deemphasis deemp; SinkManager::Stream stream; @@ -669,6 +870,8 @@ private: OptionList deempModes; OptionList ifnrPresets; + OptionList squelchModes; + OptionList ctcssTones; double audioSampleRate = 48000.0; float minBandwidth; @@ -679,8 +882,13 @@ private: int selectedDemodID = 1; bool postProcEnabled; - bool squelchEnabled = false; + int squelchModeId = 0; float squelchLevel; + int ctcssToneId = 0; + bool squelchAllowed = false; + + bool highPass = false; + bool highPassAllowed = false; int deempId = 0; bool deempAllowed; diff --git a/source_modules/dragonlabs_source/CMakeLists.txt b/source_modules/dragonlabs_source/CMakeLists.txt new file mode 100644 index 00000000..45512ff9 --- /dev/null +++ b/source_modules/dragonlabs_source/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.13) +project(dragonlabs_source) + +file(GLOB SRC "src/*.cpp") + +include(${SDRPP_MODULE_CMAKE}) + +if (MSVC) + # Debugging only + find_package(PkgConfig REQUIRED) + pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0) + target_link_libraries(dragonlabs_source PRIVATE PkgConfig::libusb) + target_include_directories(dragonlabs_source PRIVATE "C:/Users/ryzerth/Documents/DragonLabs/products/KrakenSlayer/host/src") + + target_include_directories(dragonlabs_source PRIVATE "C:/Program Files/DragonLabs/CR/include") + target_link_directories(dragonlabs_source PRIVATE "C:/Program Files/DragonLabs/CR/lib") + target_link_libraries(dragonlabs_source PRIVATE dlcr) +elseif (ANDROID) + # Not supported yet... +else (MSVC) + find_package(PkgConfig) + + pkg_check_modules(LIBDLCR REQUIRED libdlcr) + + target_include_directories(dragonlabs_source PRIVATE ${LIBDLCR_INCLUDE_DIRS}) + target_link_directories(dragonlabs_source PRIVATE ${LIBDLCR_LIBRARY_DIRS}) + target_link_libraries(dragonlabs_source PRIVATE ${LIBDLCR_LIBRARIES}) +endif () \ No newline at end of file diff --git a/source_modules/dragonlabs_source/src/main.cpp b/source_modules/dragonlabs_source/src/main.cpp new file mode 100644 index 00000000..1a528a6d --- /dev/null +++ b/source_modules/dragonlabs_source/src/main.cpp @@ -0,0 +1,448 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SDRPP_MOD_INFO{ + /* Name: */ "dragonlabs_source", + /* Description: */ "Dragon Labs Source Module", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ -1 +}; + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +class DragonLabsSourceModule : public ModuleManager::Instance { +public: + DragonLabsSourceModule(std::string name) { + this->name = name; + + // Load the register debugging values + strcpy(clkRegStr, "00"); + strcpy(clkValStr, "--"); + strcpy(synRegStr, "00"); + strcpy(synValStr, "--"); + strcpy(adcRegStr, "00"); + strcpy(adcValStr, "--"); + strcpy(tunRegStr, "00"); + strcpy(tunValStr, "--"); + + // Define the clock sources + clockSources.define("internal", "Internal", DLCR_CLOCK_INTERNAL); + clockSources.define("external", "External", DLCR_CLOCK_EXTERNAL); + + // Define the channels + channels.define("chan1", "Channel 1", 0); + channels.define("chan2", "Channel 2", 1); + channels.define("chan3", "Channel 3", 2); + channels.define("chan4", "Channel 4", 3); + channels.define("chan5", "Channel 5", 4); + channels.define("chan6", "Channel 6", 5); + channels.define("chan7", "Channel 7", 6); + channels.define("chan8", "Channel 8", 7); + + // Hardcode the samplerate + sampleRate = 12.5e6; + + // Fill out the source handler + 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 devices + refresh(); + + // Select first (TODO: Select from config) + select(""); + + sigpath::sourceManager.registerSource("Dragon Labs", &handler); + } + + ~DragonLabsSourceModule() { + + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + +private: + void refresh() { + devices.clear(); + + // Get the list of devices + dlcr_info_t* list; + int count = dlcr_list_devices(&list); + if (count < 0) { + flog::error("Failed to list devices"); + return; + } + + // Generate the menu list + for (int i = 0; i < count; i++) { + // Format device name + std::string devName = "CR8-1725 "; + devName += " ["; + devName += list[i].serial; + devName += ']'; + + // Save device + devices.define(list[i].serial, devName, list[i].serial); + } + + // Free the device list + dlcr_free_device_list(list); + } + + void select(const std::string& serial) { + // If there are no devices, give up + if (devices.empty()) { + selectedSerial.clear(); + return; + } + + // If the serial was not found, select the first available serial + if (!devices.keyExists(serial)) { + select(devices.key(0)); + return; + } + + // Save serial number + selectedSerial = serial; + } + + static void menuSelected(void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + core::setInputSampleRate(_this->sampleRate); + flog::info("DragonLabsSourceModule '{0}': Menu Select!", _this->name); + } + + static void menuDeselected(void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + flog::info("DragonLabsSourceModule '{0}': Menu Deselect!", _this->name); + } + + std::chrono::high_resolution_clock::time_point last; + + static void start(void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + if (_this->running) { return; } + + // Open the device + int err = dlcr_open(&_this->openDev, _this->selectedSerial.c_str()); + if (err) { + flog::error("Failed to open device '{}': {}", _this->selectedSerial, err); + return; + } + flog::debug("Device open"); + + // Configure the device + dlcr_set_freq(_this->openDev, DLCR_CHAN_ALL, _this->freq, true); + dlcr_set_lna_gain(_this->openDev, DLCR_CHAN_ALL, _this->lnaGain); + dlcr_set_mixer_gain(_this->openDev, DLCR_CHAN_ALL, _this->mixerGain); + dlcr_set_vga_gain(_this->openDev, DLCR_CHAN_ALL, _this->vgaGain); + + // Start device + dlcr_start(_this->openDev, 0/*TODO*/, callback, _this); + + _this->running = true; + flog::info("DragonLabsSourceModule '{0}': Start!", _this->name); + } + + static void stop(void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + if (!_this->running) { return; } + _this->running = false; + + // Stop device + dlcr_stop(_this->openDev); + + // Close device + dlcr_close(_this->openDev); + + flog::info("DragonLabsSourceModule '{0}': Stop!", _this->name); + } + + static void tune(double freq, void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + if (_this->running) { + dlcr_set_freq(_this->openDev, DLCR_CHAN_ALL, freq, _this->docal); + } + _this->calibrated = false; + _this->freq = freq; + flog::info("DragonLabsSourceModule '{0}': Tune: {1}!", _this->name, freq); + } + + double lmxFreq = 100e6; + + static void menuHandler(void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + + if (_this->running) { SmGui::BeginDisabled(); } + + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Combo(CONCAT("##_dlcr_dev_sel_", _this->name), &_this->devId, _this->devices.txt)) { + _this->select(_this->devices.key(_this->devId)); + core::setInputSampleRate(_this->sampleRate); + // TODO: Save + } + + SmGui::FillWidth(); + SmGui::ForceSync(); + if (SmGui::Button(CONCAT("Refresh##_dlcr_refr_", _this->name))) { + _this->refresh(); + _this->select(_this->selectedSerial); + core::setInputSampleRate(_this->sampleRate); + } + + if (_this->running) { SmGui::EndDisabled(); } + + SmGui::LeftLabel("Clock Source"); + SmGui::FillWidth(); + if (SmGui::Combo(CONCAT("##_dlcr_clock_", _this->name), &_this->clockSourceId, _this->clockSources.txt)) { + if (_this->running) { + dlcr_set_clock_source(_this->openDev, _this->clockSources[_this->clockSourceId]); + } + // TODO: Save + } + + SmGui::LeftLabel("Channel"); + SmGui::FillWidth(); + SmGui::Combo(CONCAT("##_dlcr_chan_", _this->name), &_this->channelId, _this->channels.txt); + + SmGui::LeftLabel("LNA Gain"); + SmGui::FillWidth(); + if (SmGui::SliderInt(CONCAT("##_dlcr_lna_gain_", _this->name), &_this->lnaGain, 0, 14)) { + if (_this->running) { + dlcr_set_lna_gain(_this->openDev, DLCR_CHAN_ALL, _this->lnaGain); + } + // TODO: Save + } + + SmGui::LeftLabel("Mixer Gain"); + SmGui::FillWidth(); + if (SmGui::SliderInt(CONCAT("##_dlcr_mixer_gain_", _this->name), &_this->mixerGain, 0, 15)) { + if (_this->running) { + dlcr_set_mixer_gain(_this->openDev, DLCR_CHAN_ALL, _this->mixerGain); + } + // TODO: Save + } + + SmGui::LeftLabel("VGA Gain"); + SmGui::FillWidth(); + if (SmGui::SliderInt(CONCAT("##_dlcr_vga_gain_", _this->name), &_this->vgaGain, 0, 15)) { + if (_this->running) { + dlcr_set_vga_gain(_this->openDev, DLCR_CHAN_ALL, _this->vgaGain); + } + // TODO: Save + } + + SmGui::Checkbox(CONCAT("Debug##_dlcr_debug_", _this->name), &_this->debug); + + if (_this->debug) { + ImGui::Separator(); + + SmGui::LeftLabel("Clockgen Reg"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_clk_reg_", _this->name), _this->clkRegStr, 256); + SmGui::LeftLabel("Clockgen Value"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_clk_val_", _this->name), _this->clkValStr, 256); + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Read##_dlcr_clk_rd_", _this->name))) { + if (_this->running) { + uint8_t val; + dlcr_si5351c_read_reg(_this->openDev, std::stoi(_this->clkRegStr, NULL, 16), &val); + sprintf(_this->clkValStr, "%02X", val); + } + } + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Write##_dlcr_clk_wr_", _this->name))) { + if (_this->running) { + dlcr_si5351c_write_reg(_this->openDev, std::stoi(_this->clkRegStr, NULL, 16), std::stoi(_this->clkValStr, NULL, 16)); + } + } + + ImGui::Separator(); + + SmGui::LeftLabel("Synth Reg"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_syn_reg_", _this->name), _this->synRegStr, 256); + SmGui::LeftLabel("Synth Value"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_syn_val_", _this->name), _this->synValStr, 256); + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Read##_dlcr_syn_rd_", _this->name))) { + if (_this->running) { + uint16_t val; + dlcr_lmx2572_read_reg(_this->openDev, std::stoi(_this->synRegStr, NULL, 16), &val); + sprintf(_this->synValStr, "%02X", val); + } + } + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Write##_dlcr_syn_wr_", _this->name))) { + if (_this->running) { + dlcr_lmx2572_write_reg(_this->openDev, std::stoi(_this->synRegStr, NULL, 16), std::stoi(_this->synValStr, NULL, 16)); + } + } + + ImGui::Separator(); + + SmGui::LeftLabel("ADC Reg"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_adc_reg_", _this->name), _this->adcRegStr, 256); + SmGui::LeftLabel("ADC Value"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_adc_val_", _this->name), _this->adcValStr, 256); + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Read##_dlcr_adc_rd_", _this->name))) { + if (_this->running) { + uint8_t val; + dlcr_mcp37211_read_reg(_this->openDev, std::stoi(_this->adcRegStr, NULL, 16), &val); + sprintf(_this->adcValStr, "%02X", val); + } + } + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Write##_dlcr_adc_wr_", _this->name))) { + if (_this->running) { + dlcr_mcp37211_write_reg(_this->openDev, std::stoi(_this->adcRegStr, NULL, 16), std::stoi(_this->adcValStr, NULL, 16)); + } + } + + ImGui::Separator(); + + SmGui::LeftLabel("Tuner Reg"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_tun_reg_", _this->name), _this->tunRegStr, 256); + SmGui::LeftLabel("Tuner Value"); + SmGui::FillWidth(); + SmGui::InputText(CONCAT("##_dlcr_tun_val_", _this->name), _this->tunValStr, 256); + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Read##_dlcr_tun_rd_", _this->name))) { + if (_this->running) { + uint8_t val[8]; + dlcr_r860_read_reg(_this->openDev, DLCR_TUNER_ALL, std::stoi(_this->tunRegStr, NULL, 16), val); + sprintf(_this->tunValStr, "%02X", val[0]); + for (int i = 0; i < 8; i++) { + printf("TUNER[%d][0x%02X] = 0x%02X\n", i, std::stoi(_this->tunRegStr, NULL, 16), val[i]); + } + } + } + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Write##_dlcr_tun_wr_", _this->name))) { + if (_this->running) { + dlcr_r860_write_reg(_this->openDev, DLCR_TUNER_ALL, std::stoi(_this->tunRegStr, NULL, 16), std::stoi(_this->tunValStr, NULL, 16)); + } + } + + ImGui::Separator(); + + SmGui::LeftLabel("Synth Freq"); + SmGui::FillWidth(); + ImGui::InputDouble("##_dlcr_synth_freq", &_this->synthFreq); + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Tune##_dlcr_synth_freq", _this->name))) { + if (_this->running) { + lmx2572_tune(_this->openDev, _this->synthFreq); + } + } + SmGui::FillWidth(); + if (SmGui::Button(CONCAT("Shutdown##_dlcr_synth_freq", _this->name))) { + if (_this->running) { + lmx2572_shutdown(_this->openDev); + } + } + + ImGui::Separator(); + + SmGui::Checkbox("Calibrate##_dlcr_cal", &_this->docal); + + ImGui::Separator(); + } + } + + static void callback(dlcr_complex_t* samples[DLCR_CHANNEL_COUNT], size_t count, size_t drops, void* ctx) { + DragonLabsSourceModule* _this = (DragonLabsSourceModule*)ctx; + + // Copy the data to the stream + memcpy(_this->stream.writeBuf, samples[_this->channelId], count * sizeof(dsp::complex_t)); + + // Send the samples + _this->stream.swap(count); + } + + std::string name; + bool enabled = true; + double sampleRate; + SourceManager::SourceHandler handler; + bool running = false; + double freq = 100e6; + + OptionList devices; + + bool debug = false; + char clkRegStr[256]; + char clkValStr[256]; + char synRegStr[256]; + char synValStr[256]; + char adcRegStr[256]; + char adcValStr[256]; + char tunRegStr[256]; + char tunValStr[256]; + double synthFreq = 100e6; + bool docal = false; + int devId = 0; + int clockSourceId = 0; + int channelId = 0; + int lnaGain = 0; + int mixerGain = 0; + int vgaGain = 0; + std::string selectedSerial; + dlcr_t* openDev = NULL; + + bool calibrated = false; + + dsp::stream stream; + OptionList clockSources; + OptionList channels; +}; + +MOD_EXPORT void _INIT_() { + // Nothing here +} + +MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) { + return new DragonLabsSourceModule(name); +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (DragonLabsSourceModule*)instance; +} + +MOD_EXPORT void _END_() { + // Nothing here +} \ No newline at end of file