-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Ralph J. Steinhagen <[email protected]>
- Loading branch information
1 parent
051af1e
commit 8d53363
Showing
7 changed files
with
225 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
add_library(gr-testing INTERFACE) | ||
target_link_libraries(gr-testing INTERFACE gnuradio-core ut-benchmark) | ||
target_include_directories(gr-testing INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/> $<INSTALL_INTERFACE:include/>) | ||
|
||
if (ENABLE_TESTING) | ||
add_subdirectory(test) | ||
endif () |
89 changes: 89 additions & 0 deletions
89
blocks/testing/include/gnuradio-4.0/testing/ImChartMonitor.hpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
#ifndef GNURADIO_IMCHARTMONITOR_HPP | ||
#define GNURADIO_IMCHARTMONITOR_HPP | ||
|
||
#include <algorithm> | ||
|
||
#include <gnuradio-4.0/algorithm/ImChart.hpp> | ||
#include <gnuradio-4.0/Block.hpp> | ||
#include <gnuradio-4.0/HistoryBuffer.hpp> | ||
|
||
#include <fmt/format.h> | ||
#include <fmt/ranges.h> | ||
|
||
namespace gr::testing { | ||
|
||
template<typename T> | ||
struct ImChartMonitor : public Block<ImChartMonitor<T>, BlockingIO<false>> { | ||
using ClockSourceType = std::chrono::system_clock; | ||
PortIn<T> in; | ||
float sample_rate = 1000.0f; | ||
std::string signal_name = "unknown signal"; | ||
|
||
HistoryBuffer<T> _historyBufferX{ 1000 }; | ||
HistoryBuffer<T> _historyBufferY{ 1000 }; | ||
HistoryBuffer<Tag> _historyBufferTags{ 1000 }; | ||
|
||
void | ||
start() { | ||
fmt::println("started sink {} aka. '{}'", this->unique_name, this->name); | ||
in.max_samples = 10UZ; | ||
} | ||
|
||
void | ||
stop() { | ||
fmt::println("stopped sink {} aka. '{}'", this->unique_name, this->name); | ||
} | ||
|
||
constexpr void | ||
processOne(const T &input) noexcept { | ||
in.max_samples = 2 * sample_rate / 25; | ||
const float Ts = 1.0f / sample_rate; | ||
_historyBufferX.push_back(_historyBufferX[1] + Ts); | ||
_historyBufferY.push_back(input); | ||
|
||
if (this->input_tags_present()) { // received tag | ||
_historyBufferTags.push_back(this->mergedInputTag()); | ||
_historyBufferTags[1].index = 0; | ||
this->_mergedInputTag.map.clear(); // TODO: provide proper API for clearing tags | ||
} else { | ||
_historyBufferTags.push_back(Tag(-1, property_map())); | ||
} | ||
} | ||
|
||
work::Status | ||
draw() noexcept { | ||
[[maybe_unused]] const work::Status status = this->invokeWork(); // calls work(...) -> processOne(...) (all in the same thread as this 'draw()' | ||
const auto [xMin, xMax] = std::ranges::minmax_element(_historyBufferX); | ||
const auto [yMin, yMax] = std::ranges::minmax_element(_historyBufferY); | ||
if (_historyBufferX.empty() || *xMin == *xMax || *yMin == *yMax) { | ||
return status; // buffer or axes' ranges are empty -> skip drawing | ||
} | ||
fmt::println("\033[2J\033[H"); | ||
// create reversed copies -- draw(...) expects std::ranges::input_range -> | ||
// TODO: change draw routine and/or write wrapper and/or provide direction option to HistoryBuffer | ||
std::vector<T> reversedX(_historyBufferX.rbegin(), _historyBufferX.rend()); | ||
std::vector<T> reversedY(_historyBufferY.rbegin(), _historyBufferY.rend()); | ||
std::vector<T> reversedTag(_historyBufferX.size()); | ||
std::transform(_historyBufferTags.rbegin(), _historyBufferTags.rend(), _historyBufferY.rbegin(), reversedTag.begin(), [](const Tag& tag, const T& yValue) { return tag.index < 0 ? T(0) : yValue; }); | ||
|
||
auto adjustRange = [](T min, T max) { | ||
min = std::min(min, T(0)); | ||
max = std::max(max, T(0)); | ||
const T margin = (max - min) * 0.2; | ||
return std::pair<double, double>{min - margin, max + margin}; | ||
}; | ||
|
||
auto chart = gr::graphs::ImChart<130, 28>({ { *xMin, *xMax }, adjustRange(*yMin, *yMax) }); | ||
chart.draw(reversedX, reversedY, signal_name); | ||
chart.draw<gr::graphs::Style::Marker>(reversedX, reversedTag, "Tags"); | ||
chart.draw(); | ||
fmt::println("buffer has {} samples - status {:10} # graph range x = [{:2.2}, {:2.2}] y = [{:2.2}, {:2.2}]", _historyBufferX.size(), magic_enum::enum_name(status), *xMin, *xMax, *yMin, *yMax); | ||
return status; | ||
} | ||
}; | ||
|
||
} // namespace gr::testing | ||
|
||
ENABLE_REFLECTION_FOR_TEMPLATE(gr::testing::ImChartMonitor, in, sample_rate, signal_name) | ||
|
||
#endif // GNURADIO_IMCHARTMONITOR_HPP |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
add_ut_test(qa_UI_Integration) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
#include "boost/ut.hpp" | ||
|
||
#include "gnuradio-4.0/Block.hpp" | ||
#include "gnuradio-4.0/Graph.hpp" | ||
#include "gnuradio-4.0/Scheduler.hpp" | ||
#include "gnuradio-4.0/Tag.hpp" | ||
|
||
#include "gnuradio-4.0/algorithm/ImChart.hpp" | ||
#include "gnuradio-4.0/basic/clock_source.hpp" | ||
#include "gnuradio-4.0/basic/FunctionGenerator.hpp" | ||
#include "gnuradio-4.0/testing/ImChartMonitor.hpp" | ||
#include "gnuradio-4.0/testing/TagMonitors.hpp" | ||
|
||
#if defined(__clang__) && __clang_major__ >= 15 | ||
// clang 16 does not like ut's default reporter_junit due to some issues with stream buffers and output redirection | ||
template<> | ||
auto boost::ut::cfg<boost::ut::override> = boost::ut::runner<boost::ut::reporter<>>{}; | ||
#endif | ||
|
||
const boost::ut::suite TagTests = [] { | ||
using namespace boost::ut; | ||
using namespace gr; | ||
using namespace gr::basic; | ||
using namespace gr::testing; | ||
|
||
"FunctionGenerator + ClockSource FAIR test"_test = [] { | ||
using namespace function_generator; | ||
constexpr std::uint64_t lengthSeconds = 10; | ||
constexpr std::uint32_t N = 1000 * lengthSeconds; | ||
constexpr float sample_rate = 1'000.f; | ||
|
||
Graph testGraph; | ||
auto &clockSrc = testGraph.emplaceBlock<gr::basic::ClockSource<float>>({ { "sample_rate", sample_rate }, { "n_samples_max", N }, { "name", "ClockSource" } }); | ||
|
||
auto addTimeTagEntry = []<typename T>(gr::basic::ClockSource<T> &clockSource, std::uint64_t timeInNanoseconds, const std::string &value) { | ||
clockSource.tag_times.push_back(timeInNanoseconds); | ||
clockSource.tag_values.push_back(value); | ||
}; | ||
|
||
// all times are in nanoseconds | ||
constexpr std::uint64_t ms = 1'000'000; // ms -> ns conversion factor (wish we had a proper C++ units-lib integration) | ||
addTimeTagEntry(clockSrc, 10 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=1"); | ||
addTimeTagEntry(clockSrc, 100 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=2"); | ||
addTimeTagEntry(clockSrc, 300 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=3"); | ||
addTimeTagEntry(clockSrc, 350 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=4"); | ||
addTimeTagEntry(clockSrc, 550 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=5"); | ||
addTimeTagEntry(clockSrc, 650 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=6"); | ||
addTimeTagEntry(clockSrc, 800 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=7"); | ||
addTimeTagEntry(clockSrc, 850 * ms, "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=8"); | ||
clockSrc.repeat_period = 1000 * ms; | ||
clockSrc.do_zero_order_hold = true; | ||
auto &funcGen = testGraph.emplaceBlock<FunctionGenerator<float>>({ { "sample_rate", sample_rate }, { "name", "FunctionGenerator" } }); | ||
funcGen.match_pred = [](const auto &tableEntry, const auto &searchEntry, const auto attempt) -> std::optional<bool> { | ||
if (searchEntry.find("context") == searchEntry.end()) { | ||
return std::nullopt; | ||
} | ||
if (tableEntry.find("context") == tableEntry.end()) { | ||
return false; | ||
} | ||
|
||
const pmtv::pmt searchEntryContext = searchEntry.at("context"); | ||
const pmtv::pmt tableEntryContext = tableEntry.at("context"); | ||
if (std::holds_alternative<std::string>(searchEntryContext) && std::holds_alternative<std::string>(tableEntryContext)) { | ||
const auto searchString = std::get<std::string>(searchEntryContext); | ||
const auto tableString = std::get<std::string>(tableEntryContext); | ||
|
||
if (!searchString.starts_with("CMD_BP_START:")) { | ||
return std::nullopt; | ||
} | ||
|
||
if (attempt >= searchString.length()) { | ||
return std::nullopt; | ||
} | ||
auto [it1, it2] = std::ranges::mismatch(searchString, tableString); | ||
if (std::distance(searchString.begin(), it1) == static_cast<std::ptrdiff_t>(searchString.length() - attempt)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
}; | ||
|
||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=1" } }, createConstPropertyMap(5.f)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=2" } }, createLinearRampPropertyMap(5.f, 30.f, 0.2f /* [s]*/)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=3" } }, createConstPropertyMap(30.f)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=4" } }, createParabolicRampPropertyMap(30.f, 20.f, .1f, 0.02f /* [s]*/)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=5" } }, createConstPropertyMap(20.f)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=6" } }, createCubicSplinePropertyMap(20.f, 10.f, 0.1f /* [s]*/)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=7" } }, createConstPropertyMap(10.f)); | ||
funcGen.addFunctionTableEntry({ { "context", "CMD_BP_START:FAIR.SELECTOR.C=1:S=1:P=8" } }, createImpulseResponsePropertyMap(10.f, 20.f, 0.02f /* [s]*/, 0.06f /* [s]*/)); | ||
|
||
auto &sink = testGraph.emplaceBlock<TagSink<float, ProcessFunction::USE_PROCESS_ONE>>({ { "name", "SampleGeneratorSink" } }); | ||
expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(clockSrc).to<"in">(funcGen))); | ||
expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(funcGen).to<"in">(sink))); | ||
|
||
// connect UI sink -- doesn't strictly need to be part of the graph due to BlockingIO<false> definition | ||
// but the present 'connect' API assumes it to be part of the Graph | ||
auto &uiSink = testGraph.emplaceBlock<testing::ImChartMonitor<float>>({ { "name", "BasicImChartSink" } }); | ||
expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(funcGen).to<"in">(uiSink))); | ||
|
||
scheduler::Simple sched{ std::move(testGraph) }; | ||
|
||
std::thread uiLoop([&uiSink]() { | ||
int i = 0; | ||
fmt::println("start UI thread"); | ||
while (uiSink.draw() != work::Status::DONE) { // mocks UI update loop with 25 Hz repetition | ||
std::this_thread::sleep_for(std::chrono::milliseconds(40)); // 25 Hz <-> 40 ms period | ||
} | ||
fmt::println("asked to finish UI thread"); | ||
std::this_thread::sleep_for(std::chrono::seconds(2)); // wait for another 2 seconds before closing down | ||
fmt::println("finished UI thread"); | ||
}); | ||
sched.runAndWait(); | ||
expect(eq(N, static_cast<std::uint32_t>(sink.samples.size()))) << "Number of samples does not match"; | ||
uiLoop.join(); | ||
}; | ||
}; | ||
|
||
int | ||
main() { /* not needed for UT */ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters