Skip to content

Commit

Permalink
UI integration tests/demo
Browse files Browse the repository at this point in the history
Signed-off-by: Ralph J. Steinhagen <[email protected]>
  • Loading branch information
RalphSteinhagen authored and drslebedev committed Jan 23, 2024
1 parent 051af1e commit 8d53363
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 4 deletions.
6 changes: 3 additions & 3 deletions algorithm/include/gnuradio-4.0/algorithm/ImChart.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,8 @@ struct ImChart {
continue;
}

const auto brailleColIndex = horAxisTransform::toScreen(x, axis_min_x, axis_max_x, horOffset, arrayWidth);
const auto brailleRowIndex = arrayHeight - verAxisTransform::toScreen(y, axis_min_y, axis_max_y, 0UZ, arrayHeight);
const auto brailleColIndex = horAxisTransform::toScreen(x, ValueType(axis_min_x), ValueType(axis_max_x), horOffset, arrayWidth);
const auto brailleRowIndex = arrayHeight - verAxisTransform::toScreen(y, ValueType(axis_min_y), ValueType(axis_max_y), 0UZ, arrayHeight);
if (brailleRowIndex >= (arrayHeight - 1UZ) || brailleColIndex >= arrayWidth || (style == Style::Bars && brailleRowIndex >= horAxisPosY)) {
continue;
}
Expand Down Expand Up @@ -443,7 +443,7 @@ struct ImChart {

[[nodiscard]] constexpr std::size_t
getVerticalAxisPositionX() const noexcept {
auto y_axis_x = std::is_same_v<horAxisTransform, LogAxisTransform> ? 0UZ : static_cast<std::size_t>(((0. - axis_min_x) / (axis_max_x - axis_min_x)) * static_cast<double>(_screen_width - 1UZ));
auto y_axis_x = std::is_same_v<horAxisTransform, LogAxisTransform> ? 0UZ : static_cast<std::size_t>((std::max(0. - axis_min_x, 0.) / (axis_max_x - axis_min_x)) * static_cast<double>(_screen_width - 1UZ));
// adjust for axis labels
std::size_t y_label_width = std::max(fmt::format("{:G}", axis_min_y).size(), fmt::format("{:G}", axis_max_y).size());
return std::clamp(y_axis_x, y_label_width + 3, _screen_width); // Ensure axis positions are within screen bounds
Expand Down
2 changes: 1 addition & 1 deletion blocks/basic/include/gnuradio-4.0/basic/clock_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct ClockSource : public gr::Block<ClockSource<T, useIoThread, ClockSourceTyp
A<float, "avg. sample rate", Visible> sample_rate = 1000.f;
A<std::uint32_t, "chunk_size", Visible, Doc<"number of samples per update">> chunk_size = 100;
std::shared_ptr<std::thread> userProvidedThread;
bool verbose_console = true;
bool verbose_console = false;

private:
std::chrono::time_point<ClockSourceType> _beginSequenceTimePoint = ClockSourceType::now();
Expand Down
4 changes: 4 additions & 0 deletions blocks/testing/CMakeLists.txt
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 blocks/testing/include/gnuradio-4.0/testing/ImChartMonitor.hpp
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
1 change: 1 addition & 0 deletions blocks/testing/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add_ut_test(qa_UI_Integration)
120 changes: 120 additions & 0 deletions blocks/testing/test/qa_UI_Integration.cpp
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 */
}
7 changes: 7 additions & 0 deletions core/include/gnuradio-4.0/HistoryBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class HistoryBuffer {
}

public:
using value_type = T;

constexpr explicit HistoryBuffer() noexcept { static_assert(N != std::dynamic_extent, "need to specify capacity"); }

constexpr explicit HistoryBuffer(std::size_t capacity) : _buffer(capacity * 2), _capacity(capacity) {
Expand Down Expand Up @@ -153,6 +155,11 @@ class HistoryBuffer {
return _size;
}

[[nodiscard]] constexpr bool
empty() const noexcept {
return _size == 0;
}

[[nodiscard]] constexpr size_t
capacity() const noexcept {
return _capacity;
Expand Down

0 comments on commit 8d53363

Please sign in to comment.