ReactiveESP32 Logo

ReactiveESP32

Fine-grained reactive programming for ESP32 microcontrollers

Arduino Library Badge PlatformIO Registry

Static Badge License

Ko-fi


Table of contents

Description

ReactiveESP32 brings modern fine-grained reactivity to ESP32 microcontrollers. Inspired by Angular Signals, it provides a declarative approach to managing state and side effects in embedded systems.

The library is designed specifically for ESP32/FreeRTOS environments, featuring automatic dependency tracking, thread-safe operations, and a dedicated dispatcher task to handle reactive updates without blocking the main application.

Key Features

  • Fine-grained Reactivity - Only affected computations re-execute when dependencies change.

  • Zero-copy Design - Efficient memory usage optimized for embedded systems.

  • No heap usage - No dynamic memory allocation, ensuring predictable performance.

  • FreeRTOS Integration - Native support for ESP32 RTOS with dedicated dispatcher task.

  • Type-safe - Leverages C++17 templates for compile-time safety.

  • Signal History - Optional tracking of previous values.

  • 50+ Built-in Filters - Comprehensive filtering library for common patterns.

  • 25+ Helper Functions - Ready-to-use utilities for reactive programming.

Quick Example

#include <ReactiveESP32.h>
using namespace RxESP32;

// Create reactive signals
Signal<int> temperature_c(20);
Signal<bool> heating_enabled(false);

// Computed signal: automatically updates when dependencies change
Computed<bool> should_heat([]() { return heating_enabled.get() && temperature_c.get() < 18; });

// Effect: runs automatically when dependencies change
Effect<> heater_control([]() {
  if (should_heat.get()) {
    Serial.println("Heater ON");
    digitalWrite(HEATER_PIN, HIGH);
  } else {
    Serial.println("Heater OFF");
    digitalWrite(HEATER_PIN, LOW);
  }
  return nullptr; // No cleanup function needed
});

void setup() {
  Serial.begin(115200);
  pinMode(HEATER_PIN, OUTPUT);

  // Start the reactive dispatcher
  if (!Dispatcher::start()) {
    Serial.println("Failed to start ReactiveESP32!");
    while (1)
      delay(1000);
  }

  Serial.println("ReactiveESP32 initialized!");
}

void loop() {
  // Read sensor and update signal
  int temp = readTemperatureSensor();
  temperature.set(temp); // Updates propagate automatically!

  delay(1000);
}

Installation

PlatformIO

Add to your platformio.ini:

[env:your_env_name]
# Use pioarduino !
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip

; Most recent changes
lib_deps =
  https://github.com/alkonosst/ReactiveESP32.git

; Release vx.y.z (using an exact version is recommended)
lib_deps =
  https://github.com/alkonosst/ReactiveESP32.git#vx.y.z

Arduino IDE

  1. Open Arduino IDE

  2. Go to Sketch > Manage Libraries…

  3. Search for “ReactiveESP32”

  4. Click Install

  5. Go to Sketch > Include Library

  6. Click ReactiveESP32

Or download the latest release from GitHub and extract to your Arduino libraries folder.

Usage

Including the library

This single header includes all core functionality and utilities:

#include <ReactiveESP32.h>

Namespace

All functionality is in the RxESP32 namespace:

using namespace RxESP32;

Signal<int> counter(0);
Computed<int> doubled([]() { return counter.get() * 2; });
Effect<> logger([]() {
    Serial.println(counter.get());
    return nullptr;
});

Core Concepts

Signal (Reactive State)

Signals hold reactive values that notify dependents when changed:

// Create signal with initial value
Signal<int> count(0);

// Read value (tracks dependencies in Computed/Effect context)
int value = count.get();

// Update value
count.set(42);

// Update with function
count.update([](const int& current) {
    return current + 1;
});

// Read without tracking dependencies
int peeked = count.peek();

Computed (Derived Values)

Computed signals automatically recompute when dependencies change:

Signal<int> width(10);
Signal<int> height(20);

// Dependencies are tracked automatically when using get()
Computed<int> area([]() { return width.get() * height.get(); });

width.set(15); // 'area' automatically recalculates

Effect (Side Effects)

Effects run automatically when their dependencies change:

Signal<int> temperature(25);

Effect<> logger([]() {
  Serial.printf("Temperature: %d C\n", temperature.get());
  return nullptr; // No cleanup function
});

// Effect with cleanup which runs before re-execution
Effect<> timer([]() {
  int interval = temperature.get() * 100;

  // Start timer...

  // Return cleanup function
  return []() {
    // Reset timer...
  };
});

Starting the Dispatcher

The dispatcher must be started before using Computed or Effect:

void setup() {
  Serial.begin(115200);

  // Start with default settings (priority 1, 4096 bytes stack)
  if (!Dispatcher::start()) {
    Serial.println("Failed to start dispatcher!");
    while (1)
      delay(1000);
  }

  // You can customize priority, stack size and a lot of parameters
  // See: ReactiveESP32Config.h
}

Configuration Options

Signal Options

Signal<int> counter(0, {
    .name = "counter",           // Optional name for debugging
    .skip_filter_check = false   // Skip filter validation
});

Computed Options

Computed<int> doubled([]() {
    return value.get() * 2;
}, {
    .name = "doubled",            // Optional name
    .priority = Priority::Normal, // Execution priority
    .skip_initial_run = true,     // Skip first computation at creation
    .lazy = false,                // Lazy evaluation (compute on-demand)
    .skip_filter_check = false    // Skip filter validation
});

Effect Options

Effect<> logger([]() {
    Serial.println(value.get());
    return nullptr;
}, {
    .name = "logger",
    .priority = Priority::Normal, // Execution priority
    .skip_initial_run = false,    // Skip first execution at creation
    .lazy = false                 // Manual execution with run()
});

Filters

Filters control when signals propagate changes to their dependents. ReactiveESP32 includes 50+ built-in filters:

using namespace RxESP32::Filters;

// Numerical filters
Signal<int, 1, Numerical::WithinRange<int, 10, 30>> temp(20);  // Only 10-30
Signal<int, 1, Numerical::OutsideTolerance<int, 5>> sensor(0); // Change > +-5
Signal<int, 1, Numerical::GreaterThan<int, 50>> level(0);      // > 50

// Temporal filters
Signal<int, 1, Temporal::EveryNthChange<int, 5>> counter(0);  // Every 5th change
Signal<int, 1, Temporal::Debounce<int, 500>> input(0);        // 500ms debounce
Signal<int, 1, Temporal::RateLimited<int, 1000>> updates(0);  // Max 1/sec

// String filters
// - Min length 3
Signal<const char*, 1, StringType::MinLength<const char*, 3>> name(""); // Min length 3

// - Contains "ERROR"
const char err_str[] = "ERROR";
Signal<const char*, 1, StringType::Contains<const char*, err_str>> msg("");

See Filters Documentation for the complete list.

Helpers

Helper functions provide common reactive patterns. ReactiveESP32 includes +30 built-in helpers:

using namespace RxESP32::Helpers;

Signal<int> value1(0);
Signal<int> value2(0);

// Map: transform values
auto doubled = Transformation::map(value1, [](const int& v) { return v * 2; });

// Combine: merge multiple signals
auto combined = Combinatorial::combine([](int a, int b) { return a + b; }, value1, value2);

See Helpers Documentation for the complete list.

Examples

The library includes comprehensive examples for all features:

  • Basic: Basic Signal, Computed and Effect nodes (start here!).

  • Intermediate: Multiple signals, History, Priority, Batch Updates, Lazy Nodes.

  • Advanced: Cleanup functions, Filters, Helpers, RTOS integration.

  • Utilities: Metrics, Dependency Graphs.

Find all examples in the examples/ directory or view them in the documentation.

Documentation

Full documentation is available at: alkonosst.github.io/ReactiveESP32

Release Status

This project is in active development. Until reaching version v1.0.0, consider it beta software. APIs may change in future releases, and some features may be incomplete or unstable. Please report any issues on the GitHub Issues.

License

This project is licensed under the MIT License - see the LICENSE file for details.