From 5bbd7463f6b3b6d8158508cbe0bbfe35d27174a1 Mon Sep 17 00:00:00 2001 From: Matti Airas Date: Thu, 6 Jun 2024 09:46:53 +0300 Subject: [PATCH] Implement different Repeat transforms --- examples/repeat_transform.cpp | 143 ++++++++++++++++++ src/sensesp/transforms/repeat.h | 180 +++++++++++++++++++++++ src/sensesp/transforms/repeat_report.cpp | 46 ------ src/sensesp/transforms/repeat_report.h | 54 +------ 4 files changed, 329 insertions(+), 94 deletions(-) create mode 100644 examples/repeat_transform.cpp create mode 100644 src/sensesp/transforms/repeat.h delete mode 100644 src/sensesp/transforms/repeat_report.cpp diff --git a/examples/repeat_transform.cpp b/examples/repeat_transform.cpp new file mode 100644 index 000000000..a766edf5f --- /dev/null +++ b/examples/repeat_transform.cpp @@ -0,0 +1,143 @@ +/** + * @file repeat_transform.cpp + * @brief Example of different Repeat transforms. + * + * Repeat transforms transmit their input value at regular intervals, ensuring + * output even at the absence of input. + * + * Try running this code with the serial monitor open ("Upload and Monitor" + * in PlatformIO menu). The program will produce capital letters every second, + * lowercase letters every 3 seconds, and integers every 10 seconds. Comment + * out and enable different Repeat transform variants to see how they affect the + * output. + * + */ + +#include "sensesp.h" + +#include + +#include "sensesp/sensors/sensor.h" +#include "sensesp/system/lambda_consumer.h" +#include "sensesp/transforms/repeat.h" +#include "sensesp_minimal_app_builder.h" + +using namespace sensesp; + +ReactESP app; + +SensESPMinimalApp* sensesp_app; + +void setup() { + SetupLogging(); + + // Note: SensESPMinimalAppBuilder is used to build the app. This creates + // a minimal app with no networking or other bells and whistles which + // would be distracting in this example. In normal use, this is not what + // you would use. Unless, of course, you know that is what you want. + SensESPMinimalAppBuilder builder; + + sensesp_app = builder.get_app(); + + // Produce capital letters every second + auto sensor_A = new RepeatSensor(1000, []() { + static char value = 'Z'; + if (value == 'Z') { + value = 'A'; + } else { + value += 1; + } + return value; + }); + + sensor_A->connect_to(new LambdaConsumer( + [](char value) { ESP_LOGD("App", " %c", value); })); + + // Produce lowercase letters every 3 seconds + auto sensor_a = new RepeatSensor(3000, []() { + static char value = 'z'; + if (value == 'z') { + value = 'a'; + } else { + value += 1; + } + return value; + }); + + sensor_a->connect_to(new LambdaConsumer( + [](char value) { ESP_LOGD("App", " %c", value); })); + + // Produce integers every 10 seconds + auto sensor_int = new RepeatSensor(10000, []() { + static int value = 0; + value += 1; + return value; + }); + + sensor_int->connect_to(new LambdaConsumer( + [](int value) { ESP_LOGD("App", " %d", value); })); + + // Repeat the values every 2 seconds + + auto repeat_A = new Repeat(2000); + auto repeat_a = new Repeat(2000); + auto repeat_int = new Repeat(2000); + + // Pay attention to the individual columns of the program console output. + // Capital letters are produced every second. Repeat gets always triggered as + // a result, but never on its own because the repeat timer always is reset + // before it expires. Lowercase letters are produced every 3 seconds. Repeat + // gets triggered immediately and then again after 2 seconds. Integers are + // produced every 10 seconds. Repeat gets triggered immediately and then + // again after every 2 seconds until the next integer is produced. + + // Try commenting out the Repeat lines above and uncommenting the + // RepeatStopping lines below. + + // auto repeat_A = new RepeatStopping(2000, 5000); + // auto repeat_a = new RepeatStopping(2000, 5000); + // auto repeat_int = new RepeatStopping(2000, 5000); + + // The maximum age is set to 5 seconds. Both the capital and lowercase + // letters are produced like before because their repetition rates are + // faster than the expiration time. However, the integers are produced + // only every 10 seconds, and they stop being repeated after 5 seconds + // until a new integer is produced. + + // auto repeat_A = new RepeatExpiring(2000, 5000, '?'); + // auto repeat_a = new RepeatExpiring(2000, 5000, '?'); + // auto repeat_int = new RepeatExpiring(2000, 5000, -1); + + // The expiration time is set to 5 seconds. Both the capital and lowercase + // letters are produced like before because their repetition rates are + // faster than the expiration time. However, the integers are produced + // only every 10 seconds, and the value does expire after 5 seconds, indicated + // by the -1 value that gets output after the expiry, until a new integer is + // produced. + + // Finally, try commenting out the RepeatExpiring lines above and uncommenting + // the RepeatConstantRate lines below. + + // auto repeat_A = new RepeatConstantRate(2000, 5000, '?'); + // auto repeat_a = new RepeatConstantRate(2000, 5000, '?'); + // auto repeat_int = new RepeatConstantRate(2000, 5000, -1); + + // Notice how the repetitions are no longer triggered by the sensors but + // are produced at a constant rate, in clusters. The letters still + // never expire, but the integers do. + + sensor_A->connect_to(repeat_A); + sensor_a->connect_to(repeat_a); + sensor_int->connect_to(repeat_int); + + repeat_A->connect_to(new LambdaConsumer( + [](char value) { ESP_LOGD("App", "Repeat: %c", value); })); + + repeat_a->connect_to(new LambdaConsumer( + [](char value) { ESP_LOGD("App", "Repeat: %c", value); })); + + repeat_int->connect_to(new LambdaConsumer( + [](int value) { ESP_LOGD("App", "Repeat: %d", value); })); +} + +void loop() { app.tick(); } diff --git a/src/sensesp/transforms/repeat.h b/src/sensesp/transforms/repeat.h new file mode 100644 index 000000000..3b4377f7e --- /dev/null +++ b/src/sensesp/transforms/repeat.h @@ -0,0 +1,180 @@ +#ifndef SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_H_ +#define SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_H_ + +#include + +#include "transform.h" + +namespace sensesp { + +/** + * @brief Repeat the input at specific intervals. + * + * Ensures that values that do not change frequently are still + * reported at a specified interval. If the value has not + * changed in interval milliseconds, the current value is emmitted + * again. + * + * The repetition only occurs if the value has changed. + * + * @param interval Maximum time, in ms, before the previous value + * is emitted again. + * + * @param config_path Path to configure this transform in the Config UI. + */ +template +class Repeat : public SymmetricTransform { + public: + Repeat(long interval) : SymmetricTransform(), interval_{interval} {} + + void set(T input, uint8_t inputChannel = 0) override { + this->emit(input); + if (repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + repeat_reaction_->remove(); + } + repeat_reaction_ = + ReactESP::app->onRepeat(interval_, [this]() { this->notify(); }); + } + + protected: + long interval_; + RepeatReaction* repeat_reaction_ = nullptr; +}; + +// For compatibility with the old RepeatReport class +template +class RepeatReport : public Repeat {}; + +/** + * @brief Repeat transform that stops emitting if the value age exceeds + * max_age. + * + * @tparam T + */ +template +class RepeatStopping : public Repeat { + public: + RepeatStopping(long interval, long max_age) + : Repeat(interval), max_age_{max_age} { + age_ = max_age; + + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + } + this->repeat_reaction_ = ReactESP::app->onRepeat( + this->interval_, [this]() { this->repeat_function(); }); + } + + virtual void set(T input, uint8_t inputChannel = 0) override { + this->emit(input); + age_ = 0; + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + } + this->repeat_reaction_ = ReactESP::app->onRepeat( + this->interval_, [this]() { this->repeat_function(); }); + } + + protected: + elapsedMillis age_; + long max_age_; + + protected: + void repeat_function() { + if (age_ < max_age_) { + this->notify(); + } else { + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + this->repeat_reaction_ = nullptr; + } + } + }; +}; + + +/** + * @brief Repeat transform that emits an expired value if the value age exceeds + * max_age. + * + * @tparam T + */ +template +class RepeatExpiring : public Repeat { + public: + RepeatExpiring(long interval, long max_age, T expired_value) + : Repeat(interval), max_age_{max_age}, expired_value_{expired_value} { + age_ = max_age; + + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + } + this->repeat_reaction_ = ReactESP::app->onRepeat( + this->interval_, [this]() { this->repeat_function(); }); + } + + virtual void set(T input, uint8_t inputChannel = 0) override { + this->emit(input); + age_ = 0; + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + } + this->repeat_reaction_ = ReactESP::app->onRepeat( + this->interval_, [this]() { this->repeat_function(); }); + } + + protected: + elapsedMillis age_; + long max_age_; + T expired_value_; + + protected: + void repeat_function() { + if (age_ < max_age_) { + this->notify(); + } else { + this->emit(expired_value_); + } + }; +}; + +/** + * @brief Repeat transform that emits the last value at a constant interval. + * + * The last value is emitted at a constant interval, regardless of whether the + * value has changed or not. If the value age exceeds max_age, expired_value is + * emitted. + * + * This is particularly useful for outputs that expect a value at a constant + * rate such as NMEA 2000. + * + */ +template +class RepeatConstantRate : public RepeatExpiring { + public: + RepeatConstantRate(long interval, long max_age, T expired_value) + : RepeatExpiring(interval, max_age, expired_value) { + if (this->repeat_reaction_ != nullptr) { + // Delete the old repeat reaction + this->repeat_reaction_->remove(); + } + + this->repeat_reaction_ = ReactESP::app->onRepeat( + interval, [this]() { this->repeat_function(); }); + } + + void set(T input, uint8_t inputChannel = 0) override { + this->output = input; + this->age_ = 0; + } +}; + +} // namespace sensesp + +#endif // SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_H_ diff --git a/src/sensesp/transforms/repeat_report.cpp b/src/sensesp/transforms/repeat_report.cpp deleted file mode 100644 index 99e00d352..000000000 --- a/src/sensesp/transforms/repeat_report.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "repeat_report.h" - -namespace sensesp { - -template -void RepeatReport::set(T input, uint8_t inputChannel) { - last_update_interval_ = 0; - this->emit(input); -} - -template -void RepeatReport::get_configuration(JsonObject& root) { - root["max_silence_interval"] = max_silence_interval_; -} - -static const char SCHEMA[] PROGMEM = R"###({ - "type": "object", - "properties": { - "max_silence_interval": { "title": "Max ms interval until value repeated", "type": "number" } - } - })###"; - -template -String RepeatReport::get_config_schema() { - return FPSTR(SCHEMA); -} - -template -bool RepeatReport::set_configuration(const JsonObject& config) { - String expected[] = {"max_silence_interval"}; - for (auto str : expected) { - if (!config.containsKey(str)) { - return false; - } - } - this->max_silence_interval_ = config["max_silence_interval"]; - return true; -} - -// Force compiler to make versions for the common data types... -template class RepeatReport; -template class RepeatReport; -template class RepeatReport; -template class RepeatReport; - -} // namespace sensesp diff --git a/src/sensesp/transforms/repeat_report.h b/src/sensesp/transforms/repeat_report.h index bb58038de..842e891fe 100644 --- a/src/sensesp/transforms/repeat_report.h +++ b/src/sensesp/transforms/repeat_report.h @@ -1,51 +1,9 @@ -#ifndef _repeat_report_H_ -#define _repeat_report_H_ +#ifndef SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_REPORT_H_ +#define SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_REPORT_H_ -#include +#include "repeat.h" -#include "transform.h" +// Output a compiler warning +#pragma message "RepeatReport is deprecated. Use Repeat instead." -namespace sensesp { - -/** - * @brief Ensures that values that do not change frequently are still - * reported at a specified maximum silence interval. If the value has not - * changed in max_silence_interval milliseconds, the current value is emmitted - * again. - * - * @param max_silence_interval Maximum time, in ms, before the previous value - * is emitted again. Default is 15000 (15 seconds). Setting the interval to - * zero disables the repeating. - * - * @param config_path Path to configure this transform in the Config UI. - */ -template -class RepeatReport : public SymmetricTransform { - public: - RepeatReport(long max_silence_interval = 15000, String config_path = "") - : SymmetricTransform(config_path), - max_silence_interval_{max_silence_interval} { - this->load_configuration(); - - ReactESP::app->onRepeat(10, [this]() { - if (max_silence_interval_ > 0 && - last_update_interval_ > max_silence_interval_) { - this->last_update_interval_ = 0; - this->notify(); - } - }); - } - - virtual void set(T input, uint8_t inputChannel = 0) override; - virtual void get_configuration(JsonObject& doc) override; - virtual bool set_configuration(const JsonObject& config) override; - virtual String get_config_schema() override; - - private: - long max_silence_interval_; - elapsedMillis last_update_interval_; -}; - -} // namespace sensesp - -#endif +#endif // SENSESP_SRC_SENSESP_TRANSFORMS_REPEAT_REPORT_H_