Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Pulse

Pulse is a millisecond timer library for ESP32.

Pulse helps you schedule short runtime timeouts, intervals, and countdowns in Arduino ESP32 projects. It is designed for uptime-based timing that should not be affected by NTP sync, timezone changes, DST changes, or system date corrections.

CIReleaseLicense: MIT

Why use Pulse?

  • Uptime timers - all timing is based on ESP-IDF's monotonic runtime timer.
  • One task - timeouts, intervals, and countdowns are coordinated by one internal Pulse task.
  • Bounded counts - configured limits cap each timer type and the command queue.
  • Task-side callbacks - callbacks run from the internal Pulse task.
  • Lifecycle-safe shutdown - shutdown is independent of command-queue capacity and may be retried after a timeout.
  • Production-minded - result-based errors, synchronized lifecycle operations, diagnostics, and no explicit exceptions.

Install

PlatformIO

[env:esp32dev]platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
https://github.com/ZekStack/pulse.git
build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11

Arduino IDE

Pulse is not published to Arduino Library Manager yet.

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.

Arduino/libraries/Pulse

Quick start

#include<Arduino.h>
#include<Pulse.h>
Pulse pulse;
PulseTimerId intervalId = 0;
voidsetup() {
Serial.begin(115200);
PulseResult initResult = pulse.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}
pulse.setTimeout([]() {
Serial.println("after one second");
}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {
Serial.println("every second");
}, 1000);
if (interval) {
intervalId = interval.id;
}
}
voidloop() {
delay(1000);
}

Important notes

Important

Pulse callbacks run from the internal Pulse task. Keep callbacks short and offload long-running work to Worker.

  • setInterval() uses delay-after-callback timing and does not catch up missed ticks.
  • Countdown callbacks first run after tickMs; the final callback is guaranteed with isFinished=true.
  • clear(), pause(), resume(), and restart() enqueue nonblocking control commands.
  • A control that returns success is accepted for the current running lifecycle generation. Shutdown supersedes pending timer controls.
  • Controls queued by a callback are processed before another already-due timer is selected.
  • A timeout and a final countdown are terminal before their callback runs. Controls for that timer return TimerNotFound from the terminal callback.
  • Calling end() from a Pulse callback returns PulseStatus::Busy because the task cannot wait for itself.
  • If end(timeoutMs) returns Timeout, shutdown remains requested. Call end() again to continue waiting.
  • Destroying Pulse from another task waits until the scheduler is quiesced. Callbacks must eventually return.
  • Zero-millisecond timer values are rejected.
  • Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
  • Stack high-water diagnostics use the ESP-IDF byte value directly.
  • PulseStackType::Auto prefers PSRAM task stacks when supported and falls back to internal RAM.

Timing guarantees

Pulse uses ESP-IDF's 64-bit monotonic runtime timer internally. It is intended for short runtime timers, not wall-clock scheduling.

Threading and lifecycle model

All callbacks run from the internal Pulse task. Timer creation and control methods synchronize against initialization and shutdown.

Each successful init() begins a new internal lifecycle generation. end() targets the generation that was running when the call began. A delayed waiter from an older generation cannot stop a newer run.

The internal lifecycle is:

Uninitialized -> Running -> Stopping -> Stopped -> Uninitialized

Stopping means shutdown was requested but an active callback or scheduler cleanup may still be in progress. Timer creation and controls return Busy in this state. Diagnostics remain available.

Stopped means scheduler resources are quiesced and the scheduler task will no longer access them. Physical FreeRTOS task deletion follows using task-local values. A waiting end() then completes public lifecycle finalization.

Shutdown has a dedicated task wakeup and does not use the bounded command queue.

Callback control semantics

Controls from interval and non-final countdown callbacks are applied after the callback returns and before another due timer is dispatched.

An interval that pauses itself resumes after a complete interval. A non-final countdown that pauses itself preserves the delay until its next countdown tick.

Timeouts and final countdowns are removed from the registry before their terminal callback. Their own clear(), pause(), resume(), and restart() calls therefore return TimerNotFound.

Memory and exception model

Pulse does not explicitly throw exceptions. Internal Pulse allocations use checked non-throwing allocation where practical.

Timer records, shared_ptr control blocks, user callbacks, and std::function captures may allocate. Construction and storage of user-provided callbacks follow the standard-library and toolchain allocation behavior. A build with exceptions disabled proves compilation compatibility; it does not guarantee graceful failure for every standard-library allocation.

Fully fixed-capacity callback and timer-record storage may be introduced in a later release.

Examples

ExampleDescription
BasicMinimal init, timeout, interval, and clear.
CountdownCountdown ticks and final completion callback.
PauseResumeRestartTimer pause, resume, restart, and state checks.
ConfigAndLimitsStack, queue, and timer limit configuration.
DiagnosticsRuntime counters and queue diagnostics.
BindableCallbacksstd::bind with private class methods.

Start with:

examples/Basic

Documentation

Detailed documentation is available in the docs/ folder.

DocumentDescription
docs/getting-started.mdStep-by-step setup and first timer flow.
docs/configuration.mdConfig options, limits, stack behavior, and queue sizing.
docs/api.mdPublic classes, lifecycle, timer controls, and diagnostics.
docs/examples.mdExplanation of all included examples.
docs/troubleshooting.mdCommon issues and solutions.

API overview

Pulse pulse;
pulse.init();
PulseTimerResult timeout = pulse.setTimeout([]() {}, 1000);
PulseTimerResult interval = pulse.setInterval([]() {}, 1000);
PulseCountdownConfig countdown;
countdown.durationMs = 10000;
countdown.tickMs = 1000;
pulse.setCountdown(countdown, [](const PulseCountdownTick &tick) {});
pulse.pause(interval.id);
pulse.resume(interval.id);
pulse.clear(interval.id);
PulseDiag diag = pulse.getDiagnostics();

For the full API, see docs/api.md.

Compatibility

ItemSupport
FrameworkArduino ESP32
Platformespressif32
LanguageC++20
Filesystemnone
PSRAMOptional for task stacks when ESP-IDF support is available
Dependenciesnone
ExceptionsNo explicit throws; std::function follows toolchain behavior
Status0.1.0 release candidate

Configuration

PulseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PulseStackType::Auto;
config.maxTimeouts = 16;
config.maxIntervals = 16;
config.maxCountdowns = 8;
config.commandQueueSize = 20;
PulseResult result = pulse.init(config);

For all options, see docs/configuration.md.

Error handling

Pulse reports operation status through PulseResult and PulseTimerResult.

PulseTimerResult result = pulse.setTimeout([]() {}, 1000);
if (!result) {
Serial.println(result.message);
return;
}

For result fields and status codes, see docs/api.md.

License

MIT - see LICENSE.md.

ZekStack

Part of the ZekStack ESP32 library stack.

About

Pulse is a millisecond timer library for ESP32.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages