diff --git a/CMakeLists.txt b/CMakeLists.txt index 38a3c83..b58b567 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(Boost REQUIRED) find_package(Volk REQUIRED) find_package(gnuradio-osmosdr REQUIRED) find_package(cxxopts REQUIRED) +find_package(prometheus-cpp CONFIG REQUIRED) include_directories(${GNURADIO_ALL_INCLUDE_DIRS}) @@ -32,12 +33,27 @@ target_include_directories(lib-tetra-receiver PUBLIC include) # Build the tool # add_executable(tetra-receiver + src/prometheus.cpp + src/prometheus_gauge_populator.cpp src/tetra-receiver.cpp ) target_compile_options(tetra-receiver PUBLIC -std=c++17 -Wall) -target_link_libraries(tetra-receiver lib-tetra-receiver log4cpp volk gnuradio-osmosdr gnuradio-digital gnuradio-analog gnuradio-filter gnuradio-blocks gnuradio-fft gnuradio-runtime gnuradio-pmt) +target_link_libraries(tetra-receiver + lib-tetra-receiver + log4cpp + volk + gnuradio-osmosdr + gnuradio-digital + gnuradio-analog + gnuradio-filter + gnuradio-blocks + gnuradio-fft + gnuradio-runtime + gnuradio-pmt + prometheus-cpp::pull +) # # Testing diff --git a/README.md b/README.md index 7933aa7..26bd5a5 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Therefore this application supports multiple stages of decimation. The config has mandatory global arguments `CenterFrequency`, `DeviceString` and `SampleRate` for the SDR. The optional argumens `RFGain`, `IFGain` and `BBGain` are for setting the gains of the SDR, by default these are zero. +Specify an optional table with the name `Prometheus` and the values `Host` and `Port` to send metrics about the currently received signal strength to a prometheus server. + If a table specifies `Frequency`, `Host` and `Port`, the signal is directly decoded from the SDR. If it is specified in a subtable, it is decoded from the decimated signal described by the associtated table. @@ -53,6 +55,10 @@ RFGain = unsigned int (default 0) IFGain = unsigned int (default 0) BBGain = unsigned int (default 0) +[Prometheus] +Host = "string" (default 127.0.0.1) +Port = unsigned int (default 9010) + [DecimateA] Frequency = unsigned int SampleRate = unsigned int @@ -72,3 +78,8 @@ Frequency = unsigned int Host = "string" Port = unsigned int ``` + +## Prometheus +The power of each stream can be exported when setting the `Prometheus` config table. + +The magnitude of the stream is filtered to match the frequency of the polling interval. diff --git a/flake.lock b/flake.lock index e71f330..ce65df2 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1713995372, - "narHash": "sha256-fFE3M0vCoiSwCX02z8VF58jXFRj9enYUSTqjyHAjrds=", + "lastModified": 1715948915, + "narHash": "sha256-dxMrggEogQuJQr6f02VAFtsSNtjEPkgxczeiyW7WOQc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dd37924974b9202f8226ed5d74a252a9785aedf8", + "rev": "bacb8503d3a51d9e9b52e52a1ba45e2c380ad07d", "type": "github" }, "original": { @@ -22,13 +22,31 @@ "utils": "utils" } }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { diff --git a/include/config.h b/include/config.h index 28bde1f..d6a5a57 100644 --- a/include/config.h +++ b/include/config.h @@ -1,18 +1,17 @@ #ifndef CONFIG_H #define CONFIG_H +#include #include +#include +#include #include #include #include +#include #include -#include #include -#include - -#ifndef INCLUDE_CONFIG_H -#define INCLUDE_CONFIG_H namespace config { @@ -21,7 +20,11 @@ namespace config { /// The default host where to send the TETRA data. Gnuradio breaks if this is a host and not an IP [[maybe_unused]] const std::string kDefaultHost = "127.0.0.1"; -[[maybe_unused]] constexpr unsigned int kDefaultPort = 42000; +[[maybe_unused]] constexpr uint16_t kDefaultPort = 42000; + +// The default host to which we send the signal strength data for prometheus +const std::string kDefaultPrometheusHost = "127.0.0.1"; +constexpr uint16_t kDefaultPrometheusPort = 9010; template class Range { private: @@ -29,6 +32,8 @@ template class Range { T max_ = 0; public: + Range() = delete; + /// A class takes two values of type T and stores the minimum value in min_, /// the maximum in max_ respectively. Range(const T lhs, const T rhs) noexcept { @@ -59,6 +64,8 @@ template class SpectrumSlice { /// the sampling frequency for this slice of EM spectrum T sample_rate_; + SpectrumSlice() = delete; + /// Get different properties for a slice of EM spectrum /// \param frequency the center frequency of the slice /// \param sample_rate the sample rate of this spectrum @@ -67,20 +74,17 @@ template class SpectrumSlice { , frequency_range_(center_frequency - sample_rate / 2, center_frequency + sample_rate / 2) , sample_rate_(sample_rate){}; - friend auto operator==(const SpectrumSlice&, const SpectrumSlice&) -> bool; - friend auto operator!=(const SpectrumSlice&, const SpectrumSlice&) -> bool; -}; - -template auto operator==(const SpectrumSlice& lhs, const SpectrumSlice& rhs) -> bool { - return lhs.center_frequency_ == rhs.center_frequency_ && lhs.sample_rate_ == rhs.sample_rate_; -}; + friend auto operator==(const SpectrumSlice& lhs, const SpectrumSlice& rhs) -> bool { + return lhs.center_frequency_ == rhs.center_frequency_ && lhs.sample_rate_ == rhs.sample_rate_; + }; -template auto operator!=(const SpectrumSlice& lhs, const SpectrumSlice& rhs) -> bool { - return !operator==(lhs, rhs); + friend auto operator!=(const SpectrumSlice& lhs, const SpectrumSlice& rhs) -> bool { return !(lhs == rhs); }; }; class Stream { public: + /// the name of the table in the config + const std::string name_; /// the slice of spectrum_ that is input to this block const SpectrumSlice input_spectrum_; /// the slice of spectrum_ of the TETRA Stream @@ -94,20 +98,25 @@ class Stream { /// Optional field /// The port to which the samples of the Stream should be sent. This defaults /// to 42000. - const unsigned int port_ = 0; + const uint16_t port_ = 0; + + Stream() = delete; /// Describe the on which frequency a TETRA Stream should be extracted and /// where data should be sent to. + /// \param name the name of the table in the config /// \param input_spectrum the slice of spectrum that is input to this block /// \param spectrum the slice of spectrum of the TETRA Stream /// \param host the to send the data to /// \param port the port to send the data to - Stream(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum, - std::string host, unsigned int port); + Stream(const std::string& name, const SpectrumSlice& input_spectrum, + const SpectrumSlice& spectrum, std::string host, uint16_t port); }; class Decimate { public: + /// the name of the table in the config + const std::string name_; /// the slice of spectrum that is input to this block const SpectrumSlice input_spectrum_; /// the slice of spectrum after decimation_ @@ -120,13 +129,34 @@ class Decimate { /// connected to. std::vector streams_; + Decimate() = delete; + /// Describe the decimation of the SDR Stream by the frequency where we want /// to extract a signal with a width of sample_rate + /// \param name the name of the table in the config /// \param input_spectrum the slice of spectrum that is input to this block /// \param spectrum the slice of spectrum after decimation /// \param streams the vector of streams the decimated signal should be sent /// to - Decimate(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum); + Decimate(const std::string& name, const SpectrumSlice& input_spectrum, + const SpectrumSlice& spectrum); +}; + +class Prometheus { +public: + /// the host to which prometheus is sending data to + const std::string host_; + /// the port to which prometheus is sending data to + const uint16_t port_; + + Prometheus() = delete; + + /// The prometheus exporter to we want to send the metrics + /// \param host the host to which prometheus is sending data + /// \param port the port to which prometheus is sending data + Prometheus(std::string host, const uint16_t port) + : host_(std::move(host)) + , port_(port){}; }; class TopLevel { @@ -147,10 +177,14 @@ class TopLevel { /// The vector of decimators which should first Decimate a signal of the SDR /// and then sent it to the vector of streams inside them. const std::vector decimators_{}; + /// Optional config element for the prometheus exporter + const std::unique_ptr prometheus_; + + TopLevel() = delete; TopLevel(const SpectrumSlice& spectrum, std::string device_string, unsigned int rf_gain, unsigned int if_gain, unsigned int bb_gain, const std::vector& streams, - const std::vector& decimators); + const std::vector& decimators, std::unique_ptr&& prometheus); }; using decimate_or_stream = std::variant; @@ -160,7 +194,7 @@ using decimate_or_stream = std::variant; namespace toml { static config::decimate_or_stream get_decimate_or_stream(const config::SpectrumSlice& input_spectrum, - const value& v) { + const std::string& name, const value& v) { std::optional sample_rate; const unsigned int frequency = find(v, "Frequency"); @@ -168,18 +202,27 @@ static config::decimate_or_stream get_decimate_or_stream(const config::SpectrumS sample_rate = find(v, "SampleRate"); const std::string host = find_or(v, "Host", config::kDefaultHost); - const unsigned int port = find_or(v, "Port", config::kDefaultPort); + const uint16_t port = find_or(v, "Port", config::kDefaultPort); // If we have a sample rate specified this is a Decimate, otherwhise this is // a Stream. if (sample_rate.has_value()) { - return config::Decimate(input_spectrum, config::SpectrumSlice(frequency, *sample_rate)); + return config::Decimate(name, input_spectrum, config::SpectrumSlice(frequency, *sample_rate)); } else { - return config::Stream(input_spectrum, config::SpectrumSlice(frequency, config::kTetraSampleRate), - host, port); + return config::Stream(name, input_spectrum, + config::SpectrumSlice(frequency, config::kTetraSampleRate), host, port); } } +template <> struct from> { + static auto from_toml(const value& v) -> std::unique_ptr { + const std::string prometheus_host = find_or(v, "Host", config::kDefaultPrometheusHost); + const uint16_t prometheus_port = find_or(v, "Port", config::kDefaultPrometheusPort); + + return std::make_unique(prometheus_host, prometheus_port); + } +}; + template <> struct from { static auto from_toml(const value& v) -> config::TopLevel { const unsigned int center_frequency = find(v, "CenterFrequency"); @@ -193,16 +236,24 @@ template <> struct from { std::vector streams; std::vector decimators; + std::unique_ptr prometheus; // Iterate over all elements in the root table for (const auto& root_kv : v.as_table()) { + const auto& name = root_kv.first; const auto& table = root_kv.second; // Find table entries. These can be decimators or streams. if (!table.is_table()) continue; - const auto element = get_decimate_or_stream(sdr_spectrum, table); + // If the table is labled "Prometheus" extract the prometheus arguments + if (name == "Prometheus") { + prometheus = get>(table); + continue; + } + + const auto element = get_decimate_or_stream(sdr_spectrum, name, table); // Save the Stream if (std::holds_alternative(element)) { @@ -219,12 +270,13 @@ template <> struct from { // Find all subtables, that must be Stream entries and add them to the // decimator for (const auto& stream_pair : table.as_table()) { + auto& stream_name = stream_pair.first; auto& stream_table = stream_pair.second; if (!stream_table.is_table()) continue; - const auto stream_element = get_decimate_or_stream(decimate_element.spectrum_, stream_table); + const auto stream_element = get_decimate_or_stream(decimate_element.spectrum_, stream_name, stream_table); if (!std::holds_alternative(stream_element)) { throw std::invalid_argument("Did not find a Stream block under the Decimate block"); @@ -241,12 +293,11 @@ template <> struct from { throw std::invalid_argument("Did not handle a derived type of decimate_or_stream"); } - return config::TopLevel(sdr_spectrum, device_string, rf_gain, if_gain, bb_gain, streams, decimators); + return config::TopLevel(sdr_spectrum, device_string, rf_gain, if_gain, bb_gain, streams, decimators, + std::move(prometheus)); } }; }; // namespace toml #endif - -#endif diff --git a/include/prometheus.h b/include/prometheus.h new file mode 100644 index 0000000..4f180c9 --- /dev/null +++ b/include/prometheus.h @@ -0,0 +1,22 @@ +#ifndef PROMETHEUS_H +#define PROMETHEUS_H + +#include + +#include +#include +#include + +class PrometheusExporter { +private: + std::shared_ptr registry_; + std::unique_ptr exposer_; + +public: + PrometheusExporter(const std::string& host) noexcept; + ~PrometheusExporter() noexcept = default; + + auto signal_strength() noexcept -> prometheus::Family&; +}; + +#endif // PROMETHEUS_H diff --git a/include/prometheus_gauge_populator.h b/include/prometheus_gauge_populator.h new file mode 100644 index 0000000..d4e4f83 --- /dev/null +++ b/include/prometheus_gauge_populator.h @@ -0,0 +1,32 @@ +#ifndef PROMETHEUS_GAUGE_POPULATOR_H +#define PROMETHEUS_GAUGE_POPULATOR_H + +#include +#include + +#include + +namespace gr::prometheus { + +/// This block takes a float as an input and writes in into a prometheus gauge +class PrometheusGaugePopulator : virtual public block { +private: + /// the prometheus gauge we are populating with this block + ::prometheus::Gauge& gauge_; + +public: + using sptr = boost::shared_ptr; + + PrometheusGaugePopulator() = delete; + + explicit PrometheusGaugePopulator(::prometheus::Gauge& gauge); + + static auto make(::prometheus::Gauge& gauge) -> sptr; + + auto general_work(int noutput_items, gr_vector_int& ninput_items, gr_vector_const_void_star& input_items, + gr_vector_void_star& output_items) -> int override; +}; + +} // namespace gr::prometheus + +#endif // PROMETHEUS_GAUGE_POPULATOR_H diff --git a/pkgs/tetra-receiver.nix b/pkgs/tetra-receiver.nix index 90f3a4f..38c2323 100644 --- a/pkgs/tetra-receiver.nix +++ b/pkgs/tetra-receiver.nix @@ -1,5 +1,4 @@ -{ clangStdenv -, pkg-config +{ pkg-config , cmake , gnuradio , gnuradioPackages @@ -7,10 +6,13 @@ , mpir , gmpxx , cxxopts -, toml11 , fetchFromGitHub , stdenv , gtest +, prometheus-cpp +, zlib +, glibc +, curlFull }: let toml11 = stdenv.mkDerivation { @@ -41,14 +43,32 @@ let outputs = [ "out" ]; }); in -clangStdenv.mkDerivation { +stdenv.mkDerivation rec { name = "tetra-receiver"; version = "0.1.0"; src = ./..; nativeBuildInputs = [ cmake pkg-config ]; - buildInputs = [ log4cpp mpir gnuradio.unwrapped gnuradio.unwrapped.boost.dev gmpxx.dev gnuradio.unwrapped.volk osmosdr cxxopts toml11 gtest ]; + buildInputs = [ + log4cpp + mpir + gnuradio.unwrapped + gnuradio.unwrapped.boost.dev + gmpxx.dev + gnuradio.unwrapped.volk + osmosdr + cxxopts + toml11 + gtest + prometheus-cpp + zlib + curlFull + #glibc + ]; + preConfigure = '' + echo "-DCMAKE_PREFIX_PATH=${osmosdr}/lib/cmake/osmosdr" + ''; cmakeFlags = [ "-DCMAKE_PREFIX_PATH=${osmosdr}/lib/cmake/osmosdr" ]; diff --git a/src/config.cpp b/src/config.cpp index 0118c61..44c5414 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -2,9 +2,10 @@ namespace config { -Stream::Stream(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum, - std::string host, unsigned int port) - : input_spectrum_(input_spectrum) +Stream::Stream(const std::string& name, const SpectrumSlice& input_spectrum, + const SpectrumSlice& spectrum, std::string host, uint16_t port) + : name_(name) + , input_spectrum_(input_spectrum) , spectrum_(spectrum) , host_(std::move(host)) , port_(port) { @@ -23,8 +24,10 @@ Stream::Stream(const SpectrumSlice& input_spectrum, const Spectrum } } -Decimate::Decimate(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum) - : input_spectrum_(input_spectrum) +Decimate::Decimate(const std::string& name, const SpectrumSlice& input_spectrum, + const SpectrumSlice& spectrum) + : name_(name) + , input_spectrum_(input_spectrum) , spectrum_(spectrum) { // check that this Stream is valid if (!input_spectrum.frequency_range_.contains(spectrum.frequency_range_)) { @@ -49,21 +52,22 @@ Decimate::Decimate(const SpectrumSlice& input_spectrum, const Spec TopLevel::TopLevel(const SpectrumSlice& spectrum, std::string device_string, const unsigned int rf_gain, const unsigned int if_gain, const unsigned int bb_gain, const std::vector& streams, - const std::vector& decimators) + const std::vector& decimators, std::unique_ptr&& prometheus) : spectrum_(spectrum) , device_string_(std::move(device_string)) , rf_gain_(rf_gain) , if_gain_(if_gain) , bb_gain_(bb_gain) , streams_(streams) - , decimators_(decimators) { + , decimators_(decimators) + , prometheus_(std::move(prometheus)) { for (const auto& stream : streams) { - if (operator!=(stream.input_spectrum_, spectrum)) { + if (stream.input_spectrum_ != spectrum) { throw std::invalid_argument("The output of Decimate does not match to the input of Stream."); } } for (const auto& decimator : decimators) { - if (operator!=(decimator.input_spectrum_, spectrum)) { + if (decimator.input_spectrum_ != spectrum) { throw std::invalid_argument("The output of Decimate does not match to the input of Stream."); } } diff --git a/src/prometheus.cpp b/src/prometheus.cpp new file mode 100644 index 0000000..a2b5858 --- /dev/null +++ b/src/prometheus.cpp @@ -0,0 +1,12 @@ +#include "prometheus.h" + +PrometheusExporter::PrometheusExporter(const std::string& prometheus_host) noexcept { + exposer_ = std::make_unique(prometheus_host); + registry_ = std::make_shared(); + + exposer_->RegisterCollectable(registry_); +} + +auto PrometheusExporter::signal_strength() noexcept -> prometheus::Family& { + return prometheus::BuildGauge().Name("signal_strength").Help("Current Signal Strength").Register(*registry_); +} diff --git a/src/prometheus_gauge_populator.cpp b/src/prometheus_gauge_populator.cpp new file mode 100644 index 0000000..ecc1332 --- /dev/null +++ b/src/prometheus_gauge_populator.cpp @@ -0,0 +1,33 @@ +#include + +#include "prometheus_gauge_populator.h" + +namespace gr::prometheus { + +PrometheusGaugePopulator::sptr PrometheusGaugePopulator::make(::prometheus::Gauge& gauge) { + return gnuradio::get_initial_sptr(new PrometheusGaugePopulator(gauge)); +} + +PrometheusGaugePopulator::PrometheusGaugePopulator(::prometheus::Gauge& gauge) + : block( + /*name=*/"PrometheusGaugePopulator", + /*input_signature=*/ + io_signature::make(/*min_streams=*/1, /*max_streams=*/1, /*sizeof_stream_items=*/sizeof(float)), + /*output_signature=*/io_signature::make(/*min_streams=*/0, /*max_streams=*/0, /*sizeof_stream_items=*/0)) + , gauge_(gauge) {} + +auto PrometheusGaugePopulator::general_work(const int, gr_vector_int&, gr_vector_const_void_star& input_items, + gr_vector_void_star&) -> int { + const auto* in = (const float*)input_items[0]; + + // Consume one and write it to the gauge. + const auto item = *in; + consume_each(1); + + gauge_.Set(item); + + // We do not produce any items. + return 0; +} + +} // namespace gr::prometheus diff --git a/src/tetra-receiver.cpp b/src/tetra-receiver.cpp index bceeeae..97caa5a 100644 --- a/src/tetra-receiver.cpp +++ b/src/tetra-receiver.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +29,8 @@ #include #include "config.h" +#include "prometheus.h" +#include "prometheus_gauge_populator.h" static auto print_gnuradio_diagnostics() -> void { const auto ver = gr::version(); @@ -40,9 +44,19 @@ static auto print_gnuradio_diagnostics() -> void { << "\n\n Compiler Flags: " << compiler_flags << "\n\n"; } -class GnuradioTopBlock { +class ApplicationData { +public: + /// The gnuradio top block + gr::top_block_sptr tb = nullptr; + /// the optional prometheus exporter + std::shared_ptr exporter = nullptr; +}; + +class GnuradioBuilder { private: - static auto from_config(const config::Stream& stream, gr::top_block_sptr tb, gr::basic_block_sptr input) -> void { + static auto from_config(const config::Stream& stream, ApplicationData& app_data, gr::basic_block_sptr input) -> void { + auto& tb = app_data.tb; + auto decimation = stream.decimation_; auto offset = static_cast(stream.spectrum_.center_frequency_) - static_cast(stream.input_spectrum_.center_frequency_); @@ -87,9 +101,32 @@ class GnuradioTopBlock { tb->connect(digital_constellation_decoder_cb, 0, digital_map_bb, 0); tb->connect(digital_map_bb, 0, blocks_unpack_k_bits_bb, 0); tb->connect(blocks_unpack_k_bits_bb, 0, blocks_udp_sink, 0); + + // create blocks to save the power of the current channel if prometheus exporter is available + if (app_data.exporter) { + auto& signal_strength = app_data.exporter->signal_strength(); + auto& stream_signal_strength = signal_strength.Add( + {{"frequency", std::to_string(stream.spectrum_.center_frequency_)}, {"name", stream.name_}}); + + auto mag_squared = gr::blocks::complex_to_mag_squared::make(); + // averaging filter over one second + unsigned tap_size = stream.spectrum_.sample_rate_; + std::vector averaging_filter(/*count=*/tap_size, /*alloc=*/1.0 / tap_size); + // do not decimate directly to the final frequency, since there will be some jitter + unsigned decimation = tap_size / 10; + auto fir = gr::filter::fir_filter_fff::make(/*decimation=*/decimation, averaging_filter); + auto populator = gr::prometheus::PrometheusGaugePopulator::make(/*gauge=*/stream_signal_strength); + + tb->connect(xlat, 0, mag_squared, 0); + tb->connect(mag_squared, 0, fir, 0); + tb->connect(fir, 0, populator, 0); + } }; - static auto from_config(const config::Decimate& decimate, gr::top_block_sptr tb, gr::basic_block_sptr input) -> void { + static auto from_config(const config::Decimate& decimate, ApplicationData& app_data, gr::basic_block_sptr input) + -> void { + auto& tb = app_data.tb; + float half_sample_rate = decimate.spectrum_.sample_rate_ / 2; auto offset = static_cast(decimate.spectrum_.center_frequency_) - static_cast(decimate.input_spectrum_.center_frequency_); @@ -101,7 +138,7 @@ class GnuradioTopBlock { tb->connect(input, 0, xlat, 0); for (auto const& stream : decimate.streams_) { - from_config(stream, tb, xlat); + from_config(stream, app_data, xlat); } // add a null sink to have at least one connected @@ -110,8 +147,17 @@ class GnuradioTopBlock { }; public: - static auto from_config(const config::TopLevel& top) -> gr::top_block_sptr { - auto tb = gr::make_top_block("fg"); + static auto from_config(const config::TopLevel& top) -> ApplicationData { + ApplicationData app_data; + auto& tb = app_data.tb; + + tb = gr::make_top_block("fg"); + + // setup prometheus exporter + if (top.prometheus_) { + std::string prometheus_addr = top.prometheus_->host_ + ":" + std::to_string(top.prometheus_->port_); + app_data.exporter = std::make_shared(prometheus_addr); + } // setup osmosdr source auto src = osmosdr::source::make(top.device_string_); @@ -126,17 +172,17 @@ class GnuradioTopBlock { src->set_bandwidth(top.spectrum_.sample_rate_ / 2, 0); for (auto const& decimate : top.decimators_) { - from_config(decimate, tb, src); + from_config(decimate, app_data, src); } for (auto const& stream : top.streams_) { - from_config(stream, tb, src); + from_config(stream, app_data, src); } // add a null sink to have at least one connected auto null_sink = gr::blocks::null_sink::make(/*sizeof_stream_item=*/sizeof(gr_complex)); tb->connect(src, 0, null_sink, 0); - return tb; + return app_data; } }; @@ -155,7 +201,7 @@ auto main(int argc, char** argv) -> int { ("center-frequency", "Center frequency of the SDR", cxxopts::value()->default_value("0")) ("offsets", "offsets of the TETRA streams", cxxopts::value>()) ("samp-rate", "Sample rate of the sdr", cxxopts::value()->default_value("1000000")) - ("udp-start", "Start UDP port. Each stream gets its own UDP port, starting at udp-start", cxxopts::value()->default_value("42000")) + ("udp-start", "Start UDP port. Each stream gets its own UDP port, starting at udp-start", cxxopts::value()->default_value("42000")) ; // clang-format on @@ -166,14 +212,14 @@ auto main(int argc, char** argv) -> int { return EXIT_SUCCESS; } - gr::top_block_sptr tb = 0; + ApplicationData app_data; // Read from config file instead if (result.count("config-file")) { auto data = toml::parse(result["config-file"].as()); - auto top = toml::get(data); - tb = GnuradioTopBlock::from_config(top); + + app_data = GnuradioBuilder::from_config(top); } else { const auto sample_rate = result["samp-rate"].as(); const auto& device_string = result["device-string"].as(); @@ -193,21 +239,24 @@ auto main(int argc, char** argv) -> int { auto udp_port = udp_start + std::distance(offsets.begin(), offsets_it); const auto tetra_spectrum = config::SpectrumSlice(stream_frequency, config::kTetraSampleRate); + std::string name = "Stream " + std::to_string(stream_frequency); - streams.emplace_back(config::Stream(input_spectrum, tetra_spectrum, config::kDefaultHost, udp_port)); + streams.emplace_back(config::Stream(name, input_spectrum, tetra_spectrum, config::kDefaultHost, udp_port)); } - auto top = config::TopLevel(input_spectrum, device_string, rf_gain, if_gain, bb_gain, /*streams=*/streams, - /*decimators=*/{}); - tb = GnuradioTopBlock::from_config(top); + config::TopLevel top(input_spectrum, device_string, rf_gain, if_gain, bb_gain, + /*streams=*/streams, + /*decimators=*/{}, /*prometheus=*/nullptr); + + app_data = GnuradioBuilder::from_config(top); } // print the gnuradio debugging information print_gnuradio_diagnostics(); - tb->start(); + app_data.tb->start(); - tb->wait(); + app_data.tb->wait(); } catch (std::exception& e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; diff --git a/test/config_test.cpp b/test/config_test.cpp index f35250c..f55560a 100644 --- a/test/config_test.cpp +++ b/test/config_test.cpp @@ -94,6 +94,44 @@ TEST(config, TopLevel_decimate_under_decimate) { EXPECT_THROW(toml::get(config_object), std::invalid_argument); } +TEST(config, TopLevel_prometheus_default) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 60000 + + [Prometheus] + )"_toml; + + const config::TopLevel t = toml::get(config_object); + + // prometheus is set + EXPECT_TRUE(t.prometheus_); + + EXPECT_EQ(t.prometheus_->host_, config::kDefaultPrometheusHost); + EXPECT_EQ(t.prometheus_->port_, config::kDefaultPrometheusPort); +} + +TEST(config, TopLevel_prometheus_set) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 60000 + + [Prometheus] + Host = "127.0.0.2" + Port = 4200 + )"_toml; + + const config::TopLevel t = toml::get(config_object); + + // prometheus is set + EXPECT_TRUE(t.prometheus_); + + EXPECT_EQ(t.prometheus_->host_, "127.0.0.2"); + EXPECT_EQ(t.prometheus_->port_, 4200); +} + TEST(config, TopLevel_valid_parser) { const toml::value config_object = u8R"( CenterFrequency = 4000000 @@ -128,9 +166,13 @@ TEST(config, TopLevel_valid_parser) { EXPECT_EQ(t.if_gain_, 14); EXPECT_EQ(t.bb_gain_, 0); + // prometheus is not set + EXPECT_FALSE(t.prometheus_); + EXPECT_EQ(t.decimators_.size(), 1); const auto& decimate_a = t.decimators_[0]; + EXPECT_EQ(decimate_a.name_, "DecimateA"); EXPECT_EQ(decimate_a.spectrum_.center_frequency_, 4250000); EXPECT_EQ(decimate_a.spectrum_.sample_rate_, 500000); EXPECT_EQ(decimate_a.decimation_, 2); @@ -139,12 +181,14 @@ TEST(config, TopLevel_valid_parser) { const auto& stream_0 = decimate_a.streams_[1]; const auto& stream_1 = decimate_a.streams_[0]; + EXPECT_EQ(stream_0.name_, "Stream0"); EXPECT_EQ(stream_0.spectrum_.center_frequency_, 4250010); EXPECT_EQ(stream_0.spectrum_.sample_rate_, config::kTetraSampleRate); EXPECT_EQ(stream_0.host_, "127.0.0.1"); EXPECT_EQ(stream_0.port_, 4100); EXPECT_EQ(stream_0.decimation_, 20); + EXPECT_EQ(stream_1.name_, "Stream1"); EXPECT_EQ(stream_1.spectrum_.center_frequency_, 4250100); EXPECT_EQ(stream_1.spectrum_.sample_rate_, config::kTetraSampleRate); EXPECT_EQ(stream_1.host_, config::kDefaultHost); @@ -154,6 +198,7 @@ TEST(config, TopLevel_valid_parser) { EXPECT_EQ(t.streams_.size(), 1); const auto& stream_2 = t.streams_[0]; + EXPECT_EQ(stream_2.name_, "Stream2"); EXPECT_EQ(stream_2.spectrum_.center_frequency_, 4000100); EXPECT_EQ(stream_2.spectrum_.sample_rate_, config::kTetraSampleRate); EXPECT_EQ(stream_2.host_, "127.0.0.2"); diff --git a/test/main.cpp b/test/main.cpp index 697a9d7..11ceafd 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,6 +1,6 @@ #include -int main(int argc, char** argv) { +auto main(int argc, char** argv) -> int { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }