diff --git a/examples/time_counter.cpp b/examples/time_counter.cpp new file mode 100644 index 000000000..e00fe583d --- /dev/null +++ b/examples/time_counter.cpp @@ -0,0 +1,118 @@ +/** + * @file constant_sensor.cpp + * @brief Example of a TimeCounter that measures the time its input value is + * true on non-zero. + * + * The main use case for this transform is to measure the total engine hours. + * The value is stored in the flash drive whenever the input state changes to + * false (the engine is turned off). + * + * This example counts the input frequency of GPIO pin 15 and counts the + * time it is non-zero. + * + * GPIO 18 is configured as output. The output frequency is increased every + * 10 seconds. Connect pin 18 to pin 15 to test the example. + * + */ + +#include "sensesp_app.h" +#include "sensesp/sensors/digital_input.h" +#include "sensesp/transforms/lambda_transform.h" +#include "sensesp/transforms/time_counter.h" +#include "sensesp/transforms/frequency.h" +#include "sensesp_app_builder.h" + +using namespace sensesp; + +reactesp::ReactESP app; + +unsigned long cycle_start_time = 0; +unsigned long freq_start_time = 0; +int freq = 0; + +void setup() { + // Some initialization boilerplate when in debug mode... +#ifndef SERIAL_DEBUG_DISABLED + SetupSerialDebug(115200); +#endif + + // Create the builder object + SensESPAppBuilder builder; + sensesp_app = builder.get_app(); + + // set GPIO 18 to output mode + pinMode(18, OUTPUT); + app.onRepeat(10, []() { + if (freq == 0) { + if (millis() - freq_start_time >= 10000) { + freq = 10; + freq_start_time = millis(); + } else { + return; + } + } else { + if (millis() - freq_start_time >= 1000) { + freq += 10; + freq_start_time = millis(); + } + if (freq > 100) { + freq = 0; + return; + } + + if ((millis() - cycle_start_time) >= 1000 / freq) { + cycle_start_time = millis(); + } else { + return; + } + } + digitalWrite(18, !digitalRead(18)); + }); + + // Create a digital input counter sensor + auto digin_counter = + new DigitalInputCounter(15, INPUT, FALLING, 500, "/Sensors/Counter"); + + // Create a frequency transform + auto* frequency = new Frequency(1, "/Transforms/Frequency"); + digin_counter->connect_to(frequency); + + // create a propulsion state lambda transform + auto* propulsion_state = new LambdaTransform( + [](bool freq) { + if (freq > 0) { + return "started"; + } else { + return "stopped"; + } + }, + "/Transforms/Propulsion State"); + + frequency->connect_to(propulsion_state); + + // create engine hours counter using PersistentDuration + auto* engine_hours = + new TimeCounter("/Transforms/Engine Hours"); + + frequency->connect_to(engine_hours); + + // create and connect the frequency output object + frequency->connect_to( + new SKOutput("propulsion.main.revolutions", "", + new SKMetadata("Hz", "Main Engine Revolutions"))); + + // create and connect the propulsion state output object + propulsion_state->connect_to( + new SKOutput("propulsion.main.state", "", + new SKMetadata("", "Main Engine State"))); + + // create and connect the engine hours output object + engine_hours->connect_to( + new SKOutput("propulsion.main.runTime", "", + new SKMetadata("s", "Main Engine running time"))); + + // Start the SensESP application running + sensesp_app->start(); +} + +void loop() { app.tick(); } diff --git a/src/sensesp/transforms/time_counter.h b/src/sensesp/transforms/time_counter.h new file mode 100644 index 000000000..5e8a533dc --- /dev/null +++ b/src/sensesp/transforms/time_counter.h @@ -0,0 +1,98 @@ +#include "sensesp/transforms/transform.h" +#ifndef SENSESP_TRANSFORMS_TIME_COUNTER_H_ +#define SENSESP_TRANSFORMS_TIME_COUNTER_H_ + +namespace sensesp { + +static const char kTimeCounterSchema[] = R"({ + "type": "object", + "properties": { + "duration": { + "type": "number", + "title": "Total Duration", + "description": "Total accumulated duration while the input state is non-zero or true, in seconds" + } + }, + "required": ["duration"] + +})"; + +/** + * @brief A transform that outputs the duration of the input value being + * true or non-null. + * + * The main use case for this transform is to measure the total engine hours + * in a persistent way. The value is stored in the flash drive whenever the + * input state changes (the engine is turned on or off). + * + * @tparam T The type of the input value. Must be castable to a boolean. + */ +template +class TimeCounter : public Transform { + public: + TimeCounter(String config_path) + : Transform(config_path) { + this->load_configuration(); + } + + virtual void set_input(T input, uint8_t input_channel = 0) override { + if (previous_state_ == -1) { + // Initialize the previous state + previous_state_ = (bool)input; + start_time_ = millis(); + duration_at_start_ = duration_; + } + + // if previous_state_ is true, accumulate duration + if (previous_state_) { + duration_ = duration_at_start_ + (millis() - start_time_); + } + + if (input) { + if (previous_state_ == 0) { + // State change from false to true + previous_state_ = 1; + start_time_ = millis(); + duration_at_start_ = duration_; + } + } else { + if (previous_state_ == 1) { + // State change from true to false + previous_state_ = 0; + duration_ = duration_at_start_ + (millis() - start_time_); + this->save_configuration(); // Save configuration to flash, so that + // the duration is persistent + } + } + this->emit((float)duration_ / 1000.); + } + + virtual void get_configuration(JsonObject& root) override { + root["duration"] = duration_; + } + + virtual bool set_configuration(const JsonObject& config) override { + debugD("Setting TimeCounter configuration"); + if (!config.containsKey("duration")) { + return false; + } + duration_at_start_ = config["duration"]; + duration_ = duration_at_start_; + debugD("duration_at_start_ = %ld", duration_at_start_); + return true; + } + + virtual String get_config_schema() override { + return kTimeCounterSchema; + } + + protected: + int previous_state_ = -1; // -1 means uninitialized + unsigned long start_time_; + unsigned long duration_ = 0.; + unsigned long duration_at_start_ = 0.; +}; + +} // namespace sensesp + +#endif // SENSESP_TRANSFORMS_TIME_COUNTER_H_