Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ if(TARGET plotjuggler_sdk::plugin_sdk)
add_subdirectory(toolbox_colormap)
add_subdirectory(toolbox_quaternion)
add_subdirectory(toolbox_reactive_scripts_editor)
add_subdirectory(toolbox_filter_editor)
add_subdirectory(toolbox_mosaico)
return()
endif()
Expand Down Expand Up @@ -153,5 +154,6 @@ else()
add_subdirectory(toolbox_fft)
add_subdirectory(toolbox_colormap)
add_subdirectory(toolbox_quaternion)
add_subdirectory(toolbox_filter_editor)
add_subdirectory(toolbox_mosaico)
endif()
48 changes: 48 additions & 0 deletions toolbox_filter_editor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
find_package(nlohmann_json REQUIRED)

include(${CMAKE_CURRENT_LIST_DIR}/../cmake/EmbedManifest.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/EmbedUi.cmake)

add_library(toolbox_filter_editor_plugin SHARED filter_editor_plugin.cpp)
target_compile_features(toolbox_filter_editor_plugin PRIVATE cxx_std_20)
target_compile_options(toolbox_filter_editor_plugin PRIVATE ${PJ_WARNING_FLAGS})

target_link_libraries(toolbox_filter_editor_plugin PRIVATE
plotjuggler_sdk::plugin_sdk
nlohmann_json::nlohmann_json
${CMAKE_DL_LIBS}
)

pj_embed_manifest(toolbox_filter_editor_plugin
HEADER ${CMAKE_CURRENT_BINARY_DIR}/generated/filter_editor_manifest.hpp
VAR_NAME kFilterEditorManifest
)

pj_embed_ui(toolbox_filter_editor_plugin
UI_FILE ${CMAKE_CURRENT_SOURCE_DIR}/filter_editor_dialog.ui
HEADER ${CMAKE_CURRENT_BINARY_DIR}/generated/filter_editor_dialog_ui.hpp
VAR_NAME kFilterEditorDialogUi
)

# v4 plugin discovery: emit a sidecar .pjmanifest.json the host can scan
# pre-dlopen. Content derived from manifest.json + {abi_major, family}.
pj_emit_plugin_manifest(toolbox_filter_editor_plugin
FAMILY toolbox
MANIFEST_FILE ${CMAKE_CURRENT_SOURCE_DIR}/manifest.json
)

# Built-in predefined transforms catalogue lives in this plugin (it provides
# them); the SDK contract they implement is in plotjuggler_sdk::plugin_sdk.
# Tests guard the per-transform PJ3-mirror math + the SDK contract (the
# default appendTail loops calculateNextPoint, save/load round-trips).
if(TARGET GTest::gtest_main)
add_executable(builtin_transforms_test builtin_transforms_test.cpp)
target_compile_features(builtin_transforms_test PRIVATE cxx_std_20)
target_compile_options(builtin_transforms_test PRIVATE ${PJ_WARNING_FLAGS})
target_link_libraries(builtin_transforms_test PRIVATE
plotjuggler_sdk::plugin_sdk
nlohmann_json::nlohmann_json
GTest::gtest_main
)
add_test(NAME builtin_transforms_test COMMAND builtin_transforms_test)
endif()
10 changes: 10 additions & 0 deletions toolbox_filter_editor/builtin_transforms.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: MPL-2.0

// The 12 builtin Filter Transform strategies live in the SDK
// (plotjuggler_sdk/pj_plugins/filter_protocol/include/pj_plugins/sdk/builtin_transforms.hpp).
// This shim re-exports them so the plugin and the host can both consume the
// same source.

#include "pj_plugins/sdk/builtin_transforms.hpp"
214 changes: 214 additions & 0 deletions toolbox_filter_editor/builtin_transforms_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// Copyright 2026 Davide Faconti
// SPDX-License-Identifier: MPL-2.0

#include "builtin_transforms.hpp"

#include <gtest/gtest.h>

#include <cmath>
#include <cstdint>
#include <limits>

namespace {

using namespace PJ::sdk; // NOLINT

// Helper: apply a transform to (xs, ys) pairs and return output points.
std::vector<Point2> apply(FilterTransform& t, std::vector<double> xs, std::vector<double> ys) {
std::vector<Point2> in;
in.reserve(xs.size());
for (size_t i = 0; i < xs.size(); ++i) {
in.push_back({xs[i], ys[i]});
}
return t.applyBatch(in);
}

TEST(Transforms, Absolute) {
AbsoluteTransform t;
auto out = apply(t, {0, 1, 2}, {-3, 4, -5});
ASSERT_EQ(out.size(), 3u);
EXPECT_DOUBLE_EQ(out[0].y, 3.0);
EXPECT_DOUBLE_EQ(out[2].y, 5.0);
}

TEST(Transforms, ScaleAndOffset) {
ScaleTransform t;
t.value_scale = 2.0;
t.value_offset = 1.0;
t.time_offset = 10.0;
auto out = apply(t, {0, 1}, {3, 4});
EXPECT_DOUBLE_EQ(out[0].x, 10.0);
EXPECT_DOUBLE_EQ(out[0].y, 7.0);
EXPECT_DOUBLE_EQ(out[1].y, 9.0);
}

TEST(Transforms, DerivativeActualDt) {
DerivativeTransform t;
auto out = apply(t, {0, 1, 2}, {0, 10, 30});
ASSERT_EQ(out.size(), 2u);
EXPECT_DOUBLE_EQ(out[0].y, 10.0);
EXPECT_DOUBLE_EQ(out[1].y, 20.0);
}

TEST(Transforms, IntegralTrapezoid) {
IntegralTransform t;
auto out = apply(t, {0, 1, 2}, {0, 2, 4});
ASSERT_EQ(out.size(), 2u);
EXPECT_DOUBLE_EQ(out[0].y, 1.0);
EXPECT_DOUBLE_EQ(out[1].y, 4.0);
}

TEST(Transforms, MovingAverageWindow) {
MovingAverageTransform t;
t.window = 2;
auto out = apply(t, {0, 1, 2, 3}, {2, 4, 6, 8});
ASSERT_EQ(out.size(), 4u);
EXPECT_DOUBLE_EQ(out[0].y, 2.0); // padded
EXPECT_DOUBLE_EQ(out[1].y, 3.0); // (2+4)/2
EXPECT_DOUBLE_EQ(out[3].y, 7.0); // (6+8)/2
}

TEST(Transforms, MovingRMS) {
MovingRMSTransform t;
t.window = 2;
auto out = apply(t, {0, 1}, {3, 4});
EXPECT_DOUBLE_EQ(out[0].y, 3.0);
EXPECT_DOUBLE_EQ(out[1].y, std::sqrt((9.0 + 16.0) / 2.0));
}

TEST(Transforms, BinaryFilterGreater) {
BinaryFilterTransform t;
t.op = BinaryOp::kGreater;
t.a = 5.0;
auto out = apply(t, {0, 1, 2}, {3, 5, 7});
EXPECT_DOUBLE_EQ(out[0].y, 0.0);
EXPECT_DOUBLE_EQ(out[1].y, 0.0);
EXPECT_DOUBLE_EQ(out[2].y, 1.0);
}

TEST(Transforms, BinaryFilterRange) {
BinaryFilterTransform t;
t.op = BinaryOp::kRange;
t.a = 2.0;
t.b = 6.0;
auto out = apply(t, {0, 1, 2}, {1, 4, 7});
EXPECT_DOUBLE_EQ(out[0].y, 0.0);
EXPECT_DOUBLE_EQ(out[1].y, 1.0);
EXPECT_DOUBLE_EQ(out[2].y, 0.0);
}

TEST(Transforms, TimeSincePrevious) {
TimeSincePreviousTransform t;
auto out = apply(t, {0, 2, 5}, {9, 9, 9});
ASSERT_EQ(out.size(), 2u);
EXPECT_DOUBLE_EQ(out[0].y, 2.0);
EXPECT_DOUBLE_EQ(out[1].y, 3.0);
}

TEST(Transforms, SamplesCounter) {
SamplesCounterTransform t;
t.samples_ms = 2000;
auto out = apply(t, {0, 1, 2, 3}, {0, 0, 0, 0});
ASSERT_EQ(out.size(), 4u);
EXPECT_DOUBLE_EQ(out[0].y, 0.0);
EXPECT_DOUBLE_EQ(out[2].y, 2.0);
}

TEST(Transforms, NoneIsPassthrough) {
NoneTransform t;
auto out = apply(t, {0, 1}, {5, 6});
ASSERT_EQ(out.size(), 2u);
EXPECT_DOUBLE_EQ(out[1].y, 6.0);
}

TEST(Transforms, FactoryRegistration) {
registerAllTransforms();
auto& f = FilterTransformFactory::instance();
const auto ids = f.registeredIds();
EXPECT_FALSE(ids.empty());
// All registered ids should be creatable
for (const auto& id : ids) {
auto t = f.create(id);
EXPECT_NE(t, nullptr) << "Could not create: " << id;
}
}

TEST(Transforms, StreamSafeFlags) {
registerAllTransforms();
// Known stream-safe
EXPECT_TRUE(AbsoluteTransform{}.isStreamSafe());
EXPECT_TRUE(ScaleTransform{}.isStreamSafe());
EXPECT_TRUE(DerivativeTransform{}.isStreamSafe());
EXPECT_TRUE(MovingAverageTransform{}.isStreamSafe());
EXPECT_TRUE(MovingRMSTransform{}.isStreamSafe());
EXPECT_TRUE(MovingVarianceTransform{}.isStreamSafe());
EXPECT_TRUE(OutlierRemovalTransform{}.isStreamSafe());
EXPECT_TRUE(BinaryFilterTransform{}.isStreamSafe());
EXPECT_TRUE(TimeSincePreviousTransform{}.isStreamSafe());
// Known non-safe (need full history)
EXPECT_FALSE(IntegralTransform{}.isStreamSafe());
EXPECT_FALSE(SamplesCounterTransform{}.isStreamSafe());
}

// ---------------------------------------------------------------------------
// Streaming equivalence: calculateNextPoint matches applyBatch
// ---------------------------------------------------------------------------

void expectStreamingMatchesBatch(FilterTransform& t_batch, FilterTransform& t_stream) {
std::vector<Point2> full;
for (int i = 0; i < 30; ++i) {
full.push_back({static_cast<double>(i), std::sin(i * 0.3) * 5.0 + static_cast<double>(i % 5)});
}
// Batch
const auto batch = t_batch.applyBatch(full);
// Streaming point-by-point
t_stream.reset();
std::vector<Point2> streamed;
for (const auto& p : full) {
if (auto r = t_stream.calculateNextPoint(p)) {
streamed.push_back(*r);
}
}
ASSERT_EQ(batch.size(), streamed.size());
for (size_t i = 0; i < batch.size(); ++i) {
EXPECT_NEAR(batch[i].x, streamed[i].x, 1e-9) << "x mismatch at " << i;
EXPECT_NEAR(batch[i].y, streamed[i].y, 1e-9) << "y mismatch at " << i;
}
}

TEST(Transforms, StreamingMatchesBatch) {
{
AbsoluteTransform a, b;
expectStreamingMatchesBatch(a, b);
}
{
DerivativeTransform a, b;
expectStreamingMatchesBatch(a, b);
}
{
IntegralTransform a, b;
expectStreamingMatchesBatch(a, b);
}
{
MovingAverageTransform a, b;
a.window = b.window = 5;
expectStreamingMatchesBatch(a, b);
}
{
MovingRMSTransform a, b;
a.window = b.window = 5;
expectStreamingMatchesBatch(a, b);
}
{
TimeSincePreviousTransform a, b;
expectStreamingMatchesBatch(a, b);
}
{
BinaryFilterTransform a, b;
a.op = b.op = BinaryOp::kGreater;
a.a = b.a = 2.0;
expectStreamingMatchesBatch(a, b);
}
}

} // namespace
24 changes: 24 additions & 0 deletions toolbox_filter_editor/conanfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os
from conan import ConanFile


_SDK_VERSION = (
open(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir, "SDK_VERSION"))
.read()
.strip()
)


class ToolboxFilterEditorConan(ConanFile):
name = "toolbox_filter_editor"
version = "0"
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps", "CMakeToolchain"
requires = (
f"plotjuggler_sdk/{_SDK_VERSION}",
"gtest/1.17.0",
"nlohmann_json/3.12.0",
)
default_options = {
"*:shared": False,
}
Loading
Loading