Skip to content

Commit

Permalink
Merge pull request #1 from tlm-solutions/feature/config-file
Browse files Browse the repository at this point in the history
Config File Parsing
  • Loading branch information
tanneberger authored May 15, 2024
2 parents 54465b0 + 0bbb5fb commit 27b02e2
Show file tree
Hide file tree
Showing 14 changed files with 835 additions and 240 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/clang-format.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name: clang-format-review

# You can be more specific, but it currently only works on pull requests
on: [pull_request]
on: [push, pull_request]

jobs:
clang-format:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Install clang-tidy
run: |
sudo apt-get update
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
result
.idea
cmake-build-debug
42 changes: 34 additions & 8 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
cmake_minimum_required(VERSION 3.22)
project(tetra-receiver)

add_executable(tetra-receiver
src/main.cpp)

target_compile_options(tetra-receiver PUBLIC -std=c++11 -Wall)

include_directories(src)
SET(CMAKE_CXX_STANDARD 17)

#
# Find required libraries
#
find_package(Gnuradio "3.8" REQUIRED)
find_package(Boost REQUIRED)
find_package(Volk REQUIRED)
Expand All @@ -15,10 +14,37 @@ 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)

#
# Turn ON Testing
#
enable_testing()

#
# Configure the tetra-receiver library
#
add_library(lib-tetra-receiver
src/config.cpp
)

target_include_directories(lib-tetra-receiver PUBLIC include)

#
# Build the tool
#
add_executable(tetra-receiver
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)

#
# Testing
#
add_subdirectory(test)

#
# Install tetra-receiver in bin folder
#
install(TARGETS tetra-receiver DESTINATION bin)
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
```
252 changes: 252 additions & 0 deletions include/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#ifndef CONFIG_H
#define CONFIG_H

#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <optional>
#include <toml.hpp>
#include <variant>

#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 <typename T> 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<T>& other) const noexcept -> bool {
return !static_cast<bool>(other.min_ < min_ || other.max_ > max_);
};
};

template <typename T> 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<T> 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<T>&, const SpectrumSlice<T>&) -> bool;
friend auto operator!=(const SpectrumSlice<T>&, const SpectrumSlice<T>&) -> bool;
};

template <typename T> auto operator==(const SpectrumSlice<T>& lhs, const SpectrumSlice<T>& rhs) -> bool {
return lhs.center_frequency_ == rhs.center_frequency_ && lhs.sample_rate_ == rhs.sample_rate_;
};

template <typename T> auto operator!=(const SpectrumSlice<T>& lhs, const SpectrumSlice<T>& rhs) -> bool {
return !operator==<T>(lhs, rhs);
};

class Stream {
public:
/// the slice of spectrum_ that is input to this block
const SpectrumSlice<unsigned int> input_spectrum_;
/// the slice of spectrum_ of the TETRA Stream
const SpectrumSlice<unsigned int> 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<unsigned int>& input_spectrum, const SpectrumSlice<unsigned int>& spectrum,
std::string host, unsigned int port);
};

class Decimate {
public:
/// the slice of spectrum that is input to this block
const SpectrumSlice<unsigned int> input_spectrum_;
/// the slice of spectrum after decimation_
const SpectrumSlice<unsigned int> 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<Stream> 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<unsigned int>& input_spectrum, const SpectrumSlice<unsigned int>& spectrum);
};

class TopLevel {
public:
/// The spectrum of the SDR
const SpectrumSlice<unsigned int> 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<Stream> 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<Decimate> decimators_{};

TopLevel(const SpectrumSlice<unsigned int>& spectrum, std::string device_string, unsigned int rf_gain,
unsigned int if_gain, unsigned int bb_gain, const std::vector<Stream>& streams,
const std::vector<Decimate>& decimators);
};

using decimate_or_stream = std::variant<Decimate, Stream>;

}; // namespace config

namespace toml {

static config::decimate_or_stream get_decimate_or_stream(const config::SpectrumSlice<unsigned int>& input_spectrum,
const value& v) {
std::optional<unsigned int> sample_rate;

const unsigned int frequency = find<unsigned int>(v, "Frequency");
if (v.contains("SampleRate"))
sample_rate = find<unsigned int>(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<unsigned int>(frequency, *sample_rate));
} else {
return config::Stream(input_spectrum, config::SpectrumSlice<unsigned int>(frequency, config::kTetraSampleRate),
host, port);
}
}

template <> struct from<config::TopLevel> {
static auto from_toml(const value& v) -> config::TopLevel {
const unsigned int center_frequency = find<unsigned int>(v, "CenterFrequency");
const std::string device_string = find<std::string>(v, "DeviceString");
const unsigned int sample_rate = find<unsigned int>(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<unsigned int> sdr_spectrum(center_frequency, sample_rate);

std::vector<config::Stream> streams;
std::vector<config::Decimate> 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<config::Stream>(element)) {
const auto& stream_element = std::get<config::Stream>(element);
streams.push_back(stream_element);

continue;
}

// Found a decimator entry
if (std::holds_alternative<config::Decimate>(element)) {
auto decimate_element = std::get<config::Decimate>(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<config::Stream>(stream_element)) {
throw std::invalid_argument("Did not find a Stream block under the Decimate block");
}

decimate_element.streams_.push_back(std::get<config::Stream>(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
Loading

0 comments on commit 27b02e2

Please sign in to comment.