From 8fd68da2db57b2628b6463e4528d34a399de525e Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 4 Sep 2026 14:11:44 +0200 Subject: [PATCH 1/4] fix(sample): Change ts compare to absolute compare Signed-off-by: Manuel --- lib/sample.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sample.cpp b/lib/sample.cpp index e9ad06f30..2974e9536 100644 --- a/lib/sample.cpp +++ b/lib/sample.cpp @@ -206,7 +206,7 @@ int villas::node::sample_cmp(struct Sample *a, struct Sample *b, double epsilon, // Compare timestamp if (flags & (int)SampleFlags::HAS_TS_ORIGIN) { - if (time_delta(&a->ts.origin, &b->ts.origin) > epsilon) { + if (abs(time_delta(&a->ts.origin, &b->ts.origin)) > epsilon) { printf("ts.origin: %f != %f\n", time_to_double(&a->ts.origin), time_to_double(&b->ts.origin)); return 3; From 1f577b85802722e810f0f48e6d3f5a0ced29e197 Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 4 Sep 2026 14:38:33 +0200 Subject: [PATCH 2/4] test(pps_ts): Added test for hook pps_ts and fix time source for hook Signed-off-by: Manuel --- lib/hooks/pps_ts.cpp | 51 +++++++++++++++++-------- tests/integration/hook-pps_ts.sh | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 15 deletions(-) create mode 100755 tests/integration/hook-pps_ts.sh diff --git a/lib/hooks/pps_ts.cpp b/lib/hooks/pps_ts.cpp index 26abfb6dd..64fec5c3c 100644 --- a/lib/hooks/pps_ts.cpp +++ b/lib/hooks/pps_ts.cpp @@ -22,6 +22,8 @@ class PpsTsHook : public SingleSignalHook { HORIZON, } mode; + enum TimeSource { CLOCK_RELATIME, SAMPLE } timeSource; + uint64_t lastSequence; double lastValue; @@ -60,19 +62,16 @@ class PpsTsHook : public SingleSignalHook { SingleSignalHook::parse(json); const char *mode_str = nullptr; + const char *timeSourceC = nullptr; - double fSmps = 1.0; - ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: f, s?: F, s?: i, s?: i }", - "mode", &mode_str, "threshold", &threshold, - "expected_smp_rate", &fSmps, "horizon_estimation", + ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: s, s?: f, s?: i, s?: i }", + "mode", &mode_str, "time_source", &timeSourceC, + "threshold", &threshold, "horizon_estimation", &horizonEstimation, "horizon_compensation", &horizonCompensation); if (ret) throw ConfigError(json, err, "node-config-hook-pps_ts"); - period = 1.0 / fSmps; - currentSecond = time(nullptr); - if (mode_str) { if (!strcmp(mode_str, "simple")) mode = Mode::SIMPLE; @@ -83,6 +82,13 @@ class PpsTsHook : public SingleSignalHook { "Unsupported mode: {}", mode_str); } + if (timeSourceC) { + if (!strcmp(timeSourceC, "CLOCK_REALTIME")) + timeSource = TimeSource::CLOCK_RELATIME; + else + timeSource = TimeSource::SAMPLE; + } + state = State::PARSED; } @@ -107,12 +113,15 @@ class PpsTsHook : public SingleSignalHook { // Detect Edge bool isEdge = lastValue < threshold && value > threshold; - if (isEdge) { + + if (isEdge) + cntEdges++; + + if (isEdge && cntEdges > 0) { tsVirt.tv_sec = currentSecond + 1; tsVirt.tv_nsec = 0; period = 1.0 / cntSmps; cntSmps = 0; - cntEdges++; currentSecond = 0; } else { struct timespec tsPeriod = time_from_double(period); @@ -122,12 +131,18 @@ class PpsTsHook : public SingleSignalHook { lastValue = value; cntSmps++; - if (!currentSecond && - tsVirt.tv_nsec > - 0.5e9) //take the second somewere in the center of the last second to reduce impact of system clock error - currentSecond = time(nullptr); + if (!currentSecond && tsVirt.tv_nsec > 0.5e9) { + //take the second somewere in the center of the last second to reduce impact of system clock error + if (timeSource == TimeSource::CLOCK_RELATIME) { + timespec t; + clock_gettime(CLOCK_RELATIME, &t); + currentSecond = t.tv_sec; + } else if (timeSource == TimeSource::SAMPLE) { + currentSecond = smp->ts.origin.tv_sec; + } + } - if (cntEdges < 5) + if (cntEdges < 2) return Hook::Reason::SKIP_SAMPLE; smp->ts.origin = tsVirt; @@ -172,7 +187,13 @@ class PpsTsHook : public SingleSignalHook { timeError / (cntSmpsAvg * horizonCompensation); period = periodEstimate + periodErrorCompensation; } else { - tsVirt.tv_sec = time(nullptr); + if (timeSource == TimeSource::CLOCK_RELATIME) { + timespec t; + clock_gettime(CLOCK_RELATIME, &t); + tsVirt.tv_sec = t.tv_sec; + } else if (timeSource == TimeSource::SAMPLE) { + tsVirt.tv_sec = smp->ts.origin.tv_sec; + } tsVirt.tv_nsec = 0; isSynced = true; cntEdges = 0; diff --git a/tests/integration/hook-pps_ts.sh b/tests/integration/hook-pps_ts.sh new file mode 100755 index 000000000..897497c29 --- /dev/null +++ b/tests/integration/hook-pps_ts.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Integration test for pps_ts hook. +# +# Author: Manuel Pitz +# SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University +# SPDX-License-Identifier: Apache-2.0 + +set -e + +DIR=$(mktemp -d) +pushd ${DIR} + +function finish { + popd +} +trap finish EXIT + +cat > input.dat < expect.dat < output.dat +villas compare output.dat expect.dat From d17ea8539a5542ad47a373b9b35dbc4d61f6e0ad Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 4 Sep 2026 16:23:43 +0200 Subject: [PATCH 3/4] test(nodes): Change integration test to ignore timestamps in compare Signed-off-by: Manuel --- tests/integration/node-hook.sh | 2 +- tests/integration/node-mapping.sh | 2 +- tests/integration/node-multiplexing.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/node-hook.sh b/tests/integration/node-hook.sh index af1498add..07221b350 100755 --- a/tests/integration/node-hook.sh +++ b/tests/integration/node-hook.sh @@ -74,4 +74,4 @@ EOF villas node config.json -villas compare output.dat expect.dat +villas compare -T output.dat expect.dat diff --git a/tests/integration/node-mapping.sh b/tests/integration/node-mapping.sh index cc97d1bd4..d09703f50 100755 --- a/tests/integration/node-mapping.sh +++ b/tests/integration/node-mapping.sh @@ -80,4 +80,4 @@ EOF villas node -d debug config.json -villas compare output.dat expect.dat +villas compare -T output.dat expect.dat diff --git a/tests/integration/node-multiplexing.sh b/tests/integration/node-multiplexing.sh index 698bea278..8717eacff 100755 --- a/tests/integration/node-multiplexing.sh +++ b/tests/integration/node-multiplexing.sh @@ -117,7 +117,7 @@ EOF villas node config.json -villas compare output.dat expect_${MODE}.dat +villas compare -T output.dat expect_${MODE}.dat rm output.dat From 469f54c9c2462eea96df7a73f16d847785cff053 Mon Sep 17 00:00:00 2001 From: Manuel Date: Fri, 4 Sep 2026 19:45:32 +0200 Subject: [PATCH 4/4] fix(pps_ts): Cleanup code and make sure variables are initialized Signed-off-by: Manuel --- lib/hooks/pps_ts.cpp | 187 ++++++++----------------------- tests/integration/hook-pps_ts.sh | 73 ++++++------ 2 files changed, 80 insertions(+), 180 deletions(-) diff --git a/lib/hooks/pps_ts.cpp b/lib/hooks/pps_ts.cpp index 64fec5c3c..523a5a530 100644 --- a/lib/hooks/pps_ts.cpp +++ b/lib/hooks/pps_ts.cpp @@ -17,41 +17,28 @@ namespace node { class PpsTsHook : public SingleSignalHook { protected: - enum Mode { - SIMPLE, - HORIZON, - } mode; - - enum TimeSource { CLOCK_RELATIME, SAMPLE } timeSource; + enum class TimeSource { OS, SAMPLE } timeSource; uint64_t lastSequence; - + bool firstSample; double lastValue; double threshold; + bool armSecDetect; - bool isSynced; - bool isLocked; struct timespec tsVirt; - double timeError; // In seconds - double periodEstimate; // In seconds - double periodErrorCompensation; // In seconds - double period; // In seconds + double period; // In seconds uintmax_t cntEdges; uintmax_t cntSmps; - uintmax_t cntSmpsTotal; - unsigned horizonCompensation; - unsigned horizonEstimation; + unsigned currentSecond; std::vector filterWindow; public: PpsTsHook(Path *p, Node *n, int fl, int prio, bool en = true) - : SingleSignalHook(p, n, fl, prio, en), mode(Mode::SIMPLE), - lastSequence(0), lastValue(0), threshold(1.5), isSynced(false), - isLocked(false), timeError(0.0), periodEstimate(0.0), - periodErrorCompensation(0.0), period(0.0), cntEdges(0), cntSmps(0), - cntSmpsTotal(0), horizonCompensation(10), horizonEstimation(10), - currentSecond(0), filterWindow(horizonEstimation + 1, 0) {} + : SingleSignalHook(p, n, fl, prio, en), timeSource(TimeSource::OS), + lastSequence(0), firstSample(false), lastValue(0), threshold(1.5), + armSecDetect(false), tsVirt({0, 0}), period(0.0), cntEdges(0), + cntSmps(0), currentSecond(0) {} void parse(json_t *json) override { int ret; @@ -61,170 +48,84 @@ class PpsTsHook : public SingleSignalHook { SingleSignalHook::parse(json); - const char *mode_str = nullptr; const char *timeSourceC = nullptr; - ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: s, s?: f, s?: i, s?: i }", - "mode", &mode_str, "time_source", &timeSourceC, - "threshold", &threshold, "horizon_estimation", - &horizonEstimation, "horizon_compensation", - &horizonCompensation); + ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: f }", "time_source", + &timeSourceC, "threshold", &threshold); if (ret) throw ConfigError(json, err, "node-config-hook-pps_ts"); - if (mode_str) { - if (!strcmp(mode_str, "simple")) - mode = Mode::SIMPLE; - else if (!strcmp(mode_str, "horizon")) - mode = Mode::HORIZON; - else - throw ConfigError(json, "node-config-hook-pps_ts-mode", - "Unsupported mode: {}", mode_str); - } - if (timeSourceC) { - if (!strcmp(timeSourceC, "CLOCK_REALTIME")) - timeSource = TimeSource::CLOCK_RELATIME; - else + if (!strcmp(timeSourceC, "sample")) timeSource = TimeSource::SAMPLE; + else + timeSource = TimeSource::OS; } state = State::PARSED; } villas::node::Hook::Reason process(struct Sample *smp) override { - switch (mode) { - case Mode::SIMPLE: - return processSimple(smp); - - case Mode::HORIZON: - return processHorizon(smp); - - default: - return Reason::ERROR; - } - } - - villas::node::Hook::Reason processSimple(struct Sample *smp) { assert(state == State::STARTED); // Get value of PPS signal float value = smp->data[signalIndex].f; // TODO check if it is really float + if (!firstSample) { + firstSample = true; + lastValue = value; + return Hook::Reason::SKIP_SAMPLE; + } + // Detect Edge bool isEdge = lastValue < threshold && value > threshold; - if (isEdge) - cntEdges++; + if (isEdge) { - if (isEdge && cntEdges > 0) { - tsVirt.tv_sec = currentSecond + 1; - tsVirt.tv_nsec = 0; - period = 1.0 / cntSmps; + if (cntEdges > 0) { + tsVirt.tv_sec = currentSecond + 1; + tsVirt.tv_nsec = 0; + period = 1.0 / cntSmps; + currentSecond = 0; + } cntSmps = 0; - currentSecond = 0; + cntEdges++; + armSecDetect = true; } else { struct timespec tsPeriod = time_from_double(period); tsVirt = time_add(&tsVirt, &tsPeriod); } - lastValue = value; - cntSmps++; - - if (!currentSecond && tsVirt.tv_nsec > 0.5e9) { - //take the second somewere in the center of the last second to reduce impact of system clock error - if (timeSource == TimeSource::CLOCK_RELATIME) { - timespec t; - clock_gettime(CLOCK_RELATIME, &t); - currentSecond = t.tv_sec; - } else if (timeSource == TimeSource::SAMPLE) { - currentSecond = smp->ts.origin.tv_sec; + if (armSecDetect) { + long current_nsec = 0; + if (timeSource == TimeSource::OS) + current_nsec = time_now().tv_nsec; + else if (timeSource == TimeSource::SAMPLE) + current_nsec = smp->ts.origin.tv_nsec; + + if (current_nsec > 0.5e9) { + //take the second somewere in the center of the last second to reduce impact of system clock error + if (timeSource == TimeSource::OS) + currentSecond = time_now().tv_sec; + else if (timeSource == TimeSource::SAMPLE) + currentSecond = smp->ts.origin.tv_sec; + armSecDetect = false; } } - if (cntEdges < 2) - return Hook::Reason::SKIP_SAMPLE; - - smp->ts.origin = tsVirt; - smp->flags |= (int)SampleFlags::HAS_TS_ORIGIN; - - if ((smp->sequence - lastSequence) > 1) - logger->warn("Samples missed: {} sampled missed", - smp->sequence - lastSequence); - - lastSequence = smp->sequence; - return Hook::Reason::OK; - } - - villas::node::Hook::Reason processHorizon(struct Sample *smp) { - assert(state == State::STARTED); - - // Get value of PPS signal - float value = smp->data[signalIndex].f; // TODO check if it is really float - - // Detect Edge - bool isEdge = lastValue < threshold && value > threshold; - lastValue = value; - - if (isEdge) { - if (isSynced) { - if (tsVirt.tv_nsec > 0.5e9) - timeError += 1.0 - (tsVirt.tv_nsec / 1.0e9); - else - timeError -= (tsVirt.tv_nsec / 1.0e9); - - filterWindow[cntEdges % filterWindow.size()] = cntSmpsTotal; - // Estimated sample period over last 'horizonEstimation' seconds - unsigned int tmp = - cntEdges < filterWindow.size() ? cntEdges : horizonEstimation; - double cntSmpsAvg = - (cntSmpsTotal - - filterWindow[(cntEdges - tmp) % filterWindow.size()]) / - tmp; - periodEstimate = 1.0 / cntSmpsAvg; - periodErrorCompensation = - timeError / (cntSmpsAvg * horizonCompensation); - period = periodEstimate + periodErrorCompensation; - } else { - if (timeSource == TimeSource::CLOCK_RELATIME) { - timespec t; - clock_gettime(CLOCK_RELATIME, &t); - tsVirt.tv_sec = t.tv_sec; - } else if (timeSource == TimeSource::SAMPLE) { - tsVirt.tv_sec = smp->ts.origin.tv_sec; - } - tsVirt.tv_nsec = 0; - isSynced = true; - cntEdges = 0; - cntSmpsTotal = 0; - } - cntSmps = 0; - cntEdges++; - - logger->debug( - "Time Error is: {} periodEstimate {} periodErrorCompensation {}", - timeError, periodEstimate, periodErrorCompensation); - } - cntSmps++; - cntSmpsTotal++; - if (cntEdges < 5) + if (cntEdges < 2) return Hook::Reason::SKIP_SAMPLE; - smp->ts.origin = tsVirt; smp->flags |= (int)SampleFlags::HAS_TS_ORIGIN; - struct timespec tsPeriod = time_from_double(period); - tsVirt = time_add(&tsVirt, &tsPeriod); - if ((smp->sequence - lastSequence) > 1) logger->warn("Samples missed: {} sampled missed", smp->sequence - lastSequence); lastSequence = smp->sequence; - return Hook::Reason::OK; } }; diff --git a/tests/integration/hook-pps_ts.sh b/tests/integration/hook-pps_ts.sh index 897497c29..5d178c3e5 100755 --- a/tests/integration/hook-pps_ts.sh +++ b/tests/integration/hook-pps_ts.sh @@ -18,47 +18,46 @@ trap finish EXIT cat > input.dat < expect.dat < output.dat +villas hook pps_ts -o signal=signal0 -o threshold=2. -o time_source=sample < input.dat > output.dat villas compare output.dat expect.dat