Skip to content
Merged
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 .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ jobs:
target: esp32
- path: 'components/thermistor/example'
target: esp32
- path: 'components/timer/example'
target: esp32
- path: 'components/wifi/example'
target: esp32

Expand Down
3 changes: 3 additions & 0 deletions components/timer/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
idf_component_register(
INCLUDE_DIRS "include"
REQUIRES logger task)
21 changes: 21 additions & 0 deletions components/timer/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)

# add the component directories that we want to use
set(EXTRA_COMPONENT_DIRS
"../../../components/"
)

set(
COMPONENTS
"main esptool_py timer"
CACHE STRING
"List of components to include"
)

project(timer_example)

set(CMAKE_CXX_STANDARD 20)
25 changes: 25 additions & 0 deletions components/timer/example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Timer Example

This example shows some various different ways of starting and stopping timers,
as well as some different examples of ways the timers can be configured, such as
repeating or one-shot, and how to use the timer's callback function.

## How to use example

### Build and Flash

Build the project and flash it to the board, then run monitor tool to view serial output:

```
idf.py -p PORT flash monitor
```

(Replace PORT with the name of the serial port to use.)

(To exit the serial monitor, type ``Ctrl-]``.)

See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.

## Example Output

![CleanShot 2023-06-30 at 09 53 37](https://github.com/esp-cpp/espp/assets/213467/9397a86b-29a6-4d37-831e-ab674e28cad1)
2 changes: 2 additions & 0 deletions components/timer/example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
idf_component_register(SRC_DIRS "."
INCLUDE_DIRS ".")
121 changes: 121 additions & 0 deletions components/timer/example/main/timer_example.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#include <chrono>
#include <vector>

#include "logger.hpp"
#include "timer.hpp"

using namespace std::chrono_literals;

extern "C" void app_main(void) {
espp::Logger logger({.tag = "Timer example", .level = espp::Logger::Verbosity::DEBUG});
size_t num_seconds_to_run = 3;
static auto start = std::chrono::high_resolution_clock::now();

static auto elapsed = []() {
auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration<float>(now - start).count();
};

// basic timer example
{
logger.info("[{:.3f}] Starting basic timer example", elapsed());
//! [timer example]
auto timer_fn = []() {
static size_t iterations{0};
fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations);
iterations++;
// we don't want to stop, so return false
return false;
};
auto timer = espp::Timer({.name = "Timer 1",
.period = 500ms,
.callback = timer_fn,
.log_level = espp::Logger::Verbosity::DEBUG});
//! [timer example]
std::this_thread::sleep_for(num_seconds_to_run * 1s);
}

// timer with delay example
{
logger.info("[{:.3f}] Starting timer with delay example", elapsed());
//! [timer delay example]
auto timer_fn = []() {
static size_t iterations{0};
fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations);
iterations++;
// we don't want to stop, so return false
return false;
};
auto timer =
espp::Timer({.name = "Timer 1",
.period = 500ms,
.delay = 500ms,
.callback = timer_fn,
.auto_start = false, // don't start the timer automatically, we'll call start()
.log_level = espp::Logger::Verbosity::DEBUG});
timer.start();
std::this_thread::sleep_for(2s);
logger.info("[{:.3f}] Cancelling timer for 2 seconds", elapsed());
timer.cancel();
std::this_thread::sleep_for(2s);
timer.start();
std::this_thread::sleep_for(2s);
logger.info("[{:.3f}] Cancelling timer for 2 seconds", elapsed());
timer.cancel();
std::this_thread::sleep_for(2s);
timer.start(1s);
//! [timer delay example]
std::this_thread::sleep_for(num_seconds_to_run * 1s);
}

// oneshot timer example
{
logger.info("[{:.3f}] Starting oneshot timer example", elapsed());
//! [timer oneshot example]
auto timer_fn = []() {
static size_t iterations{0};
fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations);
iterations++;
// we don't want to stop, so return false
return false;
};
auto timer = espp::Timer({.name = "Timer 1",
.period = 0ms, // one shot timer
.delay = 500ms,
.callback = timer_fn,
.log_level = espp::Logger::Verbosity::DEBUG});
//! [timer oneshot example]
std::this_thread::sleep_for(num_seconds_to_run * 1s);
}

// timer cancel itself example
{
logger.info("[{:.3f}] Starting timer cancel itself example", elapsed());
//! [timer cancel itself example]
auto timer_fn = []() {
static size_t iterations{0};
fmt::print("[{:.3f}] #iterations = {}\n", elapsed(), iterations);
iterations++;
// cancel the timer after 3 iterations
if (iterations == 3) {
fmt::print("[{:.3f}] auto-cancelling timer\n", elapsed());
return true;
}
return false;
};
auto timer = espp::Timer({.name = "Timer 1",
.period = 500ms,
.callback = timer_fn,
.log_level = espp::Logger::Verbosity::DEBUG});
//! [timer cancel itself example]
std::this_thread::sleep_for(num_seconds_to_run * 1s);
}

logger.info("Test ran for {:.03f} seconds", elapsed());

logger.info("Example complete!");

while (true) {
std::this_thread::sleep_for(1s);
}
}
4 changes: 4 additions & 0 deletions components/timer/example/sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Common ESP-related
#
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
186 changes: 186 additions & 0 deletions components/timer/include/timer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#pragma once

#include <chrono>
#include <functional>
#include <string>

#include "logger.hpp"
#include "task.hpp"

namespace espp {
/// @brief A timer that can be used to schedule tasks to run at a later time.
/// @details A timer can be used to schedule a task to run at a later time.
/// The timer will run in the background and will call the task when
/// the time is up. The timer can be canceled at any time. A timer
/// can be configured to run once or to repeat.
///
/// The timer uses a task to run in the background. The task will
/// sleep until the timer is ready to run. When the timer is ready to
/// run, the task will call the callback function. The callback
/// function can return true to cancel the timer or false to keep the
/// timer running. If the timer is configured to repeat, then the
/// callback function will be called again after the period has
/// elapsed. If the timer is configured to run once, then the
/// callback function will only be called once.
///
/// The timer can be configured to start automatically when it is
/// constructed. If the timer is not configured to start
/// automatically, then the timer can be started by calling start().
/// The timer can be canceled at any time by calling cancel().
///
/// @note The timer uses a task to run in the background, so the timer
/// callback function will be called in the context of the task. The
/// timer callback function should not block for a long time because it
/// will block the task. If the timer callback function blocks for a
/// long time, then the timer will not be able to keep up with the
/// period.
///
/// \section timer_ex1 Timer Example 1
/// \snippet timer_example.cpp timer example
/// \section timer_ex2 Timer Delay Example
/// \snippet timer_example.cpp timer delay example
/// \section timer_ex3 Oneshot Timer Example
/// \snippet timer_example.cpp timer oneshot example
/// \section timer_ex4 Timer Cancel Itself Example
/// \snippet timer_example.cpp timer cancel itself example
class Timer {
public:
typedef std::function<bool()>
callback_fn; ///< The callback function type. Return true to cancel the timer.

/// @brief The configuration for the timer.
struct Config {
std::string_view name; ///< The name of the timer.
std::chrono::duration<float>
period; ///< The period of the timer. If 0, the timer callback will only be called once.
std::chrono::duration<float> delay{
0}; ///< The delay before the first execution of the timer callback after start() is called.
callback_fn callback; ///< The callback function to call when the timer expires.
size_t stack_size_bytes{4096}; ///< The stack size of the task that runs the timer.
bool auto_start{true}; ///< If true, the timer will start automatically when constructed.
espp::Logger::Verbosity log_level =
espp::Logger::Verbosity::WARN; ///< The log level for the timer.
};

/// @brief Construct a new Timer object
/// @param config The configuration for the timer.
Timer(const Config &config)
: period_(config.period), delay_(config.delay), callback_(config.callback),
logger_({.tag = config.name,
.rate_limit = std::chrono::milliseconds(100),
.level = config.log_level}) {
// make the task
task_ = espp::Task::make_unique({
.name = std::string(config.name) + "_task",
.callback = std::bind(&Timer::timer_callback_fn, this, std::placeholders::_1,
std::placeholders::_2),
.stack_size_bytes = config.stack_size_bytes,
});
if (config.auto_start) {
start();
}
}

/// @brief Destroy the Timer object
/// @details Cancels the timer if it is running.
~Timer() { cancel(); }

/// @brief Start the timer.
/// @details Starts the timer. Does nothing if the timer is already running.
void start() {
logger_.info("starting with period {:.3f} s and delay {:.3f} s", period_.count(),
delay_.count());
// start the task
task_->start();
}

/// @brief Start the timer with a delay.
/// @details Starts the timer with a delay. If the timer is already running,
/// this will cancel the timer and start it again with the new
/// delay. If the timer is not running, this will start the timer
/// with the delay. Overwrites any previous delay that might have
/// been set.
/// @param delay The delay before the first execution of the timer callback.
void start(std::chrono::duration<float> delay) {
if (delay.count() < 0) {
logger_.warn("delay cannot be negative, not starting");
return;
}
if (is_running()) {
logger_.info("restarting with delay {:.3f} s", delay.count());
cancel();
}
delay_ = delay;
start();
}

/// @brief Cancel the timer.
/// @details Cancels the timer.
void cancel() {
logger_.info("canceling");
// cancel the task
task_->stop();
}

/// @brief Check if the timer is running.
/// @details Checks if the timer is running.
/// @return true if the timer is running, false otherwise.
bool is_running() const { return task_->is_running(); }

protected:
bool timer_callback_fn(std::mutex &m, std::condition_variable &cv) {
if (!callback_) {
// stop the timer, the callback is null
return true;
}
// initial delay, if any - this is only used the first time the timer
// runs
if (delay_.count() > 0) {
logger_.debug("waiting for delay {:.3f} s", delay_.count());
std::unique_lock<std::mutex> lock(m);
auto cv_retval = cv.wait_for(lock, delay_);
// now set the delay to 0
delay_ = std::chrono::duration<float>(0);
if (cv_retval == std::cv_status::no_timeout) {
// if there was no timeout, then we were notified, which means that the timer
// was canceled while waiting for the delay, so we should go ahead and return
return true;
}
}
// now run the callback
auto start = std::chrono::high_resolution_clock::now();
bool requested_stop = callback_();
if (requested_stop || period_.count() == 0) {
// stop the timer if requested or if the period is 0
return true;
}
auto end = std::chrono::high_resolution_clock::now();
float elapsed = std::chrono::duration<float>(end - start).count();
if (elapsed > period_.count()) {
// if the callback took longer than the period, then we should just
// return and run the callback again immediately
logger_.warn_rate_limited("callback took longer ({:.3f} s) than period ({:.3f} s)", elapsed,
period_.count());
return false;
}
// now wait for the period (taking into account the time it took to run
// the callback)
{
std::unique_lock<std::mutex> lock(m);
cv.wait_until(lock, start + period_);
// Note: we don't care about cv_retval here because we are going to
// return from the function anyway. If the timer was canceled, then
// the task will be stopped and the callback will not be called again.
}
// keep the timer running
return false;
}

std::chrono::duration<float> period_{
0}; ///< The period of the timer. If 0, the timer will run once.
std::chrono::duration<float> delay_{0}; ///< The delay before the timer starts.
callback_fn callback_; ///< The callback function to call when the timer expires.
std::unique_ptr<espp::Task> task_; ///< The task that runs the timer.
espp::Logger logger_; ///< The logger for the timer.
};
} // namespace espp
2 changes: 2 additions & 0 deletions doc/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ EXAMPLE_PATH += $(PROJECT_PATH)/components/state_machine/example/main/hfsm_examp
EXAMPLE_PATH += $(PROJECT_PATH)/components/tabulate/example/main/tabulate_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/task/example/main/task_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/thermistor/example/main/thermistor_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/timer/example/main/timer_example.cpp
EXAMPLE_PATH += $(PROJECT_PATH)/components/wifi/example/main/wifi_example.cpp

## The 'INPUT' statement below is used as input by script 'gen-df-input.py'
Expand Down Expand Up @@ -139,6 +140,7 @@ INPUT += $(PROJECT_PATH)/components/state_machine/include/state_machine.hpp
INPUT += $(PROJECT_PATH)/components/tabulate/include/tabulate.hpp
INPUT += $(PROJECT_PATH)/components/task/include/task.hpp
INPUT += $(PROJECT_PATH)/components/thermistor/include/thermistor.hpp
INPUT += $(PROJECT_PATH)/components/timer/include/timer.hpp
INPUT += $(PROJECT_PATH)/components/wifi/include/wifi_ap.hpp
INPUT += $(PROJECT_PATH)/components/wifi/include/wifi_sta.hpp

Expand Down
1 change: 1 addition & 0 deletions doc/en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ This is the documentation for esp-idf c++ components, ESPP (`espp <https://githu
tabulate
task
thermistor
timer
wifi/index
Loading