kopia lustrzana https://github.com/AlexandreRouma/SDRPlusPlus
Porównaj commity
30 Commity
11a7c382e8
...
7ab743d05b
Autor | SHA1 | Data |
---|---|---|
AlexandreRouma | 7ab743d05b | |
AlexandreRouma | 122e67ef65 | |
AlexandreRouma | fbeb2195da | |
AlexandreRouma | 1f2b50c9bb | |
AlexandreRouma | f486c657c1 | |
AlexandreRouma | f1f04d59fe | |
AlexandreRouma | ef42ea01d8 | |
AlexandreRouma | 3fc893568a | |
AlexandreRouma | 4b6835141e | |
AlexandreRouma | a9e59bdf3c | |
AlexandreRouma | f0bd17f9f4 | |
AlexandreRouma | a8ed213ed3 | |
AlexandreRouma | f8183739f7 | |
AlexandreRouma | 120745de19 | |
AlexandreRouma | 05ab17add3 | |
AlexandreRouma | 2ef8ee3629 | |
AlexandreRouma | 14cb839863 | |
AlexandreRouma | 9501371c6c | |
AlexandreRouma | ff23d7e43f | |
AlexandreRouma | f541328e5c | |
AlexandreRouma | be8edbfa9e | |
AlexandreRouma | bc77bab45f | |
AlexandreRouma | 97d0a07ec7 | |
AlexandreRouma | 6b5de78e80 | |
AlexandreRouma | 1cd8c2510a | |
AlexandreRouma | 32cbd726fd | |
AlexandreRouma | 175992b081 | |
AlexandreRouma | 31c9e5767e | |
AlexandreRouma | 193580caf3 | |
AlexandreRouma | 2432390600 |
|
@ -43,12 +43,14 @@ option(OPT_BUILD_FALCON9_DECODER "Build the falcon9 live decoder (Dependencies:
|
||||||
option(OPT_BUILD_KG_SSTV_DECODER "Build the KG SSTV (KG-STV) decoder module (no dependencies required)" OFF)
|
option(OPT_BUILD_KG_SSTV_DECODER "Build the KG SSTV (KG-STV) decoder module (no dependencies required)" OFF)
|
||||||
option(OPT_BUILD_M17_DECODER "Build the M17 decoder module (Dependencies: codec2)" OFF)
|
option(OPT_BUILD_M17_DECODER "Build the M17 decoder module (Dependencies: codec2)" OFF)
|
||||||
option(OPT_BUILD_METEOR_DEMODULATOR "Build the meteor demodulator module (no dependencies required)" ON)
|
option(OPT_BUILD_METEOR_DEMODULATOR "Build the meteor demodulator module (no dependencies required)" ON)
|
||||||
|
option(OPT_BUILD_PAGER_DECODER "Build the pager decoder module (no dependencies required)" OFF)
|
||||||
option(OPT_BUILD_RADIO "Main audio modulation decoder (AM, FM, SSB, etc...)" ON)
|
option(OPT_BUILD_RADIO "Main audio modulation decoder (AM, FM, SSB, etc...)" ON)
|
||||||
option(OPT_BUILD_WEATHER_SAT_DECODER "Build the HRPT decoder module (no dependencies required)" OFF)
|
option(OPT_BUILD_WEATHER_SAT_DECODER "Build the HRPT decoder module (no dependencies required)" OFF)
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
option(OPT_BUILD_DISCORD_PRESENCE "Build the Discord Rich Presence module" ON)
|
option(OPT_BUILD_DISCORD_PRESENCE "Build the Discord Rich Presence module" ON)
|
||||||
option(OPT_BUILD_FREQUENCY_MANAGER "Build the Frequency Manager module" ON)
|
option(OPT_BUILD_FREQUENCY_MANAGER "Build the Frequency Manager module" ON)
|
||||||
|
option(OPT_BUILD_IQ_EXPORTER "Build the IQ Exporter module" OFF)
|
||||||
option(OPT_BUILD_RECORDER "Audio and baseband recorder" ON)
|
option(OPT_BUILD_RECORDER "Audio and baseband recorder" ON)
|
||||||
option(OPT_BUILD_RIGCTL_CLIENT "Rigctl client to make SDR++ act as a panadapter" ON)
|
option(OPT_BUILD_RIGCTL_CLIENT "Rigctl client to make SDR++ act as a panadapter" ON)
|
||||||
option(OPT_BUILD_RIGCTL_SERVER "Rigctl backend for controlling SDR++ with software like gpredict" ON)
|
option(OPT_BUILD_RIGCTL_SERVER "Rigctl backend for controlling SDR++ with software like gpredict" ON)
|
||||||
|
@ -234,6 +236,10 @@ if (OPT_BUILD_METEOR_DEMODULATOR)
|
||||||
add_subdirectory("decoder_modules/meteor_demodulator")
|
add_subdirectory("decoder_modules/meteor_demodulator")
|
||||||
endif (OPT_BUILD_METEOR_DEMODULATOR)
|
endif (OPT_BUILD_METEOR_DEMODULATOR)
|
||||||
|
|
||||||
|
if (OPT_BUILD_PAGER_DECODER)
|
||||||
|
add_subdirectory("decoder_modules/pager_decoder")
|
||||||
|
endif (OPT_BUILD_PAGER_DECODER)
|
||||||
|
|
||||||
if (OPT_BUILD_RADIO)
|
if (OPT_BUILD_RADIO)
|
||||||
add_subdirectory("decoder_modules/radio")
|
add_subdirectory("decoder_modules/radio")
|
||||||
endif (OPT_BUILD_RADIO)
|
endif (OPT_BUILD_RADIO)
|
||||||
|
@ -252,6 +258,10 @@ if (OPT_BUILD_FREQUENCY_MANAGER)
|
||||||
add_subdirectory("misc_modules/frequency_manager")
|
add_subdirectory("misc_modules/frequency_manager")
|
||||||
endif (OPT_BUILD_FREQUENCY_MANAGER)
|
endif (OPT_BUILD_FREQUENCY_MANAGER)
|
||||||
|
|
||||||
|
if (OPT_BUILD_IQ_EXPORTER)
|
||||||
|
add_subdirectory("misc_modules/iq_exporter")
|
||||||
|
endif (OPT_BUILD_IQ_EXPORTER)
|
||||||
|
|
||||||
if (OPT_BUILD_RECORDER)
|
if (OPT_BUILD_RECORDER)
|
||||||
add_subdirectory("misc_modules/recorder")
|
add_subdirectory("misc_modules/recorder")
|
||||||
endif (OPT_BUILD_RECORDER)
|
endif (OPT_BUILD_RECORDER)
|
||||||
|
@ -302,7 +312,7 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
|
||||||
add_custom_target(do_always ALL cp \"$<TARGET_FILE_DIR:sdrpp_core>/libsdrpp_core.dylib\" \"$<TARGET_FILE_DIR:sdrpp>\")
|
add_custom_target(do_always ALL cp \"$<TARGET_FILE_DIR:sdrpp_core>/libsdrpp_core.dylib\" \"$<TARGET_FILE_DIR:sdrpp>\")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# cmake .. "-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake" -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_M17_DECODER=ON -DOPT_BUILD_SCANNER=ON -DOPT_BUILD_SCHEDULER=ON -DOPT_BUILD_USRP_SOURCE=ON
|
# cmake .. "-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake" -DOPT_BUILD_BLADERF_SOURCE=ON -DOPT_BUILD_LIMESDR_SOURCE=ON -DOPT_BUILD_SDRPLAY_SOURCE=ON -DOPT_BUILD_M17_DECODER=ON -DOPT_BUILD_SCANNER=ON -DOPT_BUILD_SCHEDULER=ON -DOPT_BUILD_USRP_SOURCE=ON -DOPT_BUILD_PAGER_DECODER=ON
|
||||||
|
|
||||||
# Create module cmake file
|
# Create module cmake file
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/sdrpp_module.cmake ${CMAKE_CURRENT_BINARY_DIR}/sdrpp_module.cmake @ONLY)
|
configure_file(${CMAKE_SOURCE_DIR}/sdrpp_module.cmake ${CMAKE_CURRENT_BINARY_DIR}/sdrpp_module.cmake @ONLY)
|
||||||
|
|
|
@ -45,7 +45,7 @@ uint8_t *history_buffer_get_slice(history_buffer *buf) { return buf->history[buf
|
||||||
|
|
||||||
shift_register_t history_buffer_search(history_buffer *buf, const distance_t *distances,
|
shift_register_t history_buffer_search(history_buffer *buf, const distance_t *distances,
|
||||||
unsigned int search_every) {
|
unsigned int search_every) {
|
||||||
shift_register_t bestpath;
|
shift_register_t bestpath = 0;
|
||||||
distance_t leasterror = USHRT_MAX;
|
distance_t leasterror = USHRT_MAX;
|
||||||
// search for a state with the least error
|
// search for a state with the least error
|
||||||
for (shift_register_t state = 0; state < buf->num_states; state += search_every) {
|
for (shift_register_t state = 0; state < buf->num_states; state += search_every) {
|
||||||
|
|
|
@ -49,6 +49,7 @@ namespace dsp::demod {
|
||||||
audioFirTaps = taps::lowPass(15000.0, 4000.0, _samplerate);
|
audioFirTaps = taps::lowPass(15000.0, 4000.0, _samplerate);
|
||||||
alFir.init(NULL, audioFirTaps);
|
alFir.init(NULL, audioFirTaps);
|
||||||
arFir.init(NULL, audioFirTaps);
|
arFir.init(NULL, audioFirTaps);
|
||||||
|
xlator.init(NULL, -57000.0, samplerate);
|
||||||
rdsResamp.init(NULL, samplerate, 5000.0);
|
rdsResamp.init(NULL, samplerate, 5000.0);
|
||||||
|
|
||||||
lmr = buffer::alloc<float>(STREAM_BUFFER_SIZE);
|
lmr = buffer::alloc<float>(STREAM_BUFFER_SIZE);
|
||||||
|
@ -56,9 +57,9 @@ namespace dsp::demod {
|
||||||
r = buffer::alloc<float>(STREAM_BUFFER_SIZE);
|
r = buffer::alloc<float>(STREAM_BUFFER_SIZE);
|
||||||
|
|
||||||
lprDelay.out.free();
|
lprDelay.out.free();
|
||||||
lmrDelay.out.free();
|
|
||||||
arFir.out.free();
|
arFir.out.free();
|
||||||
alFir.out.free();
|
alFir.out.free();
|
||||||
|
xlator.out.free();
|
||||||
rdsResamp.out.free();
|
rdsResamp.out.free();
|
||||||
|
|
||||||
base_type::init(in);
|
base_type::init(in);
|
||||||
|
@ -92,6 +93,7 @@ namespace dsp::demod {
|
||||||
alFir.setTaps(audioFirTaps);
|
alFir.setTaps(audioFirTaps);
|
||||||
arFir.setTaps(audioFirTaps);
|
arFir.setTaps(audioFirTaps);
|
||||||
|
|
||||||
|
xlator.setOffset(-57000.0, samplerate);
|
||||||
rdsResamp.setInSamplerate(samplerate);
|
rdsResamp.setInSamplerate(samplerate);
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
|
@ -139,7 +141,7 @@ namespace dsp::demod {
|
||||||
base_type::tempStart();
|
base_type::tempStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int process(int count, complex_t* in, stereo_t* out, int& rdsOutCount, float* rdsout = NULL) {
|
inline int process(int count, complex_t* in, stereo_t* out, int& rdsOutCount, complex_t* rdsout = NULL) {
|
||||||
// Demodulate
|
// Demodulate
|
||||||
demod.process(count, in, demod.out.writeBuf);
|
demod.process(count, in, demod.out.writeBuf);
|
||||||
if (_stereo) {
|
if (_stereo) {
|
||||||
|
@ -152,24 +154,24 @@ namespace dsp::demod {
|
||||||
|
|
||||||
// Delay
|
// Delay
|
||||||
lprDelay.process(count, demod.out.writeBuf, demod.out.writeBuf);
|
lprDelay.process(count, demod.out.writeBuf, demod.out.writeBuf);
|
||||||
lmrDelay.process(count, rtoc.out.writeBuf, rtoc.out.writeBuf);
|
lmrDelay.process(count, rtoc.out.writeBuf, lmrDelay.out.writeBuf);
|
||||||
|
|
||||||
// conjugate PLL output to down convert twice the L-R signal
|
// conjugate PLL output to down convert twice the L-R signal
|
||||||
math::Conjugate::process(count, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf);
|
math::Conjugate::process(count, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf);
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf);
|
math::Multiply<dsp::complex_t>::process(count, lmrDelay.out.writeBuf, pilotPLL.out.writeBuf, lmrDelay.out.writeBuf);
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf);
|
math::Multiply<dsp::complex_t>::process(count, lmrDelay.out.writeBuf, pilotPLL.out.writeBuf, lmrDelay.out.writeBuf);
|
||||||
|
|
||||||
// Do RDS demod
|
// Do RDS demod
|
||||||
if (_rdsOut) {
|
if (_rdsOut) {
|
||||||
// Since the PLL output is no longer needed after this, use it as the output
|
// Translate to 0Hz
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf);
|
xlator.process(count, rtoc.out.writeBuf, rtoc.out.writeBuf);
|
||||||
convert::ComplexToReal::process(count, pilotPLL.out.writeBuf, rdsout);
|
|
||||||
volk_32f_s32f_multiply_32f(rdsout, rdsout, 100.0, count);
|
// Resample to the output samplerate
|
||||||
rdsOutCount = rdsResamp.process(count, rdsout, rdsout);
|
rdsOutCount = rdsResamp.process(count, rtoc.out.writeBuf, rdsout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert output back to real for further processing
|
// Convert output back to real for further processing
|
||||||
convert::ComplexToReal::process(count, rtoc.out.writeBuf, lmr);
|
convert::ComplexToReal::process(count, lmrDelay.out.writeBuf, lmr);
|
||||||
|
|
||||||
// Amplify by 2x
|
// Amplify by 2x
|
||||||
volk_32f_s32f_multiply_32f(lmr, lmr, 2.0f, count);
|
volk_32f_s32f_multiply_32f(lmr, lmr, 2.0f, count);
|
||||||
|
@ -193,24 +195,11 @@ namespace dsp::demod {
|
||||||
// Convert to complex
|
// Convert to complex
|
||||||
rtoc.process(count, demod.out.writeBuf, rtoc.out.writeBuf);
|
rtoc.process(count, demod.out.writeBuf, rtoc.out.writeBuf);
|
||||||
|
|
||||||
// Filter out pilot and run through PLL
|
// Translate to 0Hz
|
||||||
pilotFir.process(count, rtoc.out.writeBuf, pilotFir.out.writeBuf);
|
xlator.process(count, rtoc.out.writeBuf, rtoc.out.writeBuf);
|
||||||
pilotPLL.process(count, pilotFir.out.writeBuf, pilotPLL.out.writeBuf);
|
|
||||||
|
|
||||||
// Delay
|
// Resample to the output samplerate
|
||||||
lprDelay.process(count, demod.out.writeBuf, demod.out.writeBuf);
|
rdsOutCount = rdsResamp.process(count, rtoc.out.writeBuf, rdsout);
|
||||||
lmrDelay.process(count, rtoc.out.writeBuf, rtoc.out.writeBuf);
|
|
||||||
|
|
||||||
// conjugate PLL output to down convert twice the L-R signal
|
|
||||||
math::Conjugate::process(count, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf);
|
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf);
|
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, rtoc.out.writeBuf);
|
|
||||||
|
|
||||||
// Since the PLL output is no longer needed after this, use it as the output
|
|
||||||
math::Multiply<dsp::complex_t>::process(count, rtoc.out.writeBuf, pilotPLL.out.writeBuf, pilotPLL.out.writeBuf);
|
|
||||||
convert::ComplexToReal::process(count, pilotPLL.out.writeBuf, rdsout);
|
|
||||||
volk_32f_s32f_multiply_32f(rdsout, rdsout, 100.0, count);
|
|
||||||
rdsOutCount = rdsResamp.process(count, rdsout, rdsout);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter if needed
|
// Filter if needed
|
||||||
|
@ -240,7 +229,7 @@ namespace dsp::demod {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
stream<float> rdsOut;
|
stream<complex_t> rdsOut;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
double _deviation;
|
double _deviation;
|
||||||
|
@ -253,13 +242,14 @@ namespace dsp::demod {
|
||||||
tap<complex_t> pilotFirTaps;
|
tap<complex_t> pilotFirTaps;
|
||||||
filter::FIR<complex_t, complex_t> pilotFir;
|
filter::FIR<complex_t, complex_t> pilotFir;
|
||||||
convert::RealToComplex rtoc;
|
convert::RealToComplex rtoc;
|
||||||
|
channel::FrequencyXlator xlator;
|
||||||
loop::PLL pilotPLL;
|
loop::PLL pilotPLL;
|
||||||
math::Delay<float> lprDelay;
|
math::Delay<float> lprDelay;
|
||||||
math::Delay<complex_t> lmrDelay;
|
math::Delay<complex_t> lmrDelay;
|
||||||
tap<float> audioFirTaps;
|
tap<float> audioFirTaps;
|
||||||
filter::FIR<float, float> arFir;
|
filter::FIR<float, float> arFir;
|
||||||
filter::FIR<float, float> alFir;
|
filter::FIR<float, float> alFir;
|
||||||
multirate::RationalResampler<float> rdsResamp;
|
multirate::RationalResampler<dsp::complex_t> rdsResamp;
|
||||||
|
|
||||||
float* lmr;
|
float* lmr;
|
||||||
float* l;
|
float* l;
|
||||||
|
|
|
@ -65,6 +65,11 @@ namespace dsp::loop {
|
||||||
if constexpr(CLAMP_PHASE) { clampPhase(); }
|
if constexpr(CLAMP_PHASE) { clampPhase(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void advancePhase() {
|
||||||
|
phase += freq;
|
||||||
|
if constexpr(CLAMP_PHASE) { clampPhase(); }
|
||||||
|
}
|
||||||
|
|
||||||
T freq;
|
T freq;
|
||||||
T phase;
|
T phase;
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,16 @@ namespace net {
|
||||||
}
|
}
|
||||||
|
|
||||||
int Socket::send(const uint8_t* data, size_t len, const Address* dest) {
|
int Socket::send(const uint8_t* data, size_t len, const Address* dest) {
|
||||||
return sendto(sock, (const char*)data, len, 0, (sockaddr*)(dest ? &dest->addr : (raddr ? &raddr->addr : NULL)), sizeof(sockaddr_in));
|
// Send data
|
||||||
|
int err = sendto(sock, (const char*)data, len, 0, (sockaddr*)(dest ? &dest->addr : (raddr ? &raddr->addr : NULL)), sizeof(sockaddr_in));
|
||||||
|
|
||||||
|
// On error, close socket
|
||||||
|
if (err <= 0 && !WOULD_BLOCK) {
|
||||||
|
close();
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Socket::sendstr(const std::string& str, const Address* dest) {
|
int Socket::sendstr(const std::string& str, const Address* dest) {
|
||||||
|
|
|
@ -7,6 +7,14 @@ namespace riff {
|
||||||
const char* LIST_SIGNATURE = "LIST";
|
const char* LIST_SIGNATURE = "LIST";
|
||||||
const size_t RIFF_LABEL_SIZE = 4;
|
const size_t RIFF_LABEL_SIZE = 4;
|
||||||
|
|
||||||
|
// Writer::Writer(const Writer&& b) {
|
||||||
|
// //file = std::move(b.file);
|
||||||
|
// }
|
||||||
|
|
||||||
|
Writer::~Writer() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
bool Writer::open(std::string path, const char form[4]) {
|
bool Writer::open(std::string path, const char form[4]) {
|
||||||
std::lock_guard<std::recursive_mutex> lck(mtx);
|
std::lock_guard<std::recursive_mutex> lck(mtx);
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,10 @@ namespace riff {
|
||||||
|
|
||||||
class Writer {
|
class Writer {
|
||||||
public:
|
public:
|
||||||
|
Writer() {}
|
||||||
|
// Writer(const Writer&& b);
|
||||||
|
~Writer();
|
||||||
|
|
||||||
bool open(std::string path, const char form[4]);
|
bool open(std::string path, const char form[4]);
|
||||||
bool isOpen();
|
bool isOpen();
|
||||||
void close();
|
void close();
|
||||||
|
@ -40,4 +44,23 @@ namespace riff {
|
||||||
std::ofstream file;
|
std::ofstream file;
|
||||||
std::stack<ChunkDesc> chunks;
|
std::stack<ChunkDesc> chunks;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// class Reader {
|
||||||
|
// public:
|
||||||
|
// Reader();
|
||||||
|
// Reader(const Reader&& b);
|
||||||
|
// ~Reader();
|
||||||
|
|
||||||
|
// bool open(std::string path);
|
||||||
|
// bool isOpen();
|
||||||
|
// void close();
|
||||||
|
|
||||||
|
// const std::string& form();
|
||||||
|
|
||||||
|
// private:
|
||||||
|
|
||||||
|
// std::string _form;
|
||||||
|
// std::recursive_mutex mtx;
|
||||||
|
// std::ofstream file;
|
||||||
|
// };
|
||||||
}
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
#pragma once
|
||||||
|
#include <dsp/loop/pll.h>
|
||||||
|
#include "chrominance_filter.h"
|
||||||
|
|
||||||
|
// TODO: Should be 60 but had to try something
|
||||||
|
#define BURST_START (63+CHROMA_FIR_DELAY)
|
||||||
|
#define BURST_END (BURST_START+28)
|
||||||
|
|
||||||
|
#define A_PHASE ((135.0/180.0)*FL_M_PI)
|
||||||
|
#define B_PHASE ((-135.0/180.0)*FL_M_PI)
|
||||||
|
|
||||||
|
namespace dsp::loop {
|
||||||
|
class ChromaPLL : public PLL {
|
||||||
|
using base_type = PLL;
|
||||||
|
public:
|
||||||
|
ChromaPLL() {}
|
||||||
|
|
||||||
|
ChromaPLL(stream<complex_t>* in, double bandwidth, double initPhase = 0.0, double initFreq = 0.0, double minFreq = -FL_M_PI, double maxFreq = FL_M_PI) {
|
||||||
|
base_type::init(in, bandwidth, initFreq, initPhase, minFreq, maxFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int process(int count, complex_t* in, complex_t* out, bool aphase = false) {
|
||||||
|
// Process the pre-burst section
|
||||||
|
for (int i = 0; i < BURST_START; i++) {
|
||||||
|
out[i] = in[i] * math::phasor(-pcl.phase);
|
||||||
|
pcl.advancePhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the burst itself
|
||||||
|
if (aphase) {
|
||||||
|
for (int i = BURST_START; i < BURST_END; i++) {
|
||||||
|
complex_t outVal = in[i] * math::phasor(-pcl.phase);
|
||||||
|
out[i] = outVal;
|
||||||
|
pcl.advance(math::normalizePhase(outVal.phase() - A_PHASE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (int i = BURST_START; i < BURST_END; i++) {
|
||||||
|
complex_t outVal = in[i] * math::phasor(-pcl.phase);
|
||||||
|
out[i] = outVal;
|
||||||
|
pcl.advance(math::normalizePhase(outVal.phase() - B_PHASE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Process the post-burst section
|
||||||
|
for (int i = BURST_END; i < count; i++) {
|
||||||
|
out[i] = in[i] * math::phasor(-pcl.phase);
|
||||||
|
pcl.advancePhase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int processBlank(int count, complex_t* in, complex_t* out) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
out[i] = in[i] * math::phasor(-pcl.phase);
|
||||||
|
pcl.advancePhase();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
#pragma once
|
||||||
|
#include <dsp/types.h>
|
||||||
|
|
||||||
|
inline const dsp::complex_t CHROMA_FIR[] = {
|
||||||
|
{-0.000005461290583903, -0.000011336784355655},
|
||||||
|
{ 0.000020060944485414, 0.000009851315045203},
|
||||||
|
{-0.000034177222729438, 0.000007245841504981},
|
||||||
|
{ 0.000027694034878705, -0.000033114740542635},
|
||||||
|
{-0.000001217597841648, 0.000039141482370942},
|
||||||
|
{-0.000008324593371228, -0.000011315001355976},
|
||||||
|
{-0.000038085228233509, -0.000010585909953738},
|
||||||
|
{ 0.000114833396071141, -0.000047778708840608},
|
||||||
|
{-0.000115428390169113, 0.000205816198882814},
|
||||||
|
{-0.000055467806072871, -0.000356692479491626},
|
||||||
|
{ 0.000349316846854190, 0.000326162940234916},
|
||||||
|
{-0.000558465829929114, -0.000048001521408724},
|
||||||
|
{ 0.000488176200631416, -0.000319593757302922},
|
||||||
|
{-0.000169437838021935, 0.000501610900725908},
|
||||||
|
{-0.000131793335799502, -0.000373003580727547},
|
||||||
|
{ 0.000166817395492786, 0.000105930895534474},
|
||||||
|
{ 0.000030499908326112, -0.000003048682668943},
|
||||||
|
{-0.000174999505027919, 0.000168008090089458},
|
||||||
|
{ 0.000054431163395030, -0.000385174790951272},
|
||||||
|
{ 0.000215876012859739, 0.000372695852521209},
|
||||||
|
{-0.000325534912280750, -0.000130173041693966},
|
||||||
|
{ 0.000154951430569290, -0.000045395998708328},
|
||||||
|
{ 0.000054324657659002, -0.000076028700470037},
|
||||||
|
{ 0.000015664427565764, 0.000348002612845696},
|
||||||
|
{-0.000345943017888332, -0.000402175417043307},
|
||||||
|
{ 0.000568731727879741, 0.000112347863435682},
|
||||||
|
{-0.000416485880859085, 0.000211750352828909},
|
||||||
|
{ 0.000087462353623011, -0.000188197153014309},
|
||||||
|
{-0.000032082305030264, -0.000136804226080664},
|
||||||
|
{ 0.000379089999045955, 0.000303466839685362},
|
||||||
|
{-0.000726760198519770, -0.000007022279302816},
|
||||||
|
{ 0.000619888661818195, -0.000476871323359809},
|
||||||
|
{-0.000151885493742993, 0.000595641190573181},
|
||||||
|
{-0.000100626407015494, -0.000227947144491108},
|
||||||
|
{-0.000201935458823941, -0.000107628631934340},
|
||||||
|
{ 0.000680260922139900, -0.000120771182888852},
|
||||||
|
{-0.000666108629277491, 0.000744775901128973},
|
||||||
|
{ 0.000067236591919755, -0.001044125966364420},
|
||||||
|
{ 0.000447037274751822, 0.000651912509450913},
|
||||||
|
{-0.000262675893448686, -0.000082499729563337},
|
||||||
|
{-0.000349821460486320, 0.000132102793530818},
|
||||||
|
{ 0.000507024815168287, -0.000837598610490618},
|
||||||
|
{ 0.000163814255478652, 0.001346530693477834},
|
||||||
|
{-0.000970457632383793, -0.000968411010101160},
|
||||||
|
{ 0.000974834882891140, 0.000116507082762032},
|
||||||
|
{-0.000225464280571542, 0.000137131865995708},
|
||||||
|
{-0.000211542240694642, 0.000563783548428947},
|
||||||
|
{-0.000414412310798766, -0.001309793399193736},
|
||||||
|
{ 0.001497010004594478, 0.001021907858926259},
|
||||||
|
{-0.001752019159639658, 0.000116536066154131},
|
||||||
|
{ 0.000872822027879430, -0.000783952720205569},
|
||||||
|
{-0.000032439446797970, 0.000184988059956734},
|
||||||
|
{ 0.000446259382722895, 0.000833040920509238},
|
||||||
|
{-0.001741577737284306, -0.000764423771425237},
|
||||||
|
{ 0.002306569133792772, -0.000593352416441601},
|
||||||
|
{-0.001336084746214192, 0.001744394557524181},
|
||||||
|
{-0.000015810020735495, -0.001342809547658260},
|
||||||
|
{ 0.000007636494885364, 0.000009498318627546},
|
||||||
|
{ 0.001403876768349702, 0.000326101441888391},
|
||||||
|
{-0.002351020828600226, 0.001098649819278302},
|
||||||
|
{ 0.001389314639579544, -0.002746943712072884},
|
||||||
|
{ 0.000526319899588909, 0.002635084366837732},
|
||||||
|
{-0.001109526585744687, -0.000950323796527721},
|
||||||
|
{-0.000307792427984886, -0.000013203419520794},
|
||||||
|
{ 0.001737955094951111, -0.001247368808692850},
|
||||||
|
{-0.000974502437588420, 0.003352512117661680},
|
||||||
|
{-0.001462571137390936, -0.003635296917435679},
|
||||||
|
{ 0.002783459090201693, 0.001604420226187745},
|
||||||
|
{-0.001471518558760170, 0.000211117948702137},
|
||||||
|
{-0.000575340825070194, 0.000601820846100026},
|
||||||
|
{ 0.000302090333345692, -0.003088058972305493},
|
||||||
|
{ 0.002496092353182990, 0.003912508340989065},
|
||||||
|
{-0.004645661091012423, -0.001630427298020200},
|
||||||
|
{ 0.003556824805628799, -0.001209822327859352},
|
||||||
|
{-0.000744999556260706, 0.001143238699138109},
|
||||||
|
{ 0.000144278726929409, 0.001638049051599065},
|
||||||
|
{-0.003025291044450178, -0.003226370992887968},
|
||||||
|
{ 0.006047866290490120, 0.000927406808799887},
|
||||||
|
{-0.005338456415106141, 0.003008811999350399},
|
||||||
|
{ 0.001642959659014839, -0.003972384205231079},
|
||||||
|
{ 0.000273874932822212, 0.000977326273749033},
|
||||||
|
{ 0.002315022846573390, 0.001695671268241410},
|
||||||
|
{-0.006240953957978884, 0.000207330368698293},
|
||||||
|
{ 0.006164252120861735, -0.005177351717451013},
|
||||||
|
{-0.001560310257561104, 0.007437030759707700},
|
||||||
|
{-0.002131333814462852, -0.004317129694157112},
|
||||||
|
{ 0.000280518918541908, 0.000134405998842553},
|
||||||
|
{ 0.004612116481180659, -0.001024468120657814},
|
||||||
|
{-0.005599300279638699, 0.006828277067771868},
|
||||||
|
{ 0.000228879728552504, -0.010675998154712657},
|
||||||
|
{ 0.005692081512980654, 0.007582243186569848},
|
||||||
|
{-0.005100500569859509, -0.001364751685737153},
|
||||||
|
{-0.000902490398043454, 0.000385770160220703},
|
||||||
|
{ 0.003673858819546609, -0.006701685283451640},
|
||||||
|
{ 0.002079056046131593, 0.012568579063417429},
|
||||||
|
{-0.010730008156911677, -0.009826454574016218},
|
||||||
|
{ 0.012092401380903161, 0.000921764172237851},
|
||||||
|
{-0.004714530989129091, 0.003151948807627123},
|
||||||
|
{-0.001055930168838909, 0.003228576712467020},
|
||||||
|
{-0.004343270165991213, -0.011924332879354394},
|
||||||
|
{ 0.016499994418955999, 0.010255324919126899},
|
||||||
|
{-0.021047239750251585, 0.002309419513135448},
|
||||||
|
{ 0.011855513874047341, -0.011604071033866310},
|
||||||
|
{-0.000777842281358575, 0.005916341648175263},
|
||||||
|
{ 0.004380939277688377, 0.007397670455730446},
|
||||||
|
{-0.021891594662401131, -0.008509480947490166},
|
||||||
|
{ 0.032787638290674201, -0.009950745850861956},
|
||||||
|
{-0.021022579272463194, 0.030030850567389102},
|
||||||
|
{-0.001508145650189953, -0.027571914870304640},
|
||||||
|
{ 0.004056649693022923, 0.004624901687718579},
|
||||||
|
{ 0.025728742586666287, 0.004824671348397606},
|
||||||
|
{-0.058002700931665603, 0.030198618296813803},
|
||||||
|
{ 0.043631619628438784, -0.096308304333327280},
|
||||||
|
{ 0.033451363423624300, 0.136687079396426990},
|
||||||
|
{-0.129387018420204200, -0.101540513046619400},
|
||||||
|
{ 0.172881344826560730, -0.000000000000005297},
|
||||||
|
{-0.129387018420198010, 0.101540513046627330},
|
||||||
|
{ 0.033451363423615862, -0.136687079396429050},
|
||||||
|
{ 0.043631619628444723, 0.096308304333324601},
|
||||||
|
{-0.058002700931667456, -0.030198618296810247},
|
||||||
|
{ 0.025728742586665992, -0.004824671348399184},
|
||||||
|
{ 0.004056649693022639, -0.004624901687718827},
|
||||||
|
{-0.001508145650188251, 0.027571914870304734},
|
||||||
|
{-0.021022579272465047, -0.030030850567387805},
|
||||||
|
{ 0.032787638290674812, 0.009950745850859947},
|
||||||
|
{-0.021891594662400610, 0.008509480947491507},
|
||||||
|
{ 0.004380939277687923, -0.007397670455730714},
|
||||||
|
{-0.000777842281358940, -0.005916341648175215},
|
||||||
|
{ 0.011855513874048058, 0.011604071033865578},
|
||||||
|
{-0.021047239750251731, -0.002309419513134139},
|
||||||
|
{ 0.016499994418955360, -0.010255324919127926},
|
||||||
|
{-0.004343270165990471, 0.011924332879354665},
|
||||||
|
{-0.001055930168839110, -0.003228576712466955},
|
||||||
|
{-0.004714530989129287, -0.003151948807626830},
|
||||||
|
{ 0.012092401380903103, -0.000921764172238603},
|
||||||
|
{-0.010730008156911072, 0.009826454574016881},
|
||||||
|
{ 0.002079056046130817, -0.012568579063417559},
|
||||||
|
{ 0.003673858819547020, 0.006701685283451416},
|
||||||
|
{-0.000902490398043478, -0.000385770160220647},
|
||||||
|
{-0.005100500569859424, 0.001364751685737466},
|
||||||
|
{ 0.005692081512980187, -0.007582243186570198},
|
||||||
|
{ 0.000228879728553163, 0.010675998154712643},
|
||||||
|
{-0.005599300279639117, -0.006828277067771524},
|
||||||
|
{ 0.004612116481180722, 0.001024468120657532},
|
||||||
|
{ 0.000280518918541900, -0.000134405998842571},
|
||||||
|
{-0.002131333814462586, 0.004317129694157243},
|
||||||
|
{-0.001560310257561563, -0.007437030759707604},
|
||||||
|
{ 0.006164252120862052, 0.005177351717450635},
|
||||||
|
{-0.006240953957978898, -0.000207330368697911},
|
||||||
|
{ 0.002315022846573286, -0.001695671268241552},
|
||||||
|
{ 0.000273874932822152, -0.000977326273749050},
|
||||||
|
{ 0.001642959659015084, 0.003972384205230976},
|
||||||
|
{-0.005338456415106324, -0.003008811999350072},
|
||||||
|
{ 0.006047866290490063, -0.000927406808800258},
|
||||||
|
{-0.003025291044449980, 0.003226370992888153},
|
||||||
|
{ 0.000144278726929308, -0.001638049051599074},
|
||||||
|
{-0.000744999556260777, -0.001143238699138063},
|
||||||
|
{ 0.003556824805628873, 0.001209822327859134},
|
||||||
|
{-0.004645661091012323, 0.001630427298020484},
|
||||||
|
{ 0.002496092353182751, -0.003912508340989219},
|
||||||
|
{ 0.000302090333345882, 0.003088058972305475},
|
||||||
|
{-0.000575340825070231, -0.000601820846099991},
|
||||||
|
{-0.001471518558760183, -0.000211117948702046},
|
||||||
|
{ 0.002783459090201593, -0.001604420226187919},
|
||||||
|
{-0.001462571137390710, 0.003635296917435769},
|
||||||
|
{-0.000974502437588628, -0.003352512117661619},
|
||||||
|
{ 0.001737955094951189, 0.001247368808692742},
|
||||||
|
{-0.000307792427984885, 0.000013203419520814},
|
||||||
|
{-0.001109526585744628, 0.000950323796527789},
|
||||||
|
{ 0.000526319899588746, -0.002635084366837765},
|
||||||
|
{ 0.001389314639579712, 0.002746943712072799},
|
||||||
|
{-0.002351020828600294, -0.001098649819278158},
|
||||||
|
{ 0.001403876768349682, -0.000326101441888477},
|
||||||
|
{ 0.000007636494885364, -0.000009498318627546},
|
||||||
|
{-0.000015810020735412, 0.001342809547658261},
|
||||||
|
{-0.001336084746214299, -0.001744394557524099},
|
||||||
|
{ 0.002306569133792808, 0.000593352416441460},
|
||||||
|
{-0.001741577737284259, 0.000764423771425344},
|
||||||
|
{ 0.000446259382722843, -0.000833040920509266},
|
||||||
|
{-0.000032439446797982, -0.000184988059956732},
|
||||||
|
{ 0.000872822027879478, 0.000783952720205515},
|
||||||
|
{-0.001752019159639665, -0.000116536066154024},
|
||||||
|
{ 0.001497010004594416, -0.001021907858926351},
|
||||||
|
{-0.000414412310798685, 0.001309793399193761},
|
||||||
|
{-0.000211542240694677, -0.000563783548428934},
|
||||||
|
{-0.000225464280571550, -0.000137131865995694},
|
||||||
|
{ 0.000974834882891133, -0.000116507082762092},
|
||||||
|
{-0.000970457632383734, 0.000968411010101219},
|
||||||
|
{ 0.000163814255478569, -0.001346530693477844},
|
||||||
|
{ 0.000507024815168339, 0.000837598610490586},
|
||||||
|
{-0.000349821460486328, -0.000132102793530797},
|
||||||
|
{-0.000262675893448681, 0.000082499729563353},
|
||||||
|
{ 0.000447037274751782, -0.000651912509450940},
|
||||||
|
{ 0.000067236591919819, 0.001044125966364416},
|
||||||
|
{-0.000666108629277537, -0.000744775901128932},
|
||||||
|
{ 0.000680260922139908, 0.000120771182888810},
|
||||||
|
{-0.000201935458823935, 0.000107628631934352},
|
||||||
|
{-0.000100626407015480, 0.000227947144491114},
|
||||||
|
{-0.000151885493743030, -0.000595641190573172},
|
||||||
|
{ 0.000619888661818225, 0.000476871323359771},
|
||||||
|
{-0.000726760198519770, 0.000007022279302861},
|
||||||
|
{ 0.000379089999045936, -0.000303466839685386},
|
||||||
|
{-0.000032082305030256, 0.000136804226080666},
|
||||||
|
{ 0.000087462353623023, 0.000188197153014303},
|
||||||
|
{-0.000416485880859098, -0.000211750352828883},
|
||||||
|
{ 0.000568731727879734, -0.000112347863435717},
|
||||||
|
{-0.000345943017888307, 0.000402175417043329},
|
||||||
|
{ 0.000015664427565742, -0.000348002612845697},
|
||||||
|
{ 0.000054324657659007, 0.000076028700470034},
|
||||||
|
{ 0.000154951430569292, 0.000045395998708319},
|
||||||
|
{-0.000325534912280742, 0.000130173041693986},
|
||||||
|
{ 0.000215876012859716, -0.000372695852521222},
|
||||||
|
{ 0.000054431163395054, 0.000385174790951269},
|
||||||
|
{-0.000174999505027930, -0.000168008090089447},
|
||||||
|
{ 0.000030499908326113, 0.000003048682668941},
|
||||||
|
{ 0.000166817395492779, -0.000105930895534485},
|
||||||
|
{-0.000131793335799479, 0.000373003580727555},
|
||||||
|
{-0.000169437838021966, -0.000501610900725898},
|
||||||
|
{ 0.000488176200631435, 0.000319593757302892},
|
||||||
|
{-0.000558465829929111, 0.000048001521408758},
|
||||||
|
{ 0.000349316846854170, -0.000326162940234938},
|
||||||
|
{-0.000055467806072849, 0.000356692479491629},
|
||||||
|
{-0.000115428390169126, -0.000205816198882806},
|
||||||
|
{ 0.000114833396071144, 0.000047778708840601},
|
||||||
|
{-0.000038085228233508, 0.000010585909953741},
|
||||||
|
{-0.000008324593371228, 0.000011315001355977},
|
||||||
|
{-0.000001217597841650, -0.000039141482370942},
|
||||||
|
{ 0.000027694034878707, 0.000033114740542633},
|
||||||
|
{-0.000034177222729439, -0.000007245841504979},
|
||||||
|
{ 0.000020060944485413, -0.000009851315045204},
|
||||||
|
{-0.000005461290583903, 0.000011336784355656},
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CHROMA_FIR_SIZE (sizeof(CHROMA_FIR)/sizeof(dsp::complex_t))
|
||||||
|
#define CHROMA_FIR_DELAY ((CHROMA_FIR_SIZE-1)/2)
|
|
@ -0,0 +1,193 @@
|
||||||
|
#pragma once
|
||||||
|
#include <dsp/processor.h>
|
||||||
|
#include <dsp/loop/phase_control_loop.h>
|
||||||
|
#include <dsp/taps/windowed_sinc.h>
|
||||||
|
#include <dsp/multirate/polyphase_bank.h>
|
||||||
|
#include <dsp/math/step.h>
|
||||||
|
|
||||||
|
class LineSync : public dsp::Processor<float, float> {
|
||||||
|
using base_type = dsp::Processor<float, float>;
|
||||||
|
public:
|
||||||
|
LineSync() {}
|
||||||
|
|
||||||
|
LineSync(dsp::stream<float>* in, double omega, double omegaGain, double muGain, double omegaRelLimit, int interpPhaseCount = 128, int interpTapCount = 8) { init(in, omega, omegaGain, muGain, omegaRelLimit, interpPhaseCount, interpTapCount); }
|
||||||
|
|
||||||
|
~LineSync() {
|
||||||
|
if (!base_type::_block_init) { return; }
|
||||||
|
base_type::stop();
|
||||||
|
dsp::multirate::freePolyphaseBank(interpBank);
|
||||||
|
dsp::buffer::free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(dsp::stream<float>* in, double omega, double omegaGain, double muGain, double omegaRelLimit, int interpPhaseCount = 128, int interpTapCount = 8) {
|
||||||
|
_omega = omega;
|
||||||
|
_omegaGain = omegaGain;
|
||||||
|
_muGain = muGain;
|
||||||
|
_omegaRelLimit = omegaRelLimit;
|
||||||
|
_interpPhaseCount = interpPhaseCount;
|
||||||
|
_interpTapCount = interpTapCount;
|
||||||
|
|
||||||
|
pcl.init(_muGain, _omegaGain, 0.0, 0.0, 1.0, _omega, _omega * (1.0 - omegaRelLimit), _omega * (1.0 + omegaRelLimit));
|
||||||
|
generateInterpTaps();
|
||||||
|
buffer = dsp::buffer::alloc<float>(STREAM_BUFFER_SIZE + _interpTapCount);
|
||||||
|
bufStart = &buffer[_interpTapCount - 1];
|
||||||
|
|
||||||
|
base_type::init(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOmegaGain(double omegaGain) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
_omegaGain = omegaGain;
|
||||||
|
pcl.setCoefficients(_muGain, _omegaGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMuGain(double muGain) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
_muGain = muGain;
|
||||||
|
pcl.setCoefficients(_muGain, _omegaGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOmegaRelLimit(double omegaRelLimit) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
_omegaRelLimit = omegaRelLimit;
|
||||||
|
pcl.setFreqLimits(_omega * (1.0 - _omegaRelLimit), _omega * (1.0 + _omegaRelLimit));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSyncLevel(float level) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
syncLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setInterpParams(int interpPhaseCount, int interpTapCount) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
base_type::tempStop();
|
||||||
|
_interpPhaseCount = interpPhaseCount;
|
||||||
|
_interpTapCount = interpTapCount;
|
||||||
|
dsp::multirate::freePolyphaseBank(interpBank);
|
||||||
|
dsp::buffer::free(buffer);
|
||||||
|
generateInterpTaps();
|
||||||
|
buffer = dsp::buffer::alloc<float>(STREAM_BUFFER_SIZE + _interpTapCount);
|
||||||
|
bufStart = &buffer[_interpTapCount - 1];
|
||||||
|
base_type::tempStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
base_type::tempStop();
|
||||||
|
offset = 0;
|
||||||
|
pcl.phase = 0.0f;
|
||||||
|
pcl.freq = _omega;
|
||||||
|
base_type::tempStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
int run() {
|
||||||
|
int count = base_type::_in->read();
|
||||||
|
if (count < 0) { return -1; }
|
||||||
|
|
||||||
|
// Copy data to work buffer
|
||||||
|
memcpy(bufStart, base_type::_in->readBuf, count * sizeof(float));
|
||||||
|
|
||||||
|
if (test2) {
|
||||||
|
test2 = false;
|
||||||
|
offset += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all samples
|
||||||
|
while (offset < count) {
|
||||||
|
// Calculate new output value
|
||||||
|
int phase = std::clamp<int>(floorf(pcl.phase * (float)_interpPhaseCount), 0, _interpPhaseCount - 1);
|
||||||
|
float outVal;
|
||||||
|
volk_32f_x2_dot_prod_32f(&outVal, &buffer[offset], interpBank.phases[phase], _interpTapCount);
|
||||||
|
base_type::out.writeBuf[outCount++] = outVal;
|
||||||
|
|
||||||
|
// If the end of the line is reached, process it and determin error
|
||||||
|
float error = 0;
|
||||||
|
if (outCount >= 720) {
|
||||||
|
// Compute averages.
|
||||||
|
float left = 0.0f, right = 0.0f;
|
||||||
|
for (int i = (720-17); i < 720; i++) {
|
||||||
|
left += base_type::out.writeBuf[i];
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 27; i++) {
|
||||||
|
left += base_type::out.writeBuf[i];
|
||||||
|
}
|
||||||
|
for (int i = 27; i < (54+17); i++) {
|
||||||
|
right += base_type::out.writeBuf[i];
|
||||||
|
}
|
||||||
|
left *= (1.0f/44.0f);
|
||||||
|
right *= (1.0f/44.0f);
|
||||||
|
|
||||||
|
// If the sync is present, compute error
|
||||||
|
if ((left < syncLevel && right < syncLevel) && !forceLock) {
|
||||||
|
error = (left + syncBias - right);
|
||||||
|
locked = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
locked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++counter >= 100) {
|
||||||
|
counter = 0;
|
||||||
|
//flog::warn("Left: {}, Right: {}, Error: {}, Freq: {}, Phase: {}", left, right, error, pcl.freq, pcl.phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output line
|
||||||
|
if (!base_type::out.swap(outCount)) { break; }
|
||||||
|
outCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance symbol offset and phase
|
||||||
|
pcl.advance(error);
|
||||||
|
float delta = floorf(pcl.phase);
|
||||||
|
offset += delta;
|
||||||
|
pcl.phase -= delta;
|
||||||
|
}
|
||||||
|
offset -= count;
|
||||||
|
|
||||||
|
// Update delay buffer
|
||||||
|
memmove(buffer, &buffer[count], (_interpTapCount - 1) * sizeof(float));
|
||||||
|
|
||||||
|
// Swap if some data was generated
|
||||||
|
base_type::_in->flush();
|
||||||
|
return outCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool locked = false;
|
||||||
|
bool test2 = false;
|
||||||
|
|
||||||
|
float syncBias = 0.0f;
|
||||||
|
bool forceLock = false;
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void generateInterpTaps() {
|
||||||
|
double bw = 0.5 / (double)_interpPhaseCount;
|
||||||
|
dsp::tap<float> lp = dsp::taps::windowedSinc<float>(_interpPhaseCount * _interpTapCount, dsp::math::hzToRads(bw, 1.0), dsp::window::nuttall, _interpPhaseCount);
|
||||||
|
interpBank = dsp::multirate::buildPolyphaseBank<float>(_interpPhaseCount, lp);
|
||||||
|
dsp::taps::free(lp);
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp::multirate::PolyphaseBank<float> interpBank;
|
||||||
|
dsp::loop::PhaseControlLoop<double, false> pcl;
|
||||||
|
|
||||||
|
double _omega;
|
||||||
|
double _omegaGain;
|
||||||
|
double _muGain;
|
||||||
|
double _omegaRelLimit;
|
||||||
|
int _interpPhaseCount;
|
||||||
|
int _interpTapCount;
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
int outCount = 0;
|
||||||
|
float* buffer;
|
||||||
|
float* bufStart;
|
||||||
|
|
||||||
|
float syncLevel = -0.03f;
|
||||||
|
};
|
|
@ -10,6 +10,15 @@
|
||||||
|
|
||||||
#include <dsp/demod/quadrature.h>
|
#include <dsp/demod/quadrature.h>
|
||||||
#include <dsp/sink/handler_sink.h>
|
#include <dsp/sink/handler_sink.h>
|
||||||
|
#include "linesync.h"
|
||||||
|
#include <dsp/loop/pll.h>
|
||||||
|
#include <dsp/convert/real_to_complex.h>
|
||||||
|
#include <dsp/filter/fir.h>
|
||||||
|
#include <dsp/taps/from_array.h>
|
||||||
|
|
||||||
|
#include "chrominance_filter.h"
|
||||||
|
|
||||||
|
#include "chroma_pll.h"
|
||||||
|
|
||||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||||
|
|
||||||
|
@ -17,7 +26,8 @@ SDRPP_MOD_INFO{/* Name: */ "atv_decoder",
|
||||||
/* Description: */ "ATV decoder for SDR++",
|
/* Description: */ "ATV decoder for SDR++",
|
||||||
/* Author: */ "Ryzerth",
|
/* Author: */ "Ryzerth",
|
||||||
/* Version: */ 0, 1, 0,
|
/* Version: */ 0, 1, 0,
|
||||||
/* Max instances */ -1};
|
/* Max instances */ -1
|
||||||
|
};
|
||||||
|
|
||||||
#define SAMPLE_RATE (625.0f * 720.0f * 25.0f)
|
#define SAMPLE_RATE (625.0f * 720.0f * 25.0f)
|
||||||
|
|
||||||
|
@ -29,9 +39,16 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 8000000.0f, SAMPLE_RATE, SAMPLE_RATE, SAMPLE_RATE, true);
|
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 8000000.0f, SAMPLE_RATE, SAMPLE_RATE, SAMPLE_RATE, true);
|
||||||
|
|
||||||
demod.init(vfo->output, SAMPLE_RATE, SAMPLE_RATE / 2.0f);
|
demod.init(vfo->output, SAMPLE_RATE, SAMPLE_RATE / 2.0f);
|
||||||
sink.init(&demod.out, handler, this);
|
sync.init(&demod.out, 1.0f, 1e-6, 1.0, 0.05);
|
||||||
|
sink.init(&sync.out, handler, this);
|
||||||
|
|
||||||
|
r2c.init(NULL);
|
||||||
|
chromaTaps = dsp::taps::fromArray(CHROMA_FIR_SIZE, CHROMA_FIR);
|
||||||
|
fir.init(NULL, chromaTaps);
|
||||||
|
pll.init(NULL, 0.01, 0.0, dsp::math::hzToRads(4433618.75, SAMPLE_RATE), dsp::math::hzToRads(4433618.75*0.90, SAMPLE_RATE), dsp::math::hzToRads(4433618.75*1.1, SAMPLE_RATE));
|
||||||
|
|
||||||
demod.start();
|
demod.start();
|
||||||
|
sync.start();
|
||||||
sink.start();
|
sink.start();
|
||||||
|
|
||||||
gui::menu.registerEntry(name, menuHandler, this, this);
|
gui::menu.registerEntry(name, menuHandler, this, this);
|
||||||
|
@ -47,9 +64,13 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
|
|
||||||
void postInit() {}
|
void postInit() {}
|
||||||
|
|
||||||
void enable() { enabled = true; }
|
void enable() {
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
void disable() { enabled = false; }
|
void disable() {
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isEnabled() { return enabled; }
|
bool isEnabled() { return enabled; }
|
||||||
|
|
||||||
|
@ -61,6 +82,8 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
style::beginDisabled();
|
style::beginDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ideal width for testing: 750pixels
|
||||||
|
|
||||||
ImGui::FillWidth();
|
ImGui::FillWidth();
|
||||||
_this->img.draw();
|
_this->img.draw();
|
||||||
|
|
||||||
|
@ -76,6 +99,28 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
ImGui::FillWidth();
|
ImGui::FillWidth();
|
||||||
ImGui::SliderFloat("##spanLvl", &_this->spanLvl, 0, 1.0);
|
ImGui::SliderFloat("##spanLvl", &_this->spanLvl, 0, 1.0);
|
||||||
|
|
||||||
|
ImGui::LeftLabel("Sync Bias");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
ImGui::SliderFloat("##syncBias", &_this->sync.syncBias,-0.1, 0.1);
|
||||||
|
|
||||||
|
if (ImGui::Button("Test2")) {
|
||||||
|
_this->sync.test2 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::Button("Switch frame")) {
|
||||||
|
std::lock_guard<std::mutex> lck(_this->evenFrameMtx);
|
||||||
|
_this->evenFrame = !_this->evenFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_this->sync.locked) {
|
||||||
|
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Locked");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::TextUnformatted("Not locked");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::Checkbox("Force Lock", &_this->sync.forceLock);
|
||||||
|
|
||||||
if (!_this->enabled) {
|
if (!_this->enabled) {
|
||||||
style::endDisabled();
|
style::endDisabled();
|
||||||
}
|
}
|
||||||
|
@ -84,70 +129,66 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
static void handler(float *data, int count, void *ctx) {
|
static void handler(float *data, int count, void *ctx) {
|
||||||
ATVDecoderModule *_this = (ATVDecoderModule *)ctx;
|
ATVDecoderModule *_this = (ATVDecoderModule *)ctx;
|
||||||
|
|
||||||
uint8_t *buf = (uint8_t *)_this->img.buffer;
|
// Convert line to complex
|
||||||
float val;
|
_this->r2c.process(720, data, _this->r2c.out.writeBuf);
|
||||||
float imval;
|
|
||||||
int pos = 0;
|
// Isolate the chroma subcarrier
|
||||||
|
_this->fir.process(720, _this->r2c.out.writeBuf, _this->fir.out.writeBuf);
|
||||||
|
|
||||||
|
// Run chroma carrier through the PLL
|
||||||
|
_this->pll.process(720, _this->fir.out.writeBuf, _this->pll.out.writeBuf, ((_this->ypos%2)==1) ^ _this->evenFrame);
|
||||||
|
|
||||||
|
// Render line to the image without color
|
||||||
|
//int lypos = _this->ypos - 1;
|
||||||
|
//if (lypos < 0) { lypos = 624; }
|
||||||
|
//uint32_t* lastLine = &((uint32_t *)_this->img.buffer)[(lypos < 313) ? (lypos*720*2) : ((((lypos - 313)*2)+1)*720) ];
|
||||||
|
//uint32_t* currentLine = &((uint32_t *)_this->img.buffer)[(_this->ypos < 313) ? (_this->ypos*720*2) : ((((_this->ypos - 313)*2)+1)*720) ];
|
||||||
|
|
||||||
|
uint32_t* currentLine = &((uint32_t *)_this->img.buffer)[_this->ypos*720];
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
val = data[i];
|
//float imval = std::clamp<float>((data[i] - _this->minLvl) * 255.0 / _this->spanLvl, 0, 255);
|
||||||
// Sync
|
uint32_t re = std::clamp<float>((_this->pll.out.writeBuf[i].re - _this->minLvl) * 255.0 / _this->spanLvl, 0, 255);
|
||||||
if (val < _this->sync_level) {
|
uint32_t im = std::clamp<float>((_this->pll.out.writeBuf[i].im - _this->minLvl) * 255.0 / _this->spanLvl, 0, 255);
|
||||||
_this->sync_count++;
|
currentLine[i] = 0xFF000000 | (im << 8) | re;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
if (_this->sync_count >= 300) {
|
// Vertical scan logic
|
||||||
_this->short_sync = 0;
|
_this->ypos++;
|
||||||
}
|
bool rollover = _this->ypos >= 625;
|
||||||
else if (_this->sync_count >= 33) {
|
if (rollover) {
|
||||||
if (_this->short_sync == 5) {
|
{
|
||||||
_this->even_field = false;
|
std::lock_guard<std::mutex> lck(_this->evenFrameMtx);
|
||||||
_this->ypos = 0;
|
_this->evenFrame = !_this->evenFrame;
|
||||||
_this->img.swap();
|
|
||||||
buf = (uint8_t *)_this->img.buffer;
|
|
||||||
}
|
|
||||||
else if (_this->short_sync == 4) {
|
|
||||||
_this->even_field = true;
|
|
||||||
_this->ypos = 0;
|
|
||||||
}
|
|
||||||
_this->xpos = 0;
|
|
||||||
_this->short_sync = 0;
|
|
||||||
}
|
|
||||||
else if (_this->sync_count >= 15) {
|
|
||||||
_this->short_sync++;
|
|
||||||
}
|
|
||||||
_this->sync_count = 0;
|
|
||||||
}
|
}
|
||||||
|
_this->ypos = 0;
|
||||||
|
_this->img.swap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Measure vsync levels
|
||||||
|
float sync0 = 0.0f, sync1 = 0.0f;
|
||||||
|
for (int i = 0; i < 306; i++) {
|
||||||
|
sync0 += data[i];
|
||||||
|
}
|
||||||
|
for (int i = (720/2); i < ((720/2)+306); i++) {
|
||||||
|
sync1 += data[i];
|
||||||
|
}
|
||||||
|
sync0 *= (1.0f/305.0f);
|
||||||
|
sync1 *= (1.0f/305.0f);
|
||||||
|
|
||||||
// Draw
|
// Save sync detection to history
|
||||||
imval = std::clamp<float>((val - _this->minLvl) * 255.0 / _this->spanLvl, 0, 255);
|
_this->syncHistory >>= 2;
|
||||||
if (_this->even_field) {
|
_this->syncHistory |= (((uint16_t)(sync1 < _this->sync_level)) << 9) | (((uint16_t)(sync0 < _this->sync_level)) << 8);
|
||||||
pos = ((720 * _this->ypos * 2) + _this->xpos) * 4;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
pos = ((720 * (_this->ypos * 2 + 1)) + _this->xpos) * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
buf[pos] = imval;
|
// Trigger vsync in case one is detected
|
||||||
buf[pos + 1] = imval;
|
// TODO: Also sync with odd field
|
||||||
buf[pos + 2] = imval;
|
if (!rollover && _this->syncHistory == 0b0000011111) {
|
||||||
buf[pos + 3] = imval;
|
{
|
||||||
|
std::lock_guard<std::mutex> lck(_this->evenFrameMtx);
|
||||||
// Image logic
|
_this->evenFrame = !_this->evenFrame;
|
||||||
_this->xpos++;
|
|
||||||
if (_this->xpos >= 720) {
|
|
||||||
_this->ypos++;
|
|
||||||
_this->xpos = 0;
|
|
||||||
}
|
|
||||||
if (_this->ypos >= 312) {
|
|
||||||
_this->ypos = 0;
|
|
||||||
_this->xpos = 0;
|
|
||||||
_this->even_field = !_this->even_field;
|
|
||||||
if (_this->even_field) {
|
|
||||||
_this->img.swap();
|
|
||||||
buf = (uint8_t *)_this->img.buffer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_this->ypos = 0;
|
||||||
|
_this->img.swap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,19 +197,27 @@ class ATVDecoderModule : public ModuleManager::Instance {
|
||||||
|
|
||||||
VFOManager::VFO *vfo = NULL;
|
VFOManager::VFO *vfo = NULL;
|
||||||
dsp::demod::Quadrature demod;
|
dsp::demod::Quadrature demod;
|
||||||
|
LineSync sync;
|
||||||
dsp::sink::Handler<float> sink;
|
dsp::sink::Handler<float> sink;
|
||||||
|
dsp::convert::RealToComplex r2c;
|
||||||
int xpos = 0;
|
dsp::tap<dsp::complex_t> chromaTaps;
|
||||||
|
dsp::filter::FIR<dsp::complex_t, dsp::complex_t> fir;
|
||||||
|
dsp::loop::ChromaPLL pll;
|
||||||
int ypos = 0;
|
int ypos = 0;
|
||||||
bool even_field = false;
|
|
||||||
|
|
||||||
float sync_level = -0.3f;
|
bool evenFrame = false;
|
||||||
|
std::mutex evenFrameMtx;
|
||||||
|
|
||||||
|
float sync_level = -0.06f;
|
||||||
int sync_count = 0;
|
int sync_count = 0;
|
||||||
int short_sync = 0;
|
int short_sync = 0;
|
||||||
|
|
||||||
float minLvl = 0.0f;
|
float minLvl = 0.0f;
|
||||||
float spanLvl = 1.0f;
|
float spanLvl = 1.0f;
|
||||||
|
|
||||||
|
bool lockedLines = 0;
|
||||||
|
uint16_t syncHistory = 0;
|
||||||
|
|
||||||
ImGui::ImageDisplay img;
|
ImGui::ImageDisplay img;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(pager_decoder)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE SRC "src/*.cpp" "src/*.c")
|
||||||
|
|
||||||
|
include(${SDRPP_MODULE_CMAKE})
|
||||||
|
|
||||||
|
target_include_directories(pager_decoder PRIVATE "src/")
|
|
@ -0,0 +1,11 @@
|
||||||
|
#pragma once
|
||||||
|
#include <signal_path/vfo_manager.h>
|
||||||
|
|
||||||
|
class Decoder {
|
||||||
|
public:
|
||||||
|
virtual ~Decoder() {}
|
||||||
|
virtual void showMenu() {};
|
||||||
|
virtual void setVFO(VFOManager::VFO* vfo) = 0;
|
||||||
|
virtual void start() = 0;
|
||||||
|
virtual void stop() = 0;
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
#pragma once
|
||||||
|
#include "../decoder.h"
|
||||||
|
#include <signal_path/vfo_manager.h>
|
||||||
|
#include <utils/optionlist.h>
|
||||||
|
#include <gui/widgets/symbol_diagram.h>
|
||||||
|
#include <gui/style.h>
|
||||||
|
#include <dsp/sink/handler_sink.h>
|
||||||
|
#include "flex.h"
|
||||||
|
|
||||||
|
class FLEXDecoder : public Decoder {
|
||||||
|
dsp::stream<float> dummy1;
|
||||||
|
dsp::stream<uint8_t> dummy2;
|
||||||
|
public:
|
||||||
|
FLEXDecoder(const std::string& name, VFOManager::VFO* vfo) : diag(0.6, 1600) {
|
||||||
|
this->name = name;
|
||||||
|
this->vfo = vfo;
|
||||||
|
|
||||||
|
// Define baudrate options
|
||||||
|
baudrates.define(1600, "1600 Baud", 1600);
|
||||||
|
baudrates.define(3200, "3200 Baud", 3200);
|
||||||
|
baudrates.define(6400, "6400 Baud", 6400);
|
||||||
|
|
||||||
|
// Init DSP
|
||||||
|
vfo->setBandwidthLimits(12500, 12500, true);
|
||||||
|
vfo->setSampleRate(16000, 12500);
|
||||||
|
reshape.init(&dummy1, 1600.0, (1600 / 30.0) - 1600.0);
|
||||||
|
dataHandler.init(&dummy2, _dataHandler, this);
|
||||||
|
diagHandler.init(&reshape.out, _diagHandler, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~FLEXDecoder() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMenu() {
|
||||||
|
ImGui::LeftLabel("Baudrate");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##pager_decoder_flex_br_" + name).c_str(), &brId, baudrates.txt)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::FillWidth();
|
||||||
|
diag.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVFO(VFOManager::VFO* vfo) {
|
||||||
|
this->vfo = vfo;
|
||||||
|
vfo->setBandwidthLimits(12500, 12500, true);
|
||||||
|
vfo->setSampleRate(24000, 12500);
|
||||||
|
// dsp.setInput(vfo->output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
flog::debug("FLEX start");
|
||||||
|
// dsp.start();
|
||||||
|
reshape.start();
|
||||||
|
dataHandler.start();
|
||||||
|
diagHandler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
flog::debug("FLEX stop");
|
||||||
|
// dsp.stop();
|
||||||
|
reshape.stop();
|
||||||
|
dataHandler.stop();
|
||||||
|
diagHandler.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void _dataHandler(uint8_t* data, int count, void* ctx) {
|
||||||
|
FLEXDecoder* _this = (FLEXDecoder*)ctx;
|
||||||
|
// _this->decoder.process(data, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _diagHandler(float* data, int count, void* ctx) {
|
||||||
|
FLEXDecoder* _this = (FLEXDecoder*)ctx;
|
||||||
|
float* buf = _this->diag.acquireBuffer();
|
||||||
|
memcpy(buf, data, count * sizeof(float));
|
||||||
|
_this->diag.releaseBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
|
||||||
|
VFOManager::VFO* vfo;
|
||||||
|
dsp::buffer::Reshaper<float> reshape;
|
||||||
|
dsp::sink::Handler<uint8_t> dataHandler;
|
||||||
|
dsp::sink::Handler<float> diagHandler;
|
||||||
|
|
||||||
|
flex::Decoder decoder;
|
||||||
|
|
||||||
|
ImGui::SymbolDiagram diag;
|
||||||
|
|
||||||
|
int brId = 0;
|
||||||
|
|
||||||
|
OptionList<int, int> baudrates;
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
#include "flex.h"
|
||||||
|
|
||||||
|
namespace flex {
|
||||||
|
// TODO
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace flex {
|
||||||
|
class Decoder {
|
||||||
|
public:
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
private:
|
||||||
|
// TODO
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <config.h>
|
||||||
|
#include <core.h>
|
||||||
|
#include <gui/style.h>
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <signal_path/signal_path.h>
|
||||||
|
#include <module.h>
|
||||||
|
#include <gui/widgets/folder_select.h>
|
||||||
|
#include <utils/optionlist.h>
|
||||||
|
#include "decoder.h"
|
||||||
|
#include "pocsag/decoder.h"
|
||||||
|
#include "flex/decoder.h"
|
||||||
|
|
||||||
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||||
|
|
||||||
|
SDRPP_MOD_INFO{
|
||||||
|
/* Name: */ "pager_decoder",
|
||||||
|
/* Description: */ "POCSAG and Flex Pager Decoder"
|
||||||
|
/* Author: */ "Ryzerth",
|
||||||
|
/* Version: */ 0, 1, 0,
|
||||||
|
/* Max instances */ -1
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfigManager config;
|
||||||
|
|
||||||
|
enum Protocol {
|
||||||
|
PROTOCOL_INVALID = -1,
|
||||||
|
PROTOCOL_POCSAG,
|
||||||
|
PROTOCOL_FLEX
|
||||||
|
};
|
||||||
|
|
||||||
|
class PagerDecoderModule : public ModuleManager::Instance {
|
||||||
|
public:
|
||||||
|
PagerDecoderModule(std::string name) {
|
||||||
|
this->name = name;
|
||||||
|
|
||||||
|
// Define protocols
|
||||||
|
protocols.define("POCSAG", PROTOCOL_POCSAG);
|
||||||
|
protocols.define("FLEX", PROTOCOL_FLEX);
|
||||||
|
|
||||||
|
// Initialize VFO with default values
|
||||||
|
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, 12500, 24000, 12500, 12500, true);
|
||||||
|
vfo->setSnapInterval(1);
|
||||||
|
|
||||||
|
// Select the protocol
|
||||||
|
selectProtocol(PROTOCOL_POCSAG);
|
||||||
|
|
||||||
|
gui::menu.registerEntry(name, menuHandler, this, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~PagerDecoderModule() {
|
||||||
|
gui::menu.removeEntry(name);
|
||||||
|
// Stop DSP
|
||||||
|
if (enabled) {
|
||||||
|
decoder->stop();
|
||||||
|
decoder.reset();
|
||||||
|
sigpath::vfoManager.deleteVFO(vfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
sigpath::sinkManager.unregisterStream(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void postInit() {}
|
||||||
|
|
||||||
|
void enable() {
|
||||||
|
double bw = gui::waterfall.getBandwidth();
|
||||||
|
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, std::clamp<double>(0, -bw / 2.0, bw / 2.0), 12500, 24000, 12500, 12500, true);
|
||||||
|
vfo->setSnapInterval(1);
|
||||||
|
|
||||||
|
decoder->setVFO(vfo);
|
||||||
|
decoder->start();
|
||||||
|
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
decoder->stop();
|
||||||
|
sigpath::vfoManager.deleteVFO(vfo);
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectProtocol(Protocol newProto) {
|
||||||
|
// Cannot change while disabled
|
||||||
|
if (!enabled) { return; }
|
||||||
|
|
||||||
|
// If the protocol hasn't changed, no need to do anything
|
||||||
|
if (newProto == proto) { return; }
|
||||||
|
|
||||||
|
// Delete current decoder
|
||||||
|
decoder.reset();
|
||||||
|
|
||||||
|
// Create a new decoder
|
||||||
|
switch (newProto) {
|
||||||
|
case PROTOCOL_POCSAG:
|
||||||
|
decoder = std::make_unique<POCSAGDecoder>(name, vfo);
|
||||||
|
break;
|
||||||
|
case PROTOCOL_FLEX:
|
||||||
|
decoder = std::make_unique<FLEXDecoder>(name, vfo);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
flog::error("Tried to select unknown pager protocol");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the new decoder
|
||||||
|
decoder->start();
|
||||||
|
|
||||||
|
// Save selected protocol
|
||||||
|
proto = newProto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void menuHandler(void* ctx) {
|
||||||
|
PagerDecoderModule* _this = (PagerDecoderModule*)ctx;
|
||||||
|
|
||||||
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
|
if (!_this->enabled) { style::beginDisabled(); }
|
||||||
|
|
||||||
|
ImGui::LeftLabel("Protocol");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##pager_decoder_proto_" + _this->name).c_str(), &_this->protoId, _this->protocols.txt)) {
|
||||||
|
_this->selectProtocol(_this->protocols.value(_this->protoId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_this->decoder) { _this->decoder->showMenu(); }
|
||||||
|
|
||||||
|
ImGui::Button(("Record##pager_decoder_show_" + _this->name).c_str(), ImVec2(menuWidth, 0));
|
||||||
|
ImGui::Button(("Show Messages##pager_decoder_show_" + _this->name).c_str(), ImVec2(menuWidth, 0));
|
||||||
|
|
||||||
|
if (!_this->enabled) { style::endDisabled(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
Protocol proto = PROTOCOL_INVALID;
|
||||||
|
int protoId = 0;
|
||||||
|
|
||||||
|
OptionList<std::string, Protocol> protocols;
|
||||||
|
|
||||||
|
// DSP Chain
|
||||||
|
VFOManager::VFO* vfo;
|
||||||
|
std::unique_ptr<Decoder> decoder;
|
||||||
|
|
||||||
|
bool showLines = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
MOD_EXPORT void _INIT_() {
|
||||||
|
// Create default recording directory
|
||||||
|
json def = json({});
|
||||||
|
config.setPath(core::args["root"].s() + "/pager_decoder_config.json");
|
||||||
|
config.load(def);
|
||||||
|
config.enableAutoSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
|
||||||
|
return new PagerDecoderModule(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT void _DELETE_INSTANCE_(void* instance) {
|
||||||
|
delete (PagerDecoderModule*)instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT void _END_() {
|
||||||
|
config.disableAutoSave();
|
||||||
|
config.save();
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
#pragma once
|
||||||
|
#include "../decoder.h"
|
||||||
|
#include <signal_path/vfo_manager.h>
|
||||||
|
#include <utils/optionlist.h>
|
||||||
|
#include <gui/widgets/symbol_diagram.h>
|
||||||
|
#include <gui/style.h>
|
||||||
|
#include <dsp/sink/handler_sink.h>
|
||||||
|
#include "dsp.h"
|
||||||
|
#include "pocsag.h"
|
||||||
|
|
||||||
|
const char* msgTypes[] = {
|
||||||
|
"Numeric",
|
||||||
|
"Unknown (0b01)",
|
||||||
|
"Unknown (0b10)",
|
||||||
|
"Alphanumeric",
|
||||||
|
};
|
||||||
|
|
||||||
|
class POCSAGDecoder : public Decoder {
|
||||||
|
public:
|
||||||
|
POCSAGDecoder(const std::string& name, VFOManager::VFO* vfo) : diag(0.6, 2400) {
|
||||||
|
this->name = name;
|
||||||
|
this->vfo = vfo;
|
||||||
|
|
||||||
|
// Define baudrate options
|
||||||
|
baudrates.define(512, "512 Baud", 512);
|
||||||
|
baudrates.define(1200, "1200 Baud", 1200);
|
||||||
|
baudrates.define(2400, "2400 Baud", 2400);
|
||||||
|
|
||||||
|
// Init DSP
|
||||||
|
vfo->setBandwidthLimits(12500, 12500, true);
|
||||||
|
vfo->setSampleRate(24000, 12500);
|
||||||
|
dsp.init(vfo->output, 24000, 2400);
|
||||||
|
reshape.init(&dsp.soft, 2400.0, (2400 / 30.0) - 2400.0);
|
||||||
|
dataHandler.init(&dsp.out, _dataHandler, this);
|
||||||
|
diagHandler.init(&reshape.out, _diagHandler, this);
|
||||||
|
|
||||||
|
// Init decoder
|
||||||
|
decoder.onMessage.bind(&POCSAGDecoder::messageHandler, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~POCSAGDecoder() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMenu() {
|
||||||
|
ImGui::LeftLabel("Baudrate");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##pager_decoder_pocsag_br_" + name).c_str(), &brId, baudrates.txt)) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::FillWidth();
|
||||||
|
diag.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setVFO(VFOManager::VFO* vfo) {
|
||||||
|
this->vfo = vfo;
|
||||||
|
vfo->setBandwidthLimits(12500, 12500, true);
|
||||||
|
vfo->setSampleRate(24000, 12500);
|
||||||
|
dsp.setInput(vfo->output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
flog::debug("POCSAG start");
|
||||||
|
dsp.start();
|
||||||
|
reshape.start();
|
||||||
|
dataHandler.start();
|
||||||
|
diagHandler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
flog::debug("POCSAG stop");
|
||||||
|
dsp.stop();
|
||||||
|
reshape.stop();
|
||||||
|
dataHandler.stop();
|
||||||
|
diagHandler.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void _dataHandler(uint8_t* data, int count, void* ctx) {
|
||||||
|
POCSAGDecoder* _this = (POCSAGDecoder*)ctx;
|
||||||
|
_this->decoder.process(data, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _diagHandler(float* data, int count, void* ctx) {
|
||||||
|
POCSAGDecoder* _this = (POCSAGDecoder*)ctx;
|
||||||
|
float* buf = _this->diag.acquireBuffer();
|
||||||
|
memcpy(buf, data, count * sizeof(float));
|
||||||
|
_this->diag.releaseBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void messageHandler(pocsag::Address addr, pocsag::MessageType type, const std::string& msg) {
|
||||||
|
flog::debug("[{}]: '{}'", (uint32_t)addr, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
VFOManager::VFO* vfo;
|
||||||
|
|
||||||
|
POCSAGDSP dsp;
|
||||||
|
dsp::buffer::Reshaper<float> reshape;
|
||||||
|
dsp::sink::Handler<uint8_t> dataHandler;
|
||||||
|
dsp::sink::Handler<float> diagHandler;
|
||||||
|
|
||||||
|
pocsag::Decoder decoder;
|
||||||
|
|
||||||
|
ImGui::SymbolDiagram diag;
|
||||||
|
|
||||||
|
int brId = 2;
|
||||||
|
|
||||||
|
OptionList<int, int> baudrates;
|
||||||
|
};
|
|
@ -0,0 +1,71 @@
|
||||||
|
#pragma once
|
||||||
|
#include <dsp/stream.h>
|
||||||
|
#include <dsp/buffer/reshaper.h>
|
||||||
|
#include <dsp/multirate/rational_resampler.h>
|
||||||
|
#include <dsp/sink/handler_sink.h>
|
||||||
|
#include <dsp/demod/quadrature.h>
|
||||||
|
#include <dsp/clock_recovery/mm.h>
|
||||||
|
#include <dsp/taps/root_raised_cosine.h>
|
||||||
|
#include <dsp/correction/dc_blocker.h>
|
||||||
|
#include <dsp/loop/fast_agc.h>
|
||||||
|
#include <dsp/digital/binary_slicer.h>
|
||||||
|
#include <dsp/routing/doubler.h>
|
||||||
|
|
||||||
|
class POCSAGDSP : public dsp::Processor<dsp::complex_t, uint8_t> {
|
||||||
|
using base_type = dsp::Processor<dsp::complex_t, uint8_t>;
|
||||||
|
public:
|
||||||
|
POCSAGDSP() {}
|
||||||
|
POCSAGDSP(dsp::stream<dsp::complex_t>* in, double samplerate, double baudrate) { init(in, samplerate, baudrate); }
|
||||||
|
|
||||||
|
void init(dsp::stream<dsp::complex_t>* in, double samplerate, double baudrate) {
|
||||||
|
// Save settings
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
// Configure blocks
|
||||||
|
demod.init(NULL, -4500.0, samplerate);
|
||||||
|
dcBlock.init(NULL, 0.001);
|
||||||
|
float taps[] = { 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f, 0.1f };
|
||||||
|
shape = dsp::taps::fromArray<float>(10, taps);
|
||||||
|
fir.init(NULL, shape);
|
||||||
|
recov.init(NULL, samplerate/baudrate, 1e5, 0.1, 0.05);
|
||||||
|
|
||||||
|
// Free useless buffers
|
||||||
|
dcBlock.out.free();
|
||||||
|
fir.out.free();
|
||||||
|
recov.out.free();
|
||||||
|
|
||||||
|
// Init base
|
||||||
|
base_type::init(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
int process(int count, dsp::complex_t* in, float* softOut, uint8_t* out) {
|
||||||
|
count = demod.process(count, in, demod.out.readBuf);
|
||||||
|
count = dcBlock.process(count, demod.out.readBuf, demod.out.readBuf);
|
||||||
|
count = fir.process(count, demod.out.readBuf, demod.out.readBuf);
|
||||||
|
count = recov.process(count, demod.out.readBuf, softOut);
|
||||||
|
dsp::digital::BinarySlicer::process(count, softOut, out);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int run() {
|
||||||
|
int count = base_type::_in->read();
|
||||||
|
if (count < 0) { return -1; }
|
||||||
|
|
||||||
|
count = process(count, base_type::_in->readBuf, soft.writeBuf, base_type::out.writeBuf);
|
||||||
|
|
||||||
|
base_type::_in->flush();
|
||||||
|
if (!base_type::out.swap(count)) { return -1; }
|
||||||
|
if (!soft.swap(count)) { return -1; }
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp::stream<float> soft;
|
||||||
|
|
||||||
|
private:
|
||||||
|
dsp::demod::Quadrature demod;
|
||||||
|
dsp::correction::DCBlocker<float> dcBlock;
|
||||||
|
dsp::tap<float> shape;
|
||||||
|
dsp::filter::FIR<float, float> fir;
|
||||||
|
dsp::clock_recovery::MM<float> recov;
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,140 @@
|
||||||
|
#include "pocsag.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <utils/flog.h>
|
||||||
|
|
||||||
|
#define POCSAG_FRAME_SYNC_CODEWORD ((uint32_t)(0b01111100110100100001010111011000))
|
||||||
|
#define POCSAG_IDLE_CODEWORD_DATA ((uint32_t)(0b011110101100100111000))
|
||||||
|
#define POCSAG_BATCH_BIT_COUNT (POCSAG_BATCH_CODEWORD_COUNT*32)
|
||||||
|
|
||||||
|
#define POCSAG_GEN_POLY ((uint32_t)(0b11101101001))
|
||||||
|
|
||||||
|
namespace pocsag {
|
||||||
|
const char NUMERIC_CHARSET[] = {
|
||||||
|
'0',
|
||||||
|
'1',
|
||||||
|
'2',
|
||||||
|
'3',
|
||||||
|
'4',
|
||||||
|
'5',
|
||||||
|
'6',
|
||||||
|
'7',
|
||||||
|
'8',
|
||||||
|
'9',
|
||||||
|
'*',
|
||||||
|
'U',
|
||||||
|
' ',
|
||||||
|
'-',
|
||||||
|
']',
|
||||||
|
'['
|
||||||
|
};
|
||||||
|
|
||||||
|
void Decoder::process(uint8_t* symbols, int count) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
// Get symbol
|
||||||
|
uint32_t s = symbols[i];
|
||||||
|
|
||||||
|
// If not sync, try to acquire sync (TODO: sync confidence)
|
||||||
|
if (!synced) {
|
||||||
|
// Append new symbol to sync shift register
|
||||||
|
syncSR = (syncSR << 1) | s;
|
||||||
|
|
||||||
|
// Test for sync
|
||||||
|
synced = (distance(syncSR, POCSAG_FRAME_SYNC_CODEWORD) <= POCSAG_SYNC_DIST);
|
||||||
|
|
||||||
|
// Go to next symbol
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Flush message on desync
|
||||||
|
|
||||||
|
// Append bit to batch
|
||||||
|
batch[batchOffset >> 5] |= (s << (31 - (batchOffset & 0b11111)));
|
||||||
|
batchOffset++;
|
||||||
|
|
||||||
|
// On end of batch, decode and reset
|
||||||
|
if (batchOffset >= POCSAG_BATCH_BIT_COUNT) {
|
||||||
|
decodeBatch();
|
||||||
|
batchOffset = 0;
|
||||||
|
synced = false;
|
||||||
|
memset(batch, 0, sizeof(batch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Decoder::distance(uint32_t a, uint32_t b) {
|
||||||
|
uint32_t diff = a ^ b;
|
||||||
|
int dist = 0;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
dist += (diff >> i ) & 1;
|
||||||
|
}
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Decoder::correctCodeword(Codeword in, Codeword& out) {
|
||||||
|
|
||||||
|
|
||||||
|
return true; // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::flushMessage() {
|
||||||
|
if (!msg.empty()) {
|
||||||
|
onMessage(addr, msgType, msg);
|
||||||
|
msg.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::decodeBatch() {
|
||||||
|
for (int i = 0; i < POCSAG_BATCH_CODEWORD_COUNT; i++) {
|
||||||
|
// Get codeword
|
||||||
|
Codeword cw = batch[i];
|
||||||
|
|
||||||
|
// Correct errors. If corrupted, skip
|
||||||
|
if (!correctCodeword(cw, cw)) { continue; }
|
||||||
|
// TODO: End message if two consecutive are corrupt
|
||||||
|
|
||||||
|
// Get codeword type
|
||||||
|
CodewordType type = (CodewordType)((cw >> 31) & 1);
|
||||||
|
if (type == CODEWORD_TYPE_ADDRESS && (cw >> 11) == POCSAG_IDLE_CODEWORD_DATA) {
|
||||||
|
type = CODEWORD_TYPE_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode codeword
|
||||||
|
if (type == CODEWORD_TYPE_IDLE) {
|
||||||
|
// If a non-empty message is available, send it out and clear
|
||||||
|
flushMessage();
|
||||||
|
flog::debug("[{}:{}]: IDLE", (i >> 1), i&1);
|
||||||
|
}
|
||||||
|
else if (type == CODEWORD_TYPE_ADDRESS) {
|
||||||
|
// If a non-empty message is available, send it out and clear
|
||||||
|
flushMessage();
|
||||||
|
|
||||||
|
// Decode message type
|
||||||
|
msgType = (MessageType)((cw >> 11) & 0b11);
|
||||||
|
|
||||||
|
// Decode address and append lower 8 bits from position
|
||||||
|
addr = ((cw >> 13) & 0b111111111111111111) << 3;
|
||||||
|
addr |= (i >> 1);
|
||||||
|
}
|
||||||
|
else if (type == CODEWORD_TYPE_MESSAGE) {
|
||||||
|
// Extract the 20 data bits
|
||||||
|
uint32_t data = (cw >> 11) & 0b11111111111111111111;
|
||||||
|
|
||||||
|
// Decode data depending on message type
|
||||||
|
if (msgType == MESSAGE_TYPE_NUMERIC) {
|
||||||
|
// Numeric messages pack 5 characters per message codeword
|
||||||
|
msg += NUMERIC_CHARSET[(data >> 16) & 0b1111];
|
||||||
|
msg += NUMERIC_CHARSET[(data >> 12) & 0b1111];
|
||||||
|
msg += NUMERIC_CHARSET[(data >> 8) & 0b1111];
|
||||||
|
msg += NUMERIC_CHARSET[(data >> 4) & 0b1111];
|
||||||
|
msg += NUMERIC_CHARSET[data & 0b1111];
|
||||||
|
}
|
||||||
|
else if (msgType == MESSAGE_TYPE_ALPHANUMERIC) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save last data
|
||||||
|
lastMsgData = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <utils/new_event.h>
|
||||||
|
|
||||||
|
#define POCSAG_SYNC_DIST 4
|
||||||
|
#define POCSAG_BATCH_CODEWORD_COUNT 16
|
||||||
|
|
||||||
|
namespace pocsag {
|
||||||
|
enum CodewordType {
|
||||||
|
CODEWORD_TYPE_IDLE = -1,
|
||||||
|
CODEWORD_TYPE_ADDRESS = 0,
|
||||||
|
CODEWORD_TYPE_MESSAGE = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum MessageType {
|
||||||
|
MESSAGE_TYPE_NUMERIC = 0b00,
|
||||||
|
MESSAGE_TYPE_ALPHANUMERIC = 0b11
|
||||||
|
};
|
||||||
|
|
||||||
|
using Codeword = uint32_t;
|
||||||
|
using Address = uint32_t;
|
||||||
|
|
||||||
|
class Decoder {
|
||||||
|
public:
|
||||||
|
void process(uint8_t* symbols, int count);
|
||||||
|
|
||||||
|
NewEvent<Address, MessageType, const std::string&> onMessage;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int distance(uint32_t a, uint32_t b);
|
||||||
|
bool correctCodeword(Codeword in, Codeword& out);
|
||||||
|
void flushMessage();
|
||||||
|
void decodeBatch();
|
||||||
|
|
||||||
|
uint32_t syncSR = 0;
|
||||||
|
bool synced = false;
|
||||||
|
int batchOffset = 0;
|
||||||
|
|
||||||
|
Codeword batch[POCSAG_BATCH_CODEWORD_COUNT];
|
||||||
|
|
||||||
|
Address addr;
|
||||||
|
MessageType msgType;
|
||||||
|
std::string msg;
|
||||||
|
|
||||||
|
uint32_t lastMsgData;
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,22 +1,22 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "../demod.h"
|
#include "../demod.h"
|
||||||
#include <dsp/demod/broadcast_fm.h>
|
#include <dsp/demod/broadcast_fm.h>
|
||||||
#include <dsp/clock_recovery/mm.h>
|
#include "../rds_demod.h"
|
||||||
#include <dsp/clock_recovery/fd.h>
|
|
||||||
#include <dsp/taps/root_raised_cosine.h>
|
|
||||||
#include <dsp/digital/binary_slicer.h>
|
|
||||||
#include <dsp/digital/manchester_decoder.h>
|
|
||||||
#include <dsp/digital/differential_decoder.h>
|
|
||||||
#include <gui/widgets/symbol_diagram.h>
|
#include <gui/widgets/symbol_diagram.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <rds.h>
|
#include <rds.h>
|
||||||
|
|
||||||
namespace demod {
|
namespace demod {
|
||||||
|
enum RDSRegion {
|
||||||
|
RDS_REGION_EUROPE,
|
||||||
|
RDS_REGION_NORTH_AMERICA
|
||||||
|
};
|
||||||
|
|
||||||
class WFM : public Demodulator {
|
class WFM : public Demodulator {
|
||||||
public:
|
public:
|
||||||
WFM() {}
|
WFM() : diag(0.5, 4096) {}
|
||||||
|
|
||||||
WFM(std::string name, ConfigManager* config, dsp::stream<dsp::complex_t>* input, double bandwidth, double audioSR) {
|
WFM(std::string name, ConfigManager* config, dsp::stream<dsp::complex_t>* input, double bandwidth, double audioSR) : diag(0.5, 4096) {
|
||||||
init(name, config, input, bandwidth, audioSR);
|
init(name, config, input, bandwidth, audioSR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,10 +29,18 @@ namespace demod {
|
||||||
this->name = name;
|
this->name = name;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
|
||||||
|
// Define RDS regions
|
||||||
|
rdsRegions.define("eu", "Europe", RDS_REGION_EUROPE);
|
||||||
|
rdsRegions.define("na", "North America", RDS_REGION_NORTH_AMERICA);
|
||||||
|
|
||||||
|
// Register FFT draw handler
|
||||||
fftRedrawHandler.handler = fftRedraw;
|
fftRedrawHandler.handler = fftRedraw;
|
||||||
fftRedrawHandler.ctx = this;
|
fftRedrawHandler.ctx = this;
|
||||||
gui::waterfall.onFFTRedraw.bindHandler(&fftRedrawHandler);
|
gui::waterfall.onFFTRedraw.bindHandler(&fftRedrawHandler);
|
||||||
|
|
||||||
|
// Default
|
||||||
|
std::string rdsRegionStr = "eu";
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
_config->acquire();
|
_config->acquire();
|
||||||
bool modified = false;
|
bool modified = false;
|
||||||
|
@ -45,33 +53,50 @@ namespace demod {
|
||||||
if (config->conf[name][getName()].contains("rds")) {
|
if (config->conf[name][getName()].contains("rds")) {
|
||||||
_rds = config->conf[name][getName()]["rds"];
|
_rds = config->conf[name][getName()]["rds"];
|
||||||
}
|
}
|
||||||
|
if (config->conf[name][getName()].contains("rdsInfo")) {
|
||||||
|
_rdsInfo = config->conf[name][getName()]["rdsInfo"];
|
||||||
|
}
|
||||||
|
if (config->conf[name][getName()].contains("rdsRegion")) {
|
||||||
|
rdsRegionStr = config->conf[name][getName()]["rdsRegion"];
|
||||||
|
}
|
||||||
_config->release(modified);
|
_config->release(modified);
|
||||||
|
|
||||||
// Define structure
|
// Load RDS region
|
||||||
|
if (rdsRegions.keyExists(rdsRegionStr)) {
|
||||||
|
rdsRegionId = rdsRegions.keyId(rdsRegionStr);
|
||||||
|
rdsRegion = rdsRegions.value(rdsRegionId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rdsRegion = RDS_REGION_EUROPE;
|
||||||
|
rdsRegionId = rdsRegions.valueId(rdsRegion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init DSP
|
||||||
demod.init(input, bandwidth / 2.0f, getIFSampleRate(), _stereo, _lowPass, _rds);
|
demod.init(input, bandwidth / 2.0f, getIFSampleRate(), _stereo, _lowPass, _rds);
|
||||||
recov.init(&demod.rdsOut, 5000.0 / 2375, omegaGain, muGain, 0.01);
|
rdsDemod.init(&demod.rdsOut, _rdsInfo);
|
||||||
slice.init(&recov.out);
|
hs.init(&rdsDemod.out, rdsHandler, this);
|
||||||
manch.init(&slice.out);
|
reshape.init(&rdsDemod.soft, 4096, (1187 / 30) - 4096);
|
||||||
diff.init(&manch.out, 2);
|
diagHandler.init(&reshape.out, _diagHandler, this);
|
||||||
hs.init(&diff.out, rdsHandler, this);
|
|
||||||
|
// Init RDS display
|
||||||
|
diag.lines.push_back(-0.8);
|
||||||
|
diag.lines.push_back(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
demod.start();
|
demod.start();
|
||||||
recov.start();
|
rdsDemod.start();
|
||||||
slice.start();
|
|
||||||
manch.start();
|
|
||||||
diff.start();
|
|
||||||
hs.start();
|
hs.start();
|
||||||
|
reshape.start();
|
||||||
|
diagHandler.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
demod.stop();
|
demod.stop();
|
||||||
recov.stop();
|
rdsDemod.stop();
|
||||||
slice.stop();
|
|
||||||
manch.stop();
|
|
||||||
diff.stop();
|
|
||||||
hs.stop();
|
hs.stop();
|
||||||
|
reshape.stop();
|
||||||
|
diagHandler.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMenu() {
|
void showMenu() {
|
||||||
|
@ -94,14 +119,129 @@ namespace demod {
|
||||||
_config->release(true);
|
_config->release(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (_rds) {
|
// TODO: This might break when the entire radio module is disabled
|
||||||
// if (rdsDecode.countryCodeValid()) { ImGui::Text("Country code: %d", rdsDecode.getCountryCode()); }
|
if (!_rds) { ImGui::BeginDisabled(); }
|
||||||
// if (rdsDecode.programCoverageValid()) { ImGui::Text("Program coverage: %d", rdsDecode.getProgramCoverage()); }
|
if (ImGui::Checkbox(("Advanced RDS Info##_radio_wfm_rds_info_" + name).c_str(), &_rdsInfo)) {
|
||||||
// if (rdsDecode.programRefNumberValid()) { ImGui::Text("Reference number: %d", rdsDecode.getProgramRefNumber()); }
|
setAdvancedRds(_rdsInfo);
|
||||||
// if (rdsDecode.programTypeValid()) { ImGui::Text("Program type: %d", rdsDecode.getProgramType()); }
|
_config->acquire();
|
||||||
// if (rdsDecode.PSNameValid()) { ImGui::Text("Program name: [%s]", rdsDecode.getPSName().c_str()); }
|
_config->conf[name][getName()]["rdsInfo"] = _rdsInfo;
|
||||||
// if (rdsDecode.radioTextValid()) { ImGui::Text("Radiotext: [%s]", rdsDecode.getRadioText().c_str()); }
|
_config->release(true);
|
||||||
// }
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##_radio_wfm_rds_region_" + name).c_str(), &rdsRegionId, rdsRegions.txt)) {
|
||||||
|
rdsRegion = rdsRegions.value(rdsRegionId);
|
||||||
|
_config->acquire();
|
||||||
|
_config->conf[name][getName()]["rdsRegion"] = rdsRegions.key(rdsRegionId);
|
||||||
|
_config->release(true);
|
||||||
|
}
|
||||||
|
if (!_rds) { ImGui::EndDisabled(); }
|
||||||
|
|
||||||
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
|
if (_rds && _rdsInfo) {
|
||||||
|
ImGui::BeginTable(("##radio_wfm_rds_info_tbl_" + name).c_str(), 2, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders);
|
||||||
|
if (rdsDecode.piCodeValid()) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("PI Code");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
if (rdsRegion == RDS_REGION_NORTH_AMERICA) {
|
||||||
|
ImGui::Text("0x%04X (%s)", rdsDecode.getPICode(), rdsDecode.getCallsign().c_str());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::Text("0x%04X", rdsDecode.getPICode());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Country Code");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%d", rdsDecode.getCountryCode());
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Program Coverage");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%s (%d)", rds::AREA_COVERAGE_TO_STR[rdsDecode.getProgramCoverage()], rdsDecode.getProgramCoverage());
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Reference Number");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%d", rdsDecode.getProgramRefNumber());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("PI Code");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
if (rdsRegion == RDS_REGION_NORTH_AMERICA) {
|
||||||
|
ImGui::TextUnformatted("0x---- (----)");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::TextUnformatted("0x----");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Country Code");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextUnformatted("--"); // TODO: String
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Program Coverage");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextUnformatted("------- (--)");
|
||||||
|
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Reference Number");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextUnformatted("--");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rdsDecode.programTypeValid()) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Program Type");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
if (rdsRegion == RDS_REGION_NORTH_AMERICA) {
|
||||||
|
ImGui::Text("%s (%d)", rds::PROGRAM_TYPE_US_TO_STR[rdsDecode.getProgramType()], rdsDecode.getProgramType());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::Text("%s (%d)", rds::PROGRAM_TYPE_EU_TO_STR[rdsDecode.getProgramType()], rdsDecode.getProgramType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Program Type");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextUnformatted("------- (--)"); // TODO: String
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rdsDecode.musicValid()) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Music");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::Text("%s", rdsDecode.getMusic() ? "Yes":"No");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
ImGui::TextUnformatted("Music");
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
ImGui::TextUnformatted("---");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::EndTable();
|
||||||
|
|
||||||
|
ImGui::SetNextItemWidth(menuWidth);
|
||||||
|
diag.draw();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBandwidth(double bandwidth) {
|
void setBandwidth(double bandwidth) {
|
||||||
|
@ -139,12 +279,24 @@ namespace demod {
|
||||||
demod.setStereo(_stereo);
|
demod.setStereo(_stereo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setAdvancedRds(bool enabled) {
|
||||||
|
rdsDemod.setSoftEnabled(enabled);
|
||||||
|
_rdsInfo = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void rdsHandler(uint8_t* data, int count, void* ctx) {
|
static void rdsHandler(uint8_t* data, int count, void* ctx) {
|
||||||
WFM* _this = (WFM*)ctx;
|
WFM* _this = (WFM*)ctx;
|
||||||
_this->rdsDecode.process(data, count);
|
_this->rdsDecode.process(data, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void _diagHandler(float* data, int count, void* ctx) {
|
||||||
|
WFM* _this = (WFM*)ctx;
|
||||||
|
float* buf = _this->diag.acquireBuffer();
|
||||||
|
memcpy(buf, data, count * sizeof(float));
|
||||||
|
_this->diag.releaseBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
static void fftRedraw(ImGui::WaterFall::FFTRedrawArgs args, void* ctx) {
|
static void fftRedraw(ImGui::WaterFall::FFTRedrawArgs args, void* ctx) {
|
||||||
WFM* _this = (WFM*)ctx;
|
WFM* _this = (WFM*)ctx;
|
||||||
if (!_this->_rds) { return; }
|
if (!_this->_rds) { return; }
|
||||||
|
@ -186,23 +338,31 @@ namespace demod {
|
||||||
}
|
}
|
||||||
|
|
||||||
dsp::demod::BroadcastFM demod;
|
dsp::demod::BroadcastFM demod;
|
||||||
dsp::clock_recovery::FD recov;
|
RDSDemod rdsDemod;
|
||||||
dsp::digital::BinarySlicer slice;
|
|
||||||
dsp::digital::ManchesterDecoder manch;
|
|
||||||
dsp::digital::DifferentialDecoder diff;
|
|
||||||
dsp::sink::Handler<uint8_t> hs;
|
dsp::sink::Handler<uint8_t> hs;
|
||||||
EventHandler<ImGui::WaterFall::FFTRedrawArgs> fftRedrawHandler;
|
EventHandler<ImGui::WaterFall::FFTRedrawArgs> fftRedrawHandler;
|
||||||
|
|
||||||
rds::RDSDecoder rdsDecode;
|
dsp::buffer::Reshaper<float> reshape;
|
||||||
|
dsp::sink::Handler<float> diagHandler;
|
||||||
|
ImGui::SymbolDiagram diag;
|
||||||
|
|
||||||
|
rds::Decoder rdsDecode;
|
||||||
|
|
||||||
ConfigManager* _config = NULL;
|
ConfigManager* _config = NULL;
|
||||||
|
|
||||||
bool _stereo = false;
|
bool _stereo = false;
|
||||||
bool _lowPass = true;
|
bool _lowPass = true;
|
||||||
bool _rds = false;
|
bool _rds = false;
|
||||||
|
bool _rdsInfo = false;
|
||||||
float muGain = 0.01;
|
float muGain = 0.01;
|
||||||
float omegaGain = (0.01*0.01)/4.0;
|
float omegaGain = (0.01*0.01)/4.0;
|
||||||
|
|
||||||
|
int rdsRegionId = 0;
|
||||||
|
RDSRegion rdsRegion = RDS_REGION_EUROPE;
|
||||||
|
|
||||||
|
OptionList<std::string, RDSRegion> rdsRegions;
|
||||||
|
|
||||||
|
|
||||||
std::string name;
|
std::string name;
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -3,6 +3,8 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <utils/flog.h>
|
||||||
|
|
||||||
namespace rds {
|
namespace rds {
|
||||||
std::map<uint16_t, BlockType> SYNDROMES = {
|
std::map<uint16_t, BlockType> SYNDROMES = {
|
||||||
{ 0b1111011000, BLOCK_TYPE_A },
|
{ 0b1111011000, BLOCK_TYPE_A },
|
||||||
|
@ -20,6 +22,98 @@ namespace rds {
|
||||||
{ BLOCK_TYPE_D, 0b0110110100 }
|
{ BLOCK_TYPE_D, 0b0110110100 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::map<uint16_t, const char*> THREE_LETTER_CALLS = {
|
||||||
|
{ 0x99A5, "KBW" },
|
||||||
|
{ 0x99A6, "KCY" },
|
||||||
|
{ 0x9990, "KDB" },
|
||||||
|
{ 0x99A7, "KDF" },
|
||||||
|
{ 0x9950, "KEX" },
|
||||||
|
{ 0x9951, "KFH" },
|
||||||
|
{ 0x9952, "KFI" },
|
||||||
|
{ 0x9953, "KGA" },
|
||||||
|
{ 0x9991, "KGB" },
|
||||||
|
{ 0x9954, "KGO" },
|
||||||
|
{ 0x9955, "KGU" },
|
||||||
|
{ 0x9956, "KGW" },
|
||||||
|
{ 0x9957, "KGY" },
|
||||||
|
{ 0x99AA, "KHQ" },
|
||||||
|
{ 0x9958, "KID" },
|
||||||
|
{ 0x9959, "KIT" },
|
||||||
|
{ 0x995A, "KJR" },
|
||||||
|
{ 0x995B, "KLO" },
|
||||||
|
{ 0x995C, "KLZ" },
|
||||||
|
{ 0x995D, "KMA" },
|
||||||
|
{ 0x995E, "KMJ" },
|
||||||
|
{ 0x995F, "KNX" },
|
||||||
|
{ 0x9960, "KOA" },
|
||||||
|
{ 0x99AB, "KOB" },
|
||||||
|
{ 0x9992, "KOY" },
|
||||||
|
{ 0x9993, "KPQ" },
|
||||||
|
{ 0x9964, "KQV" },
|
||||||
|
{ 0x9994, "KSD" },
|
||||||
|
{ 0x9965, "KSL" },
|
||||||
|
{ 0x9966, "KUJ" },
|
||||||
|
{ 0x9995, "KUT" },
|
||||||
|
{ 0x9967, "KVI" },
|
||||||
|
{ 0x9968, "KWG" },
|
||||||
|
{ 0x9996, "KXL" },
|
||||||
|
{ 0x9997, "KXO" },
|
||||||
|
{ 0x996B, "KYW" },
|
||||||
|
{ 0x9999, "WBT" },
|
||||||
|
{ 0x996D, "WBZ" },
|
||||||
|
{ 0x996E, "WDZ" },
|
||||||
|
{ 0x996F, "WEW" },
|
||||||
|
{ 0x999A, "WGH" },
|
||||||
|
{ 0x9971, "WGL" },
|
||||||
|
{ 0x9972, "WGN" },
|
||||||
|
{ 0x9973, "WGR" },
|
||||||
|
{ 0x999B, "WGY" },
|
||||||
|
{ 0x9975, "WHA" },
|
||||||
|
{ 0x9976, "WHB" },
|
||||||
|
{ 0x9977, "WHK" },
|
||||||
|
{ 0x9978, "WHO" },
|
||||||
|
{ 0x999C, "WHP" },
|
||||||
|
{ 0x999D, "WIL" },
|
||||||
|
{ 0x997A, "WIP" },
|
||||||
|
{ 0x99B3, "WIS" },
|
||||||
|
{ 0x997B, "WJR" },
|
||||||
|
{ 0x99B4, "WJW" },
|
||||||
|
{ 0x99B5, "WJZ" },
|
||||||
|
{ 0x997C, "WKY" },
|
||||||
|
{ 0x997D, "WLS" },
|
||||||
|
{ 0x997E, "WLW" },
|
||||||
|
{ 0x999E, "WMC" },
|
||||||
|
{ 0x999F, "WMT" },
|
||||||
|
{ 0x9981, "WOC" },
|
||||||
|
{ 0x99A0, "WOI" },
|
||||||
|
{ 0x9983, "WOL" },
|
||||||
|
{ 0x9984, "WOR" },
|
||||||
|
{ 0x99A1, "WOW" },
|
||||||
|
{ 0x99B9, "WRC" },
|
||||||
|
{ 0x99A2, "WRR" },
|
||||||
|
{ 0x99A3, "WSB" },
|
||||||
|
{ 0x99A4, "WSM" },
|
||||||
|
{ 0x9988, "WWJ" },
|
||||||
|
{ 0x9989, "WWL" }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<uint16_t, const char*> NAT_LOC_LINKED_STATIONS = {
|
||||||
|
{ 0xB01, "NPR-1" },
|
||||||
|
{ 0xB02, "CBC - Radio One" },
|
||||||
|
{ 0xB03, "CBC - Radio Two" },
|
||||||
|
{ 0xB04, "Radio-Canada - Première Chaîne" },
|
||||||
|
{ 0xB05, "Radio-Canada - Espace Musique" },
|
||||||
|
{ 0xB06, "CBC" },
|
||||||
|
{ 0xB07, "CBC" },
|
||||||
|
{ 0xB08, "CBC" },
|
||||||
|
{ 0xB09, "CBC" },
|
||||||
|
{ 0xB0A, "NPR-2" },
|
||||||
|
{ 0xB0B, "NPR-3" },
|
||||||
|
{ 0xB0C, "NPR-4" },
|
||||||
|
{ 0xB0D, "NPR-5" },
|
||||||
|
{ 0xB0E, "NPR-6" }
|
||||||
|
};
|
||||||
|
|
||||||
// 9876543210
|
// 9876543210
|
||||||
const uint16_t LFSR_POLY = 0b0110111001;
|
const uint16_t LFSR_POLY = 0b0110111001;
|
||||||
const uint16_t IN_POLY = 0b1100011011;
|
const uint16_t IN_POLY = 0b1100011011;
|
||||||
|
@ -28,7 +122,7 @@ namespace rds {
|
||||||
const int DATA_LEN = 16;
|
const int DATA_LEN = 16;
|
||||||
const int POLY_LEN = 10;
|
const int POLY_LEN = 10;
|
||||||
|
|
||||||
void RDSDecoder::process(uint8_t* symbols, int count) {
|
void Decoder::process(uint8_t* symbols, int count) {
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
// Shift in the bit
|
// Shift in the bit
|
||||||
shiftReg = ((shiftReg << 1) & 0x3FFFFFF) | (symbols[i] & 1);
|
shiftReg = ((shiftReg << 1) & 0x3FFFFFF) | (symbols[i] & 1);
|
||||||
|
@ -54,18 +148,26 @@ namespace rds {
|
||||||
type = (BlockType)((lastType + 1) % _BLOCK_TYPE_COUNT);
|
type = (BlockType)((lastType + 1) % _BLOCK_TYPE_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save block while correcting errors (NOT YET)
|
// Save block while correcting errors (NOT YET) <- idk why the "not yet is here", TODO: find why
|
||||||
blocks[type] = correctErrors(shiftReg, type, blockAvail[type]);
|
blocks[type] = correctErrors(shiftReg, type, blockAvail[type]);
|
||||||
|
|
||||||
// Update continous group count
|
// If block type is A, decode it directly, otherwise, update continous count
|
||||||
if (type == BLOCK_TYPE_A) { contGroup = 1; }
|
if (type == BLOCK_TYPE_A) {
|
||||||
else if (type == BLOCK_TYPE_B && lastType == BLOCK_TYPE_A) { contGroup++; }
|
decodeBlockA();
|
||||||
|
}
|
||||||
|
else if (type == BLOCK_TYPE_B) { contGroup = 1; }
|
||||||
else if ((type == BLOCK_TYPE_C || type == BLOCK_TYPE_CP) && lastType == BLOCK_TYPE_B) { contGroup++; }
|
else if ((type == BLOCK_TYPE_C || type == BLOCK_TYPE_CP) && lastType == BLOCK_TYPE_B) { contGroup++; }
|
||||||
else if (type == BLOCK_TYPE_D && (lastType == BLOCK_TYPE_C || lastType == BLOCK_TYPE_CP)) { contGroup++; }
|
else if (type == BLOCK_TYPE_D && (lastType == BLOCK_TYPE_C || lastType == BLOCK_TYPE_CP)) { contGroup++; }
|
||||||
else { contGroup = 0; }
|
else {
|
||||||
|
// If block B is available, decode it alone.
|
||||||
|
if (contGroup == 1) {
|
||||||
|
decodeBlockB();
|
||||||
|
}
|
||||||
|
contGroup = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// If we've got an entire group, process it
|
// If we've got an entire group, process it
|
||||||
if (contGroup >= 4) {
|
if (contGroup >= 3) {
|
||||||
contGroup = 0;
|
contGroup = 0;
|
||||||
decodeGroup();
|
decodeGroup();
|
||||||
}
|
}
|
||||||
|
@ -76,7 +178,7 @@ namespace rds {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t RDSDecoder::calcSyndrome(uint32_t block) {
|
uint16_t Decoder::calcSyndrome(uint32_t block) {
|
||||||
uint16_t syn = 0;
|
uint16_t syn = 0;
|
||||||
|
|
||||||
// Calculate the syndrome using a LFSR
|
// Calculate the syndrome using a LFSR
|
||||||
|
@ -95,7 +197,7 @@ namespace rds {
|
||||||
return syn;
|
return syn;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t RDSDecoder::correctErrors(uint32_t block, BlockType type, bool& recovered) {
|
uint32_t Decoder::correctErrors(uint32_t block, BlockType type, bool& recovered) {
|
||||||
// Subtract the offset from block
|
// Subtract the offset from block
|
||||||
block ^= (uint32_t)OFFSETS[type];
|
block ^= (uint32_t)OFFSETS[type];
|
||||||
uint32_t out = block;
|
uint32_t out = block;
|
||||||
|
@ -124,96 +226,264 @@ namespace rds {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RDSDecoder::decodeGroup() {
|
void Decoder::decodeBlockA() {
|
||||||
std::lock_guard<std::mutex> lck(groupMtx);
|
// Acquire lock
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
std::lock_guard<std::mutex> lck(blockAMtx);
|
||||||
anyGroupLastUpdate = now;
|
|
||||||
|
|
||||||
// Make sure blocks A and B are available
|
// If it didn't decode properly return
|
||||||
if (!blockAvail[BLOCK_TYPE_A] || !blockAvail[BLOCK_TYPE_B]) { return; }
|
if (!blockAvail[BLOCK_TYPE_A]) { return; }
|
||||||
|
|
||||||
// Decode PI code
|
// Decode PI code
|
||||||
|
piCode = (blocks[BLOCK_TYPE_A] >> 10) & 0xFFFF;
|
||||||
countryCode = (blocks[BLOCK_TYPE_A] >> 22) & 0xF;
|
countryCode = (blocks[BLOCK_TYPE_A] >> 22) & 0xF;
|
||||||
programCoverage = (AreaCoverage)((blocks[BLOCK_TYPE_A] >> 18) & 0xF);
|
programCoverage = (AreaCoverage)((blocks[BLOCK_TYPE_A] >> 18) & 0xF);
|
||||||
programRefNumber = (blocks[BLOCK_TYPE_A] >> 10) & 0xFF;
|
programRefNumber = (blocks[BLOCK_TYPE_A] >> 10) & 0xFF;
|
||||||
|
callsign = decodeCallsign(piCode);
|
||||||
|
|
||||||
|
// Update timeout
|
||||||
|
blockALastUpdate = std::chrono::high_resolution_clock::now();;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::decodeBlockB() {
|
||||||
|
// Acquire lock
|
||||||
|
std::lock_guard<std::mutex> lck(blockBMtx);
|
||||||
|
|
||||||
|
// If it didn't decode properly return (TODO: Make sure this is not needed)
|
||||||
|
if (!blockAvail[BLOCK_TYPE_B]) { return; }
|
||||||
|
|
||||||
// Decode group type and version
|
// Decode group type and version
|
||||||
uint8_t groupType = (blocks[BLOCK_TYPE_B] >> 22) & 0xF;
|
groupType = (blocks[BLOCK_TYPE_B] >> 22) & 0xF;
|
||||||
GroupVersion groupVer = (GroupVersion)((blocks[BLOCK_TYPE_B] >> 21) & 1);
|
groupVer = (GroupVersion)((blocks[BLOCK_TYPE_B] >> 21) & 1);
|
||||||
|
|
||||||
// Decode traffic program and program type
|
// Decode traffic program and program type
|
||||||
trafficProgram = (blocks[BLOCK_TYPE_B] >> 20) & 1;
|
trafficProgram = (blocks[BLOCK_TYPE_B] >> 20) & 1;
|
||||||
programType = (ProgramType)((blocks[BLOCK_TYPE_B] >> 15) & 0x1F);
|
programType = (ProgramType)((blocks[BLOCK_TYPE_B] >> 15) & 0x1F);
|
||||||
|
|
||||||
if (groupType == 0) {
|
|
||||||
group0LastUpdate = now;
|
|
||||||
trafficAnnouncement = (blocks[BLOCK_TYPE_B] >> 14) & 1;
|
|
||||||
music = (blocks[BLOCK_TYPE_B] >> 13) & 1;
|
|
||||||
uint8_t diBit = (blocks[BLOCK_TYPE_B] >> 12) & 1;
|
|
||||||
uint8_t offset = ((blocks[BLOCK_TYPE_B] >> 10) & 0b11);
|
|
||||||
uint8_t diOffset = 3 - offset;
|
|
||||||
uint8_t psOffset = offset * 2;
|
|
||||||
|
|
||||||
if (groupVer == GROUP_VER_A && blockAvail[BLOCK_TYPE_C]) {
|
// Update timeout
|
||||||
alternateFrequency = (blocks[BLOCK_TYPE_C] >> 10) & 0xFFFF;
|
blockBLastUpdate = std::chrono::high_resolution_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::decodeGroup0() {
|
||||||
|
// Acquire lock
|
||||||
|
std::lock_guard<std::mutex> lck(group0Mtx);
|
||||||
|
|
||||||
|
// Decode Block B data
|
||||||
|
trafficAnnouncement = (blocks[BLOCK_TYPE_B] >> 14) & 1;
|
||||||
|
music = (blocks[BLOCK_TYPE_B] >> 13) & 1;
|
||||||
|
uint8_t diBit = (blocks[BLOCK_TYPE_B] >> 12) & 1;
|
||||||
|
uint8_t offset = ((blocks[BLOCK_TYPE_B] >> 10) & 0b11);
|
||||||
|
uint8_t diOffset = 3 - offset;
|
||||||
|
uint8_t psOffset = offset * 2;
|
||||||
|
|
||||||
|
// Decode Block C data
|
||||||
|
if (groupVer == GROUP_VER_A && blockAvail[BLOCK_TYPE_C]) {
|
||||||
|
alternateFrequency = (blocks[BLOCK_TYPE_C] >> 10) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write DI bit to the decoder identification
|
||||||
|
decoderIdent &= ~(1 << diOffset);
|
||||||
|
decoderIdent |= (diBit << diOffset);
|
||||||
|
|
||||||
|
// Write chars at offset the PSName
|
||||||
|
if (blockAvail[BLOCK_TYPE_D]) {
|
||||||
|
programServiceName[psOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
||||||
|
programServiceName[psOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update timeout
|
||||||
|
group0LastUpdate = std::chrono::high_resolution_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::decodeGroup2() {
|
||||||
|
// Acquire lock
|
||||||
|
std::lock_guard<std::mutex> lck(group2Mtx);
|
||||||
|
|
||||||
|
// Get char offset and write chars in the Radiotext
|
||||||
|
bool nAB = (blocks[BLOCK_TYPE_B] >> 14) & 1;
|
||||||
|
uint8_t offset = (blocks[BLOCK_TYPE_B] >> 10) & 0xF;
|
||||||
|
|
||||||
|
// Clear text field if the A/B flag changed
|
||||||
|
if (nAB != rtAB) {
|
||||||
|
radioText = " ";
|
||||||
|
}
|
||||||
|
rtAB = nAB;
|
||||||
|
|
||||||
|
// Write char at offset in Radiotext
|
||||||
|
if (groupVer == GROUP_VER_A) {
|
||||||
|
uint8_t rtOffset = offset * 4;
|
||||||
|
if (blockAvail[BLOCK_TYPE_C]) {
|
||||||
|
radioText[rtOffset] = (blocks[BLOCK_TYPE_C] >> 18) & 0xFF;
|
||||||
|
radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_C] >> 10) & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write DI bit to the decoder identification
|
|
||||||
decoderIdent &= ~(1 << diOffset);
|
|
||||||
decoderIdent |= (diBit << diOffset);
|
|
||||||
|
|
||||||
// Write chars at offset the PSName
|
|
||||||
if (blockAvail[BLOCK_TYPE_D]) {
|
if (blockAvail[BLOCK_TYPE_D]) {
|
||||||
programServiceName[psOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
radioText[rtOffset + 2] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
||||||
programServiceName[psOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
radioText[rtOffset + 3] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (groupType == 2) {
|
else {
|
||||||
group2LastUpdate = now;
|
uint8_t rtOffset = offset * 2;
|
||||||
// Get char offset and write chars in the Radiotext
|
if (blockAvail[BLOCK_TYPE_D]) {
|
||||||
bool nAB = (blocks[BLOCK_TYPE_B] >> 14) & 1;
|
radioText[rtOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
||||||
uint8_t offset = (blocks[BLOCK_TYPE_B] >> 10) & 0xF;
|
radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
||||||
|
|
||||||
// Clear text field if the A/B flag changed
|
|
||||||
if (nAB != rtAB) {
|
|
||||||
radioText = " ";
|
|
||||||
}
|
}
|
||||||
rtAB = nAB;
|
}
|
||||||
|
|
||||||
// Write char at offset in Radiotext
|
// Update timeout
|
||||||
if (groupVer == GROUP_VER_A) {
|
group2LastUpdate = std::chrono::high_resolution_clock::now();
|
||||||
uint8_t rtOffset = offset * 4;
|
}
|
||||||
if (blockAvail[BLOCK_TYPE_C]) {
|
|
||||||
radioText[rtOffset] = (blocks[BLOCK_TYPE_C] >> 18) & 0xFF;
|
void Decoder::decodeGroup10() {
|
||||||
radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_C] >> 10) & 0xFF;
|
// Acquire lock
|
||||||
}
|
std::lock_guard<std::mutex> lck(group10Mtx);
|
||||||
if (blockAvail[BLOCK_TYPE_D]) {
|
|
||||||
radioText[rtOffset + 2] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
// Check if the text needs to be cleared
|
||||||
radioText[rtOffset + 3] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
bool ab = (blocks[BLOCK_TYPE_B] >> 14) & 1;
|
||||||
}
|
if (ab != ptnAB) {
|
||||||
|
programTypeName = " ";
|
||||||
|
}
|
||||||
|
ptnAB = ab;
|
||||||
|
|
||||||
|
// Decode segment address
|
||||||
|
bool addr = (blocks[BLOCK_TYPE_B] >> 10) & 1;
|
||||||
|
|
||||||
|
// Save text depending on address
|
||||||
|
if (addr) {
|
||||||
|
if (blockAvail[BLOCK_TYPE_C]) {
|
||||||
|
programTypeName[4] = (blocks[BLOCK_TYPE_C] >> 18) & 0xFF;
|
||||||
|
programTypeName[5] = (blocks[BLOCK_TYPE_C] >> 10) & 0xFF;
|
||||||
|
}
|
||||||
|
if (blockAvail[BLOCK_TYPE_D]) {
|
||||||
|
programTypeName[6] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
||||||
|
programTypeName[7] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (blockAvail[BLOCK_TYPE_C]) {
|
||||||
|
programTypeName[0] = (blocks[BLOCK_TYPE_C] >> 18) & 0xFF;
|
||||||
|
programTypeName[1] = (blocks[BLOCK_TYPE_C] >> 10) & 0xFF;
|
||||||
|
}
|
||||||
|
if (blockAvail[BLOCK_TYPE_D]) {
|
||||||
|
programTypeName[2] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
||||||
|
programTypeName[3] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update timeout
|
||||||
|
group10LastUpdate = std::chrono::high_resolution_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Decoder::decodeGroup() {
|
||||||
|
// Make sure blocks B is available
|
||||||
|
if (!blockAvail[BLOCK_TYPE_B]) { return; }
|
||||||
|
|
||||||
|
// Decode block B
|
||||||
|
decodeBlockB();
|
||||||
|
|
||||||
|
// Decode depending on group type
|
||||||
|
switch (groupType) {
|
||||||
|
case 0:
|
||||||
|
decodeGroup0();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
decodeGroup2();
|
||||||
|
break;
|
||||||
|
case 10:
|
||||||
|
decodeGroup10();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Decoder::base26ToCall(uint16_t pi) {
|
||||||
|
// Determin first better based on offset
|
||||||
|
bool w = (pi >= 21672);
|
||||||
|
std::string callsign(w ? "W" : "K");
|
||||||
|
|
||||||
|
// Base25 decode the rest
|
||||||
|
std::string restStr;
|
||||||
|
int rest = pi - (w ? 21672 : 4096);
|
||||||
|
while (rest) {
|
||||||
|
restStr += 'A' + (rest % 26);
|
||||||
|
rest /= 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad with As
|
||||||
|
while (restStr.size() < 3) {
|
||||||
|
restStr += 'A';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder chars
|
||||||
|
for (int i = restStr.size() - 1; i >= 0; i--) {
|
||||||
|
callsign += restStr[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return callsign;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Decoder::decodeCallsign(uint16_t pi) {
|
||||||
|
if ((pi >> 8) == 0xAF) {
|
||||||
|
// AFXY -> XY00
|
||||||
|
return base26ToCall((pi & 0xFF) << 8);
|
||||||
|
}
|
||||||
|
else if ((pi >> 12) == 0xA) {
|
||||||
|
// AXYZ -> X0YZ
|
||||||
|
return base26ToCall((((pi >> 8) & 0xF) << 12) | (pi & 0xFF));
|
||||||
|
}
|
||||||
|
else if (pi >= 0x9950 && pi <= 0x9EFF) {
|
||||||
|
// 3 letter callsigns
|
||||||
|
if (THREE_LETTER_CALLS.find(pi) != THREE_LETTER_CALLS.end()) {
|
||||||
|
return THREE_LETTER_CALLS[pi];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
uint8_t rtOffset = offset * 2;
|
return "Not Assigned";
|
||||||
if (blockAvail[BLOCK_TYPE_D]) {
|
|
||||||
radioText[rtOffset] = (blocks[BLOCK_TYPE_D] >> 18) & 0xFF;
|
|
||||||
radioText[rtOffset + 1] = (blocks[BLOCK_TYPE_D] >> 10) & 0xFF;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (pi >= 0x1000 && pi <= 0x994F) {
|
||||||
|
// Normal encoding
|
||||||
|
if ((pi & 0xFF) == 0 || ((pi >> 8) & 0xF) == 0) {
|
||||||
|
return "Not Assigned";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return base26ToCall(pi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pi >= 0xB000 && pi <= 0xEFFF) {
|
||||||
|
uint16_t _pi = ((pi >> 12) << 8) | (pi & 0xFF);
|
||||||
|
if (NAT_LOC_LINKED_STATIONS.find(_pi) != NAT_LOC_LINKED_STATIONS.end()) {
|
||||||
|
return NAT_LOC_LINKED_STATIONS[_pi];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "Not Assigned";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "Not Assigned";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RDSDecoder::anyGroupValid() {
|
bool Decoder::blockAValid() {
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - anyGroupLastUpdate)).count() < 5000.0;
|
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - blockALastUpdate)).count() < RDS_BLOCK_A_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RDSDecoder::group0Valid() {
|
bool Decoder::blockBValid() {
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - group0LastUpdate)).count() < 5000.0;
|
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - blockBLastUpdate)).count() < RDS_BLOCK_B_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RDSDecoder::group2Valid() {
|
bool Decoder::group0Valid() {
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - group2LastUpdate)).count() < 5000.0;
|
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - group0LastUpdate)).count() < RDS_GROUP_0_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Decoder::group2Valid() {
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - group2LastUpdate)).count() < RDS_GROUP_2_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Decoder::group10Valid() {
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
return (std::chrono::duration_cast<std::chrono::milliseconds>(now - group10LastUpdate)).count() < RDS_GROUP_10_TIMEOUT_MS;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,12 @@
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
|
#define RDS_BLOCK_A_TIMEOUT_MS 5000.0
|
||||||
|
#define RDS_BLOCK_B_TIMEOUT_MS 5000.0
|
||||||
|
#define RDS_GROUP_0_TIMEOUT_MS 5000.0
|
||||||
|
#define RDS_GROUP_2_TIMEOUT_MS 5000.0
|
||||||
|
#define RDS_GROUP_10_TIMEOUT_MS 5000.0
|
||||||
|
|
||||||
namespace rds {
|
namespace rds {
|
||||||
enum BlockType {
|
enum BlockType {
|
||||||
BLOCK_TYPE_A,
|
BLOCK_TYPE_A,
|
||||||
|
@ -20,22 +26,42 @@ namespace rds {
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AreaCoverage {
|
enum AreaCoverage {
|
||||||
AREA_COVERAGE_LOCAL,
|
AREA_COVERAGE_INVALID = -1,
|
||||||
AREA_COVERAGE_INTERNATIONAL,
|
AREA_COVERAGE_LOCAL = 0,
|
||||||
AREA_COVERAGE_NATIONAL,
|
AREA_COVERAGE_INTERNATIONAL = 1,
|
||||||
AREA_COVERAGE_SUPRA_NATIONAL,
|
AREA_COVERAGE_NATIONAL = 2,
|
||||||
AREA_COVERAGE_REGIONAL1,
|
AREA_COVERAGE_SUPRA_NATIONAL = 3,
|
||||||
AREA_COVERAGE_REGIONAL2,
|
AREA_COVERAGE_REGIONAL1 = 4,
|
||||||
AREA_COVERAGE_REGIONAL3,
|
AREA_COVERAGE_REGIONAL2 = 5,
|
||||||
AREA_COVERAGE_REGIONAL4,
|
AREA_COVERAGE_REGIONAL3 = 6,
|
||||||
AREA_COVERAGE_REGIONAL5,
|
AREA_COVERAGE_REGIONAL4 = 7,
|
||||||
AREA_COVERAGE_REGIONAL6,
|
AREA_COVERAGE_REGIONAL5 = 8,
|
||||||
AREA_COVERAGE_REGIONAL7,
|
AREA_COVERAGE_REGIONAL6 = 9,
|
||||||
AREA_COVERAGE_REGIONAL8,
|
AREA_COVERAGE_REGIONAL7 = 10,
|
||||||
AREA_COVERAGE_REGIONAL9,
|
AREA_COVERAGE_REGIONAL8 = 11,
|
||||||
AREA_COVERAGE_REGIONAL10,
|
AREA_COVERAGE_REGIONAL9 = 12,
|
||||||
AREA_COVERAGE_REGIONAL11,
|
AREA_COVERAGE_REGIONAL10 = 13,
|
||||||
AREA_COVERAGE_REGIONAL12
|
AREA_COVERAGE_REGIONAL11 = 14,
|
||||||
|
AREA_COVERAGE_REGIONAL12 = 15
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char* AREA_COVERAGE_TO_STR[] = {
|
||||||
|
"Local",
|
||||||
|
"International",
|
||||||
|
"National",
|
||||||
|
"Supra-National",
|
||||||
|
"Regional 1",
|
||||||
|
"Regional 2",
|
||||||
|
"Regional 3",
|
||||||
|
"Regional 4",
|
||||||
|
"Regional 5",
|
||||||
|
"Regional 6",
|
||||||
|
"Regional 7",
|
||||||
|
"Regional 8",
|
||||||
|
"Regional 9",
|
||||||
|
"Regional 10",
|
||||||
|
"Regional 11",
|
||||||
|
"Regional 12",
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ProgramType {
|
enum ProgramType {
|
||||||
|
@ -108,6 +134,76 @@ namespace rds {
|
||||||
PROGRAM_TYPE_EU_ALARM = 31
|
PROGRAM_TYPE_EU_ALARM = 31
|
||||||
};
|
};
|
||||||
|
|
||||||
|
inline const char* PROGRAM_TYPE_EU_TO_STR[] = {
|
||||||
|
"None",
|
||||||
|
"News",
|
||||||
|
"Current Affairs",
|
||||||
|
"Information",
|
||||||
|
"Sports",
|
||||||
|
"Education",
|
||||||
|
"Drama",
|
||||||
|
"Culture",
|
||||||
|
"Science",
|
||||||
|
"Varied",
|
||||||
|
"Pop Music",
|
||||||
|
"Rock Music",
|
||||||
|
"Easy Listening Music",
|
||||||
|
"Light Classical",
|
||||||
|
"Serious Classical",
|
||||||
|
"Other Music",
|
||||||
|
"Weather",
|
||||||
|
"Finance",
|
||||||
|
"Children Program",
|
||||||
|
"Social Affairs",
|
||||||
|
"Religion",
|
||||||
|
"Phone-in",
|
||||||
|
"Travel",
|
||||||
|
"Leisure",
|
||||||
|
"Jazz Music",
|
||||||
|
"Country Music",
|
||||||
|
"National Music",
|
||||||
|
"Oldies Music",
|
||||||
|
"Folk Music",
|
||||||
|
"Documentary",
|
||||||
|
"Alarm Test",
|
||||||
|
"Alarm",
|
||||||
|
};
|
||||||
|
|
||||||
|
inline const char* PROGRAM_TYPE_US_TO_STR[] = {
|
||||||
|
"None",
|
||||||
|
"News",
|
||||||
|
"Information",
|
||||||
|
"Sports",
|
||||||
|
"Talk",
|
||||||
|
"Rock",
|
||||||
|
"Classic Rock",
|
||||||
|
"Adult Hits",
|
||||||
|
"Soft Rock",
|
||||||
|
"Top 40",
|
||||||
|
"Country",
|
||||||
|
"Oldies",
|
||||||
|
"Soft",
|
||||||
|
"Nostalgia",
|
||||||
|
"Jazz",
|
||||||
|
"Classical",
|
||||||
|
"Rythm and Blues",
|
||||||
|
"Soft Rythm and Blues",
|
||||||
|
"Foreign Language",
|
||||||
|
"Religious Music",
|
||||||
|
"Religious Talk",
|
||||||
|
"Personality",
|
||||||
|
"Public",
|
||||||
|
"College",
|
||||||
|
"Unassigned",
|
||||||
|
"Unassigned",
|
||||||
|
"Unassigned",
|
||||||
|
"Unassigned",
|
||||||
|
"Unassigned",
|
||||||
|
"Weather",
|
||||||
|
"Emergency Test",
|
||||||
|
"Emergency",
|
||||||
|
};
|
||||||
|
|
||||||
enum DecoderIdentification {
|
enum DecoderIdentification {
|
||||||
DECODER_IDENT_STEREO = (1 << 0),
|
DECODER_IDENT_STEREO = (1 << 0),
|
||||||
DECODER_IDENT_ARTIFICIAL_HEAD = (1 << 1),
|
DECODER_IDENT_ARTIFICIAL_HEAD = (1 << 1),
|
||||||
|
@ -115,35 +211,49 @@ namespace rds {
|
||||||
DECODER_IDENT_VARIABLE_PTY = (1 << 0)
|
DECODER_IDENT_VARIABLE_PTY = (1 << 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
class RDSDecoder {
|
class Decoder {
|
||||||
public:
|
public:
|
||||||
void process(uint8_t* symbols, int count);
|
void process(uint8_t* symbols, int count);
|
||||||
|
|
||||||
bool countryCodeValid() { std::lock_guard<std::mutex> lck(groupMtx); return anyGroupValid(); }
|
bool piCodeValid() { std::lock_guard<std::mutex> lck(blockAMtx); return blockAValid(); }
|
||||||
uint8_t getCountryCode() { std::lock_guard<std::mutex> lck(groupMtx); return countryCode; }
|
uint16_t getPICode() { std::lock_guard<std::mutex> lck(blockAMtx); return piCode; }
|
||||||
bool programCoverageValid() { std::lock_guard<std::mutex> lck(groupMtx); return anyGroupValid(); }
|
uint8_t getCountryCode() { std::lock_guard<std::mutex> lck(blockAMtx); return countryCode; }
|
||||||
uint8_t getProgramCoverage() { std::lock_guard<std::mutex> lck(groupMtx); return programCoverage; }
|
uint8_t getProgramCoverage() { std::lock_guard<std::mutex> lck(blockAMtx); return programCoverage; }
|
||||||
bool programRefNumberValid() { std::lock_guard<std::mutex> lck(groupMtx); return anyGroupValid(); }
|
uint8_t getProgramRefNumber() { std::lock_guard<std::mutex> lck(blockAMtx); return programRefNumber; }
|
||||||
uint8_t getProgramRefNumber() { std::lock_guard<std::mutex> lck(groupMtx); return programRefNumber; }
|
std::string getCallsign() { std::lock_guard<std::mutex> lck(blockAMtx); return callsign; }
|
||||||
bool programTypeValid() { std::lock_guard<std::mutex> lck(groupMtx); return anyGroupValid(); }
|
|
||||||
ProgramType getProgramType() { std::lock_guard<std::mutex> lck(groupMtx); return programType; }
|
bool programTypeValid() { std::lock_guard<std::mutex> lck(blockBMtx); return blockBValid(); }
|
||||||
|
ProgramType getProgramType() { std::lock_guard<std::mutex> lck(blockBMtx); return programType; }
|
||||||
|
|
||||||
bool musicValid() { std::lock_guard<std::mutex> lck(groupMtx); return group0Valid(); }
|
bool musicValid() { std::lock_guard<std::mutex> lck(group0Mtx); return group0Valid(); }
|
||||||
bool getMusic() { std::lock_guard<std::mutex> lck(groupMtx); return music; }
|
bool getMusic() { std::lock_guard<std::mutex> lck(group0Mtx); return music; }
|
||||||
bool PSNameValid() { std::lock_guard<std::mutex> lck(groupMtx); return group0Valid(); }
|
bool PSNameValid() { std::lock_guard<std::mutex> lck(group0Mtx); return group0Valid(); }
|
||||||
std::string getPSName() { std::lock_guard<std::mutex> lck(groupMtx); return programServiceName; }
|
std::string getPSName() { std::lock_guard<std::mutex> lck(group0Mtx); return programServiceName; }
|
||||||
|
|
||||||
bool radioTextValid() { std::lock_guard<std::mutex> lck(groupMtx); return group2Valid(); }
|
bool radioTextValid() { std::lock_guard<std::mutex> lck(group2Mtx); return group2Valid(); }
|
||||||
std::string getRadioText() { std::lock_guard<std::mutex> lck(groupMtx); return radioText; }
|
std::string getRadioText() { std::lock_guard<std::mutex> lck(group2Mtx); return radioText; }
|
||||||
|
|
||||||
|
bool programTypeNameValid() { std::lock_guard<std::mutex> lck(group10Mtx); return group10Valid(); }
|
||||||
|
std::string getProgramTypeName() { std::lock_guard<std::mutex> lck(group10Mtx); return programTypeName; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static uint16_t calcSyndrome(uint32_t block);
|
static uint16_t calcSyndrome(uint32_t block);
|
||||||
static uint32_t correctErrors(uint32_t block, BlockType type, bool& recovered);
|
static uint32_t correctErrors(uint32_t block, BlockType type, bool& recovered);
|
||||||
|
void decodeBlockA();
|
||||||
|
void decodeBlockB();
|
||||||
|
void decodeGroup0();
|
||||||
|
void decodeGroup2();
|
||||||
|
void decodeGroup10();
|
||||||
void decodeGroup();
|
void decodeGroup();
|
||||||
|
|
||||||
bool anyGroupValid();
|
static std::string base26ToCall(uint16_t pi);
|
||||||
|
static std::string decodeCallsign(uint16_t pi);
|
||||||
|
|
||||||
|
bool blockAValid();
|
||||||
|
bool blockBValid();
|
||||||
bool group0Valid();
|
bool group0Valid();
|
||||||
bool group2Valid();
|
bool group2Valid();
|
||||||
|
bool group10Valid();
|
||||||
|
|
||||||
// State machine
|
// State machine
|
||||||
uint32_t shiftReg = 0;
|
uint32_t shiftReg = 0;
|
||||||
|
@ -154,17 +264,26 @@ namespace rds {
|
||||||
uint32_t blocks[_BLOCK_TYPE_COUNT];
|
uint32_t blocks[_BLOCK_TYPE_COUNT];
|
||||||
bool blockAvail[_BLOCK_TYPE_COUNT];
|
bool blockAvail[_BLOCK_TYPE_COUNT];
|
||||||
|
|
||||||
// All groups
|
// Block A (All groups)
|
||||||
std::mutex groupMtx;
|
std::mutex blockAMtx;
|
||||||
std::chrono::time_point<std::chrono::high_resolution_clock> anyGroupLastUpdate;
|
std::chrono::time_point<std::chrono::high_resolution_clock> blockALastUpdate{}; // 1970-01-01
|
||||||
|
uint16_t piCode;
|
||||||
uint8_t countryCode;
|
uint8_t countryCode;
|
||||||
AreaCoverage programCoverage;
|
AreaCoverage programCoverage;
|
||||||
uint8_t programRefNumber;
|
uint8_t programRefNumber;
|
||||||
|
std::string callsign;
|
||||||
|
|
||||||
|
// Block B (All groups)
|
||||||
|
std::mutex blockBMtx;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> blockBLastUpdate{}; // 1970-01-01
|
||||||
|
uint8_t groupType;
|
||||||
|
GroupVersion groupVer;
|
||||||
bool trafficProgram;
|
bool trafficProgram;
|
||||||
ProgramType programType;
|
ProgramType programType;
|
||||||
|
|
||||||
// Group type 0
|
// Group type 0
|
||||||
std::chrono::time_point<std::chrono::high_resolution_clock> group0LastUpdate;
|
std::mutex group0Mtx;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> group0LastUpdate{}; // 1970-01-01
|
||||||
bool trafficAnnouncement;
|
bool trafficAnnouncement;
|
||||||
bool music;
|
bool music;
|
||||||
uint8_t decoderIdent;
|
uint8_t decoderIdent;
|
||||||
|
@ -172,9 +291,16 @@ namespace rds {
|
||||||
std::string programServiceName = " ";
|
std::string programServiceName = " ";
|
||||||
|
|
||||||
// Group type 2
|
// Group type 2
|
||||||
std::chrono::time_point<std::chrono::high_resolution_clock> group2LastUpdate;
|
std::mutex group2Mtx;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> group2LastUpdate{}; // 1970-01-01
|
||||||
bool rtAB = false;
|
bool rtAB = false;
|
||||||
std::string radioText = " ";
|
std::string radioText = " ";
|
||||||
|
|
||||||
|
// Group type 10
|
||||||
|
std::mutex group10Mtx;
|
||||||
|
std::chrono::time_point<std::chrono::high_resolution_clock> group10LastUpdate{}; // 1970-01-01
|
||||||
|
bool ptnAB = false;
|
||||||
|
std::string programTypeName = " ";
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
#pragma once
|
||||||
|
#include <dsp/processor.h>
|
||||||
|
#include <dsp/loop/fast_agc.h>
|
||||||
|
#include <dsp/loop/costas.h>
|
||||||
|
#include <dsp/taps/band_pass.h>
|
||||||
|
#include <dsp/filter/fir.h>
|
||||||
|
#include <dsp/convert/complex_to_real.h>
|
||||||
|
#include <dsp/clock_recovery/mm.h>
|
||||||
|
#include <dsp/digital/binary_slicer.h>
|
||||||
|
#include <dsp/digital/differential_decoder.h>
|
||||||
|
|
||||||
|
class RDSDemod : public dsp::Processor<dsp::complex_t, uint8_t> {
|
||||||
|
using base_type = dsp::Processor<dsp::complex_t, uint8_t>;
|
||||||
|
public:
|
||||||
|
RDSDemod() {}
|
||||||
|
RDSDemod(dsp::stream<dsp::complex_t>* in, bool enableSoft) { init(in, enableSoft); }
|
||||||
|
~RDSDemod() {}
|
||||||
|
|
||||||
|
void init(dsp::stream<dsp::complex_t>* in, bool enableSoft) {
|
||||||
|
// Save config
|
||||||
|
this->enableSoft = enableSoft;
|
||||||
|
|
||||||
|
// Initialize the DSP
|
||||||
|
agc.init(NULL, 1.0, 1e6, 0.1);
|
||||||
|
costas.init(NULL, 0.005f);
|
||||||
|
taps = dsp::taps::bandPass<dsp::complex_t>(0, 2375, 100, 5000);
|
||||||
|
fir.init(NULL, taps);
|
||||||
|
double baudfreq = dsp::math::hzToRads(2375.0/2.0, 5000);
|
||||||
|
costas2.init(NULL, 0.01, 0.0, baudfreq, baudfreq - (baudfreq*0.1), baudfreq + (baudfreq*0.1));
|
||||||
|
recov.init(NULL, 5000.0 / (2375.0 / 2.0), 1e-6, 0.01, 0.01);
|
||||||
|
diff.init(NULL, 2);
|
||||||
|
|
||||||
|
// Free useless buffers
|
||||||
|
agc.out.free();
|
||||||
|
fir.out.free();
|
||||||
|
costas2.out.free();
|
||||||
|
recov.out.free();
|
||||||
|
|
||||||
|
// Init the rest
|
||||||
|
base_type::init(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSoftEnabled(bool enable) {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
base_type::tempStop();
|
||||||
|
enableSoft = enable;
|
||||||
|
base_type::tempStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
assert(base_type::_block_init);
|
||||||
|
std::lock_guard<std::recursive_mutex> lck(base_type::ctrlMtx);
|
||||||
|
base_type::tempStop();
|
||||||
|
agc.reset();
|
||||||
|
costas.reset();
|
||||||
|
fir.reset();
|
||||||
|
costas2.reset();
|
||||||
|
recov.reset();
|
||||||
|
diff.reset();
|
||||||
|
base_type::tempStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int process(int count, dsp::complex_t* in, float* softOut, uint8_t* hardOut) {
|
||||||
|
count = agc.process(count, in, costas.out.readBuf);
|
||||||
|
count = costas.process(count, costas.out.readBuf, costas.out.writeBuf);
|
||||||
|
count = fir.process(count, costas.out.writeBuf, costas.out.writeBuf);
|
||||||
|
count = costas2.process(count, costas.out.writeBuf, costas.out.readBuf);
|
||||||
|
count = dsp::convert::ComplexToReal::process(count, costas.out.readBuf, softOut);
|
||||||
|
count = recov.process(count, softOut, softOut);
|
||||||
|
count = dsp::digital::BinarySlicer::process(count, softOut, diff.out.readBuf);
|
||||||
|
count = diff.process(count, diff.out.readBuf, hardOut);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int run() {
|
||||||
|
int count = base_type::_in->read();
|
||||||
|
if (count < 0) { return -1; }
|
||||||
|
|
||||||
|
count = process(count, base_type::_in->readBuf, soft.writeBuf, base_type::out.writeBuf);
|
||||||
|
|
||||||
|
base_type::_in->flush();
|
||||||
|
if (!base_type::out.swap(count)) { return -1; }
|
||||||
|
if (enableSoft) {
|
||||||
|
if (!soft.swap(count)) { return -1; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
dsp::stream<float> soft;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool enableSoft = false;
|
||||||
|
|
||||||
|
dsp::loop::FastAGC<dsp::complex_t> agc;
|
||||||
|
dsp::loop::Costas<2> costas;
|
||||||
|
dsp::tap<dsp::complex_t> taps;
|
||||||
|
dsp::filter::FIR<dsp::complex_t, dsp::complex_t> fir;
|
||||||
|
dsp::loop::Costas<2> costas2;
|
||||||
|
dsp::clock_recovery::MM<float> recov;
|
||||||
|
dsp::digital::DifferentialDecoder diff;
|
||||||
|
};
|
|
@ -531,7 +531,7 @@ private:
|
||||||
ImGui::TableSetColumnIndex(0);
|
ImGui::TableSetColumnIndex(0);
|
||||||
if (ImGui::Button(("Import##_freq_mgr_imp_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)) && !_this->importOpen) {
|
if (ImGui::Button(("Import##_freq_mgr_imp_" + _this->name).c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)) && !_this->importOpen) {
|
||||||
_this->importOpen = true;
|
_this->importOpen = true;
|
||||||
_this->importDialog = new pfd::open_file("Import bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" }, true);
|
_this->importDialog = new pfd::open_file("Import bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" }, pfd::opt::multiselect);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TableSetColumnIndex(1);
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
@ -544,7 +544,7 @@ private:
|
||||||
}
|
}
|
||||||
config.release();
|
config.release();
|
||||||
_this->exportOpen = true;
|
_this->exportOpen = true;
|
||||||
_this->exportDialog = new pfd::save_file("Export bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" }, true);
|
_this->exportDialog = new pfd::save_file("Export bookmarks", "", { "JSON Files (*.json)", "*.json", "All Files", "*" });
|
||||||
}
|
}
|
||||||
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::endDisabled(); }
|
if (selectedNames.size() == 0 && _this->selectedListName != "") { style::endDisabled(); }
|
||||||
ImGui::EndTable();
|
ImGui::EndTable();
|
||||||
|
@ -787,7 +787,7 @@ private:
|
||||||
|
|
||||||
void exportBookmarks(std::string path) {
|
void exportBookmarks(std::string path) {
|
||||||
std::ofstream fs(path);
|
std::ofstream fs(path);
|
||||||
exportedBookmarks >> fs;
|
fs << exportedBookmarks;
|
||||||
fs.close();
|
fs.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(iq_exporter)
|
||||||
|
|
||||||
|
file(GLOB SRC "src/*.cpp")
|
||||||
|
|
||||||
|
include(${SDRPP_MODULE_CMAKE})
|
|
@ -0,0 +1,589 @@
|
||||||
|
#include <utils/net.h>
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <module.h>
|
||||||
|
#include <gui/gui.h>
|
||||||
|
#include <gui/style.h>
|
||||||
|
#include <utils/optionlist.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <dsp/sink/handler_sink.h>
|
||||||
|
#include <volk/volk.h>
|
||||||
|
#include <signal_path/signal_path.h>
|
||||||
|
#include <dsp/buffer/reshaper.h>
|
||||||
|
#include <gui/dialogs/dialog_box.h>
|
||||||
|
#include <core.h>
|
||||||
|
|
||||||
|
SDRPP_MOD_INFO{
|
||||||
|
/* Name: */ "iq_exporter",
|
||||||
|
/* Description: */ "Export raw IQ through TCP or UDP",
|
||||||
|
/* Author: */ "Ryzerth",
|
||||||
|
/* Version: */ 0, 1, 0,
|
||||||
|
/* Max instances */ -1
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfigManager config;
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
MODE_NONE = -1,
|
||||||
|
MODE_BASEBAND,
|
||||||
|
MODE_VFO
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Protocol {
|
||||||
|
PROTOCOL_TCP_SERVER,
|
||||||
|
PROTOCOL_TCP_CLIENT,
|
||||||
|
PROTOCOL_UDP
|
||||||
|
};
|
||||||
|
|
||||||
|
enum SampleType {
|
||||||
|
SAMPLE_TYPE_INT8,
|
||||||
|
SAMPLE_TYPE_INT16,
|
||||||
|
SAMPLE_TYPE_INT32,
|
||||||
|
SAMPLE_TYPE_FLOAT32
|
||||||
|
};
|
||||||
|
|
||||||
|
class IQExporterModule : public ModuleManager::Instance {
|
||||||
|
public:
|
||||||
|
IQExporterModule(std::string name) {
|
||||||
|
this->name = name;
|
||||||
|
|
||||||
|
// Define operating modes
|
||||||
|
modes.define("Baseband", MODE_BASEBAND);
|
||||||
|
modes.define("VFO", MODE_VFO);
|
||||||
|
|
||||||
|
// Define VFO samplerates
|
||||||
|
for (int i = 3000; i <= 192000; i <<= 1) {
|
||||||
|
samplerates.define(i, getSrScaled(i), i);
|
||||||
|
}
|
||||||
|
for (int i = 250000; i < 1000000; i += 250000) {
|
||||||
|
samplerates.define(i, getSrScaled(i), i);
|
||||||
|
}
|
||||||
|
for (int i = 1000000; i < 10000000; i += 500000) {
|
||||||
|
samplerates.define(i, getSrScaled(i), i);
|
||||||
|
}
|
||||||
|
for (int i = 10000000; i <= 100000000; i += 5000000) {
|
||||||
|
samplerates.define(i, getSrScaled(i), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define protocols
|
||||||
|
protocols.define("TCP (Server)", PROTOCOL_TCP_SERVER);
|
||||||
|
protocols.define("TCP (Client)", PROTOCOL_TCP_CLIENT);
|
||||||
|
protocols.define("UDP", PROTOCOL_UDP);
|
||||||
|
|
||||||
|
// Define sample types
|
||||||
|
sampleTypes.define("Int8", SAMPLE_TYPE_INT8);
|
||||||
|
sampleTypes.define("Int16", SAMPLE_TYPE_INT16);
|
||||||
|
sampleTypes.define("Int32", SAMPLE_TYPE_INT32);
|
||||||
|
sampleTypes.define("Float32", SAMPLE_TYPE_FLOAT32);
|
||||||
|
|
||||||
|
// Define packet sizes
|
||||||
|
for (int i = 8; i <= 32768; i <<= 1) {
|
||||||
|
char buf[16];
|
||||||
|
sprintf(buf, "%d Bytes", i);
|
||||||
|
packetSizes.define(i, buf, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
bool autoStart = false;
|
||||||
|
Mode nMode = MODE_BASEBAND;
|
||||||
|
config.acquire();
|
||||||
|
if (config.conf[name].contains("mode")) {
|
||||||
|
std::string modeStr = config.conf[name]["mode"];
|
||||||
|
if (modes.keyExists(modeStr)) { nMode = modes.value(modes.keyId(modeStr)); }
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("samplerate")) {
|
||||||
|
int sr = config.conf[name]["samplerate"];
|
||||||
|
if (samplerates.keyExists(sr)) { samplerate = samplerates.value(samplerates.keyId(sr)); }
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("protocol")) {
|
||||||
|
std::string protoStr = config.conf[name]["protocol"];
|
||||||
|
if (protocols.keyExists(protoStr)) { proto = protocols.value(protocols.keyId(protoStr)); }
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("sampleType")) {
|
||||||
|
std::string sampTypeStr = config.conf[name]["sampleType"];
|
||||||
|
if (sampleTypes.keyExists(sampTypeStr)) { sampType = sampleTypes.value(sampleTypes.keyId(sampTypeStr)); }
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("packetSize")) {
|
||||||
|
int size = config.conf[name]["packetSize"];
|
||||||
|
if (packetSizes.keyExists(size)) { packetSize = packetSizes.value(packetSizes.keyId(size)); }
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("host")) {
|
||||||
|
std::string hostStr = config.conf[name]["host"];
|
||||||
|
strcpy(hostname, hostStr.c_str());
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("port")) {
|
||||||
|
port = config.conf[name]["port"];
|
||||||
|
port = std::clamp<int>(port, 1, 65535);
|
||||||
|
}
|
||||||
|
if (config.conf[name].contains("running")) {
|
||||||
|
autoStart = config.conf[name]["running"];
|
||||||
|
}
|
||||||
|
config.release();
|
||||||
|
|
||||||
|
// Set menu IDs
|
||||||
|
modeId = modes.valueId(nMode);
|
||||||
|
srId = samplerates.valueId(samplerate);
|
||||||
|
protoId = protocols.valueId(proto);
|
||||||
|
sampTypeId = sampleTypes.valueId(sampType);
|
||||||
|
packetSizeId = packetSizes.valueId(packetSize);
|
||||||
|
|
||||||
|
// Allocate buffer
|
||||||
|
buffer = dsp::buffer::alloc<uint8_t>(STREAM_BUFFER_SIZE * sizeof(dsp::complex_t));
|
||||||
|
|
||||||
|
// Init DSP
|
||||||
|
reshape.init(&iqStream, packetSize/sampleSize(), 0);
|
||||||
|
handler.init(&reshape.out, dataHandler, this);
|
||||||
|
|
||||||
|
// Set operating mode
|
||||||
|
setMode(nMode);
|
||||||
|
|
||||||
|
// Start if needed
|
||||||
|
if (autoStart) { start(); }
|
||||||
|
|
||||||
|
// Register menu entry
|
||||||
|
gui::menu.registerEntry(name, menuHandler, this, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~IQExporterModule() {
|
||||||
|
// Un-register menu entry
|
||||||
|
gui::menu.removeEntry(name);
|
||||||
|
|
||||||
|
// Stop networking
|
||||||
|
stop();
|
||||||
|
|
||||||
|
// Stop DSP
|
||||||
|
setMode(MODE_NONE);
|
||||||
|
|
||||||
|
// Free buffer
|
||||||
|
dsp::buffer::free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void postInit() {}
|
||||||
|
|
||||||
|
void enable() {
|
||||||
|
// Rebind streams and start DSP
|
||||||
|
setMode(mode, true);
|
||||||
|
|
||||||
|
// Restart networking if it was running
|
||||||
|
if (wasRunning) { start(); }
|
||||||
|
|
||||||
|
// Mark as running
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void disable() {
|
||||||
|
// Save running state
|
||||||
|
wasRunning = running;
|
||||||
|
|
||||||
|
// Stop networking
|
||||||
|
stop();
|
||||||
|
|
||||||
|
// Stop the DSP and unbind streams
|
||||||
|
setMode(MODE_NONE);
|
||||||
|
|
||||||
|
// Mark as disabled
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
if (running) { return; }
|
||||||
|
|
||||||
|
// Acquire lock on the socket
|
||||||
|
std::lock_guard lck1(sockMtx);
|
||||||
|
|
||||||
|
// Start listening or open UDP socket
|
||||||
|
try {
|
||||||
|
if (proto == PROTOCOL_TCP_SERVER) {
|
||||||
|
// Create listener
|
||||||
|
listener = net::listen(hostname, port);
|
||||||
|
|
||||||
|
// Start listen worker
|
||||||
|
listenWorkerThread = std::thread(&IQExporterModule::listenWorker, this);
|
||||||
|
}
|
||||||
|
else if (proto == PROTOCOL_TCP_CLIENT) {
|
||||||
|
// Connect to TCP server
|
||||||
|
sock = net::connect(hostname, port);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Open UDP socket
|
||||||
|
sock = net::openudp(hostname, port, "0.0.0.0", 0, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
flog::error("[IQExporter] Could not start socket: {}", e.what());
|
||||||
|
errorStr = e.what();
|
||||||
|
showError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
if (!running) { return; }
|
||||||
|
|
||||||
|
// Acquire lock on the socket
|
||||||
|
std::lock_guard lck1(sockMtx);
|
||||||
|
|
||||||
|
// Stop listening or close UDP socket
|
||||||
|
if (proto == PROTOCOL_TCP_SERVER) {
|
||||||
|
// Stop listener
|
||||||
|
if (listener) {
|
||||||
|
listener->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for worker to stop
|
||||||
|
if (listenWorkerThread.joinable()) { listenWorkerThread.join(); }
|
||||||
|
|
||||||
|
// Free listener
|
||||||
|
listener.reset();
|
||||||
|
|
||||||
|
// Close socket and free it
|
||||||
|
if (sock) {
|
||||||
|
sock->close();
|
||||||
|
sock.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Close socket and free it
|
||||||
|
if (sock) {
|
||||||
|
sock->close();
|
||||||
|
sock.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string getSrScaled(double sr) {
|
||||||
|
char buf[1024];
|
||||||
|
if (sr >= 1000000.0) {
|
||||||
|
sprintf(buf, "%.1lf MS/s", sr / 1000000.0);
|
||||||
|
}
|
||||||
|
else if (sr >= 1000.0) {
|
||||||
|
sprintf(buf, "%.1lf KS/s", sr / 1000.0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(buf, "%.1lf S/s", sr);
|
||||||
|
}
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void menuHandler(void* ctx) {
|
||||||
|
IQExporterModule* _this = (IQExporterModule*)ctx;
|
||||||
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
|
// Error message box
|
||||||
|
ImGui::GenericDialog("##iq_exporter_err_", _this->showError, GENERIC_DIALOG_BUTTONS_OK, [=](){
|
||||||
|
ImGui::Text("Error: %s", _this->errorStr.c_str());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!_this->enabled) { ImGui::BeginDisabled(); }
|
||||||
|
|
||||||
|
if (_this->running) { ImGui::BeginDisabled(); }
|
||||||
|
|
||||||
|
// Mode selector
|
||||||
|
ImGui::LeftLabel("Mode");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##iq_exporter_mode_" + _this->name).c_str(), &_this->modeId, _this->modes.txt)) {
|
||||||
|
_this->setMode(_this->modes.value(_this->modeId));
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["mode"] = _this->modes.key(_this->modeId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In VFO mode, show samplerate selector
|
||||||
|
if (_this->mode == MODE_VFO) {
|
||||||
|
ImGui::LeftLabel("Samplerate");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##iq_exporter_sr_" + _this->name).c_str(), &_this->srId, _this->samplerates.txt)) {
|
||||||
|
_this->samplerate = _this->samplerates.value(_this->srId);
|
||||||
|
if (_this->vfo) {
|
||||||
|
_this->vfo->setBandwidthLimits(_this->samplerate, _this->samplerate, true);
|
||||||
|
_this->vfo->setSampleRate(_this->samplerate, _this->samplerate);
|
||||||
|
}
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["samplerate"] = _this->samplerates.key(_this->srId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode protocol selector
|
||||||
|
ImGui::LeftLabel("Protocol");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##iq_exporter_proto_" + _this->name).c_str(), &_this->protoId, _this->protocols.txt)) {
|
||||||
|
_this->proto = _this->protocols.value(_this->protoId);
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["protocol"] = _this->protocols.key(_this->protoId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample type selector
|
||||||
|
ImGui::LeftLabel("Sample type");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##iq_exporter_samp_" + _this->name).c_str(), &_this->sampTypeId, _this->sampleTypes.txt)) {
|
||||||
|
_this->sampType = _this->sampleTypes.value(_this->sampTypeId);
|
||||||
|
_this->reshape.setKeep(_this->packetSize/_this->sampleSize());
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampTypeId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet size selector
|
||||||
|
ImGui::LeftLabel("Packet size");
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::Combo(("##iq_exporter_pkt_sz_" + _this->name).c_str(), &_this->packetSizeId, _this->packetSizes.txt)) {
|
||||||
|
_this->packetSize = _this->packetSizes.value(_this->packetSizeId);
|
||||||
|
_this->reshape.setKeep(_this->packetSize/_this->sampleSize());
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["packetSize"] = _this->packetSizes.key(_this->packetSizeId);
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname and port field
|
||||||
|
if (ImGui::InputText(("##iq_exporter_host_" + _this->name).c_str(), _this->hostname, sizeof(_this->hostname))) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["host"] = _this->hostname;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::FillWidth();
|
||||||
|
if (ImGui::InputInt(("##iq_exporter_port_" + _this->name).c_str(), &_this->port, 0, 0)) {
|
||||||
|
_this->port = std::clamp<int>(_this->port, 1, 65535);
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["port"] = _this->port;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_this->running) { ImGui::EndDisabled(); }
|
||||||
|
|
||||||
|
// Start/Stop buttons
|
||||||
|
if (_this->running || (!_this->enabled && _this->wasRunning)) {
|
||||||
|
if (ImGui::Button(("Stop##iq_exporter_stop_" + _this->name).c_str(), ImVec2(menuWidth, 0))) {
|
||||||
|
_this->stop();
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["running"] = false;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (ImGui::Button(("Start##iq_exporter_start_" + _this->name).c_str(), ImVec2(menuWidth, 0))) {
|
||||||
|
_this->start();
|
||||||
|
config.acquire();
|
||||||
|
config.conf[_this->name]["running"] = true;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the socket is open by attempting a read
|
||||||
|
bool sockOpen;
|
||||||
|
{
|
||||||
|
uint8_t dummy;
|
||||||
|
sockOpen = !(!_this->sock || !_this->sock->isOpen() || (_this->proto != PROTOCOL_UDP && _this->sock->recv(&dummy, 1, false, net::NONBLOCKING) == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status text
|
||||||
|
ImGui::TextUnformatted("Status:");
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (sockOpen) {
|
||||||
|
ImGui::TextColored(ImVec4(0.0, 1.0, 0.0, 1.0), (_this->proto == PROTOCOL_TCP_SERVER || _this->proto == PROTOCOL_TCP_CLIENT) ? "Connected" : "Sending");
|
||||||
|
}
|
||||||
|
else if (_this->listener && _this->listener->listening()) {
|
||||||
|
ImGui::TextColored(ImVec4(1.0, 1.0, 0.0, 1.0), "Listening");
|
||||||
|
}
|
||||||
|
else if (!_this->enabled) {
|
||||||
|
ImGui::TextUnformatted("Disabled");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we're idle and still supposed to be running, the server has closed the connection (TODO: kinda jank...)
|
||||||
|
if (_this->running) { _this->stop(); }
|
||||||
|
|
||||||
|
ImGui::TextUnformatted("Idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_this->enabled) { ImGui::EndDisabled(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMode(Mode newMode, bool fromDisabled = false) {
|
||||||
|
// If there is no mode to change, do nothing
|
||||||
|
if (!fromDisabled && mode == newMode) { return; }
|
||||||
|
|
||||||
|
// Stop the DSP
|
||||||
|
reshape.stop();
|
||||||
|
handler.stop();
|
||||||
|
|
||||||
|
// Delete VFO or unbind IQ stream
|
||||||
|
if (vfo) {
|
||||||
|
sigpath::vfoManager.deleteVFO(vfo);
|
||||||
|
vfo = NULL;
|
||||||
|
}
|
||||||
|
if (mode == MODE_BASEBAND && !fromDisabled) {
|
||||||
|
sigpath::iqFrontEnd.unbindIQStream(&iqStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the mode was none, we're done
|
||||||
|
if (newMode == MODE_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VFO or bind IQ stream
|
||||||
|
if (newMode == MODE_VFO) {
|
||||||
|
// Create VFO
|
||||||
|
vfo = sigpath::vfoManager.createVFO(name, ImGui::WaterfallVFO::REF_CENTER, 0, samplerate, samplerate, samplerate, samplerate, true);
|
||||||
|
|
||||||
|
// Set its output as the input to the DSP
|
||||||
|
reshape.setInput(vfo->output);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Bind IQ stream
|
||||||
|
sigpath::iqFrontEnd.bindIQStream(&iqStream);
|
||||||
|
|
||||||
|
// Set its output as the input to the DSP
|
||||||
|
reshape.setInput(&iqStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start DSP
|
||||||
|
reshape.start();
|
||||||
|
handler.start();
|
||||||
|
|
||||||
|
// Update mode
|
||||||
|
mode = newMode;
|
||||||
|
modeId = modes.valueId(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void listenWorker() {
|
||||||
|
while (true) {
|
||||||
|
// Accept a client
|
||||||
|
auto newSock = listener->accept();
|
||||||
|
if (!newSock) { break; }
|
||||||
|
|
||||||
|
// Update socket
|
||||||
|
{
|
||||||
|
std::lock_guard lck(sockMtx);
|
||||||
|
sock = newSock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int sampleSize() {
|
||||||
|
switch (sampType) {
|
||||||
|
case SAMPLE_TYPE_INT8:
|
||||||
|
return sizeof(int8_t)*2;
|
||||||
|
case SAMPLE_TYPE_INT16:
|
||||||
|
return sizeof(int16_t)*2;
|
||||||
|
case SAMPLE_TYPE_INT32:
|
||||||
|
return sizeof(int32_t)*2;
|
||||||
|
case SAMPLE_TYPE_FLOAT32:
|
||||||
|
return sizeof(dsp::complex_t);
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dataHandler(dsp::complex_t* data, int count, void* ctx) {
|
||||||
|
IQExporterModule* _this = (IQExporterModule*)ctx;
|
||||||
|
|
||||||
|
// Try to cquire lock on socket
|
||||||
|
if (!_this->sockMtx.try_lock()) { return; }
|
||||||
|
|
||||||
|
// If not valid or open, give uo
|
||||||
|
if (!_this->sock || !_this->sock->isOpen()) {
|
||||||
|
// Unlock socket mutex
|
||||||
|
_this->sockMtx.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the samples or send directory for float32
|
||||||
|
int size;
|
||||||
|
switch (_this->sampType) {
|
||||||
|
case SAMPLE_TYPE_INT8:
|
||||||
|
volk_32f_s32f_convert_8i((int8_t*)_this->buffer, (float*)data, 128.0f, count*2);
|
||||||
|
size = sizeof(int8_t)*2;
|
||||||
|
break;
|
||||||
|
case SAMPLE_TYPE_INT16:
|
||||||
|
volk_32f_s32f_convert_16i((int16_t*)_this->buffer, (float*)data, 32768.0f, count*2);
|
||||||
|
size = sizeof(int16_t)*2;
|
||||||
|
break;
|
||||||
|
case SAMPLE_TYPE_INT32:
|
||||||
|
volk_32f_s32f_convert_32i((int32_t*)_this->buffer, (float*)data, (float)2147483647.0f, count*2);
|
||||||
|
size = sizeof(int32_t)*2;
|
||||||
|
break;
|
||||||
|
case SAMPLE_TYPE_FLOAT32:
|
||||||
|
_this->sock->send((uint8_t*)data, count*sizeof(dsp::complex_t));
|
||||||
|
default:
|
||||||
|
// Unlock socket mutex
|
||||||
|
_this->sockMtx.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send converted samples
|
||||||
|
_this->sock->send(_this->buffer, count*size);
|
||||||
|
|
||||||
|
// Unlock socket mutex
|
||||||
|
_this->sockMtx.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
Mode mode = MODE_NONE;
|
||||||
|
int modeId;
|
||||||
|
int samplerate = 1000000.0;
|
||||||
|
int srId;
|
||||||
|
Protocol proto = PROTOCOL_TCP_SERVER;
|
||||||
|
int protoId;
|
||||||
|
SampleType sampType = SAMPLE_TYPE_INT16;
|
||||||
|
int sampTypeId;
|
||||||
|
int packetSize = 1024;
|
||||||
|
int packetSizeId;
|
||||||
|
char hostname[1024] = "localhost";
|
||||||
|
int port = 1234;
|
||||||
|
bool running = false;
|
||||||
|
bool wasRunning = false;
|
||||||
|
|
||||||
|
bool showError = false;
|
||||||
|
std::string errorStr = "";
|
||||||
|
|
||||||
|
OptionList<std::string, Mode> modes;
|
||||||
|
OptionList<int, int> samplerates;
|
||||||
|
OptionList<std::string, Protocol> protocols;
|
||||||
|
OptionList<std::string, SampleType> sampleTypes;
|
||||||
|
OptionList<int, int> packetSizes;
|
||||||
|
|
||||||
|
VFOManager::VFO* vfo = NULL;
|
||||||
|
dsp::stream<dsp::complex_t> iqStream;
|
||||||
|
dsp::buffer::Reshaper<dsp::complex_t> reshape;
|
||||||
|
dsp::sink::Handler<dsp::complex_t> handler;
|
||||||
|
uint8_t* buffer = NULL;
|
||||||
|
|
||||||
|
std::thread listenWorkerThread;
|
||||||
|
|
||||||
|
std::mutex sockMtx;
|
||||||
|
std::shared_ptr<net::Socket> sock;
|
||||||
|
std::shared_ptr<net::Listener> listener;
|
||||||
|
};
|
||||||
|
|
||||||
|
MOD_EXPORT void _INIT_() {
|
||||||
|
json def = json({});
|
||||||
|
std::string root = (std::string)core::args["root"];
|
||||||
|
config.setPath(root + "/iq_exporter_config_config.json");
|
||||||
|
config.load(def);
|
||||||
|
config.enableAutoSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
|
||||||
|
return new IQExporterModule(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT void _DELETE_INSTANCE_(void* instance) {
|
||||||
|
delete (IQExporterModule*)instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT void _END_() {
|
||||||
|
config.disableAutoSave();
|
||||||
|
config.save();
|
||||||
|
}
|
|
@ -334,7 +334,7 @@ Modules in beta are still included in releases for the most part but not enabled
|
||||||
| hackrf_source | Working | libhackrf | OPT_BUILD_HACKRF_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 | ⛔ | ✅ | ✅ |
|
| limesdr_source | Working | liblimesuite | OPT_BUILD_LIMESDR_SOURCE | ⛔ | ✅ | ✅ |
|
||||||
| perseus_source | Beta | libperseus-sdr | OPT_BUILD_PERSEUS_SOURCE | ⛔ | ⛔ | ⛔ |
|
| perseus_source | Beta | libperseus-sdr | OPT_BUILD_PERSEUS_SOURCE | ⛔ | ✅ | ✅ |
|
||||||
| plutosdr_source | Working | libiio, libad9361 | OPT_BUILD_PLUTOSDR_SOURCE | ✅ | ✅ | ✅ |
|
| plutosdr_source | Working | libiio, libad9361 | OPT_BUILD_PLUTOSDR_SOURCE | ✅ | ✅ | ✅ |
|
||||||
| rfspace_source | Working | - | OPT_BUILD_RFSPACE_SOURCE | ✅ | ✅ | ✅ |
|
| rfspace_source | Working | - | OPT_BUILD_RFSPACE_SOURCE | ✅ | ✅ | ✅ |
|
||||||
| rtl_sdr_source | Working | librtlsdr | OPT_BUILD_RTL_SDR_SOURCE | ✅ | ✅ | ✅ |
|
| rtl_sdr_source | Working | librtlsdr | OPT_BUILD_RTL_SDR_SOURCE | ✅ | ✅ | ✅ |
|
||||||
|
@ -367,6 +367,7 @@ Modules in beta are still included in releases for the most part but not enabled
|
||||||
| kgsstv_decoder | Unfinished | - | OPT_BUILD_KGSSTV_DECODER | ⛔ | ⛔ | ⛔ |
|
| kgsstv_decoder | Unfinished | - | OPT_BUILD_KGSSTV_DECODER | ⛔ | ⛔ | ⛔ |
|
||||||
| m17_decoder | Beta | - | OPT_BUILD_M17_DECODER | ⛔ | ✅ | ⛔ |
|
| m17_decoder | Beta | - | OPT_BUILD_M17_DECODER | ⛔ | ✅ | ⛔ |
|
||||||
| meteor_demodulator | Working | - | OPT_BUILD_METEOR_DEMODULATOR | ✅ | ✅ | ⛔ |
|
| meteor_demodulator | Working | - | OPT_BUILD_METEOR_DEMODULATOR | ✅ | ✅ | ⛔ |
|
||||||
|
| pager_decoder | Unfinished | - | OPT_BUILD_PAGER_DECODER | ⛔ | ⛔ | ⛔ |
|
||||||
| radio | Working | - | OPT_BUILD_RADIO | ✅ | ✅ | ✅ |
|
| radio | Working | - | OPT_BUILD_RADIO | ✅ | ✅ | ✅ |
|
||||||
| weather_sat_decoder | Unfinished | - | OPT_BUILD_WEATHER_SAT_DECODER | ⛔ | ⛔ | ⛔ |
|
| weather_sat_decoder | Unfinished | - | OPT_BUILD_WEATHER_SAT_DECODER | ⛔ | ⛔ | ⛔ |
|
||||||
|
|
||||||
|
@ -376,6 +377,7 @@ Modules in beta are still included in releases for the most part but not enabled
|
||||||
|---------------------|------------|--------------|-----------------------------|:----------------:|:----------------:|:---------------------------:|
|
|---------------------|------------|--------------|-----------------------------|:----------------:|:----------------:|:---------------------------:|
|
||||||
| discord_integration | Working | - | OPT_BUILD_DISCORD_PRESENCE | ✅ | ✅ | ⛔ |
|
| discord_integration | Working | - | OPT_BUILD_DISCORD_PRESENCE | ✅ | ✅ | ⛔ |
|
||||||
| frequency_manager | Working | - | OPT_BUILD_FREQUENCY_MANAGER | ✅ | ✅ | ✅ |
|
| frequency_manager | Working | - | OPT_BUILD_FREQUENCY_MANAGER | ✅ | ✅ | ✅ |
|
||||||
|
| iq_exporter | Unfinished | - | OPT_BUILD_IQ_EXPORTER | ⛔ | ⛔ | ⛔ |
|
||||||
| recorder | Working | - | OPT_BUILD_RECORDER | ✅ | ✅ | ✅ |
|
| 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 | ✅ | ✅ | ✅ |
|
| rigctl_server | Working | - | OPT_BUILD_RIGCTL_SERVER | ✅ | ✅ | ✅ |
|
||||||
|
|
|
@ -347,7 +347,7 @@ private:
|
||||||
static void start(void* ctx) {
|
static void start(void* ctx) {
|
||||||
BladeRFSourceModule* _this = (BladeRFSourceModule*)ctx;
|
BladeRFSourceModule* _this = (BladeRFSourceModule*)ctx;
|
||||||
if (_this->running) { return; }
|
if (_this->running) { return; }
|
||||||
if (_this->devCount == 0) { return; }
|
if (_this->devCount <= 0) { return; }
|
||||||
|
|
||||||
// Open device
|
// Open device
|
||||||
bladerf_devinfo info = _this->devInfoList[_this->devId];
|
bladerf_devinfo info = _this->devInfoList[_this->devId];
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
#include <iio.h>
|
#include <iio.h>
|
||||||
#include <ad9361.h>
|
#include <ad9361.h>
|
||||||
#include <utils/optionlist.h>
|
#include <utils/optionlist.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
#define CONCAT(a, b) ((std::string(a) + b).c_str())
|
||||||
|
|
||||||
|
@ -26,61 +28,32 @@ public:
|
||||||
PlutoSDRSourceModule(std::string name) {
|
PlutoSDRSourceModule(std::string name) {
|
||||||
this->name = name;
|
this->name = name;
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
config.acquire();
|
|
||||||
if (config.conf.contains("IP")) {
|
|
||||||
std::string _ip = config.conf["IP"];
|
|
||||||
strcpy(&ip[3], _ip.c_str());
|
|
||||||
}
|
|
||||||
if (config.conf.contains("sampleRate")) {
|
|
||||||
sampleRate = config.conf["sampleRate"];
|
|
||||||
}
|
|
||||||
if (config.conf.contains("bandwidth")) {
|
|
||||||
bandwidth = config.conf["bandwidth"];
|
|
||||||
}
|
|
||||||
if (config.conf.contains("gainMode")) {
|
|
||||||
gainMode = config.conf["gainMode"];
|
|
||||||
}
|
|
||||||
if (config.conf.contains("gain")) {
|
|
||||||
gain = config.conf["gain"];
|
|
||||||
}
|
|
||||||
config.release();
|
|
||||||
|
|
||||||
// Define valid samplerates
|
// Define valid samplerates
|
||||||
for (int sr = 1000000; sr <= 61440000; sr += 500000) {
|
for (int sr = 1000000; sr <= 61440000; sr += 500000) {
|
||||||
samplerates.define(sr, getBandwdithScaled(sr), sr);
|
samplerates.define(sr, getBandwdithScaled(sr), sr);
|
||||||
}
|
}
|
||||||
samplerates.define(61440000, getBandwdithScaled(61440000.0), 61440000.0);
|
samplerates.define(61440000, getBandwdithScaled(61440000.0), 61440000.0);
|
||||||
|
|
||||||
// Set samplerate ID
|
|
||||||
if (samplerates.keyExists(sampleRate)) {
|
|
||||||
srId = samplerates.keyId(sampleRate);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
srId = 0;
|
|
||||||
sampleRate = samplerates.value(srId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define valid bandwidths
|
// Define valid bandwidths
|
||||||
bandwidths.define(0, "Auto", 0);
|
bandwidths.define(0, "Auto", 0);
|
||||||
for (int bw = 1000000.0; bw <= 52000000; bw += 500000) {
|
for (int bw = 1000000.0; bw <= 52000000; bw += 500000) {
|
||||||
bandwidths.define(bw, getBandwdithScaled(bw), bw);
|
bandwidths.define(bw, getBandwdithScaled(bw), bw);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set bandwidth ID
|
|
||||||
if (bandwidths.keyExists(bandwidth)) {
|
|
||||||
bwId = bandwidths.keyId(bandwidth);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
bwId = 0;
|
|
||||||
bandwidth = bandwidths.value(bwId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define gain modes
|
// Define gain modes
|
||||||
gainModes.define(0, "Manual", "manual");
|
gainModes.define("manual", "Manual", "manual");
|
||||||
gainModes.define(1, "Fast Attack", "fast_attack");
|
gainModes.define("fast_attack", "Fast Attack", "fast_attack");
|
||||||
gainModes.define(2, "Slow Attack", "slow_attack");
|
gainModes.define("slow_attack", "Slow Attack", "slow_attack");
|
||||||
gainModes.define(3, "Hybrid", "hybrid");
|
gainModes.define("hybrid", "Hybrid", "hybrid");
|
||||||
|
|
||||||
|
// Enumerate devices
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
// Select device
|
||||||
|
config.acquire();
|
||||||
|
devDesc = config.conf["device"];
|
||||||
|
config.release();
|
||||||
|
select(devDesc);
|
||||||
|
|
||||||
// Register source
|
// Register source
|
||||||
handler.ctx = this;
|
handler.ctx = this;
|
||||||
|
@ -128,9 +101,157 @@ private:
|
||||||
return std::string(buf);
|
return std::string(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
// Clear device list
|
||||||
|
devices.clear();
|
||||||
|
|
||||||
|
// Create scan context
|
||||||
|
iio_scan_context* sctx = iio_create_scan_context(NULL, 0);
|
||||||
|
if (!sctx) {
|
||||||
|
flog::error("Failed get scan context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create parsing regexes
|
||||||
|
std::regex backendRgx(".+(?=:)", std::regex::ECMAScript);
|
||||||
|
std::regex modelRgx("\\(.+(?=\\),)", std::regex::ECMAScript);
|
||||||
|
std::regex serialRgx("serial=[0-9A-Za-z]+", std::regex::ECMAScript);
|
||||||
|
|
||||||
|
// Enumerate devices
|
||||||
|
iio_context_info** ctxInfoList;
|
||||||
|
ssize_t count = iio_scan_context_get_info_list(sctx, &ctxInfoList);
|
||||||
|
if (count < 0) {
|
||||||
|
flog::error("Failed to enumerate contexts");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ssize_t i = 0; i < count; i++) {
|
||||||
|
iio_context_info* info = ctxInfoList[i];
|
||||||
|
std::string desc = iio_context_info_get_description(info);
|
||||||
|
std::string duri = iio_context_info_get_uri(info);
|
||||||
|
|
||||||
|
// If the device is not a plutosdr, don't include it
|
||||||
|
if (desc.find("PlutoSDR") == std::string::npos) {
|
||||||
|
flog::warn("Ignored IIO device: [{}] {}", duri, desc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the backend
|
||||||
|
std::string backend = "unknown";
|
||||||
|
std::smatch backendMatch;
|
||||||
|
if (std::regex_search(duri, backendMatch, backendRgx)) {
|
||||||
|
backend = backendMatch[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the model
|
||||||
|
std::string model = "Unknown";
|
||||||
|
std::smatch modelMatch;
|
||||||
|
if (std::regex_search(desc, modelMatch, modelRgx)) {
|
||||||
|
model = modelMatch[0];
|
||||||
|
int parenthPos = model.find('(');
|
||||||
|
if (parenthPos != std::string::npos) {
|
||||||
|
model = model.substr(parenthPos+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the serial
|
||||||
|
std::string serial = "unknown";
|
||||||
|
std::smatch serialMatch;
|
||||||
|
if (std::regex_search(desc, serialMatch, serialRgx)) {
|
||||||
|
serial = serialMatch[0].str().substr(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the device name
|
||||||
|
std::string devName = '(' + backend + ") " + model + " [" + serial + ']';
|
||||||
|
|
||||||
|
// Save device
|
||||||
|
devices.define(desc, devName, duri);
|
||||||
|
}
|
||||||
|
iio_context_info_list_free(ctxInfoList);
|
||||||
|
|
||||||
|
// Destroy scan context
|
||||||
|
iio_scan_context_destroy(sctx);
|
||||||
|
|
||||||
|
#ifdef __ANDROID__
|
||||||
|
// On Android, a default IP entry must be made (TODO: This is not ideal since the IP cannot be changed)
|
||||||
|
const char* androidURI = "ip:192.168.2.1";
|
||||||
|
const char* androidName = "Default (192.168.2.1)";
|
||||||
|
devices.define(androidName, androidName, androidURI);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void select(const std::string& desc) {
|
||||||
|
// If no device is available, give up
|
||||||
|
if (devices.empty()) {
|
||||||
|
devDesc.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the device is not available, select the first one
|
||||||
|
if (!devices.keyExists(desc)) {
|
||||||
|
select(devices.key(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update URI
|
||||||
|
devDesc = desc;
|
||||||
|
uri = devices.value(devices.keyId(desc));
|
||||||
|
|
||||||
|
// TODO: Enumerate capabilities
|
||||||
|
|
||||||
|
// Load defaults
|
||||||
|
samplerate = 4000000;
|
||||||
|
bandwidth = 0;
|
||||||
|
gmId = 0;
|
||||||
|
gain = -1.0f;
|
||||||
|
|
||||||
|
// Load device config
|
||||||
|
config.acquire();
|
||||||
|
if (config.conf["devices"][devDesc].contains("samplerate")) {
|
||||||
|
samplerate = config.conf["devices"][devDesc]["samplerate"];
|
||||||
|
}
|
||||||
|
if (config.conf["devices"][devDesc].contains("bandwidth")) {
|
||||||
|
bandwidth = config.conf["devices"][devDesc]["bandwidth"];
|
||||||
|
}
|
||||||
|
if (config.conf["devices"][devDesc].contains("gainMode")) {
|
||||||
|
// Select given gain mode or default if invalid
|
||||||
|
std::string gm = config.conf["devices"][devDesc]["gainMode"];
|
||||||
|
if (gainModes.keyExists(gm)) {
|
||||||
|
gmId = gainModes.keyId(gm);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gmId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.conf["devices"][devDesc].contains("gain")) {
|
||||||
|
gain = config.conf["devices"][devDesc]["gain"];
|
||||||
|
gain = std::clamp<int>(gain, -1.0f, 73.0f);
|
||||||
|
}
|
||||||
|
config.release();
|
||||||
|
|
||||||
|
// Update samplerate ID
|
||||||
|
if (samplerates.keyExists(samplerate)) {
|
||||||
|
srId = samplerates.keyId(samplerate);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
srId = 0;
|
||||||
|
samplerate = samplerates.value(srId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update bandwidth ID
|
||||||
|
if (bandwidths.keyExists(bandwidth)) {
|
||||||
|
bwId = bandwidths.keyId(bandwidth);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bwId = 0;
|
||||||
|
bandwidth = bandwidths.value(bwId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update core samplerate
|
||||||
|
core::setInputSampleRate(samplerate);
|
||||||
|
}
|
||||||
|
|
||||||
static void menuSelected(void* ctx) {
|
static void menuSelected(void* ctx) {
|
||||||
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
||||||
core::setInputSampleRate(_this->sampleRate);
|
core::setInputSampleRate(_this->samplerate);
|
||||||
flog::info("PlutoSDRSourceModule '{0}': Menu Select!", _this->name);
|
flog::info("PlutoSDRSourceModule '{0}': Menu Select!", _this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,10 +264,13 @@ private:
|
||||||
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
||||||
if (_this->running) { return; }
|
if (_this->running) { return; }
|
||||||
|
|
||||||
|
// If no device is selected, give up
|
||||||
|
if (_this->devDesc.empty() || _this->uri.empty()) { return; }
|
||||||
|
|
||||||
// Open context
|
// Open context
|
||||||
_this->ctx = iio_create_context_from_uri(_this->ip);
|
_this->ctx = iio_create_context_from_uri(_this->uri.c_str());
|
||||||
if (_this->ctx == NULL) {
|
if (_this->ctx == NULL) {
|
||||||
flog::error("Could not open pluto");
|
flog::error("Could not open pluto ({})", _this->uri);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,15 +298,15 @@ private:
|
||||||
|
|
||||||
// Configure RX channel
|
// Configure RX channel
|
||||||
iio_channel_attr_write(_this->rxChan, "rf_port_select", "A_BALANCED");
|
iio_channel_attr_write(_this->rxChan, "rf_port_select", "A_BALANCED");
|
||||||
iio_channel_attr_write_longlong(_this->rxLO, "frequency", round(_this->freq)); // Freq
|
iio_channel_attr_write_longlong(_this->rxLO, "frequency", round(_this->freq)); // Freq
|
||||||
iio_channel_attr_write_bool(_this->rxChan, "filter_fir_en", true); // Digital filter
|
iio_channel_attr_write_bool(_this->rxChan, "filter_fir_en", true); // Digital filter
|
||||||
iio_channel_attr_write_longlong(_this->rxChan, "sampling_frequency", round(_this->sampleRate)); // Sample rate
|
iio_channel_attr_write_longlong(_this->rxChan, "sampling_frequency", round(_this->samplerate)); // Sample rate
|
||||||
iio_channel_attr_write_longlong(_this->rxChan, "hardwaregain", round(_this->gain)); // Gain
|
iio_channel_attr_write_double(_this->rxChan, "hardwaregain", _this->gain); // Gain
|
||||||
iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gainMode).c_str()); // Gain mode
|
iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gmId).c_str()); // Gain mode
|
||||||
_this->setBandwidth(_this->bandwidth);
|
_this->setBandwidth(_this->bandwidth);
|
||||||
|
|
||||||
// Configure the ADC filters
|
// Configure the ADC filters
|
||||||
ad9361_set_bb_rate(_this->phy, round(_this->sampleRate));
|
ad9361_set_bb_rate(_this->phy, round(_this->samplerate));
|
||||||
|
|
||||||
// Start worker thread
|
// Start worker thread
|
||||||
_this->running = true;
|
_this->running = true;
|
||||||
|
@ -214,7 +338,7 @@ private:
|
||||||
_this->freq = freq;
|
_this->freq = freq;
|
||||||
if (_this->running) {
|
if (_this->running) {
|
||||||
// Tune device
|
// Tune device
|
||||||
iio_channel_attr_write_longlong(iio_device_find_channel(_this->phy, "altvoltage0", true), "frequency", round(freq));
|
iio_channel_attr_write_longlong(_this->rxLO, "frequency", round(freq));
|
||||||
}
|
}
|
||||||
flog::info("PlutoSDRSourceModule '{0}': Tune: {1}!", _this->name, freq);
|
flog::info("PlutoSDRSourceModule '{0}': Tune: {1}!", _this->name, freq);
|
||||||
}
|
}
|
||||||
|
@ -223,22 +347,33 @@ private:
|
||||||
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
||||||
|
|
||||||
if (_this->running) { SmGui::BeginDisabled(); }
|
if (_this->running) { SmGui::BeginDisabled(); }
|
||||||
SmGui::LeftLabel("IP");
|
|
||||||
SmGui::FillWidth();
|
SmGui::FillWidth();
|
||||||
if (SmGui::InputText(CONCAT("##_pluto_ip_", _this->name), &_this->ip[3], 16)) {
|
SmGui::ForceSync();
|
||||||
|
if (SmGui::Combo("##plutosdr_dev_sel", &_this->devId, _this->devices.txt)) {
|
||||||
|
_this->select(_this->devices.key(_this->devId));
|
||||||
config.acquire();
|
config.acquire();
|
||||||
config.conf["IP"] = &_this->ip[3];
|
config.conf["device"] = _this->devices.key(_this->devId);
|
||||||
config.release(true);
|
config.release(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
SmGui::LeftLabel("Samplerate");
|
|
||||||
SmGui::FillWidth();
|
|
||||||
if (SmGui::Combo(CONCAT("##_pluto_sr_", _this->name), &_this->srId, _this->samplerates.txt)) {
|
if (SmGui::Combo(CONCAT("##_pluto_sr_", _this->name), &_this->srId, _this->samplerates.txt)) {
|
||||||
_this->sampleRate = _this->samplerates.value(_this->srId);
|
_this->samplerate = _this->samplerates.value(_this->srId);
|
||||||
core::setInputSampleRate(_this->sampleRate);
|
core::setInputSampleRate(_this->samplerate);
|
||||||
config.acquire();
|
if (!_this->devDesc.empty()) {
|
||||||
config.conf["sampleRate"] = _this->sampleRate;
|
config.acquire();
|
||||||
config.release(true);
|
config.conf["devices"][_this->devDesc]["samplerate"] = _this->samplerate;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
SmGui::SameLine();
|
||||||
|
SmGui::FillWidth();
|
||||||
|
SmGui::ForceSync();
|
||||||
|
if (SmGui::Button(CONCAT("Refresh##_pluto_refr_", _this->name))) {
|
||||||
|
_this->refresh();
|
||||||
|
_this->select(_this->devDesc);
|
||||||
|
|
||||||
}
|
}
|
||||||
if (_this->running) { SmGui::EndDisabled(); }
|
if (_this->running) { SmGui::EndDisabled(); }
|
||||||
|
|
||||||
|
@ -249,35 +384,41 @@ private:
|
||||||
if (_this->running) {
|
if (_this->running) {
|
||||||
_this->setBandwidth(_this->bandwidth);
|
_this->setBandwidth(_this->bandwidth);
|
||||||
}
|
}
|
||||||
config.acquire();
|
if (!_this->devDesc.empty()) {
|
||||||
config.conf["bandwidth"] = _this->bandwidth;
|
config.acquire();
|
||||||
config.release(true);
|
config.conf["devices"][_this->devDesc]["bandwidth"] = _this->bandwidth;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SmGui::LeftLabel("Gain Mode");
|
SmGui::LeftLabel("Gain Mode");
|
||||||
SmGui::FillWidth();
|
SmGui::FillWidth();
|
||||||
SmGui::ForceSync();
|
SmGui::ForceSync();
|
||||||
if (SmGui::Combo(CONCAT("##_gainmode_select_", _this->name), &_this->gainMode, _this->gainModes.txt)) {
|
if (SmGui::Combo(CONCAT("##_pluto_gainmode_select_", _this->name), &_this->gmId, _this->gainModes.txt)) {
|
||||||
if (_this->running) {
|
if (_this->running) {
|
||||||
iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gainMode).c_str());
|
iio_channel_attr_write(_this->rxChan, "gain_control_mode", _this->gainModes.value(_this->gmId).c_str());
|
||||||
|
}
|
||||||
|
if (!_this->devDesc.empty()) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf["devices"][_this->devDesc]["gainMode"] = _this->gainModes.key(_this->gmId);
|
||||||
|
config.release(true);
|
||||||
}
|
}
|
||||||
config.acquire();
|
|
||||||
config.conf["gainMode"] = _this->gainMode;
|
|
||||||
config.release(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SmGui::LeftLabel("PGA Gain");
|
SmGui::LeftLabel("Gain");
|
||||||
if (_this->gainMode) { SmGui::BeginDisabled(); }
|
if (_this->gmId) { SmGui::BeginDisabled(); }
|
||||||
SmGui::FillWidth();
|
SmGui::FillWidth();
|
||||||
if (SmGui::SliderFloat(CONCAT("##_gain_select_", _this->name), &_this->gain, 0, 76)) {
|
if (SmGui::SliderFloatWithSteps(CONCAT("##_pluto_gain__", _this->name), &_this->gain, -1.0f, 73.0f, 1.0f, SmGui::FMT_STR_FLOAT_DB_NO_DECIMAL)) {
|
||||||
if (_this->running) {
|
if (_this->running) {
|
||||||
iio_channel_attr_write_longlong(_this->rxChan, "hardwaregain", round(_this->gain));
|
iio_channel_attr_write_double(_this->rxChan, "hardwaregain", _this->gain);
|
||||||
|
}
|
||||||
|
if (!_this->devDesc.empty()) {
|
||||||
|
config.acquire();
|
||||||
|
config.conf["devices"][_this->devDesc]["gain"] = _this->gain;
|
||||||
|
config.release(true);
|
||||||
}
|
}
|
||||||
config.acquire();
|
|
||||||
config.conf["gain"] = _this->gain;
|
|
||||||
config.release(true);
|
|
||||||
}
|
}
|
||||||
if (_this->gainMode) { SmGui::EndDisabled(); }
|
if (_this->gmId) { SmGui::EndDisabled(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBandwidth(int bw) {
|
void setBandwidth(int bw) {
|
||||||
|
@ -285,17 +426,21 @@ private:
|
||||||
iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", bw);
|
iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", bw);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", std::min<int>(sampleRate, 52000000));
|
iio_channel_attr_write_longlong(rxChan, "rf_bandwidth", std::min<int>(samplerate, 52000000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void worker(void* ctx) {
|
static void worker(void* ctx) {
|
||||||
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
PlutoSDRSourceModule* _this = (PlutoSDRSourceModule*)ctx;
|
||||||
int blockSize = _this->sampleRate / 200.0f;
|
int blockSize = _this->samplerate / 200.0f;
|
||||||
|
|
||||||
// Acquire channels
|
// Acquire channels
|
||||||
iio_channel* rx0_i = iio_device_find_channel(_this->dev, "voltage0", 0);
|
iio_channel* rx0_i = iio_device_find_channel(_this->dev, "voltage0", 0);
|
||||||
iio_channel* rx0_q = iio_device_find_channel(_this->dev, "voltage1", 0);
|
iio_channel* rx0_q = iio_device_find_channel(_this->dev, "voltage1", 0);
|
||||||
|
if (!rx0_i || !rx0_q) {
|
||||||
|
flog::error("Failed to acquire RX channels");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start streaming
|
// Start streaming
|
||||||
iio_channel_enable(rx0_i);
|
iio_channel_enable(rx0_i);
|
||||||
|
@ -315,6 +460,7 @@ private:
|
||||||
|
|
||||||
// Get buffer pointer
|
// Get buffer pointer
|
||||||
int16_t* buf = (int16_t*)iio_buffer_first(rxbuf, rx0_i);
|
int16_t* buf = (int16_t*)iio_buffer_first(rxbuf, rx0_i);
|
||||||
|
if (!buf) { break; }
|
||||||
|
|
||||||
// Convert samples to CF32
|
// Convert samples to CF32
|
||||||
volk_16i_s32f_convert_32f((float*)_this->stream.writeBuf, buf, 32768.0f, blockSize * 2);
|
volk_16i_s32f_convert_32f((float*)_this->stream.writeBuf, buf, 32768.0f, blockSize * 2);
|
||||||
|
@ -343,26 +489,42 @@ private:
|
||||||
iio_channel* rxChan = NULL;
|
iio_channel* rxChan = NULL;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
|
||||||
double freq;
|
std::string devDesc = "";
|
||||||
char ip[1024] = "ip:192.168.2.1";
|
std::string uri = "";
|
||||||
float sampleRate = 4000000;
|
|
||||||
int bandwidth = 0;
|
|
||||||
int gainMode = 0;
|
|
||||||
float gain = 0;
|
|
||||||
|
|
||||||
|
double freq;
|
||||||
|
int samplerate = 4000000;
|
||||||
|
int bandwidth = 0;
|
||||||
|
float gain = -1;
|
||||||
|
|
||||||
|
int devId = 0;
|
||||||
int srId = 0;
|
int srId = 0;
|
||||||
int bwId = 0;
|
int bwId = 0;
|
||||||
|
int gmId = 0;
|
||||||
|
|
||||||
|
OptionList<std::string, std::string> devices;
|
||||||
OptionList<int, double> samplerates;
|
OptionList<int, double> samplerates;
|
||||||
OptionList<int, double> bandwidths;
|
OptionList<int, double> bandwidths;
|
||||||
OptionList<int, std::string> gainModes;
|
OptionList<std::string, std::string> gainModes;
|
||||||
};
|
};
|
||||||
|
|
||||||
MOD_EXPORT void _INIT_() {
|
MOD_EXPORT void _INIT_() {
|
||||||
json defConf = {};
|
json defConf = {};
|
||||||
|
defConf["device"] = "";
|
||||||
|
defConf["devices"] = {};
|
||||||
config.setPath(core::args["root"].s() + "/plutosdr_source_config.json");
|
config.setPath(core::args["root"].s() + "/plutosdr_source_config.json");
|
||||||
config.load(defConf);
|
config.load(defConf);
|
||||||
config.enableAutoSave();
|
config.enableAutoSave();
|
||||||
|
|
||||||
|
// Reset the configuration if the old format is still used
|
||||||
|
config.acquire();
|
||||||
|
if (!config.conf.contains("device") || !config.conf.contains("devices")) {
|
||||||
|
config.conf = defConf;
|
||||||
|
config.release(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
config.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
|
MOD_EXPORT ModuleManager::Instance* _CREATE_INSTANCE_(std::string name) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ SDRPP_MOD_INFO{
|
||||||
/* Name: */ "sdrpp_server_source",
|
/* Name: */ "sdrpp_server_source",
|
||||||
/* Description: */ "SDR++ Server source module for SDR++",
|
/* Description: */ "SDR++ Server source module for SDR++",
|
||||||
/* Author: */ "Ryzerth",
|
/* Author: */ "Ryzerth",
|
||||||
/* Version: */ 0, 1, 0,
|
/* Version: */ 0, 2, 0,
|
||||||
/* Max instances */ 1
|
/* Max instances */ 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,10 +109,10 @@ private:
|
||||||
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
||||||
if (_this->running) { return; }
|
if (_this->running) { return; }
|
||||||
|
|
||||||
// Try to connect if not already connected
|
// Try to connect if not already connected (Play button is locked anyway so not sure why I put this here)
|
||||||
if (!_this->client) {
|
if (!_this->connected()) {
|
||||||
_this->tryConnect();
|
_this->tryConnect();
|
||||||
if (!_this->client) { return; }
|
if (!_this->connected()) { return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set configuration
|
// Set configuration
|
||||||
|
@ -127,7 +127,7 @@ private:
|
||||||
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
||||||
if (!_this->running) { return; }
|
if (!_this->running) { return; }
|
||||||
|
|
||||||
if (_this->client) { _this->client->stop(); }
|
if (_this->connected()) { _this->client->stop(); }
|
||||||
|
|
||||||
_this->running = false;
|
_this->running = false;
|
||||||
flog::info("SDRPPServerSourceModule '{0}': Stop!", _this->name);
|
flog::info("SDRPPServerSourceModule '{0}': Stop!", _this->name);
|
||||||
|
@ -135,7 +135,7 @@ private:
|
||||||
|
|
||||||
static void tune(double freq, void* ctx) {
|
static void tune(double freq, void* ctx) {
|
||||||
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
||||||
if (_this->running && _this->client) {
|
if (_this->running && _this->connected()) {
|
||||||
_this->client->setFrequency(freq);
|
_this->client->setFrequency(freq);
|
||||||
}
|
}
|
||||||
_this->freq = freq;
|
_this->freq = freq;
|
||||||
|
@ -146,7 +146,7 @@ private:
|
||||||
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
SDRPPServerSourceModule* _this = (SDRPPServerSourceModule*)ctx;
|
||||||
float menuWidth = ImGui::GetContentRegionAvail().x;
|
float menuWidth = ImGui::GetContentRegionAvail().x;
|
||||||
|
|
||||||
bool connected = (_this->client && _this->client->isOpen());
|
bool connected = _this->connected();
|
||||||
gui::mainWindow.playButtonLocked = !connected;
|
gui::mainWindow.playButtonLocked = !connected;
|
||||||
|
|
||||||
ImGui::GenericDialog("##sdrpp_srv_src_err_dialog", _this->serverBusy, GENERIC_DIALOG_BUTTONS_OK, [=](){
|
ImGui::GenericDialog("##sdrpp_srv_src_err_dialog", _this->serverBusy, GENERIC_DIALOG_BUTTONS_OK, [=](){
|
||||||
|
@ -227,6 +227,10 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool connected() {
|
||||||
|
return client && client->isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
void tryConnect() {
|
void tryConnect() {
|
||||||
try {
|
try {
|
||||||
if (client) { client.reset(); }
|
if (client) { client.reset(); }
|
||||||
|
@ -281,7 +285,7 @@ private:
|
||||||
int sampleTypeId;
|
int sampleTypeId;
|
||||||
bool compression = false;
|
bool compression = false;
|
||||||
|
|
||||||
server::Client client;
|
std::shared_ptr<server::Client> client;
|
||||||
};
|
};
|
||||||
|
|
||||||
MOD_EXPORT void _INIT_() {
|
MOD_EXPORT void _INIT_() {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
namespace server {
|
namespace server {
|
||||||
ClientClass::ClientClass(net::Conn conn, dsp::stream<dsp::complex_t>* out) {
|
Client::Client(std::shared_ptr<net::Socket> sock, dsp::stream<dsp::complex_t>* out) {
|
||||||
client = std::move(conn);
|
this->sock = sock;
|
||||||
output = out;
|
output = out;
|
||||||
|
|
||||||
// Allocate buffers
|
// Allocate buffers
|
||||||
|
@ -37,8 +37,8 @@ namespace server {
|
||||||
decomp.start();
|
decomp.start();
|
||||||
link.start();
|
link.start();
|
||||||
|
|
||||||
// Start readers
|
// Start worker thread
|
||||||
client->readAsync(sizeof(PacketHeader), rbuffer, tcpHandler, this);
|
workerThread = std::thread(&Client::worker, this);
|
||||||
|
|
||||||
// Ask for a UI
|
// Ask for a UI
|
||||||
int res = getUI();
|
int res = getUI();
|
||||||
|
@ -46,14 +46,14 @@ namespace server {
|
||||||
else if (res == -2) { throw std::runtime_error("Server busy"); }
|
else if (res == -2) { throw std::runtime_error("Server busy"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientClass::~ClientClass() {
|
Client::~Client() {
|
||||||
close();
|
close();
|
||||||
ZSTD_freeDCtx(dctx);
|
ZSTD_freeDCtx(dctx);
|
||||||
delete[] rbuffer;
|
delete[] rbuffer;
|
||||||
delete[] sbuffer;
|
delete[] sbuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::showMenu() {
|
void Client::showMenu() {
|
||||||
std::string diffId = "";
|
std::string diffId = "";
|
||||||
SmGui::DrawListElem diffValue;
|
SmGui::DrawListElem diffValue;
|
||||||
bool syncRequired = false;
|
bool syncRequired = false;
|
||||||
|
@ -96,8 +96,8 @@ namespace server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::setFrequency(double freq) {
|
void Client::setFrequency(double freq) {
|
||||||
if (!client || !client->isOpen()) { return; }
|
if (!isOpen()) { return; }
|
||||||
*(double*)s_cmd_data = freq;
|
*(double*)s_cmd_data = freq;
|
||||||
sendCommand(COMMAND_SET_FREQUENCY, sizeof(double));
|
sendCommand(COMMAND_SET_FREQUENCY, sizeof(double));
|
||||||
auto waiter = awaitCommandAck(COMMAND_SET_FREQUENCY);
|
auto waiter = awaitCommandAck(COMMAND_SET_FREQUENCY);
|
||||||
|
@ -105,119 +105,126 @@ namespace server {
|
||||||
waiter->handled();
|
waiter->handled();
|
||||||
}
|
}
|
||||||
|
|
||||||
double ClientClass::getSampleRate() {
|
double Client::getSampleRate() {
|
||||||
return currentSampleRate;
|
return currentSampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::setSampleType(dsp::compression::PCMType type) {
|
void Client::setSampleType(dsp::compression::PCMType type) {
|
||||||
|
if (!isOpen()) { return; }
|
||||||
s_cmd_data[0] = type;
|
s_cmd_data[0] = type;
|
||||||
sendCommand(COMMAND_SET_SAMPLE_TYPE, 1);
|
sendCommand(COMMAND_SET_SAMPLE_TYPE, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::setCompression(bool enabled) {
|
void Client::setCompression(bool enabled) {
|
||||||
|
if (!isOpen()) { return; }
|
||||||
s_cmd_data[0] = enabled;
|
s_cmd_data[0] = enabled;
|
||||||
sendCommand(COMMAND_SET_COMPRESSION, 1);
|
sendCommand(COMMAND_SET_COMPRESSION, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::start() {
|
void Client::start() {
|
||||||
if (!client || !client->isOpen()) { return; }
|
if (!isOpen()) { return; }
|
||||||
sendCommand(COMMAND_START, 0);
|
sendCommand(COMMAND_START, 0);
|
||||||
getUI();
|
getUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::stop() {
|
void Client::stop() {
|
||||||
if (!client || !client->isOpen()) { return; }
|
if (!isOpen()) { return; }
|
||||||
sendCommand(COMMAND_STOP, 0);
|
sendCommand(COMMAND_STOP, 0);
|
||||||
getUI();
|
getUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::close() {
|
void Client::close() {
|
||||||
|
// Stop worker
|
||||||
|
decompIn.stopWriter();
|
||||||
|
if (sock) { sock->close(); }
|
||||||
|
if (workerThread.joinable()) { workerThread.join(); }
|
||||||
|
decompIn.clearWriteStop();
|
||||||
|
|
||||||
|
// Stop DSP
|
||||||
decomp.stop();
|
decomp.stop();
|
||||||
link.stop();
|
link.stop();
|
||||||
decompIn.stopWriter();
|
|
||||||
client->close();
|
|
||||||
decompIn.clearWriteStop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ClientClass::isOpen() {
|
bool Client::isOpen() {
|
||||||
return client->isOpen();
|
return sock && sock->isOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::tcpHandler(int count, uint8_t* buf, void* ctx) {
|
void Client::worker() {
|
||||||
ClientClass* _this = (ClientClass*)ctx;
|
while (true) {
|
||||||
|
// Receive header
|
||||||
// Read the rest of the data (TODO: CHECK SIZE OR SHIT WILL BE FUCKED)
|
if (sock->recv(rbuffer, sizeof(PacketHeader), true) <= 0) {
|
||||||
int len = 0;
|
break;
|
||||||
int read = 0;
|
|
||||||
int goal = _this->r_pkt_hdr->size - sizeof(PacketHeader);
|
|
||||||
while (len < goal) {
|
|
||||||
read = _this->client->read(goal - len, &buf[sizeof(PacketHeader) + len]);
|
|
||||||
if (read < 0) {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
len += read;
|
|
||||||
}
|
|
||||||
_this->bytes += _this->r_pkt_hdr->size;
|
|
||||||
|
|
||||||
if (_this->r_pkt_hdr->type == PACKET_TYPE_COMMAND) {
|
|
||||||
// TODO: Move to command handler
|
|
||||||
if (_this->r_cmd_hdr->cmd == COMMAND_SET_SAMPLERATE && _this->r_pkt_hdr->size == sizeof(PacketHeader) + sizeof(CommandHeader) + sizeof(double)) {
|
|
||||||
_this->currentSampleRate = *(double*)_this->r_cmd_data;
|
|
||||||
core::setInputSampleRate(_this->currentSampleRate);
|
|
||||||
}
|
}
|
||||||
else if (_this->r_cmd_hdr->cmd == COMMAND_DISCONNECT) {
|
|
||||||
flog::error("Asked to disconnect by the server");
|
|
||||||
_this->serverBusy = true;
|
|
||||||
|
|
||||||
// Cancel waiters
|
// Receive remaining data
|
||||||
|
if (sock->recv(&rbuffer[sizeof(PacketHeader)], r_pkt_hdr->size - sizeof(PacketHeader), true, PROTOCOL_TIMEOUT_MS) <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment data counter
|
||||||
|
bytes += r_pkt_hdr->size;
|
||||||
|
|
||||||
|
// Decode packet
|
||||||
|
if (r_pkt_hdr->type == PACKET_TYPE_COMMAND) {
|
||||||
|
// TODO: Move to command handler
|
||||||
|
if (r_cmd_hdr->cmd == COMMAND_SET_SAMPLERATE && r_pkt_hdr->size == sizeof(PacketHeader) + sizeof(CommandHeader) + sizeof(double)) {
|
||||||
|
currentSampleRate = *(double*)r_cmd_data;
|
||||||
|
core::setInputSampleRate(currentSampleRate);
|
||||||
|
}
|
||||||
|
else if (r_cmd_hdr->cmd == COMMAND_DISCONNECT) {
|
||||||
|
flog::error("Asked to disconnect by the server");
|
||||||
|
serverBusy = true;
|
||||||
|
|
||||||
|
// Cancel waiters
|
||||||
|
std::vector<PacketWaiter*> toBeRemoved;
|
||||||
|
for (auto& [waiter, cmd] : commandAckWaiters) {
|
||||||
|
waiter->cancel();
|
||||||
|
toBeRemoved.push_back(waiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove handled waiters
|
||||||
|
for (auto& waiter : toBeRemoved) {
|
||||||
|
commandAckWaiters.erase(waiter);
|
||||||
|
delete waiter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (r_pkt_hdr->type == PACKET_TYPE_COMMAND_ACK) {
|
||||||
|
// Notify waiters
|
||||||
std::vector<PacketWaiter*> toBeRemoved;
|
std::vector<PacketWaiter*> toBeRemoved;
|
||||||
for (auto& [waiter, cmd] : _this->commandAckWaiters) {
|
for (auto& [waiter, cmd] : commandAckWaiters) {
|
||||||
waiter->cancel();
|
if (cmd != r_cmd_hdr->cmd) { continue; }
|
||||||
|
waiter->notify();
|
||||||
toBeRemoved.push_back(waiter);
|
toBeRemoved.push_back(waiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove handled waiters
|
// Remove handled waiters
|
||||||
for (auto& waiter : toBeRemoved) {
|
for (auto& waiter : toBeRemoved) {
|
||||||
_this->commandAckWaiters.erase(waiter);
|
commandAckWaiters.erase(waiter);
|
||||||
delete waiter;
|
delete waiter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else if (r_pkt_hdr->type == PACKET_TYPE_BASEBAND) {
|
||||||
else if (_this->r_pkt_hdr->type == PACKET_TYPE_COMMAND_ACK) {
|
memcpy(decompIn.writeBuf, &rbuffer[sizeof(PacketHeader)], r_pkt_hdr->size - sizeof(PacketHeader));
|
||||||
// Notify waiters
|
if (!decompIn.swap(r_pkt_hdr->size - sizeof(PacketHeader))) { break; }
|
||||||
std::vector<PacketWaiter*> toBeRemoved;
|
|
||||||
for (auto& [waiter, cmd] : _this->commandAckWaiters) {
|
|
||||||
if (cmd != _this->r_cmd_hdr->cmd) { continue; }
|
|
||||||
waiter->notify();
|
|
||||||
toBeRemoved.push_back(waiter);
|
|
||||||
}
|
}
|
||||||
|
else if (r_pkt_hdr->type == PACKET_TYPE_BASEBAND_COMPRESSED) {
|
||||||
// Remove handled waiters
|
size_t outCount = ZSTD_decompressDCtx(dctx, decompIn.writeBuf, STREAM_BUFFER_SIZE, r_pkt_data, r_pkt_hdr->size - sizeof(PacketHeader));
|
||||||
for (auto& waiter : toBeRemoved) {
|
if (outCount) {
|
||||||
_this->commandAckWaiters.erase(waiter);
|
if (!decompIn.swap(outCount)) { break; }
|
||||||
delete waiter;
|
};
|
||||||
|
}
|
||||||
|
else if (r_pkt_hdr->type == PACKET_TYPE_ERROR) {
|
||||||
|
flog::error("SDR++ Server Error: {0}", rbuffer[sizeof(PacketHeader)]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
flog::error("Invalid packet type: {0}", r_pkt_hdr->type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (_this->r_pkt_hdr->type == PACKET_TYPE_BASEBAND) {
|
|
||||||
memcpy(_this->decompIn.writeBuf, &buf[sizeof(PacketHeader)], _this->r_pkt_hdr->size - sizeof(PacketHeader));
|
|
||||||
_this->decompIn.swap(_this->r_pkt_hdr->size - sizeof(PacketHeader));
|
|
||||||
}
|
|
||||||
else if (_this->r_pkt_hdr->type == PACKET_TYPE_BASEBAND_COMPRESSED) {
|
|
||||||
size_t outCount = ZSTD_decompressDCtx(_this->dctx, _this->decompIn.writeBuf, STREAM_BUFFER_SIZE, _this->r_pkt_data, _this->r_pkt_hdr->size - sizeof(PacketHeader));
|
|
||||||
if (outCount) { _this->decompIn.swap(outCount); };
|
|
||||||
}
|
|
||||||
else if (_this->r_pkt_hdr->type == PACKET_TYPE_ERROR) {
|
|
||||||
flog::error("SDR++ Server Error: {0}", buf[sizeof(PacketHeader)]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
flog::error("Invalid packet type: {0}", _this->r_pkt_hdr->type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart an async read
|
|
||||||
_this->client->readAsync(sizeof(PacketHeader), _this->rbuffer, tcpHandler, _this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int ClientClass::getUI() {
|
int Client::getUI() {
|
||||||
|
if (!isOpen()) { return -1; }
|
||||||
auto waiter = awaitCommandAck(COMMAND_GET_UI);
|
auto waiter = awaitCommandAck(COMMAND_GET_UI);
|
||||||
sendCommand(COMMAND_GET_UI, 0);
|
sendCommand(COMMAND_GET_UI, 0);
|
||||||
if (waiter->await(PROTOCOL_TIMEOUT_MS)) {
|
if (waiter->await(PROTOCOL_TIMEOUT_MS)) {
|
||||||
|
@ -233,37 +240,35 @@ namespace server {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::sendPacket(PacketType type, int len) {
|
void Client::sendPacket(PacketType type, int len) {
|
||||||
s_pkt_hdr->type = type;
|
s_pkt_hdr->type = type;
|
||||||
s_pkt_hdr->size = sizeof(PacketHeader) + len;
|
s_pkt_hdr->size = sizeof(PacketHeader) + len;
|
||||||
client->write(s_pkt_hdr->size, sbuffer);
|
sock->send(sbuffer, s_pkt_hdr->size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::sendCommand(Command cmd, int len) {
|
void Client::sendCommand(Command cmd, int len) {
|
||||||
s_cmd_hdr->cmd = cmd;
|
s_cmd_hdr->cmd = cmd;
|
||||||
sendPacket(PACKET_TYPE_COMMAND, sizeof(CommandHeader) + len);
|
sendPacket(PACKET_TYPE_COMMAND, sizeof(CommandHeader) + len);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::sendCommandAck(Command cmd, int len) {
|
void Client::sendCommandAck(Command cmd, int len) {
|
||||||
s_cmd_hdr->cmd = cmd;
|
s_cmd_hdr->cmd = cmd;
|
||||||
sendPacket(PACKET_TYPE_COMMAND_ACK, sizeof(CommandHeader) + len);
|
sendPacket(PACKET_TYPE_COMMAND_ACK, sizeof(CommandHeader) + len);
|
||||||
}
|
}
|
||||||
|
|
||||||
PacketWaiter* ClientClass::awaitCommandAck(Command cmd) {
|
PacketWaiter* Client::awaitCommandAck(Command cmd) {
|
||||||
PacketWaiter* waiter = new PacketWaiter;
|
PacketWaiter* waiter = new PacketWaiter;
|
||||||
commandAckWaiters[waiter] = cmd;
|
commandAckWaiters[waiter] = cmd;
|
||||||
return waiter;
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientClass::dHandler(dsp::complex_t *data, int count, void *ctx) {
|
void Client::dHandler(dsp::complex_t *data, int count, void *ctx) {
|
||||||
ClientClass* _this = (ClientClass*)ctx;
|
Client* _this = (Client*)ctx;
|
||||||
memcpy(_this->output->writeBuf, data, count * sizeof(dsp::complex_t));
|
memcpy(_this->output->writeBuf, data, count * sizeof(dsp::complex_t));
|
||||||
_this->output->swap(count);
|
_this->output->swap(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
Client connect(std::string host, uint16_t port, dsp::stream<dsp::complex_t>* out) {
|
std::shared_ptr<Client> connect(std::string host, uint16_t port, dsp::stream<dsp::complex_t>* out) {
|
||||||
net::Conn conn = net::connect(host, port);
|
return std::make_shared<Client>(net::connect(host, port), out);
|
||||||
if (!conn) { return NULL; }
|
|
||||||
return Client(new ClientClass(std::move(conn), out));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <utils/networking.h>
|
#include <utils/net.h>
|
||||||
#include <dsp/stream.h>
|
#include <dsp/stream.h>
|
||||||
#include <dsp/types.h>
|
#include <dsp/types.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
@ -13,10 +13,6 @@
|
||||||
#include <dsp/routing/stream_link.h>
|
#include <dsp/routing/stream_link.h>
|
||||||
#include <zstd.h>
|
#include <zstd.h>
|
||||||
|
|
||||||
#define RFSPACE_MAX_SIZE 8192
|
|
||||||
#define RFSPACE_HEARTBEAT_INTERVAL_MS 1000
|
|
||||||
#define RFSPACE_TIMEOUT_MS 3000
|
|
||||||
|
|
||||||
#define PROTOCOL_TIMEOUT_MS 10000
|
#define PROTOCOL_TIMEOUT_MS 10000
|
||||||
|
|
||||||
namespace server {
|
namespace server {
|
||||||
|
@ -75,10 +71,10 @@ namespace server {
|
||||||
std::mutex handledMtx;
|
std::mutex handledMtx;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClientClass {
|
class Client {
|
||||||
public:
|
public:
|
||||||
ClientClass(net::Conn conn, dsp::stream<dsp::complex_t>* out);
|
Client(std::shared_ptr<net::Socket> sock, dsp::stream<dsp::complex_t>* out);
|
||||||
~ClientClass();
|
~Client();
|
||||||
|
|
||||||
void showMenu();
|
void showMenu();
|
||||||
|
|
||||||
|
@ -98,7 +94,7 @@ namespace server {
|
||||||
bool serverBusy = false;
|
bool serverBusy = false;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void tcpHandler(int count, uint8_t* buf, void* ctx);
|
void worker();
|
||||||
|
|
||||||
int getUI();
|
int getUI();
|
||||||
|
|
||||||
|
@ -112,7 +108,7 @@ namespace server {
|
||||||
|
|
||||||
static void dHandler(dsp::complex_t *data, int count, void *ctx);
|
static void dHandler(dsp::complex_t *data, int count, void *ctx);
|
||||||
|
|
||||||
net::Conn client;
|
std::shared_ptr<net::Socket> sock;
|
||||||
|
|
||||||
dsp::stream<uint8_t> decompIn;
|
dsp::stream<uint8_t> decompIn;
|
||||||
dsp::compression::SampleStreamDecompressor decomp;
|
dsp::compression::SampleStreamDecompressor decomp;
|
||||||
|
@ -137,10 +133,10 @@ namespace server {
|
||||||
|
|
||||||
ZSTD_DCtx* dctx;
|
ZSTD_DCtx* dctx;
|
||||||
|
|
||||||
|
std::thread workerThread;
|
||||||
|
|
||||||
double currentSampleRate = 1000000.0;
|
double currentSampleRate = 1000000.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::unique_ptr<ClientClass> Client;
|
std::shared_ptr<Client> connect(std::string host, uint16_t port, dsp::stream<dsp::complex_t>* out);
|
||||||
|
|
||||||
Client connect(std::string host, uint16_t port, dsp::stream<dsp::complex_t>* out);
|
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue