From d503ccca4d796b473d86c6f01dd549d36f9a573d Mon Sep 17 00:00:00 2001 From: tanneberger Date: Wed, 15 May 2024 21:37:39 +0200 Subject: [PATCH] config format to specify decoding chain Co-authored-by: Markus Schmidl --- .clang-format | 8 + .clang-tidy | 214 +++++++++++++++++ .github/workflows/clang-format.yml | 18 ++ .github/workflows/tetra-receiver-x86_64.yml | 1 + .gitignore | 2 + CMakeLists.txt | 29 ++- README.md | 47 ++++ flake.nix | 2 +- include/config.h | 252 ++++++++++++++++++++ nixos-modules/tetra-receiver.nix | 52 +--- pkgs/tetra-receiver.nix | 40 +++- src/config.cpp | 72 ++++++ src/main.cpp | 161 ------------- src/tetra-receiver.cpp | 217 +++++++++++++++++ test/CMakeLists.txt | 19 ++ test/config_test.cpp | 162 +++++++++++++ test/main.cpp | 6 + 17 files changed, 1089 insertions(+), 213 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .github/workflows/clang-format.yml create mode 100644 include/config.h create mode 100644 src/config.cpp delete mode 100644 src/main.cpp create mode 100644 src/tetra-receiver.cpp create mode 100644 test/CMakeLists.txt create mode 100644 test/config_test.cpp create mode 100644 test/main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ac94ae0 --- /dev/null +++ b/.clang-format @@ -0,0 +1,8 @@ +--- +BasedOnStyle: LLVM +Language: Cpp +IndentWidth: 2 +BreakConstructorInitializersBeforeComma: 'true' +AllowShortFunctionsOnASingleLine: All +PointerAlignment: Left +ColumnLimit: 120 \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..bc97812 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,214 @@ +Checks: ' + clang-diagnostic-*, + boost-*, + Google-*, + clang-analyzer-*, + modernize-*, + performance-*, + portability-*, + readability-*, + cppcoreguidelines-*, + llvm-*, + cert-*, + -clang-analyzer-core.CallAndMessage' +WarningsAsErrors: true +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: '0' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: cppcoreguidelines-avoid-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: cppcoreguidelines-explicit-virtual-functions.FinalSpelling + value: final + - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors + value: '1' + - key: cppcoreguidelines-explicit-virtual-functions.OverrideSpelling + value: override + - key: cppcoreguidelines-macro-usage.AllowedRegexp + value: '^DEBUG_*' + - key: cppcoreguidelines-macro-usage.CheckCapsOnly + value: '0' + - key: cppcoreguidelines-macro-usage.IgnoreCommandLineMacros + value: '1' + - key: cppcoreguidelines-no-malloc.Allocations + value: '::malloc;::calloc' + - key: cppcoreguidelines-no-malloc.Deallocations + value: '::free' + - key: cppcoreguidelines-no-malloc.Reallocations + value: '::realloc' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: '1' + - key: cppcoreguidelines-owning-memory.LegacyResourceConsumers + value: '::free;::realloc;::freopen;::fclose' + - key: cppcoreguidelines-owning-memory.LegacyResourceProducers + value: '::malloc;::aligned_alloc;::realloc;::calloc;::fopen;::freopen;::tmpfile' + - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader + value: '' + - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle + value: 'llvm' + - key: cppcoreguidelines-pro-type-member-init.IgnoreArrays + value: '0' + - key: cppcoreguidelines-pro-type-member-init.UseAssignment + value: '0' + - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions + value: '0' + - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor + value: '0' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: llvm-namespace-comment.ShortNamespaceLines + value: '1' + - key: llvm-namespace-comment.SpacesBeforeComments + value: '1' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-make-shared.IgnoreMacros + value: '1' + - key: modernize-make-shared.IncludeStyle + value: 'llvm' + - key: modernize-make-shared.MakeSmartPtrFunction + value: 'std::make_shared' + - key: modernize-make-shared.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-make-unique.IgnoreMacros + value: '1' + - key: modernize-make-unique.IncludeStyle + value: 'llvm' + - key: modernize-make-unique.MakeSmartPtrFunction + value: 'std::make_unique' + - key: modernize-make-unique.MakeSmartPtrFunctionHeader + value: memory + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-pass-by-value.ValuesOnly + value: '0' + - key: modernize-raw-string-literal.ReplaceShorterLiterals + value: '0' + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-replace-random-shuffle.IncludeStyle + value: llvm + - key: modernize-use-auto.MinTypeNameLength + value: '5' + - key: modernize-use-auto.RemoveStars + value: '0' + - key: modernize-use-default-member-init.IgnoreMacros + value: '1' + - key: modernize-use-default-member-init.UseAssignment + value: '0' + - key: modernize-use-emplace.ContainersWithPushBack + value: '::std::vector;::std::list;::std::deque' + - key: modernize-use-emplace.SmartPointers + value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' + - key: modernize-use-emplace.TupleMakeFunctions + value: '::std::make_pair;::std::make_tuple' + - key: modernize-use-emplace.TupleTypes + value: '::std::pair;::std::tuple' + - key: modernize-use-equals-default.IgnoreMacros + value: '1' + - key: modernize-use-equals-delete.IgnoreMacros + value: '1' + - key: modernize-use-nodiscard.ReplacementString + value: '[[nodiscard]]' + - key: modernize-use-noexcept.ReplacementString + value: '' + - key: modernize-use-noexcept.UseNoexceptFalse + value: '1' + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: modernize-use-override.FinalSpelling + value: final + - key: modernize-use-override.IgnoreDestructors + value: '0' + - key: modernize-use-override.OverrideSpelling + value: override + - key: modernize-use-transparent-functors.SafeMode + value: '0' + - key: modernize-use-using.IgnoreMacros + value: '1' + - key: performance-faster-string-find.StringLikeClasses + value: 'std::basic_string' + - key: performance-for-range-copy.AllowedTypes + value: '' + - key: performance-for-range-copy.WarnOnAllAutoCopies + value: '0' + - key: performance-inefficient-string-concatenation.StrictMode + value: '0' + - key: performance-inefficient-vector-operation.VectorLikeClasses + value: '::std::vector' + - key: performance-move-const-arg.CheckTriviallyCopyableMove + value: '1' + - key: performance-move-constructor-init.IncludeStyle + value: llvm + - key: performance-type-promotion-in-math-fn.IncludeStyle + value: llvm + - key: performance-unnecessary-copy-initialization.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.AllowedTypes + value: '' + - key: performance-unnecessary-value-param.IncludeStyle + value: llvm + - key: portability-simd-intrinsics.Std + value: '' + - key: portability-simd-intrinsics.Suggest + value: '0' + - key: readability-braces-around-statements.ShortStatementLines + value: '0' + - key: readability-function-size.BranchThreshold + value: '4294967295' + - key: readability-function-size.LineThreshold + value: '4294967295' + - key: readability-function-size.NestingThreshold + value: '4294967295' + - key: readability-function-size.ParameterThreshold + value: '4294967295' + - key: readability-function-size.StatementThreshold + value: '800' + - key: readability-function-size.VariableThreshold + value: '4294967295' + - key: readability-identifier-length.MinimumParameterNameLength + value: 2 + - key: readability-identifier-naming.IgnoreFailedSplit + value: '0' + - key: readability-implicit-bool-conversion.AllowIntegerConditions + value: '0' + - key: readability-implicit-bool-conversion.AllowPointerConditions + value: '0' + - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros + value: '1' + - key: readability-inconsistent-declaration-parameter-name.Strict + value: '0' + - key: readability-magic-numbers.IgnoredFloatingPointValues + value: '1.0;100.0;' + - key: readability-magic-numbers.IgnoredIntegerValues + value: '1;2;3;4;' + - key: readability-redundant-smartptr-get.IgnoreMacros + value: '1' + - key: readability-simplify-boolean-expr.ChainedConditionalAssignment + value: '0' + - key: readability-simplify-boolean-expr.ChainedConditionalReturn + value: '0' + - key: readability-simplify-subscript-expr.Types + value: '::std::basic_string;::std::basic_string_view;::std::vector;::std::array' + - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold + value: '3' + - key: readability-uppercase-literal-suffix.IgnoreMacros + value: '1' + - key: readability-uppercase-literal-suffix.NewSuffixes + value: '' diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml new file mode 100644 index 0000000..6d6bca0 --- /dev/null +++ b/.github/workflows/clang-format.yml @@ -0,0 +1,18 @@ +name: clang-format-review + +# You can be more specific, but it currently only works on pull requests +on: [push, pull_request] + +jobs: + clang-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install clang-tidy + run: | + sudo apt-get update + sudo apt-get install -y clang-tidy + - name: Analyze + run: | + clang-format --dry-run --Werror -style=file $(find ./ -name '*.cpp' -print) + clang-format --dry-run --Werror -style=file $(find ./ -name '*.h' -print) diff --git a/.github/workflows/tetra-receiver-x86_64.yml b/.github/workflows/tetra-receiver-x86_64.yml index a9aacca..24d2f3d 100644 --- a/.github/workflows/tetra-receiver-x86_64.yml +++ b/.github/workflows/tetra-receiver-x86_64.yml @@ -10,3 +10,4 @@ jobs: - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v17 - run: nix build -vL .\#packages.x86_64-linux.tetra-receiver + - run: ./result/bin/unit_tests diff --git a/.gitignore b/.gitignore index b2be92b..fe205ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ result +.idea +cmake-build-debug diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ae828e..a51627e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,25 @@ cmake_minimum_required(VERSION 3.22) +project(tetra-receiver) -add_executable(tetra-receiver - src/main.cpp) +SET(CMAKE_CXX_STANDARD 17) + +enable_testing() -target_compile_options(tetra-receiver PUBLIC -std=c++11 -Wall) +# +# Configure the tetra-receiver library +# +add_library(lib-tetra-receiver + src/config.cpp +) +target_include_directories(lib-tetra-receiver PUBLIC include) -include_directories(src) +# +# Build the tool +# +add_executable(tetra-receiver + src/tetra-receiver.cpp +) +target_compile_options(tetra-receiver PUBLIC -std=c++17 -Wall) find_package(Gnuradio "3.8" REQUIRED) find_package(Boost REQUIRED) @@ -15,6 +29,11 @@ find_package(cxxopts REQUIRED) include_directories(${GNURADIO_ALL_INCLUDE_DIRS}) -target_link_libraries(tetra-receiver log4cpp gnuradio-digital gnuradio-analog gnuradio-filter gnuradio-blocks gnuradio-fft gnuradio-runtime gnuradio-pmt volk gnuradio-osmosdr) +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) install(TARGETS tetra-receiver DESTINATION bin) + +# +# Testing +# +add_subdirectory(test) diff --git a/README.md b/README.md index 46d61b3..7933aa7 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,16 @@ The tetra streams can be decoding using [`tetra-rx` from the osmocom tetra proje Usage with tetra-rx: `socat STDIO UDP-LISTEN:42000 | stdbuf -i0 -o0 tetra-rx /dev/stdin` ## Usage +The program can be either used with command line arguments or with a TOML config file, the contents are described in the section below. + ``` Receive multiple TETRA streams at once and send the bits out via UDP Usage: tetra-receiver [OPTION...] -h, --help Print usage + --config-file arg Instead of these options read the config from + a config file (default: "") --rf arg RF gain (default: 10) --if arg IF gain (default: 10) --bb arg BB gain (default: 10) @@ -25,3 +29,46 @@ Usage: --udp-start arg Start UDP port. Each stream gets its own UDP port, starting at udp-start (default: 42000) ``` + +## Toml Config Format + +When decoding TETRA streams the downlink and uplink are often a number of MHz apart. +To solve the problem of decoding multiple uplinks and downlinks without having big FIR filters operating at the SDRs sampling rate, one wants to first decimate into two smaller streams. One for all uplink and downlink channels respectively. + +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. + +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. + +If a table specifies `Frequency` and `SampleRate`, the signal from the SDR is first decimated by the given parameters and then passed to the decoders specified in the subtables. + +``` +CenterFrequency = unsigned int +DeviceString = "string" +SampleRate = unsigned int +RFGain = unsigned int (default 0) +IFGain = unsigned int (default 0) +BBGain = unsigned int (default 0) + +[DecimateA] +Frequency = unsigned int +SampleRate = unsigned int + +[DecimateA.Stream0] +Frequency = unsigned int +Host = "string" +Port = unsigned int + +[DecimateA.Stream1] +Frequency = unsigned int +Host = "string" +Port = unsigned int + +[Stream2] +Frequency = unsigned int +Host = "string" +Port = unsigned int +``` diff --git a/flake.nix b/flake.nix index 57b5801..efa235c 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = github:NixOS/nixpkgs/nixos-23.11; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; utils.url = "github:numtide/flake-utils"; }; diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..28bde1f --- /dev/null +++ b/include/config.h @@ -0,0 +1,252 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifndef INCLUDE_CONFIG_H +#define INCLUDE_CONFIG_H + +namespace config { + +/// The sample rate of the TETRA Stream +[[maybe_unused]] static constexpr unsigned int kTetraSampleRate = 25000; + +/// 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; + +template class Range { +private: + T min_ = 0; + T max_ = 0; + +public: + /// 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 { + if (lhs < rhs) { + this->min_ = lhs; + this->max_ = rhs; + } else { + this->min_ = rhs; + this->max_ = lhs; + } + }; + + [[nodiscard]] auto lower_bound() const noexcept -> T { return min_; }; + [[nodiscard]] auto upper_bound() const noexcept -> T { return max_; }; + + /// Check that the given Range is inside the bounds (inclusive) of this Range. + [[nodiscard]] auto contains(const Range& other) const noexcept -> bool { + return !static_cast(other.min_ < min_ || other.max_ > max_); + }; +}; + +template class SpectrumSlice { +public: + /// the center frequency of this slice of EM spectrum + T center_frequency_; + /// the frequency Range of this slice of EM spectrum + Range frequency_range_; + /// the sampling frequency for this slice of EM spectrum + T sample_rate_; + + /// 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 + SpectrumSlice(const T center_frequency, const T sample_rate) noexcept + : center_frequency_(center_frequency) + , 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_; +}; + +template auto operator!=(const SpectrumSlice& lhs, const SpectrumSlice& rhs) -> bool { + return !operator==(lhs, rhs); +}; + +class Stream { +public: + /// the slice of spectrum_ that is input to this block + const SpectrumSlice input_spectrum_; + /// the slice of spectrum_ of the TETRA Stream + const SpectrumSlice spectrum_; + /// the decimation_ of this block + unsigned int decimation_ = 0; + /// Optional field + /// The host to which the samples of the Stream should be sent. This defaults + /// to "locahost". + const std::string host_{}; + /// Optional field + /// The port to which the samples of the Stream should be sent. This defaults + /// to 42000. + const unsigned int port_ = 0; + + /// Describe the on which frequency a TETRA Stream should be extracted and + /// where data should be sent to. + /// \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); +}; + +class Decimate { +public: + /// the slice of spectrum that is input to this block + const SpectrumSlice input_spectrum_; + /// the slice of spectrum after decimation_ + const SpectrumSlice spectrum_; + + /// the decimation of this block + unsigned int decimation_ = 0; + + /// The vector of streams the output of this Decimate block should be + /// connected to. + std::vector streams_; + + /// Describe the decimation of the SDR Stream by the frequency where we want + /// to extract a signal with a width of sample_rate + /// \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); +}; + +class TopLevel { +public: + /// The spectrum of the SDR + const SpectrumSlice spectrum_; + /// The device string for the SDR source block + const std::string device_string_{}; + /// The RF gain setting of the SDR + const unsigned int rf_gain_; + /// The IF gain setting of the SDR + const unsigned int if_gain_; + /// The BB gain setting of the SDR + const unsigned int bb_gain_; + /// The vector of Streams which should be directly decoded from the input of + /// the SDR. + const std::vector streams_{}; + /// 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_{}; + + 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); +}; + +using decimate_or_stream = std::variant; + +}; // namespace config + +namespace toml { + +static config::decimate_or_stream get_decimate_or_stream(const config::SpectrumSlice& input_spectrum, + const value& v) { + std::optional sample_rate; + + const unsigned int frequency = find(v, "Frequency"); + if (v.contains("SampleRate")) + 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); + + // 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)); + } else { + return config::Stream(input_spectrum, config::SpectrumSlice(frequency, config::kTetraSampleRate), + host, port); + } +} + +template <> struct from { + static auto from_toml(const value& v) -> config::TopLevel { + const unsigned int center_frequency = find(v, "CenterFrequency"); + const std::string device_string = find(v, "DeviceString"); + const unsigned int sample_rate = find(v, "SampleRate"); + const unsigned int rf_gain = find_or(v, "RFGain", 0); + const unsigned int if_gain = find_or(v, "IFGain", 0); + const unsigned int bb_gain = find_or(v, "BBGain", 0); + + config::SpectrumSlice sdr_spectrum(center_frequency, sample_rate); + + std::vector streams; + std::vector decimators; + + // Iterate over all elements in the root table + for (const auto& root_kv : v.as_table()) { + 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); + + // Save the Stream + if (std::holds_alternative(element)) { + const auto& stream_element = std::get(element); + streams.push_back(stream_element); + + continue; + } + + // Found a decimator entry + if (std::holds_alternative(element)) { + auto decimate_element = std::get(element); + + // Find all subtables, that must be Stream entries and add them to the + // decimator + for (const auto& stream_pair : table.as_table()) { + 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); + + if (!std::holds_alternative(stream_element)) { + throw std::invalid_argument("Did not find a Stream block under the Decimate block"); + } + + decimate_element.streams_.push_back(std::get(stream_element)); + } + + decimators.push_back(decimate_element); + + continue; + } + + 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); + } +}; + +}; // namespace toml + +#endif + +#endif diff --git a/nixos-modules/tetra-receiver.nix b/nixos-modules/tetra-receiver.nix index 3c75c85..49feedd 100644 --- a/nixos-modules/tetra-receiver.nix +++ b/nixos-modules/tetra-receiver.nix @@ -3,47 +3,10 @@ let cfg = config.services.tetra-receiver; in { options.services.tetra-receiver = with lib; { enable = mkEnableOption "tetra-receiver"; - rfGain = mkOption { - type = types.int; - default = 10; - description = " RF gain (default: 10)\n"; - }; - ifGain = mkOption { - type = types.int; - default = 10; - description = " IF gain (default: 10)\n"; - }; - bbGain = mkOption { - type = types.int; - default = 10; - description = " BB gain (default: 10)\n"; - }; - deviceString = mkOption { + configFile = mkOption { type = types.str; default = ""; - description = - " additional device arguments for osmosdr, see https://projects.osmocom.org/projects/gr-osmosdr/wiki/GrOsmoSDR (default: \"\")\n"; - }; - centerFrequency = mkOption { - type = types.int; - default = 0; - description = " Center frequency of the SDR (default: 0)\n"; - }; - offsets = mkOption { - type = types.listOf types.int; - default = [ ]; - description = " Offsets of the TETRA streams\n"; - }; - sampRate = mkOption { - type = types.int; - default = 1000000; - description = " Sample rate of the sdr (default: 1000000)\n"; - }; - udpStart = mkOption { - type = types.int; - default = 42000; - description = - " Start UDP port. Each stream gets its own UDP port, starting at udp-start (default: 42000)\n"; + description = "The contents of the toml config file."; }; user = mkOption { type = types.str; @@ -66,10 +29,13 @@ in { enable = true; wantedBy = [ "multi-user.target" ]; - script = '' - exec ${pkgs.expect}/bin/unbuffer ${pkgs.tetra-receiver}/bin/tetra-receiver --rf ${toString cfg.rfGain} --if ${toString cfg.ifGain} --bb ${toString cfg.bbGain} --device-string "${cfg.deviceString}" --offsets ${ - lib.concatMapStringsSep "," toString cfg.offsets - } --center-frequency ${toString cfg.centerFrequency} --samp-rate ${toString cfg.sampRate} --udp-start ${toString cfg.udpStart} & + script = let + configFile = pkgs.writeTextFile { + name = "tetra-receiver-config.toml"; + text = cfg.configFile; + }; + in '' + exec ${pkgs.expect}/bin/unbuffer ${pkgs.tetra-receiver}/bin/tetra-receiver --config-file ${configFile} & ''; serviceConfig = { diff --git a/pkgs/tetra-receiver.nix b/pkgs/tetra-receiver.nix index 9372971..e0c9cab 100644 --- a/pkgs/tetra-receiver.nix +++ b/pkgs/tetra-receiver.nix @@ -1,4 +1,4 @@ -{ stdenv +{ clangStdenv , pkg-config , cmake , gnuradio @@ -7,20 +7,54 @@ , mpir , gmpxx , cxxopts +, toml11 +, fetchFromGitHub +, stdenv +, gtest }: let + toml11 = stdenv.mkDerivation { + pname = "toml11"; + version = "3.8.1"; + + src = fetchFromGitHub { + owner = "ToruNiina"; + repo = "toml11"; + rev = "v3.8.1"; + hash = "sha256-XgpsCv38J9k8Tsq71UdZpuhaVHK3/60IQycs9CeLssA="; + }; + phases = ["unpackPhase" "installPhase"]; + + nativeBuildInputs = [ + cmake + ]; + + installPhase = '' + mkdir -p $out/include + + cp -r ./toml.hpp $out/include/ + cp -r ./toml $out/include + cp -r cmake $out/ + ''; + }; osmosdr = gnuradioPackages.osmosdr.overrideAttrs(_oldAttrs: { outputs = [ "out" ]; }); in -stdenv.mkDerivation { +clangStdenv.mkDerivation { 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 ]; + buildInputs = [ log4cpp mpir gnuradio.unwrapped gnuradio.unwrapped.boost.dev gmpxx.dev gnuradio.unwrapped.volk osmosdr cxxopts toml11 gtest ]; cmakeFlags = [ "-DCMAKE_PREFIX_PATH=${osmosdr}/lib/cmake/osmosdr" ]; + + installPhase = '' + mkdir -p $out/bin + cp ./test/unit_tests $out/bin/ + cp ./tetra-receiver $out/bin/ + ''; } diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..0118c61 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,72 @@ +#include "config.h" + +namespace config { + +Stream::Stream(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum, + std::string host, unsigned int port) + : input_spectrum_(input_spectrum) + , spectrum_(spectrum) + , host_(std::move(host)) + , port_(port) { + // check that this Stream is valid + if (!input_spectrum.frequency_range_.contains(spectrum.frequency_range_)) { + throw std::invalid_argument("Frequency Range of the Streams in not " + "inside the frequency Range of the input."); + } + + const auto& input_sample_rate = input_spectrum.sample_rate_; + const auto& sample_rate = spectrum.sample_rate_; + decimation_ = input_sample_rate / sample_rate; + auto remainder = input_sample_rate % sample_rate; + if (remainder != 0) { + throw std::invalid_argument("Input sample rate is not divisible by Stream block sample rate."); + } +} + +Decimate::Decimate(const SpectrumSlice& input_spectrum, const SpectrumSlice& spectrum) + : input_spectrum_(input_spectrum) + , spectrum_(spectrum) { + // check that this Stream is valid + if (!input_spectrum.frequency_range_.contains(spectrum.frequency_range_)) { + throw std::invalid_argument("Decimator frequency Range is not inside the one of the SDR"); + } + + const auto& input_sample_rate = input_spectrum.sample_rate_; + const auto& sample_rate = spectrum.sample_rate_; + decimation_ = input_sample_rate / sample_rate; + auto remainder = input_sample_rate % sample_rate; + + if (remainder != 0) { + throw std::invalid_argument("Input sample rate is not divisible by Decimate block sample rate."); + } + + for (const auto& stream : streams_) { + if (stream.input_spectrum_ != spectrum) { + throw std::invalid_argument("The output of Decimate does not match to the input of Stream."); + } + } +} + +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) + : spectrum_(spectrum) + , device_string_(std::move(device_string)) + , rf_gain_(rf_gain) + , if_gain_(if_gain) + , bb_gain_(bb_gain) + , streams_(streams) + , decimators_(decimators) { + for (const auto& stream : streams) { + if (operator!=(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)) { + throw std::invalid_argument("The output of Decimate does not match to the input of Stream."); + } + } +} + +} // namespace config diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index ceec962..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -int main(int argc, char **argv) { - osmosdr::source::sptr src; - std::vector offsets; - unsigned int samp_rate; - unsigned int udp_start; - - try { - cxxopts::Options options( - "tetra-receiver", - "Receive multiple TETRA streams at once and send the bits out via UDP"); - - // clang-format off - options.add_options() - ("h,help", "Print usage") - ("rf", "RF gain", cxxopts::value()->default_value("10")) - ("if", "IF gain", cxxopts::value()->default_value("10")) - ("bb", "BB gain", cxxopts::value()->default_value("10")) - ("device-string", "additional device arguments for osmosdr, see https://projects.osmocom.org/projects/gr-osmosdr/wiki/GrOsmoSDR", cxxopts::value()->default_value("")) - ("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")) - ; - // clang-format on - - auto result = options.parse(argc, argv); - - if (result.count("help")) { - std::cout << options.help() << std::endl; - exit(0); - } - - samp_rate = result["samp-rate"].as(); - - std::string ver = gr::version(); - std::string cCompiler = gr::c_compiler(); - std::string cxxCompiler = gr::cxx_compiler(); - std::string compilerFlags = gr::compiler_flags(); - std::string prefs = gr::prefs::singleton()->to_string(); - - std::cout << "GNU Radio Version: " << ver - << "\n\n C Compiler: " << cCompiler - << "\n\n CXX Compiler: " << cxxCompiler << "\n\n Prefs: " << prefs - << "\n\n Compiler Flags: " << compilerFlags << "\n\n"; - - // setup osmosdr source - src = osmosdr::source::make(result["device-string"].as()); - src->set_block_alias("src"); - - src->set_sample_rate(samp_rate); - src->set_center_freq(result["center-frequency"].as()); - src->set_gain_mode(false, 0); - src->set_gain(result["rf"].as(), "RF", 0); - src->set_gain(result["if"].as(), "IF", 0); - src->set_gain(result["bb"].as(), "BB", 0); - src->set_bandwidth(samp_rate / 2, 0); - - // create the decoding blocks for each tetra stream - offsets = result["offsets"].as>(); - udp_start = result["udp-start"].as(); - } catch (std::exception &e) { - std::cout << "error parsing options: " << e.what() << std::endl; - return EXIT_FAILURE; - } - - auto tb = gr::make_top_block("fg"); - - auto it = offsets.begin(); - for (; it != offsets.end(); ++it) { - auto offset = *it; - auto udp_port = udp_start + std::distance(offsets.begin(), it); - auto decimation = samp_rate / 1000000.0f * 40.0f; - auto channel_rate = 36000; - auto sps = 2; - auto nfilts = 32; - auto constellation = gr::digital::constellation_dqpsk::make(); - constellation->gen_soft_dec_lut(8); - - auto xlat_taps = gr::filter::firdes::complex_band_pass(1, samp_rate, -12500, - 12500, 12500 * 0.2); - auto rrc_taps = gr::filter::firdes::root_raised_cosine( - nfilts, nfilts, 1.0 / static_cast(sps), 0.35, 11 * sps * nfilts); - - auto xlat = gr::filter::freq_xlating_fir_filter_ccc::make( - decimation, xlat_taps, offset, samp_rate); - auto mmse_resampler_cc = gr::filter::mmse_resampler_cc::make( - 0, static_cast(samp_rate) / (static_cast(decimation) * - static_cast(channel_rate))); - auto agc = gr::analog::feedforward_agc_cc::make(8, 1); - auto digital_fll_band_edge_cc = - gr::digital::fll_band_edge_cc::make(sps, 0.35, 45, M_PI / 100.0f); - auto digital_pfb_clock_sync_xxx = gr::digital::pfb_clock_sync_ccf::make( - sps, 2 * M_PI / 100.0f, rrc_taps, nfilts, nfilts / 2.0, 1.5, sps); - auto digital_cma_equalizer_cc = - gr::digital::cma_equalizer_cc::make(15, 1, 10e-3, sps); - auto diff_phasor_cc = gr::digital::diff_phasor_cc::make(); - auto digital_constellation_decoder_cb = - gr::digital::constellation_decoder_cb::make(constellation); - auto digital_map_bb = - gr::digital::map_bb::make(constellation->pre_diff_code()); - auto blocks_unpack_k_bits_bb = - gr::blocks::unpack_k_bits_bb::make(constellation->bits_per_symbol()); - auto blocks_udp_sink = gr::blocks::udp_sink::make(sizeof(char), "127.0.0.1", - udp_port, 1472, false); - - try { - tb->connect(src, 0, xlat, 0); - tb->connect(xlat, 0, mmse_resampler_cc, 0); - tb->connect(mmse_resampler_cc, 0, agc, 0); - tb->connect(agc, 0, digital_fll_band_edge_cc, 0); - tb->connect(digital_fll_band_edge_cc, 0, digital_pfb_clock_sync_xxx, 0); - tb->connect(digital_pfb_clock_sync_xxx, 0, digital_cma_equalizer_cc, 0); - tb->connect(digital_cma_equalizer_cc, 0, diff_phasor_cc, 0); - tb->connect(diff_phasor_cc, 0, digital_constellation_decoder_cb, 0); - 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); - } catch (const std::invalid_argument &e) { - std::cerr << "Error creating gnuradio blocks for offset number " - << std::distance(offsets.begin(), it) << " with value " - << offset << "\n" - << e.what(); - return EXIT_FAILURE; - } - } - - tb->start(); - - tb->wait(); - - return EXIT_SUCCESS; -} diff --git a/src/tetra-receiver.cpp b/src/tetra-receiver.cpp new file mode 100644 index 0000000..bceeeae --- /dev/null +++ b/src/tetra-receiver.cpp @@ -0,0 +1,217 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +static auto print_gnuradio_diagnostics() -> void { + const auto ver = gr::version(); + const auto c_compiler = gr::c_compiler(); + const auto cxx_compiler = gr::cxx_compiler(); + const auto compiler_flags = gr::compiler_flags(); + const auto prefs = gr::prefs::singleton()->to_string(); + + std::cout << "GNU Radio Version: " << ver << "\n\n C Compiler: " << c_compiler + << "\n\n CXX Compiler: " << cxx_compiler << "\n\n Prefs: " << prefs + << "\n\n Compiler Flags: " << compiler_flags << "\n\n"; +} + +class GnuradioTopBlock { +private: + static auto from_config(const config::Stream& stream, gr::top_block_sptr tb, gr::basic_block_sptr input) -> void { + auto decimation = stream.decimation_; + auto offset = static_cast(stream.spectrum_.center_frequency_) - + static_cast(stream.input_spectrum_.center_frequency_); + auto sample_rate = stream.input_spectrum_.sample_rate_; + + float half_sample_rate = stream.spectrum_.sample_rate_ / 2; + auto xlat_taps = gr::filter::firdes::complex_band_pass(1, stream.input_spectrum_.sample_rate_, -half_sample_rate, + half_sample_rate, half_sample_rate * 0.2); + auto xlat = gr::filter::freq_xlating_fir_filter_ccc::make(decimation, xlat_taps, offset, + stream.input_spectrum_.sample_rate_); + + auto channel_rate = 36000; + auto sps = 2; + auto nfilts = 32; + auto constellation = gr::digital::constellation_dqpsk::make(); + constellation->gen_soft_dec_lut(8); + + auto rrc_taps = + gr::filter::firdes::root_raised_cosine(nfilts, nfilts, 1.0 / static_cast(sps), 0.35, 11 * sps * nfilts); + + auto mmse_resampler_cc = gr::filter::mmse_resampler_cc::make( + 0, static_cast(sample_rate) / (static_cast(decimation) * static_cast(channel_rate))); + auto agc = gr::analog::feedforward_agc_cc::make(8, 1); + auto digital_fll_band_edge_cc = gr::digital::fll_band_edge_cc::make(sps, 0.35, 45, M_PI / 100.0f); + auto digital_pfb_clock_sync_xxx = + gr::digital::pfb_clock_sync_ccf::make(sps, 2 * M_PI / 100.0f, rrc_taps, nfilts, nfilts / 2.0, 1.5, sps); + auto digital_cma_equalizer_cc = gr::digital::cma_equalizer_cc::make(15, 1, 10e-3, sps); + auto diff_phasor_cc = gr::digital::diff_phasor_cc::make(); + auto digital_constellation_decoder_cb = gr::digital::constellation_decoder_cb::make(constellation); + auto digital_map_bb = gr::digital::map_bb::make(constellation->pre_diff_code()); + auto blocks_unpack_k_bits_bb = gr::blocks::unpack_k_bits_bb::make(constellation->bits_per_symbol()); + auto blocks_udp_sink = gr::blocks::udp_sink::make(sizeof(char), stream.host_, stream.port_, 1472, false); + + tb->connect(input, 0, xlat, 0); + tb->connect(xlat, 0, mmse_resampler_cc, 0); + tb->connect(mmse_resampler_cc, 0, agc, 0); + tb->connect(agc, 0, digital_fll_band_edge_cc, 0); + tb->connect(digital_fll_band_edge_cc, 0, digital_pfb_clock_sync_xxx, 0); + tb->connect(digital_pfb_clock_sync_xxx, 0, digital_cma_equalizer_cc, 0); + tb->connect(digital_cma_equalizer_cc, 0, diff_phasor_cc, 0); + tb->connect(diff_phasor_cc, 0, digital_constellation_decoder_cb, 0); + 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); + }; + + static auto from_config(const config::Decimate& decimate, gr::top_block_sptr tb, gr::basic_block_sptr input) -> void { + float half_sample_rate = decimate.spectrum_.sample_rate_ / 2; + auto offset = static_cast(decimate.spectrum_.center_frequency_) - + static_cast(decimate.input_spectrum_.center_frequency_); + auto xlat_taps = gr::filter::firdes::complex_band_pass(1, decimate.input_spectrum_.sample_rate_, -half_sample_rate, + half_sample_rate, half_sample_rate * 0.2); + auto xlat = gr::filter::freq_xlating_fir_filter_ccc::make(decimate.decimation_, xlat_taps, offset, + decimate.input_spectrum_.sample_rate_); + + tb->connect(input, 0, xlat, 0); + + for (auto const& stream : decimate.streams_) { + from_config(stream, tb, xlat); + } + + // 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(xlat, 0, null_sink, 0); + }; + +public: + static auto from_config(const config::TopLevel& top) -> gr::top_block_sptr { + auto tb = gr::make_top_block("fg"); + + // setup osmosdr source + auto src = osmosdr::source::make(top.device_string_); + src->set_block_alias("src"); + + src->set_sample_rate(top.spectrum_.sample_rate_); + src->set_center_freq(top.spectrum_.center_frequency_); + src->set_gain_mode(false, 0); + src->set_gain(top.rf_gain_, "RF", 0); + src->set_gain(top.if_gain_, "IF", 0); + src->set_gain(top.bb_gain_, "BB", 0); + src->set_bandwidth(top.spectrum_.sample_rate_ / 2, 0); + + for (auto const& decimate : top.decimators_) { + from_config(decimate, tb, src); + } + for (auto const& stream : top.streams_) { + from_config(stream, tb, 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; + } +}; + +auto main(int argc, char** argv) -> int { + try { + cxxopts::Options options("tetra-receiver", "Receive multiple TETRA streams at once and send the bits out via UDP"); + + // clang-format off + options.add_options() + ("h,help", "Print usage") + ("config-file", "Instead of these options read the config from a config file", cxxopts::value()->default_value("")) + ("rf", "RF gain", cxxopts::value()->default_value("10")) + ("if", "IF gain", cxxopts::value()->default_value("10")) + ("bb", "BB gain", cxxopts::value()->default_value("10")) + ("device-string", "additional device arguments for osmosdr, see https://projects.osmocom.org/projects/gr-osmosdr/wiki/GrOsmoSDR", cxxopts::value()->default_value("")) + ("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")) + ; + // clang-format on + + auto result = options.parse(argc, argv); + + if (result.count("help")) { + std::cout << options.help() << std::endl; + return EXIT_SUCCESS; + } + + gr::top_block_sptr tb = 0; + + // 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); + } else { + const auto sample_rate = result["samp-rate"].as(); + const auto& device_string = result["device-string"].as(); + const auto center_frequency = result["center-frequency"].as(); + const auto rf_gain = result["rf"].as(); + const auto if_gain = result["if"].as(); + const auto bb_gain = result["bb"].as(); + const auto& offsets = result["offsets"].as>(); + const auto udp_start = result["udp-start"].as(); + + std::vector streams; + const auto input_spectrum = config::SpectrumSlice(center_frequency, sample_rate); + + // create the decoding blocks for each tetra Stream + for (auto offsets_it = offsets.begin(); offsets_it != offsets.end(); ++offsets_it) { + auto stream_frequency = center_frequency + *offsets_it; + auto udp_port = udp_start + std::distance(offsets.begin(), offsets_it); + + const auto tetra_spectrum = config::SpectrumSlice(stream_frequency, config::kTetraSampleRate); + + streams.emplace_back(config::Stream(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); + } + + // print the gnuradio debugging information + print_gnuradio_diagnostics(); + + tb->start(); + + tb->wait(); + } catch (std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..6a58ee4 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,19 @@ +find_package(GTest REQUIRED) +message(STATUS "GTEST_INCLUDE_DIR: ${GTEST_INCLUDE_DIR}") +message(STATUS "GTEST_LIBRARIES: ${GTEST_LIBRARIES}") + +# configure build of googletest +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) + +add_executable( + unit_tests + config_test.cpp + main.cpp +) + +#target_include_directories(unit_tests PUBLIC ${GTEST_INCLUDE_DIR}) +target_link_libraries(unit_tests PUBLIC ${GTEST_LIBRARIES}) +target_link_libraries(unit_tests PRIVATE lib-tetra-receiver) + +install(TARGETS unit_tests DESTINATION bin) diff --git a/test/config_test.cpp b/test/config_test.cpp new file mode 100644 index 0000000..f35250c --- /dev/null +++ b/test/config_test.cpp @@ -0,0 +1,162 @@ +#include + +#include "config.h" + +using namespace toml::literals::toml_literals; + +TEST(config, Range_order) { + config::Range ordered_range(10, 12); + EXPECT_EQ(ordered_range.lower_bound(), 10); + EXPECT_EQ(ordered_range.upper_bound(), 12); + + config::Range unordered_range(12, 10); + EXPECT_EQ(unordered_range.lower_bound(), 10); + EXPECT_EQ(unordered_range.upper_bound(), 12); +} + +TEST(config, Range_contains) { + config::Range ref_range(10, 13); + + // a range contains itself + EXPECT_TRUE(ref_range.contains(ref_range)); + // it contains a sub range + EXPECT_TRUE(ref_range.contains(config::Range(11, 12))); + // it contains a sub range also with same lower bound + EXPECT_TRUE(ref_range.contains(config::Range(10, 12))); + // it contains a sub range also with same upper bound + EXPECT_TRUE(ref_range.contains(config::Range(11, 13))); + // it does not when the the upper bound is bigger + EXPECT_FALSE(ref_range.contains(config::Range(11, 14))); + // it does not when the the lower bound is smaller + EXPECT_FALSE(ref_range.contains(config::Range(9, 12))); + // compare range is bigger, but includes ref range + EXPECT_FALSE(ref_range.contains(config::Range(9, 14))); + // compare range is outside of ref range + EXPECT_FALSE(ref_range.contains(config::Range(1, 2))); + // compare range is outside of ref range + EXPECT_FALSE(ref_range.contains(config::Range(14, 15))); +} + +TEST(config, SpectrumSlice_range) { + config::SpectrumSlice slice(/*center_frequency=*/1000, /*sample_rate=*/200); + EXPECT_EQ(slice.center_frequency_, 1000); + EXPECT_EQ(slice.frequency_range_.lower_bound(), 900); + EXPECT_EQ(slice.frequency_range_.upper_bound(), 1100); + EXPECT_EQ(slice.sample_rate_, 200); +} + +TEST(config, TopLevel_decimate_not_divisible) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 1000000 + + [DecimateA] + Frequency = 4250000 + SampleRate = 500001 + )"_toml; + + // Input sample rate is not divisible by Decimate block sample rate. + EXPECT_THROW(toml::get(config_object), std::invalid_argument); +} + +TEST(config, TopLevel_stream_not_divisible) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 60000 + + [Stream0] + Frequency = 4000000 + )"_toml; + + // Input sample rate is not divisible by Stream block sample rate. + // 60000 is not divisible by default 25000 TETRA stream sample rate + EXPECT_THROW(toml::get(config_object), std::invalid_argument); +} + +TEST(config, TopLevel_decimate_under_decimate) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 60000 + + [DecmiateA] + Frequency = 4250000 + SampleRate = 500000 + + [DecmiateA.DecimateB] + Frequency = 4250000 + SampleRate = 250000 + )"_toml; + + // Did not find a Stream block under the Decimate block + EXPECT_THROW(toml::get(config_object), std::invalid_argument); +} + +TEST(config, TopLevel_valid_parser) { + const toml::value config_object = u8R"( + CenterFrequency = 4000000 + DeviceString = "device_string_abc" + SampleRate = 1000000 + IFGain = 14 + + [DecimateA] + Frequency = 4250000 + SampleRate = 500000 + + [DecimateA.Stream0] + Frequency = 4250010 + Host = "127.0.0.1" + Port = 4100 + + [DecimateA.Stream1] + Frequency = 4250100 + + [Stream2] + Frequency = 4000100 + Host = "127.0.0.2" + Port = 4200 + )"_toml; + + const config::TopLevel t = toml::get(config_object); + + EXPECT_EQ(t.spectrum_.center_frequency_, 4000000); + EXPECT_EQ(t.spectrum_.sample_rate_, 1000000); + EXPECT_EQ(t.device_string_, "device_string_abc"); + EXPECT_EQ(t.rf_gain_, 0); + EXPECT_EQ(t.if_gain_, 14); + EXPECT_EQ(t.bb_gain_, 0); + + EXPECT_EQ(t.decimators_.size(), 1); + const auto& decimate_a = t.decimators_[0]; + + EXPECT_EQ(decimate_a.spectrum_.center_frequency_, 4250000); + EXPECT_EQ(decimate_a.spectrum_.sample_rate_, 500000); + EXPECT_EQ(decimate_a.decimation_, 2); + + EXPECT_EQ(decimate_a.streams_.size(), 2); + const auto& stream_0 = decimate_a.streams_[1]; + const auto& stream_1 = decimate_a.streams_[0]; + + 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.spectrum_.center_frequency_, 4250100); + EXPECT_EQ(stream_1.spectrum_.sample_rate_, config::kTetraSampleRate); + EXPECT_EQ(stream_1.host_, config::kDefaultHost); + EXPECT_EQ(stream_1.port_, config::kDefaultPort); + EXPECT_EQ(stream_1.decimation_, 20); + + EXPECT_EQ(t.streams_.size(), 1); + const auto& stream_2 = t.streams_[0]; + + 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"); + EXPECT_EQ(stream_2.port_, 4200); + EXPECT_EQ(stream_2.decimation_, 40); +} diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..697a9d7 --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}