1
0
mirror of https://github.com/gnss-sdr/gnss-sdr synced 2026-06-30 00:18:50 +00:00

Add work on GNSS Sofware Defined Receiver Metadata Standard

This commit is contained in:
Carles Fernandez
2026-06-21 21:14:21 +02:00
parent ef90183ac2
commit d95d3cbcb5
8 changed files with 1119 additions and 185 deletions
@@ -23,6 +23,8 @@
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <iterator>
#include <limits>
#include <stdexcept>
#if USE_GLOG_AND_GFLAGS
@@ -69,30 +71,83 @@ IONGSMSSignalSource::IONGSMSSignalSource(const ConfigurationInterface* configura
throw std::runtime_error("ION_GSMS_Signal_Source no configured streams were found in the metadata");
}
for (const auto& source : sources_)
for (const auto& source_data : sources_)
{
const auto& source = source_data.source;
for (std::size_t i = 0; i < source->output_stream_count(); ++i)
{
copy_blocks_.emplace_back(gr::blocks::copy::make(source->output_stream_item_size(i)));
valves_.emplace_back(gnss_sdr_make_valve(source->output_stream_item_size(i), valve_sample_count(source->output_stream_total_sample_count(i)), queue));
valves_.emplace_back(gnss_sdr_make_valve(source->output_stream_item_size(i), valve_sample_count(source->output_stream_total_sample_count(i), source_data.sampling_frequency), queue));
}
}
}
const char* IONGSMSSignalSource::file_uri_scheme()
{
return "file://";
}
bool IONGSMSSignalSource::starts_with(const std::string& value, const std::string& prefix)
{
return value.compare(0, prefix.size(), prefix) == 0;
}
fs::path IONGSMSSignalSource::absolute_path_key(const fs::path& path)
{
try
{
return fs::canonical(path);
}
catch (const fs::filesystem_error&)
{
return fs::absolute(path);
}
}
fs::path IONGSMSSignalSource::resolve_local_metadata_uri(const fs::path& metadata_path, const std::string& uri)
{
if (uri.empty())
{
throw std::runtime_error("ION_GSMS metadata include URI is empty");
}
fs::path include_path;
const std::string scheme(file_uri_scheme());
if (starts_with(uri, scheme))
{
include_path = fs::path(uri.substr(scheme.size()));
}
else if (uri.find("://") != std::string::npos)
{
throw std::runtime_error(
"ION_GSMS metadata include URI '" + uri + "' is not a local file URI");
}
else
{
include_path = fs::path(uri);
}
if (include_path.is_absolute())
{
return include_path;
}
return metadata_path.parent_path() / include_path;
}
void IONGSMSSignalSource::load_metadata()
{
metadata_ = std::make_shared<GnssMetadata::Metadata>();
metadata_files_.clear();
try
{
GnssMetadata::XmlProcessor xml_proc;
if (!xml_proc.Load(metadata_filepath_.c_str(), false, *metadata_))
{
LOG(WARNING) << "Could not load XML metadata file " << metadata_filepath_;
std::cerr << "Could not load XML metadata file " << metadata_filepath_ << std::endl;
std::cout << "GNSS-SDR program ended.\n";
exit(1);
}
std::vector<std::string> include_stack;
load_metadata_file(fs::path(metadata_filepath_), *metadata_, include_stack);
}
catch (GnssMetadata::ApiException& e)
{
@@ -111,6 +166,54 @@ void IONGSMSSignalSource::load_metadata()
}
void IONGSMSSignalSource::load_metadata_file(
const fs::path& metadata_path,
GnssMetadata::Metadata& metadata,
std::vector<std::string>& include_stack)
{
const auto metadata_key = absolute_path_key(metadata_path).string();
if (std::find(include_stack.begin(), include_stack.end(), metadata_key) != include_stack.end())
{
throw std::runtime_error("ION_GSMS metadata include cycle detected at " + metadata_path.string());
}
include_stack.push_back(metadata_key);
try
{
GnssMetadata::XmlProcessor xml_proc;
const auto metadata_path_string = metadata_path.string();
if (!xml_proc.Load(metadata_path_string.c_str(), false, metadata))
{
throw std::runtime_error("Could not load XML metadata file " + metadata_path_string);
}
const auto metadata_directory = metadata_path.parent_path();
for (const auto& file : metadata.Files())
{
metadata_files_.push_back({&file, metadata_directory});
}
const std::vector<GnssMetadata::AnyUri> includes(metadata.Includes().begin(), metadata.Includes().end());
metadata.Includes().clear();
for (const auto& include : includes)
{
GnssMetadata::Metadata included_metadata;
load_metadata_file(
resolve_local_metadata_uri(metadata_path, include.Value()),
included_metadata,
include_stack);
metadata.Splice(included_metadata);
}
}
catch (...)
{
include_stack.pop_back();
throw;
}
include_stack.pop_back();
}
std::vector<std::string> IONGSMSSignalSource::parse_comma_list(const std::string& str)
{
std::vector<std::string> list{};
@@ -175,13 +278,24 @@ std::size_t IONGSMSSignalSource::chunk_cycle_bytes(const GnssMetadata::Block& bl
std::size_t IONGSMSSignalSource::infer_block_cycles(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
const std::size_t block_start_offset)
const std::size_t block_start_offset,
const bool block_extends_to_eof)
{
if (block.Cycles() != 0)
{
return block.Cycles();
}
if (!block_extends_to_eof)
{
throw std::runtime_error(
"ION_GSMS_Signal_Source block has cycles=0 before the final lane block; "
"refusing EOF-based cycle inference because later blocks would be unreachable");
}
LOG(WARNING) << "ION_GSMS_Signal_Source block at offset " << block_start_offset
<< " in " << data_filepath.string()
<< " has cycles=0; inferring cycle count from EOF. This is a non-standard metadata extension supported only for the final block in a lane.";
const std::size_t cycle_bytes = chunk_cycle_bytes(block);
if (cycle_bytes == 0)
{
@@ -208,56 +322,256 @@ std::size_t IONGSMSSignalSource::infer_block_cycles(
std::size_t IONGSMSSignalSource::block_storage_bytes(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
const std::size_t block_start_offset)
const std::size_t block_start_offset,
const bool block_extends_to_eof)
{
const std::size_t cycle_count = infer_block_cycles(data_filepath, block, block_start_offset);
const std::size_t cycle_count = infer_block_cycles(data_filepath, block, block_start_offset, block_extends_to_eof);
return block.SizeHeader() + cycle_count * chunk_cycle_bytes(block) + block.SizeFooter();
}
std::vector<IONGSMSFileSource::sptr> IONGSMSSignalSource::make_stream_sources(const std::vector<std::string>& stream_ids) const
std::vector<IONGSMSSignalSource::StreamSourceData> IONGSMSSignalSource::make_stream_sources(const std::vector<std::string>& stream_ids) const
{
std::vector<IONGSMSFileSource::sptr> sources{};
for (const auto& file : metadata_->Files())
std::vector<StreamSourceData> sources{};
const auto files = ordered_metadata_files();
for (const auto& stream_id : stream_ids)
{
const fs::path data_filepath = fs::path(metadata_filepath_).parent_path() / file.Url().Value();
for (const auto& lane : metadata_->Lanes())
std::vector<IONGSMSFileSource::SegmentDescriptor> segments{};
std::int64_t stream_sampling_frequency = 0;
for (const auto& metadata_file : files)
{
if (lane.Id() == file.Lane().Id())
const auto* file = metadata_file.file;
const fs::path data_filepath = metadata_file.metadata_directory / file->Url().Value();
for (const auto& lane : metadata_->Lanes())
{
std::size_t block_start_offset = file.Offset();
for (const auto& block : lane.Blocks())
if (lane.Id() == file->Lane().Id())
{
if (block_contains_stream(block, stream_ids))
std::size_t block_start_offset = file->Offset();
const auto& blocks = lane.Blocks();
for (auto block_iter = blocks.begin(); block_iter != blocks.end(); ++block_iter)
{
auto source = gnss_make_shared<IONGSMSFileSource>(
metadata_filepath_,
file,
block,
block_start_offset,
stream_ids);
sources.push_back(source);
const auto& block = *block_iter;
const bool block_extends_to_eof = std::next(block_iter) == blocks.end();
if (block_contains_stream(block, std::vector<std::string>{stream_id}))
{
segments.push_back({data_filepath, &block, block_start_offset, block_extends_to_eof});
stream_sampling_frequency = reconcile_sampling_frequency(
stream_sampling_frequency,
stream_sampling_frequency_hz(lane, block, stream_id),
stream_id);
}
if (!block_extends_to_eof)
{
block_start_offset += block_storage_bytes(data_filepath, block, block_start_offset, block_extends_to_eof);
}
}
block_start_offset += block_storage_bytes(data_filepath, block, block_start_offset);
break;
}
break;
}
}
if (segments.empty())
{
throw std::runtime_error("ION_GSMS_Signal_Source requested stream '" + stream_id + "' was not found in the metadata");
}
if (stream_sampling_frequency == 0)
{
stream_sampling_frequency = sampling_frequency_;
}
sources.push_back({gnss_make_shared<IONGSMSFileSource>(segments, std::vector<std::string>{stream_id}), stream_sampling_frequency});
}
return sources;
}
std::uint64_t IONGSMSSignalSource::valve_sample_count(std::uint64_t total_sample_count) const
std::vector<IONGSMSSignalSource::MetadataFileData> IONGSMSSignalSource::ordered_metadata_files() const
{
if (total_sample_count == 0 || sampling_frequency_ <= 0)
std::vector<MetadataFileData> files = metadata_files_;
if (files.empty())
{
const auto metadata_directory = fs::path(metadata_filepath_).parent_path();
for (const auto& file : metadata_->Files())
{
files.push_back({&file, metadata_directory});
}
}
std::vector<MetadataFileData> ordered_files{};
std::vector<MetadataFileData> remaining_files = files;
auto find_by_url = [](const std::vector<MetadataFileData>& candidates, const std::string& url) {
return std::find_if(candidates.begin(), candidates.end(), [&url](const auto& file) {
return file.file != nullptr && file.file->Url().Value() == url;
});
};
auto append_chain = [&ordered_files, &remaining_files, &find_by_url](MetadataFileData file_data) {
while (file_data.file != nullptr)
{
ordered_files.push_back(file_data);
remaining_files.erase(std::remove_if(remaining_files.begin(), remaining_files.end(), [&file_data](const auto& candidate) {
return candidate.file == file_data.file;
}),
remaining_files.end());
const auto next = file_data.file->Next().Value();
if (next.empty())
{
file_data = {};
}
else
{
const auto next_file = find_by_url(remaining_files, next);
file_data = next_file == remaining_files.end() ? MetadataFileData{} : *next_file;
}
}
};
for (const auto& fileset : metadata_->FileSets())
{
for (const auto& file_url : fileset.FileUrls())
{
const auto first_file = find_by_url(remaining_files, file_url.Value());
if (first_file != remaining_files.end())
{
append_chain(*first_file);
}
}
}
while (!remaining_files.empty())
{
auto first_file = std::find_if(remaining_files.begin(), remaining_files.end(), [&remaining_files, &find_by_url](const auto& file) {
const auto previous = file.file->Previous().Value();
return previous.empty() || find_by_url(remaining_files, previous) == remaining_files.end();
});
if (first_file == remaining_files.end())
{
first_file = remaining_files.begin();
}
append_chain(*first_file);
}
return ordered_files;
}
const GnssMetadata::System* IONGSMSSignalSource::resolve_system(const GnssMetadata::System& system) const
{
if (!system.IsReference() && system.BaseFrequency().toHertz() > 0.0)
{
return &system;
}
for (const auto& metadata_system : metadata_->Systems())
{
if (metadata_system.Id() == system.Id())
{
return &metadata_system;
}
}
return system.IsReference() ? nullptr : &system;
}
double IONGSMSSignalSource::lane_base_frequency_hz(const GnssMetadata::Lane& lane) const
{
double base_frequency = 0.0;
for (const auto& lane_system : lane.Systems())
{
const auto* system = resolve_system(lane_system);
if (system == nullptr)
{
continue;
}
const double system_base_frequency = system->BaseFrequency().toHertz();
if (system_base_frequency <= 0.0)
{
continue;
}
if (base_frequency > 0.0 && std::llround(base_frequency) != std::llround(system_base_frequency))
{
throw std::runtime_error("ION_GSMS_Signal_Source lane references systems with inconsistent freqbase values");
}
base_frequency = system_base_frequency;
}
return base_frequency;
}
std::int64_t IONGSMSSignalSource::stream_sampling_frequency_hz(
const GnssMetadata::Lane& lane,
const GnssMetadata::Block& block,
const std::string& stream_id) const
{
const double base_frequency = lane_base_frequency_hz(lane);
if (base_frequency <= 0.0)
{
return 0;
}
std::size_t rate_factor = 0;
for (const auto& chunk : block.Chunks())
{
for (const auto& lump : chunk.Lumps())
{
for (const auto& stream : lump.Streams())
{
if (stream.Id() != stream_id)
{
continue;
}
if (rate_factor != 0 && rate_factor != stream.RateFactor())
{
throw std::runtime_error("ION_GSMS_Signal_Source stream '" + stream_id + "' appears with inconsistent ratefactor values");
}
rate_factor = stream.RateFactor();
}
}
}
if (rate_factor == 0)
{
return 0;
}
const double sampling_frequency = base_frequency * static_cast<double>(rate_factor);
if (sampling_frequency <= 0.0 || sampling_frequency > static_cast<double>(std::numeric_limits<std::int64_t>::max()))
{
throw std::runtime_error("ION_GSMS_Signal_Source stream '" + stream_id + "' has an unsupported sampling frequency");
}
return static_cast<std::int64_t>(std::llround(sampling_frequency));
}
std::int64_t IONGSMSSignalSource::reconcile_sampling_frequency(
const std::int64_t current_frequency,
const std::int64_t candidate_frequency,
const std::string& stream_id)
{
if (candidate_frequency <= 0)
{
return current_frequency;
}
if (current_frequency > 0 && current_frequency != candidate_frequency)
{
throw std::runtime_error("ION_GSMS_Signal_Source stream '" + stream_id + "' appears with inconsistent sampling frequencies");
}
return candidate_frequency;
}
std::uint64_t IONGSMSSignalSource::valve_sample_count(std::uint64_t total_sample_count, const std::int64_t sampling_frequency) const
{
if (total_sample_count == 0 || sampling_frequency <= 0)
{
return total_sample_count;
}
const auto tail_samples = static_cast<std::uint64_t>(std::ceil(minimum_tail_s_ * static_cast<double>(sampling_frequency_)));
const auto tail_samples = static_cast<std::uint64_t>(std::ceil(minimum_tail_s_ * static_cast<double>(sampling_frequency)));
if (total_sample_count <= tail_samples)
{
std::cout << "Warning: ION_GSMS_Signal_Source stream has " << total_sample_count
@@ -273,8 +587,9 @@ std::uint64_t IONGSMSSignalSource::valve_sample_count(std::uint64_t total_sample
void IONGSMSSignalSource::connect(gr::top_block_sptr top_block)
{
std::size_t cumulative_index = 0;
for (const auto& source : sources_)
for (const auto& source_data : sources_)
{
const auto& source = source_data.source;
for (std::size_t i = 0; i < source->output_stream_count(); ++i, ++cumulative_index)
{
top_block->connect(source, i, copy_blocks_[cumulative_index], 0);
@@ -287,8 +602,9 @@ void IONGSMSSignalSource::connect(gr::top_block_sptr top_block)
void IONGSMSSignalSource::disconnect(gr::top_block_sptr top_block)
{
std::size_t cumulative_index = 0;
for (const auto& source : sources_)
for (const auto& source_data : sources_)
{
const auto& source = source_data.source;
for (std::size_t i = 0; i < source->output_stream_count(); ++i, ++cumulative_index)
{
top_block->disconnect(source, i, copy_blocks_[cumulative_index], 0);
@@ -314,14 +630,16 @@ gr::basic_block_sptr IONGSMSSignalSource::get_right_block()
gr::basic_block_sptr IONGSMSSignalSource::get_right_block(int RF_channel)
{
if (RF_channel < 0 || RF_channel >= static_cast<int>(copy_blocks_.size()))
if (RF_channel < 0 || RF_channel >= static_cast<int>(valves_.size()))
{
LOG(WARNING) << "'RF_channel' out of bounds while trying to get signal source right block.";
if (valves_.empty())
{
return {};
}
return valves_[0];
return {};
}
return valves_[RF_channel];
}
size_t IONGSMSSignalSource::getRfChannels() const
{
return valves_.size();
}
@@ -21,6 +21,7 @@
#include "configuration_interface.h"
#include "file_source_base.h"
#include "gnss_sdr_filesystem.h"
#include "gnss_sdr_timestamp.h"
#include "ion_gsms.h"
#include <GnssMetadata.h>
@@ -54,6 +55,7 @@ protected:
gr::basic_block_sptr get_left_block() override;
gr::basic_block_sptr get_right_block() override;
gr::basic_block_sptr get_right_block(int RF_channel) override;
size_t getRfChannels() const override;
inline size_t item_size() override
{
@@ -61,36 +63,70 @@ protected:
{
return 0;
}
return (*sources_.begin())->output_stream_item_size(0);
return sources_.begin()->source->output_stream_item_size(0);
}
private:
static constexpr double kMinimumTailSeconds = 0.2;
struct StreamSourceData
{
IONGSMSFileSource::sptr source;
std::int64_t sampling_frequency = 0;
};
struct MetadataFileData
{
const GnssMetadata::File* file = nullptr;
fs::path metadata_directory;
};
static std::vector<std::string> parse_comma_list(const std::string& str);
static bool block_contains_stream(const GnssMetadata::Block& block, const std::vector<std::string>& stream_ids);
static std::size_t chunk_cycle_bytes(const GnssMetadata::Block& block);
static std::size_t infer_block_cycles(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
std::size_t block_start_offset);
std::size_t block_start_offset,
bool block_extends_to_eof);
static std::size_t block_storage_bytes(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
std::size_t block_start_offset);
std::size_t block_start_offset,
bool block_extends_to_eof);
static const char* file_uri_scheme();
static bool starts_with(const std::string& value, const std::string& prefix);
static fs::path absolute_path_key(const fs::path& path);
static fs::path resolve_local_metadata_uri(const fs::path& metadata_path, const std::string& uri);
std::vector<IONGSMSFileSource::sptr> make_stream_sources(const std::vector<std::string>& stream_ids) const;
std::vector<StreamSourceData> make_stream_sources(const std::vector<std::string>& stream_ids) const;
std::vector<MetadataFileData> ordered_metadata_files() const;
const GnssMetadata::System* resolve_system(const GnssMetadata::System& system) const;
double lane_base_frequency_hz(const GnssMetadata::Lane& lane) const;
std::int64_t stream_sampling_frequency_hz(
const GnssMetadata::Lane& lane,
const GnssMetadata::Block& block,
const std::string& stream_id) const;
static std::int64_t reconcile_sampling_frequency(
std::int64_t current_frequency,
std::int64_t candidate_frequency,
const std::string& stream_id);
void load_metadata();
std::uint64_t valve_sample_count(std::uint64_t total_sample_count) const;
void load_metadata_file(
const fs::path& metadata_path,
GnssMetadata::Metadata& metadata,
std::vector<std::string>& include_stack);
std::uint64_t valve_sample_count(std::uint64_t total_sample_count, std::int64_t sampling_frequency) const;
std::vector<std::string> stream_ids_;
std::vector<IONGSMSFileSource::sptr> sources_;
std::vector<StreamSourceData> sources_;
std::vector<gnss_shared_ptr<gr::block>> copy_blocks_;
std::vector<gnss_shared_ptr<gr::block>> valves_;
std::string metadata_filepath_;
std::shared_ptr<GnssMetadata::Metadata> metadata_;
std::vector<MetadataFileData> metadata_files_;
gnss_shared_ptr<Gnss_Sdr_Timestamp> timestamp_block_;
std::string timestamp_file_;
@@ -35,70 +35,120 @@ IONGSMSFileSource::IONGSMSFileSource(
const GnssMetadata::Block& block,
const std::size_t block_start_offset,
const std::vector<std::string>& stream_ids)
: IONGSMSFileSource(
std::vector<SegmentDescriptor>{make_segment_descriptor(metadata_filepath, file, block, block_start_offset)},
stream_ids)
{
}
IONGSMSFileSource::IONGSMSFileSource(
const std::vector<SegmentDescriptor>& segments,
const std::vector<std::string>& stream_ids)
: gr::sync_block(
"ion_gsms_file_source",
gr::io_signature::make(0, 0, 0),
make_output_signature(block, stream_ids)),
file_stream_(metadata_filepath.parent_path() / file.Url().Value(), std::ios::in | std::ios::binary),
make_output_signature(segments, stream_ids)),
io_buffer_offset_(0),
maximum_item_rate_(0),
chunk_cycle_length_(0),
cycles_remaining_(0)
cycles_remaining_(0),
current_segment_index_(0),
output_stream_ids_(segment_output_stream_ids(segments, stream_ids))
{
fs::path data_filepath = metadata_filepath.parent_path() / file.Url().Value();
const auto output_stream_ids = block_output_stream_ids(block, stream_ids);
if (!file_stream_.is_open())
{
LOG(WARNING) << "ION_GSMS_Signal_Source - Unable to open the samples file: " << (data_filepath).c_str();
std::cerr << "ION_GSMS_Signal_Source - Unable to open the samples file: " << (data_filepath).c_str() << std::endl;
std::cout << "GNSS-SDR program ended.\n";
exit(1);
}
// Skip to this block's sample payload, after the lane offset and block header.
file_stream_.seekg(static_cast<std::streamoff>(block_start_offset + block.SizeHeader()), std::ios::beg);
output_stream_count_ = output_stream_ids.size();
output_stream_count_ = output_stream_ids_.size();
output_stream_item_sizes_.assign(output_stream_count_, 0);
output_stream_item_rates_.assign(output_stream_count_, 0);
output_stream_total_sample_counts_.assign(output_stream_count_, 0);
for (const auto& chunk : block.Chunks())
if (segments.empty())
{
chunk_data_.emplace_back(std::make_shared<IONGSMSChunkData>(chunk, output_stream_ids, 0));
chunk_cycle_length_ += chunk.CountWords() * chunk.SizeWord();
throw std::runtime_error("ION_GSMS_Signal_Source requires at least one source segment");
}
for (const auto& descriptor : segments)
{
if (descriptor.block == nullptr)
{
throw std::runtime_error("ION_GSMS_Signal_Source source segment has no block metadata");
}
SegmentData segment;
segment.data_filepath = descriptor.data_filepath;
segment.block = descriptor.block;
segment.block_start_offset = descriptor.block_start_offset;
segment.output_stream_item_rates.assign(output_stream_count_, 0);
for (const auto& chunk : descriptor.block->Chunks())
{
segment.chunk_data.emplace_back(std::make_shared<IONGSMSChunkData>(chunk, output_stream_ids_, 0));
segment.chunk_cycle_length += chunk.CountWords() * chunk.SizeWord();
for (std::size_t i = 0; i < output_stream_count_; ++i)
{
const auto chunk_item_rate = segment.chunk_data.back()->output_stream_item_rate(i);
if (chunk_item_rate == 0)
{
continue;
}
const auto chunk_item_size = segment.chunk_data.back()->output_stream_item_size(i);
if (output_stream_item_sizes_[i] != 0 && output_stream_item_sizes_[i] != chunk_item_size)
{
throw std::runtime_error("ION_GSMS_Signal_Source stream appears with inconsistent output item sizes");
}
output_stream_item_sizes_[i] = chunk_item_size;
segment.output_stream_item_rates[i] += chunk_item_rate;
segment.maximum_item_rate = std::max(segment.output_stream_item_rates[i], segment.maximum_item_rate);
}
}
std::size_t cycle_count = descriptor.block->Cycles();
if (cycle_count == 0)
{
if (!descriptor.block_extends_to_eof)
{
throw std::runtime_error(
"ION_GSMS_Signal_Source block has cycles=0 before the final lane block; "
"refusing EOF-based cycle inference because later blocks would be unreachable");
}
const std::string warning = "ION_GSMS_Signal_Source block at offset " + std::to_string(descriptor.block_start_offset) +
" in " + segment.data_filepath.string() +
" has cycles=0; inferring cycle count from EOF. This is a non-standard metadata extension supported only for the final block in a lane.";
LOG(WARNING) << warning;
std::cerr << "Warning: " << warning << std::endl;
cycle_count = infer_cycle_count_from_file(segment.data_filepath, *descriptor.block, descriptor.block_start_offset, segment.chunk_cycle_length);
}
segment.cycle_count = cycle_count;
for (std::size_t i = 0; i < output_stream_count_; ++i)
{
const auto chunk_item_rate = chunk_data_.back()->output_stream_item_rate(i);
if (chunk_item_rate == 0)
{
continue;
}
const auto chunk_item_size = chunk_data_.back()->output_stream_item_size(i);
if (output_stream_item_sizes_[i] != 0 && output_stream_item_sizes_[i] != chunk_item_size)
{
throw std::runtime_error("ION_GSMS_Signal_Source stream appears with inconsistent output item sizes");
}
output_stream_item_sizes_[i] = chunk_item_size;
output_stream_item_rates_[i] += chunk_item_rate;
maximum_item_rate_ = std::max(output_stream_item_rates_[i], maximum_item_rate_);
output_stream_item_rates_[i] = std::max(output_stream_item_rates_[i], segment.output_stream_item_rates[i]);
output_stream_total_sample_counts_[i] += cycle_count * segment.output_stream_item_rates[i];
}
maximum_item_rate_ = std::max(maximum_item_rate_, segment.maximum_item_rate);
segments_.push_back(std::move(segment));
}
output_stream_total_sample_counts_.resize(output_stream_count_);
std::size_t cycle_count = block.Cycles();
if (cycle_count == 0)
{
cycle_count = infer_cycle_count_from_file(data_filepath, block, block_start_offset, chunk_cycle_length_);
}
cycles_remaining_ = cycle_count;
for (std::size_t i = 0; i < output_stream_count_; ++i)
{
output_stream_total_sample_counts_[i] = cycle_count * output_stream_item_rates_[i];
if (output_stream_item_sizes_[i] == 0)
{
throw std::runtime_error("ION_GSMS_Signal_Source requested stream is not present in source segments");
}
}
current_segment_index_ = 0;
advance_to_next_segment();
}
IONGSMSFileSource::SegmentDescriptor IONGSMSFileSource::make_segment_descriptor(
const fs::path& metadata_filepath,
const GnssMetadata::File& file,
const GnssMetadata::Block& block,
const std::size_t block_start_offset)
{
return {metadata_filepath.parent_path() / file.Url().Value(), &block, block_start_offset, true};
}
@@ -166,6 +216,94 @@ int IONGSMSFileSource::output_item_size_for_stream_id(const GnssMetadata::Block&
}
std::vector<std::string> IONGSMSFileSource::segment_output_stream_ids(const std::vector<SegmentDescriptor>& segments, const std::vector<std::string>& stream_ids)
{
std::vector<std::string> output_stream_ids;
for (const auto& stream_id : stream_ids)
{
for (const auto& segment : segments)
{
if (segment.block != nullptr && block_contains_stream_id(*segment.block, stream_id))
{
output_stream_ids.push_back(stream_id);
break;
}
}
}
return output_stream_ids;
}
int IONGSMSFileSource::output_item_size_for_stream_id(const std::vector<SegmentDescriptor>& segments, const std::string& stream_id)
{
int item_size = 0;
for (const auto& segment : segments)
{
if (segment.block == nullptr)
{
continue;
}
const auto current_item_size = output_item_size_for_stream_id(*segment.block, stream_id);
if (current_item_size == 0)
{
continue;
}
if (item_size != 0 && item_size != current_item_size)
{
throw std::runtime_error("ION_GSMS_Signal_Source stream appears with inconsistent output item sizes");
}
item_size = current_item_size;
}
return item_size;
}
std::size_t IONGSMSFileSource::output_stream_count() const
{
return output_stream_count_;
}
std::size_t IONGSMSFileSource::output_stream_item_size(std::size_t stream_index) const
{
return output_stream_item_sizes_[stream_index];
}
std::size_t IONGSMSFileSource::output_stream_total_sample_count(std::size_t stream_index) const
{
return output_stream_total_sample_counts_[stream_index];
}
gr::io_signature::sptr IONGSMSFileSource::make_output_signature(const std::vector<SegmentDescriptor>& segments, const std::vector<std::string>& stream_ids)
{
const auto output_stream_ids = segment_output_stream_ids(segments, stream_ids);
if (output_stream_ids.empty())
{
throw std::runtime_error("ION_GSMS_Signal_Source requested streams are not present in source segments");
}
std::vector<int> item_sizes{};
for (const auto& stream_id : output_stream_ids)
{
const auto item_size = output_item_size_for_stream_id(segments, stream_id);
if (item_size == 0)
{
throw std::runtime_error("ION_GSMS_Signal_Source requested stream is not present in source segments");
}
item_sizes.push_back(item_size);
}
return gr::io_signature::makev(
static_cast<int>(item_sizes.size()),
static_cast<int>(item_sizes.size()),
item_sizes);
}
std::size_t IONGSMSFileSource::infer_cycle_count_from_file(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
@@ -194,43 +332,38 @@ std::size_t IONGSMSFileSource::infer_cycle_count_from_file(
}
std::size_t IONGSMSFileSource::output_stream_count() const
bool IONGSMSFileSource::advance_to_next_segment()
{
return output_stream_count_;
}
std::size_t IONGSMSFileSource::output_stream_item_size(std::size_t stream_index) const
{
return output_stream_item_sizes_[stream_index];
}
std::size_t IONGSMSFileSource::output_stream_total_sample_count(std::size_t stream_index) const
{
return output_stream_total_sample_counts_[stream_index];
}
gr::io_signature::sptr IONGSMSFileSource::make_output_signature(const GnssMetadata::Block& block, const std::vector<std::string>& stream_ids)
{
const auto output_stream_ids = block_output_stream_ids(block, stream_ids);
std::vector<int> item_sizes{};
for (const auto& stream_id : output_stream_ids)
file_stream_.close();
while (current_segment_index_ < segments_.size())
{
const auto item_size = output_item_size_for_stream_id(block, stream_id);
if (item_size == 0)
auto& segment = segments_[current_segment_index_];
if (segment.cycle_count == 0)
{
throw std::runtime_error("ION_GSMS_Signal_Source requested stream is not present in block");
++current_segment_index_;
continue;
}
item_sizes.push_back(item_size);
file_stream_.open(segment.data_filepath, std::ios::in | std::ios::binary);
if (!file_stream_.is_open())
{
LOG(WARNING) << "ION_GSMS_Signal_Source - Unable to open the samples file: " << segment.data_filepath.c_str();
std::cerr << "ION_GSMS_Signal_Source - Unable to open the samples file: " << segment.data_filepath.c_str() << std::endl;
std::cout << "GNSS-SDR program ended.\n";
exit(1);
}
file_stream_.seekg(static_cast<std::streamoff>(segment.block_start_offset + segment.block->SizeHeader()), std::ios::beg);
cycles_remaining_ = segment.cycle_count;
chunk_cycle_length_ = segment.chunk_cycle_length;
maximum_item_rate_ = segment.maximum_item_rate;
return true;
}
return gr::io_signature::makev(
static_cast<int>(item_sizes.size()),
static_cast<int>(item_sizes.size()),
item_sizes);
cycles_remaining_ = 0;
chunk_cycle_length_ = 0;
maximum_item_rate_ = 0;
return false;
}
@@ -241,7 +374,10 @@ int IONGSMSFileSource::work(
{
if (cycles_remaining_ == 0)
{
return WORK_DONE;
if (!advance_to_next_segment())
{
return WORK_DONE;
}
}
if (noutput_items <= 0 || maximum_item_rate_ == 0 || chunk_cycle_length_ == 0)
{
@@ -256,6 +392,7 @@ int IONGSMSFileSource::work(
return 0;
}
const std::size_t cycles_to_read = std::min(max_sample_output, cycles_remaining_);
auto& segment = segments_[current_segment_index_];
// Resize the IO buffer to fit exactly the maximum amount of samples that will be outputted.
io_buffer_.resize(cycles_to_read * chunk_cycle_length_);
@@ -272,7 +409,8 @@ int IONGSMSFileSource::work(
if (cycles_read == 0)
{
cycles_remaining_ = 0;
return WORK_DONE;
++current_segment_index_;
return 0;
}
cycles_remaining_ = cycles_read >= cycles_remaining_ ? 0 : cycles_remaining_ - cycles_read;
if (bytes_read < bytes_to_read)
@@ -288,7 +426,7 @@ int IONGSMSFileSource::work(
while (io_buffer_offset_ < bytes_to_decode)
{
// Iterate chunks within a chunk cycle
for (auto& chunk : chunk_data_)
for (auto& chunk : segment.chunk_data)
{
// Copy chunk into a separate buffer where the samples will be shifted from.
const std::size_t bytes_copied = chunk->read_from_buffer(reinterpret_cast<uint8_t*>(io_buffer_.data()), io_buffer_offset_);
@@ -301,6 +439,11 @@ int IONGSMSFileSource::work(
}
}
if (cycles_remaining_ == 0)
{
++current_segment_index_;
}
// Call `produce(int, int)` with the appropriate item count for each output stream.
for (std::size_t i = 0; i < items_produced_.size(); ++i)
{
@@ -39,6 +39,14 @@ class IONGSMSFileSource : public gr::sync_block
public:
using sptr = gnss_shared_ptr<IONGSMSFileSource>;
struct SegmentDescriptor
{
fs::path data_filepath;
const GnssMetadata::Block* block = nullptr;
std::size_t block_start_offset = 0;
bool block_extends_to_eof = false;
};
IONGSMSFileSource(
const fs::path& metadata_filepath,
const GnssMetadata::File& file,
@@ -46,6 +54,10 @@ public:
std::size_t block_start_offset,
const std::vector<std::string>& stream_ids);
IONGSMSFileSource(
const std::vector<SegmentDescriptor>& segments,
const std::vector<std::string>& stream_ids);
int work(
int noutput_items,
gr_vector_const_void_star& input_items,
@@ -56,15 +68,35 @@ public:
std::size_t output_stream_total_sample_count(std::size_t stream_index) const;
private:
static gr::io_signature::sptr make_output_signature(const GnssMetadata::Block& block, const std::vector<std::string>& stream_ids);
struct SegmentData
{
fs::path data_filepath;
const GnssMetadata::Block* block = nullptr;
std::size_t block_start_offset = 0;
std::size_t cycle_count = 0;
std::size_t chunk_cycle_length = 0;
std::size_t maximum_item_rate = 0;
std::vector<std::size_t> output_stream_item_rates;
std::vector<std::shared_ptr<IONGSMSChunkData>> chunk_data;
};
static SegmentDescriptor make_segment_descriptor(
const fs::path& metadata_filepath,
const GnssMetadata::File& file,
const GnssMetadata::Block& block,
std::size_t block_start_offset);
static gr::io_signature::sptr make_output_signature(const std::vector<SegmentDescriptor>& segments, const std::vector<std::string>& stream_ids);
static bool block_contains_stream_id(const GnssMetadata::Block& block, const std::string& stream_id);
static std::vector<std::string> block_output_stream_ids(const GnssMetadata::Block& block, const std::vector<std::string>& stream_ids);
static std::vector<std::string> segment_output_stream_ids(const std::vector<SegmentDescriptor>& segments, const std::vector<std::string>& stream_ids);
static int output_item_size_for_stream_id(const GnssMetadata::Block& block, const std::string& stream_id);
static int output_item_size_for_stream_id(const std::vector<SegmentDescriptor>& segments, const std::string& stream_id);
static std::size_t infer_cycle_count_from_file(
const fs::path& data_filepath,
const GnssMetadata::Block& block,
std::size_t block_start_offset,
std::size_t chunk_cycle_length);
bool advance_to_next_segment();
std::ifstream file_stream_;
std::vector<char> io_buffer_;
@@ -75,9 +107,11 @@ private:
std::vector<std::size_t> output_stream_item_rates_;
std::vector<std::size_t> output_stream_total_sample_counts_;
std::size_t maximum_item_rate_;
std::vector<std::shared_ptr<IONGSMSChunkData>> chunk_data_;
std::size_t chunk_cycle_length_;
std::size_t cycles_remaining_;
std::vector<SegmentData> segments_;
std::size_t current_segment_index_;
std::vector<std::string> output_stream_ids_;
};
/** \} */
@@ -66,13 +66,10 @@ IONGSMSChunkData::IONGSMSChunkData(const GnssMetadata::Chunk& chunk, const std::
throw std::runtime_error("ION_GSMS_Signal_Source metadata describes a lump pattern larger than its chunk");
}
const std::size_t pattern_repeat_count = total_bitsize / pattern_bitsize;
output_stream_count_ = stream_ids.size();
output_stream_item_size_.assign(output_stream_count_, 0);
output_stream_item_rate_.assign(output_stream_count_, 0);
std::vector<bool> output_stream_seen(output_stream_count_, false);
std::vector<std::size_t> pattern_output_item_rates(output_stream_count_, 0);
std::vector<stream_metadata_t> pattern_streams;
for (const auto& lump : chunk.Lumps())
{
for (const auto& stream : lump.Streams())
@@ -90,8 +87,8 @@ IONGSMSChunkData::IONGSMSChunkData(const GnssMetadata::Chunk& chunk, const std::
output_index = static_cast<int>(relative_output_index + output_stream_offset);
output_item_rate = stream_output_item_rate(stream);
output_item_size = stream_output_item_size(stream);
output_item_offset = pattern_output_item_rates[relative_output_index];
pattern_output_item_rates[relative_output_index] += output_item_rate;
output_item_offset = output_stream_item_rate_[relative_output_index];
output_stream_item_rate_[relative_output_index] += output_item_rate;
if (output_stream_item_size_[relative_output_index] != 0 &&
output_stream_item_size_[relative_output_index] != output_item_size)
@@ -107,40 +104,11 @@ IONGSMSChunkData::IONGSMSChunkData(const GnssMetadata::Chunk& chunk, const std::
}
}
pattern_streams.emplace_back(lump, stream, stream_encoding, output_index, output_item_offset, output_item_size);
streams_.emplace_back(lump, stream, stream_encoding, output_index, output_item_offset, output_item_size);
}
}
for (std::size_t i = 0; i < output_stream_item_rate_.size(); ++i)
{
output_stream_item_rate_[i] = pattern_output_item_rates[i] * pattern_repeat_count;
}
for (std::size_t repeat = 0; repeat < pattern_repeat_count; ++repeat)
{
for (const auto& stream_metadata : pattern_streams)
{
std::size_t output_item_offset = 0;
if (stream_metadata.output_index != -1)
{
const auto relative_output_index = static_cast<std::size_t>(stream_metadata.output_index) - output_stream_offset_;
const std::size_t chronological_repeat = stream_metadata.lump.Shift() == GnssMetadata::Lump::shiftRight
? (pattern_repeat_count - repeat - 1)
: repeat;
output_item_offset = chronological_repeat * pattern_output_item_rates[relative_output_index] +
stream_metadata.output_item_offset;
}
streams_.emplace_back(
stream_metadata.lump,
stream_metadata.stream,
stream_metadata.stream_encoding,
stream_metadata.output_index,
output_item_offset,
stream_metadata.output_item_size);
}
}
padding_bitsize_ = total_bitsize - pattern_bitsize * pattern_repeat_count;
padding_bitsize_ = total_bitsize - pattern_bitsize;
}
@@ -258,12 +226,7 @@ void IONGSMSChunkData::unpack_words(gr_vector_void_star& outputs, std::vector<in
}
}
if (chunk_.Shift() == GnssMetadata::Chunk::Right)
{
std::reverse(data, data + countwords_);
}
IONGSMSChunkUnpackingCtx<WT> ctx{data, countwords_};
IONGSMSChunkUnpackingCtx<WT> ctx{data, countwords_, chunk_.Shift() == GnssMetadata::Chunk::Right};
// Head padding
if (padding_bitsize_ > 0 && chunk_.Padding() == GnssMetadata::Chunk::Head)
@@ -36,11 +36,14 @@ struct IONGSMSChunkUnpackingCtx
const WT* data_ = nullptr; // Not owned by this class, MUST NOT destroy.
std::size_t word_count_ = 0;
std::size_t bit_offset_ = 0;
bool read_lsb_first_ = false;
IONGSMSChunkUnpackingCtx(
WT* data_buffer,
std::size_t data_buffer_word_count) : data_(data_buffer),
word_count_(data_buffer_word_count)
std::size_t data_buffer_word_count,
bool read_lsb_first) : data_(data_buffer),
word_count_(data_buffer_word_count),
read_lsb_first_(read_lsb_first)
{
}
@@ -61,9 +64,17 @@ struct IONGSMSChunkUnpackingCtx
throw std::runtime_error("ION_GSMS_Signal_Source tried to read past the chunk boundary");
}
const std::size_t bit_index = absolute_bit % word_bitsize_;
const std::size_t word_bit = word_bitsize_ - 1 - bit_index;
value <<= 1;
value |= (static_cast<uint64_t>(data_[word_index]) >> word_bit) & 0x01U;
const std::size_t word_bit = read_lsb_first_ ? bit_index : word_bitsize_ - 1 - bit_index;
const uint64_t bit_value = (static_cast<uint64_t>(data_[word_index]) >> word_bit) & 0x01U;
if (read_lsb_first_)
{
value |= bit_value << bit;
}
else
{
value <<= 1;
value |= bit_value;
}
}
bit_offset_ += bit_count;
+1
View File
@@ -1166,6 +1166,7 @@ if(NOT ENABLE_PACKAGING AND NOT ENABLE_FPGA)
GTest::GTest
GTest::Main
Volkgnsssdr::volkgnsssdr
signal_source_adapters
signal_source_gr_blocks
signal_source_libs
core_receiver
@@ -15,12 +15,16 @@
#include "gnss_sdr_filesystem.h"
#include "gnss_sdr_flags.h"
#include "gnss_sdr_make_unique.h"
#include "in_memory_configuration.h"
#include "ion_gsms.h"
#include "ion_gsms_chunk_data.h"
#include "ion_gsms_signal_source.h"
#include <gnuradio/blocks/vector_sink.h>
#include <gnuradio/gr_complex.h>
#include <gnuradio/top_block.h>
#include <gtest/gtest.h>
#include <pmt/pmt.h>
#include <cstdint>
#include <cstring>
#include <fstream>
@@ -196,7 +200,7 @@ GnssMetadata::Block make_file_block(
const std::size_t size_header = 0,
const std::size_t size_footer = 0)
{
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 2, 1);
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 8, 4);
auto chunk = make_chunk(stream, 1, 1);
GnssMetadata::Block block;
@@ -218,6 +222,13 @@ void write_binary_file(const fs::path& path, const std::vector<uint8_t>& bytes)
}
void write_text_file(const fs::path& path, const std::string& text)
{
std::ofstream file(path.c_str(), std::ios::out | std::ios::trunc);
file << text;
}
GnssMetadata::File make_data_file(const fs::path& data_path, const std::size_t offset)
{
GnssMetadata::File file;
@@ -250,6 +261,26 @@ std::vector<int8_t> run_file_source(
}
std::vector<int8_t> run_file_source(
const std::vector<IONGSMSFileSource::SegmentDescriptor>& segments)
{
auto source = gnss_make_shared<IONGSMSFileSource>(segments, std::vector<std::string>{"L1"});
auto sink = gr::blocks::vector_sink_b::make();
auto top_block = gr::make_top_block("IONGSMSFileSourceTest");
top_block->connect(source, 0, sink, 0);
top_block->run();
const auto sink_data = sink->data();
std::vector<int8_t> output;
output.reserve(sink_data.size());
for (const auto item : sink_data)
{
output.push_back(static_cast<int8_t>(item));
}
return output;
}
std::vector<gr_complex> run_complex_file_source(
const fs::path& metadata_path,
const GnssMetadata::File& file,
@@ -311,6 +342,17 @@ TEST(IONGSMSChunkDataTest, HonorsStreamShiftRightWithoutOverrun)
}
TEST(IONGSMSChunkDataTest, HonorsRightShiftedChunkBitOrder)
{
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 8, 4);
const auto output = decode_int8_stream(stream, {0b00111001}, 1, 1, GnssMetadata::Chunk::Undefined, GnssMetadata::Chunk::Tail, GnssMetadata::Chunk::Right);
const std::vector<int8_t> expected{1, -2, -1, 0};
EXPECT_EQ(expected, output);
}
TEST(IONGSMSChunkDataTest, DecodesBigEndianWordsAsUnsigned)
{
const auto stream = make_stream(GnssMetadata::IonStream::IF, "OB", 4, 16, 1, GnssMetadata::IonStream::Left);
@@ -322,13 +364,13 @@ TEST(IONGSMSChunkDataTest, DecodesBigEndianWordsAsUnsigned)
}
TEST(IONGSMSChunkDataTest, RepeatsSingleLumpPayloadToFillChunk)
TEST(IONGSMSChunkDataTest, DecodesOneDeclaredLumpAndLeavesTailPadding)
{
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 2, 1);
const auto output = decode_int8_stream(stream, {0b01101100});
const std::vector<int8_t> expected{1, -2, -1, 0};
const std::vector<int8_t> expected{1};
EXPECT_EQ(expected, output);
}
@@ -362,7 +404,7 @@ TEST(IONGSMSChunkDataTest, DecodesMultipleLumpsInDeclaredOrder)
}
TEST(IONGSMSChunkDataTest, RepeatsOrderedMultiLumpPatternToFillChunk)
TEST(IONGSMSChunkDataTest, DecodesOrderedMultiLumpPatternOnce)
{
const auto first_stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 2, 1, GnssMetadata::IonStream::Undefined, GnssMetadata::IonStream::shiftUndefined, "L1");
const auto second_stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 2, 1, GnssMetadata::IonStream::Undefined, GnssMetadata::IonStream::shiftUndefined, "L2");
@@ -384,8 +426,8 @@ TEST(IONGSMSChunkDataTest, RepeatsOrderedMultiLumpPatternToFillChunk)
const auto output = decode_int8_chunk(chunk, {"L1", "L2"}, {0b01101100});
ASSERT_EQ(2U, output.size());
const std::vector<int8_t> expected_first{1, -1};
const std::vector<int8_t> expected_second{-2, 0};
const std::vector<int8_t> expected_first{1};
const std::vector<int8_t> expected_second{-2};
EXPECT_EQ(expected_first, output[0]);
EXPECT_EQ(expected_second, output[1]);
}
@@ -412,7 +454,7 @@ TEST(IONGSMSChunkDataTest, CollapsesRepeatedStreamIdIntoOneOutput)
const auto output = decode_int8_chunk(chunk, {"L1"}, {0b01101100});
ASSERT_EQ(1U, output.size());
const std::vector<int8_t> expected{1, -2, -1, 0};
const std::vector<int8_t> expected{1, -2};
EXPECT_EQ(expected, output[0]);
}
@@ -506,6 +548,38 @@ TEST(IONGSMSFileSourceTest, DecodesOnlyCompleteCyclesReadFromFile)
}
TEST(IONGSMSFileSourceTest, InfersZeroCyclesFromEofForStandaloneBlock)
{
const fs::path temp_dir(GetTempDir());
const fs::path data_path = temp_dir / "ion_gsms_file_source_zero_cycles_standalone.bin";
const fs::path metadata_path = temp_dir / "ion_gsms_file_source_zero_cycles_standalone.sdrx";
write_binary_file(data_path, {static_cast<uint8_t>(0b01101100U), static_cast<uint8_t>(0b00011011U)});
const auto file = make_data_file(data_path, 0);
const auto block = make_file_block(0);
const auto output = run_file_source(metadata_path, file, block, 0);
const std::vector<int8_t> expected{1, -2, -1, 0, 0, 1, -2, -1};
EXPECT_EQ(expected, output);
fs::remove(data_path);
}
TEST(IONGSMSFileSourceTest, RejectsZeroCycleSegmentUnlessItExtendsToEof)
{
const fs::path temp_dir(GetTempDir());
const fs::path data_path = temp_dir / "ion_gsms_file_source_zero_cycles_nonfinal.bin";
const auto block = make_file_block(0);
const std::vector<IONGSMSFileSource::SegmentDescriptor> segments{
{data_path, &block, 0}};
EXPECT_THROW(gnss_make_shared<IONGSMSFileSource>(segments, std::vector<std::string>{"L1"}), std::runtime_error);
}
TEST(IONGSMSFileSourceTest, CollapsesRepeatedStreamIdAcrossChunksIntoOneOutput)
{
const fs::path temp_dir(GetTempDir());
@@ -513,7 +587,7 @@ TEST(IONGSMSFileSourceTest, CollapsesRepeatedStreamIdAcrossChunksIntoOneOutput)
const fs::path metadata_path = temp_dir / "ion_gsms_file_source_repeated_stream.sdrx";
write_binary_file(data_path, {static_cast<uint8_t>(0b01101100U), static_cast<uint8_t>(0b00011011U)});
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 2, 1);
const auto stream = make_stream(GnssMetadata::IonStream::IF, "TC", 2, 8, 4);
auto first_chunk = make_chunk(stream, 1, 1);
auto second_chunk = make_chunk(stream, 1, 1);
@@ -532,6 +606,29 @@ TEST(IONGSMSFileSourceTest, CollapsesRepeatedStreamIdAcrossChunksIntoOneOutput)
}
TEST(IONGSMSFileSourceTest, ConcatenatesSegmentsForSameStream)
{
const fs::path temp_dir(GetTempDir());
const fs::path first_data_path = temp_dir / "ion_gsms_file_source_segment_1.bin";
const fs::path second_data_path = temp_dir / "ion_gsms_file_source_segment_2.bin";
write_binary_file(first_data_path, {static_cast<uint8_t>(0b01101100U)});
write_binary_file(second_data_path, {static_cast<uint8_t>(0b00011011U)});
const auto block = make_file_block(1);
const std::vector<IONGSMSFileSource::SegmentDescriptor> segments{
{first_data_path, &block, 0},
{second_data_path, &block, 0}};
const auto output = run_file_source(segments);
const std::vector<int8_t> expected{1, -2, -1, 0, 0, 1, -2, -1};
EXPECT_EQ(expected, output);
fs::remove(first_data_path);
fs::remove(second_data_path);
}
TEST(IONGSMSFileSourceTest, StartsReadingAtProvidedBlockOffset)
{
const fs::path temp_dir(GetTempDir());
@@ -575,3 +672,334 @@ TEST(IONGSMSFileSourceTest, EmitsFp32IqAsComplexItems)
fs::remove(data_path);
}
TEST(IONGSMSSignalSourceTest, UsesMetadataSamplingFrequencyPerRequestedStream)
{
const fs::path temp_dir(GetTempDir());
const fs::path data_path = temp_dir / "ion_gsms_signal_source_multirate.bin";
const fs::path metadata_path = temp_dir / "ion_gsms_signal_source_multirate.sdrx";
write_binary_file(data_path, std::vector<uint8_t>(10, 0));
write_text_file(metadata_path,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<metadata xmlns=\"http://www.ion.org/standards/sdrwg/schema/metadata.xsd\">\n"
" <system id=\"sys\">\n"
" <freqbase format=\"Hz\">10.0</freqbase>\n"
" </system>\n"
" <band id=\"B\">\n"
" <centerfreq format=\"Hz\">1.0</centerfreq>\n"
" <translatedfreq format=\"Hz\">0.0</translatedfreq>\n"
" <bandwidth format=\"Hz\">1.0</bandwidth>\n"
" </band>\n"
" <lane id=\"lane\">\n"
" <system id=\"sys\"/>\n"
" <block>\n"
" <cycles>10</cycles>\n"
" <chunk>\n"
" <sizeword>1</sizeword>\n"
" <countwords>1</countwords>\n"
" <wordshift>Left</wordshift>\n"
" <lump>\n"
" <stream id=\"L1\">\n"
" <ratefactor>2</ratefactor>\n"
" <quantization>1</quantization>\n"
" <packedbits>2</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>SIGN</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" <stream id=\"L2\">\n"
" <ratefactor>5</ratefactor>\n"
" <quantization>1</quantization>\n"
" <packedbits>5</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>SIGN</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" </lump>\n"
" </chunk>\n"
" </block>\n"
" </lane>\n"
" <file>\n"
" <url>ion_gsms_signal_source_multirate.bin</url>\n"
" <timestamp>2026-06-21T00:00:00Z</timestamp>\n"
" <lane id=\"lane\"/>\n"
" </file>\n"
"</metadata>\n");
auto queue = std::make_shared<Concurrent_Queue<pmt::pmt_t>>();
auto config = std::make_shared<InMemoryConfiguration>();
config->set_property("Test.metadata_filename", metadata_path.string());
config->set_property("Test.streams", "L1,L2");
config->set_property("Test.sampling_frequency", "100");
std::unique_ptr<SignalSourceInterface> source =
std::make_unique<IONGSMSSignalSource>(config.get(), "Test", 0, 1, queue.get());
EXPECT_EQ(2U, source->getRfChannels());
EXPECT_EQ(nullptr, source->get_right_block(2));
auto first_sink = gr::blocks::vector_sink_b::make();
auto second_sink = gr::blocks::vector_sink_b::make();
auto top_block = gr::make_top_block("IONGSMSSignalSourceTest");
source->connect(top_block);
top_block->connect(source->get_right_block(0), 0, first_sink, 0);
top_block->connect(source->get_right_block(1), 0, second_sink, 0);
top_block->run();
EXPECT_EQ(16U, first_sink->data().size());
EXPECT_EQ(40U, second_sink->data().size());
fs::remove(data_path);
fs::remove(metadata_path);
}
TEST(IONGSMSSignalSourceTest, LoadsStreamsFromRelativeIncludedMetadata)
{
const fs::path temp_dir(GetTempDir());
const fs::path include_dir = temp_dir / "ion_gsms_signal_source_include_dir";
const fs::path data_path = include_dir / "ion_gsms_signal_source_included.bin";
const fs::path root_metadata_path = temp_dir / "ion_gsms_signal_source_include_root.sdrx";
const fs::path included_metadata_path = include_dir / "child.sdrx";
fs::create_directories(include_dir);
write_binary_file(data_path, std::vector<uint8_t>(10, 0));
write_text_file(root_metadata_path,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<metadata xmlns=\"http://www.ion.org/standards/sdrwg/schema/metadata.xsd\">\n"
" <include>ion_gsms_signal_source_include_dir/child.sdrx</include>\n"
"</metadata>\n");
write_text_file(included_metadata_path,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<metadata xmlns=\"http://www.ion.org/standards/sdrwg/schema/metadata.xsd\">\n"
" <system id=\"sys\">\n"
" <freqbase format=\"Hz\">10.0</freqbase>\n"
" </system>\n"
" <band id=\"B\">\n"
" <centerfreq format=\"Hz\">1.0</centerfreq>\n"
" <translatedfreq format=\"Hz\">0.0</translatedfreq>\n"
" <bandwidth format=\"Hz\">1.0</bandwidth>\n"
" </band>\n"
" <lane id=\"lane\">\n"
" <system id=\"sys\"/>\n"
" <block>\n"
" <cycles>10</cycles>\n"
" <chunk>\n"
" <sizeword>1</sizeword>\n"
" <countwords>1</countwords>\n"
" <wordshift>Left</wordshift>\n"
" <lump>\n"
" <stream id=\"L1\">\n"
" <ratefactor>4</ratefactor>\n"
" <quantization>2</quantization>\n"
" <packedbits>8</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>TC</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" </lump>\n"
" </chunk>\n"
" </block>\n"
" </lane>\n"
" <file>\n"
" <url>ion_gsms_signal_source_included.bin</url>\n"
" <timestamp>2026-06-21T00:00:00Z</timestamp>\n"
" <lane id=\"lane\"/>\n"
" </file>\n"
"</metadata>\n");
auto queue = std::make_shared<Concurrent_Queue<pmt::pmt_t>>();
auto config = std::make_shared<InMemoryConfiguration>();
config->set_property("Test.metadata_filename", root_metadata_path.string());
config->set_property("Test.streams", "L1");
config->set_property("Test.sampling_frequency", "100");
std::unique_ptr<SignalSourceInterface> source =
std::make_unique<IONGSMSSignalSource>(config.get(), "Test", 0, 1, queue.get());
EXPECT_EQ(1U, source->getRfChannels());
auto sink = gr::blocks::vector_sink_b::make();
auto top_block = gr::make_top_block("IONGSMSSignalSourceIncludeTest");
source->connect(top_block);
top_block->connect(source->get_right_block(0), 0, sink, 0);
top_block->run();
EXPECT_EQ(32U, sink->data().size());
fs::remove(data_path);
fs::remove(included_metadata_path);
fs::remove(root_metadata_path);
fs::remove(include_dir);
}
TEST(IONGSMSSignalSourceTest, UsesFileSetEntriesAsPreferredFileStarts)
{
const fs::path temp_dir(GetTempDir());
const fs::path first_data_path = temp_dir / "ion_gsms_signal_source_fileset_first.bin";
const fs::path second_data_path = temp_dir / "ion_gsms_signal_source_fileset_second.bin";
const fs::path metadata_path = temp_dir / "ion_gsms_signal_source_fileset.sdrx";
write_binary_file(first_data_path, {static_cast<uint8_t>(0b01101100U), static_cast<uint8_t>(0b01101100U)});
write_binary_file(second_data_path, {static_cast<uint8_t>(0b00011011U), static_cast<uint8_t>(0b00011011U)});
write_text_file(metadata_path,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<metadata xmlns=\"http://www.ion.org/standards/sdrwg/schema/metadata.xsd\">\n"
" <system id=\"sys\">\n"
" <freqbase format=\"Hz\">10.0</freqbase>\n"
" </system>\n"
" <band id=\"B\">\n"
" <centerfreq format=\"Hz\">1.0</centerfreq>\n"
" <translatedfreq format=\"Hz\">0.0</translatedfreq>\n"
" <bandwidth format=\"Hz\">1.0</bandwidth>\n"
" </band>\n"
" <fileset id=\"set\">\n"
" <file>ion_gsms_signal_source_fileset_first.bin</file>\n"
" </fileset>\n"
" <lane id=\"lane\">\n"
" <system id=\"sys\"/>\n"
" <block>\n"
" <cycles>2</cycles>\n"
" <chunk>\n"
" <sizeword>1</sizeword>\n"
" <countwords>1</countwords>\n"
" <wordshift>Left</wordshift>\n"
" <lump>\n"
" <stream id=\"L1\">\n"
" <ratefactor>4</ratefactor>\n"
" <quantization>2</quantization>\n"
" <packedbits>8</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>TC</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" </lump>\n"
" </chunk>\n"
" </block>\n"
" </lane>\n"
" <file>\n"
" <url>ion_gsms_signal_source_fileset_second.bin</url>\n"
" <timestamp>2026-06-21T00:00:01Z</timestamp>\n"
" <lane id=\"lane\"/>\n"
" </file>\n"
" <file>\n"
" <url>ion_gsms_signal_source_fileset_first.bin</url>\n"
" <timestamp>2026-06-21T00:00:00Z</timestamp>\n"
" <lane id=\"lane\"/>\n"
" </file>\n"
"</metadata>\n");
auto queue = std::make_shared<Concurrent_Queue<pmt::pmt_t>>();
auto config = std::make_shared<InMemoryConfiguration>();
config->set_property("Test.metadata_filename", metadata_path.string());
config->set_property("Test.streams", "L1");
config->set_property("Test.sampling_frequency", "100");
std::unique_ptr<SignalSourceInterface> source =
std::make_unique<IONGSMSSignalSource>(config.get(), "Test", 0, 1, queue.get());
auto sink = gr::blocks::vector_sink_b::make();
auto top_block = gr::make_top_block("IONGSMSSignalSourceFileSetTest");
source->connect(top_block);
top_block->connect(source->get_right_block(0), 0, sink, 0);
top_block->run();
std::vector<int8_t> output;
output.reserve(sink->data().size());
for (const auto item : sink->data())
{
output.push_back(static_cast<int8_t>(item));
}
const std::vector<int8_t> expected{1, -2, -1, 0, 1, -2, -1, 0};
EXPECT_EQ(expected, output);
fs::remove(first_data_path);
fs::remove(second_data_path);
fs::remove(metadata_path);
}
TEST(IONGSMSSignalSourceTest, RejectsZeroCycleBlockBeforeLaterLaneBlocks)
{
const fs::path temp_dir(GetTempDir());
const fs::path data_path = temp_dir / "ion_gsms_signal_source_zero_cycles_nonfinal.bin";
const fs::path metadata_path = temp_dir / "ion_gsms_signal_source_zero_cycles_nonfinal.sdrx";
write_binary_file(data_path, {static_cast<uint8_t>(0b01101100U), static_cast<uint8_t>(0b00011011U)});
write_text_file(metadata_path,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<metadata xmlns=\"http://www.ion.org/standards/sdrwg/schema/metadata.xsd\">\n"
" <system id=\"sys\">\n"
" <freqbase format=\"Hz\">10.0</freqbase>\n"
" </system>\n"
" <band id=\"B\">\n"
" <centerfreq format=\"Hz\">1.0</centerfreq>\n"
" <translatedfreq format=\"Hz\">0.0</translatedfreq>\n"
" <bandwidth format=\"Hz\">1.0</bandwidth>\n"
" </band>\n"
" <lane id=\"lane\">\n"
" <system id=\"sys\"/>\n"
" <block>\n"
" <cycles>0</cycles>\n"
" <chunk>\n"
" <sizeword>1</sizeword>\n"
" <countwords>1</countwords>\n"
" <wordshift>Left</wordshift>\n"
" <lump>\n"
" <stream id=\"L2\">\n"
" <ratefactor>4</ratefactor>\n"
" <quantization>2</quantization>\n"
" <packedbits>8</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>TC</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" </lump>\n"
" </chunk>\n"
" </block>\n"
" <block>\n"
" <cycles>1</cycles>\n"
" <chunk>\n"
" <sizeword>1</sizeword>\n"
" <countwords>1</countwords>\n"
" <wordshift>Left</wordshift>\n"
" <lump>\n"
" <stream id=\"L1\">\n"
" <ratefactor>4</ratefactor>\n"
" <quantization>2</quantization>\n"
" <packedbits>8</packedbits>\n"
" <alignment>Left</alignment>\n"
" <format>IF</format>\n"
" <encoding>TC</encoding>\n"
" <band id=\"B\"/>\n"
" </stream>\n"
" </lump>\n"
" </chunk>\n"
" </block>\n"
" </lane>\n"
" <file>\n"
" <url>ion_gsms_signal_source_zero_cycles_nonfinal.bin</url>\n"
" <timestamp>2026-06-21T00:00:00Z</timestamp>\n"
" <lane id=\"lane\"/>\n"
" </file>\n"
"</metadata>\n");
auto queue = std::make_shared<Concurrent_Queue<pmt::pmt_t>>();
auto config = std::make_shared<InMemoryConfiguration>();
config->set_property("Test.metadata_filename", metadata_path.string());
config->set_property("Test.streams", "L1");
config->set_property("Test.sampling_frequency", "100");
EXPECT_THROW(
{
std::unique_ptr<SignalSourceInterface> source =
std::make_unique<IONGSMSSignalSource>(config.get(), "Test", 0, 1, queue.get());
(void)source;
},
std::runtime_error);
fs::remove(data_path);
fs::remove(metadata_path);
}