Signal

template<typename T, size_t MaxDeps = 8, typename Filter = std::not_equal_to<T>, size_t HistorySize = 0>
class Signal : public RxESP32::ReactiveNode

Reactive state container with automatic dependency tracking.

Signal is the foundational reactive primitive that holds a mutable value. When the value changes, all dependent Computed nodes and Effect nodes are automatically notified and updated through the dispatcher system.

Since

v0.1.0

Key features:

  • Automatic dependency tracking when accessed within Computed or Effect.

  • Customizable change detection via Filter template parameter.

  • Optional signal history tracking.

  • Thread-safe operations with FreeRTOS mutexes.

  • Freeze/unfreeze for batch updates.

Usage:

// Basic int signal with initial value 0
Signal<int> counter(0);

// Named signal for debugging
Signal<float> temperature(25.0f, {.name = "temp"});

// Custom quantity of dependents (16)
Signal<bool, 16> counter(0);

// Custom change detection (only propagate if absolute change > 1.0)
Signal<float, 8, Filters::Numerical::OutsideTolerance<float, 1.0>> sensor(0.0f);

// With history tracking of last 5 values (*if enabled*)
Signal<int, 8, std::not_equal_to<int>, 5> historical(0);

Note

Signal history requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

Template Parameters:
typename T

Value type stored in the signal.

size_t MaxDeps = 8

Maximum number of dependent nodes.

typename Filter = std::not_equal_to<T>

Change detection predicate.

size_t HistorySize = 0

Number of historical values to track.

Public Types

using Callback = std::function<void(const T&)>

Callback function type.

Public Functions

inline explicit Signal(const T &initial_value = T(), const Options &options = {})

Construct a Signal with initial value.

Creates a new Signal instance and initializes it with the provided value. Automatically registers the signal for dependency tracking if enabled.

Since

v0.1.0

Signal<int> counter(0);
Signal<const char*> name("ESP32", {.name = "device_name"});
Signal<float> sensor(0.0f, {.skip_filter_check = true});

Parameters:
const T &initial_value = T()

Initial value for the signal.

const Options &options = {}

Configuration options for the signal.

inline ~Signal()

Destructor.

Cleans up resources and unregisters the signal from the dependency graph.

Since

v0.1.0

inline virtual Type getType() const override

Get the runtime type of this node.

Since

v0.1.0

Returns:

Type::Signal.

inline T get()

Get the current value of the signal.

If called within a Computed or Effect, automatically registers that node as a dependent. When the signal changes, all dependents are notified.

Since

v0.1.0

Signal<int> counter(0);
int value = counter.get();  // Simple read

// Automatic dependency tracking:
Computed<int> doubled([]() {
  return counter.get() * 2;  // counter.get() registers dependency
});

Returns:

Current value of the signal.

inline Status set(const T &new_value, bool force_notify = false)

Set a new value for the signal.

Updates the signal’s value and notifies all dependent Computed and Effect nodes if the value has changed according to the Filter predicate. The change detection can be bypassed with force_notify.

Since

v0.1.0

Signal<int> counter(0);
counter.set(5); // Sets to 5 and notifies dependents
counter.set(5); // Status::Unchanged (no notification)

// Force notification even if value is same
counter.set(5, true); // Notifies dependents regardless

Note

  • If signal is frozen, change is deferred until unfreeze().

  • All dependents are notified in a single dispatcher wake.

Parameters:
const T &new_value

The new value to set.

bool force_notify = false

If true, always notify dependents even if Filter says unchanged.

Returns:

Status::Ok on success, Status::Unchanged if value didn’t change, error code otherwise.

inline Status update(const std::function<T(const T&)> &updater, bool force_notify = false)

Update signal value using a transformation function.

Applies an updater function to the current value to compute the new value. This is atomic and thread-safe - the updater sees the current value and produces a new value without race conditions.

Since

v0.1.0

Signal<int> counter(0);

// Increment atomically
counter.update([](const int& current) { return current + 1; });

// Accumulate
counter.update([](const int& current) { return current + getNewValue(); });

Parameters:
const std::function<T(const T&)> &updater

Function that takes current value and returns new value.

bool force_notify = false

If true, always notify dependents even if Filter says unchanged.

Returns:

Status::Ok on success, Status::Unchanged if value didn’t change, error code otherwise.

inline Status mutate(const std::function<void(T&)> &mutator)

Mutate signal value in-place.

Provides direct mutable access to the signal’s value for in-place modification. This is more efficient than update() for complex types where you want to modify the value directly rather than create a copy.

Since

v0.1.0

Signal<std::array<int, 5>> numbers({1, 2, 3, 4, 5});

// Efficient in-place modification
numbers.mutate([](auto& vec) {
  for (auto& n : vec) {
    n *= 2; // Double each element
  }
});

// Modify struct fields
struct Config { int value; std::string name; };
Signal<Config> config({0, "default"});
config.mutate([](Config& c) {
  c.value = 42;
  c.name = "updated";
});

Note

  • Always notifies dependents (no change detection).

  • Ideal for modifying containers, large structs, or complex types.

  • If history is enabled, the new value after mutation is stored.

Parameters:
const std::function<void(T&)> &mutator

Function that takes mutable reference to current value.

Returns:

Status::Ok on success, error code otherwise.

inline Signal &operator=(const T &new_value)

Assignment operator for convenient value setting.

Equivalent to calling set() with the new value. Provides natural assignment syntax for signals.

Since

v0.1.0

Signal<int> counter(0);
counter = 5;  // Same as counter.set(5)

Warning

Using this operator you can’t obtain the Status result of set(). Use it with caution.

Parameters:
const T &new_value

The new value to set.

Returns:

Reference to this signal for chaining.

inline T operator()()

Function call operator for convenient value access.

Equivalent to calling get(). Provides function-style access to signal value. Automatically tracks dependencies when called inside Computed or Effect.

Since

v0.1.0

Signal<int> counter(0);
int value = counter(); // Same as counter.get()

Returns:

Current value of the signal.

inline T peek()

Read signal value without tracking dependencies.

Returns the current value without registering the caller as a dependent. This is useful when you need to access the value inside a Computed or Effect but don’t want changes to this signal to trigger re-execution.

Since

v0.1.0

Signal<int> counter(0);
Signal<int> multiplier(2);

// This Computed only re-executes when counter changes,
// not when multiplier changes
Computed<int> result([]() {
  return counter.get() * multiplier.peek(); // peek() prevents tracking
});

Returns:

Current value of the signal.

inline Status setQuietly(const T &new_value)

Set signal value without notifying dependents.

Updates the signal’s value silently without triggering any dependent Computed or Effect nodes. Useful for initializing values or making changes that shouldn’t propagate.

Since

v0.1.0

Signal<int> counter(0);
Effect counter_logger([]() {
  Serial.println(counter.get());
});

// This won't trigger the Effect
counter.setQuietly(5);

// But the value is updated
Serial.println(counter.peek()); // Prints: 5

Note

History is still updated (if enabled) and Filter check is bypassed.

Parameters:
const T &new_value

The new value to set.

Returns:

Status::Ok on success, error code otherwise.

inline Status freeze()

Freeze signal to defer notifications.

Prevents the signal from notifying dependents when set() is called. Changes are accumulated and notifications are deferred until unfreeze(). This is useful for batch updates to avoid intermediate computations.

Since

v0.1.0

Signal<int> x(0);
Signal<int> y(0);
Computed<int> sum([]() { return x.get() + y.get(); });

// Batch update - sum only recomputes once
x.freeze();
y.freeze();
x.set(10);
y.set(20);
x.unfreeze(false); // no notification yet
y.unfreeze(true); // "y" unfreeze triggers notification to dependents

Note

Value changes are still tracked and stored.

Returns:

Status::Ok on success, error code otherwise.

inline Status unfreeze(bool should_notify = true)

Unfreeze signal and optionally notify dependents.

Resumes normal operation after freeze(). If any changes occurred while frozen and should_notify is true, all dependents are notified once.

Since

v0.1.0

Signal<int> counter(0);

counter.freeze();
counter.set(1);
counter.set(2);
counter.set(3);
counter.unfreeze(true); // Dependents notified once

// Set but don't notify
counter.freeze();
counter.set(100);
counter.unfreeze(false); // No notification sent

Note

  • Only one notification is sent even if multiple changes occurred.

  • Use should_notify = false to discard accumulated changes.

Parameters:
bool should_notify = true

If true (default), notify dependents of accumulated changes.

Returns:

Status::Ok on success, error code otherwise.

inline bool isFrozen() const

Check if signal is currently frozen.

Returns whether the signal is in a frozen state (notifications deferred).

Since

v0.1.0

Signal<int> counter(0);
counter.freeze();

if (counter.isFrozen()) {
  // Make changes without triggering notifications
  counter.set(100);
}

counter.unfreeze(); // Notify dependents

Returns:

true if frozen, false otherwise.

inline T getHistory(size_t index) const

Get historical value at specified index.

Returns a previous value from the signal’s history buffer. Only available when RXESP32_ENABLE_SIGNAL_HISTORY is defined and template argument HistorySize > 1.

Since

v0.1.0

Signal<int, 1, std::not_equal_to<int>, 5> counter(0); // History size 5

counter.set(10);
counter.set(20);
counter.set(30);

int current = counter.get();          // 30
int previous = counter.getHistory(1); // 20
int older = counter.getHistory(2);    // 10

Note

  • Requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

  • History is stored in circular buffer, oldest entries are overwritten.

Parameters:
size_t index

History index (0 = most recent, 1 = previous, etc.).

Returns:

Historical value at index, or default T() if index out of range.

inline size_t getHistoryCount() const

Get number of values stored in history.

Returns how many historical values are currently available in the buffer. Maximum is HistorySize, but may be less if signal hasn’t been updated enough times.

Since

v0.1.0

Signal<int, 1, std::not_equal_to<int>, 5> counter(0);
counter.set(1);
counter.set(2);

size_t count = counter.getHistoryCount(); // Returns 3 (includes initial value)

Note

Requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

Returns:

Number of historical values available (0 if history disabled).

inline size_t getHistorySize() const

Get the configured history size.

Returns the maximum number of historical values that can be stored, as defined by the HistorySize template parameter.

Since

v0.1.0

Signal<int, 1, std::not_equal_to<int>, 5> counter(0);
size_t size = counter.getHistorySize(); // Returns 5

Returns:

Configured history size (0 if history disabled).

inline T getPrevious() const

Get the previous value (shorthand for getHistory(1)).

Convenience method to get the immediately previous value.

Since

v0.1.0

Note

Requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

Returns:

Previous value, or default T() if not available.

template<typename R, typename Fn>
inline R foldHistory(const R &initial, Fn &&fn) const

Fold/reduce over history with accumulator function.

Applies a binary function to accumulate all historical values from the newest to oldest. Useful for computing aggregates like sum, max, min, average, etc. This function is thread-safe: history is locked during entire fold operation.

Since

v0.1.0

Signal<int, Filters::AlwaysUpdate, 5> values(0);
values.set(10);
values.set(20);
values.set(30);

// Sum all history: 30 + 20 + 10 + 0 = 60
int sum = values.foldHistory(0, [](const int& acc, const int& val) {
  return acc + val;
});

// Find maximum
int max_val = values.foldHistory(INT_MIN, [](const int& acc, const int& val) {
  return max(acc, val);
});

Note

Requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

Warning

Function must accept both parameters as const references (const R&, const T&) to avoid unnecessary copies for large types.

Template Parameters:
typename R

Result type of the accumulation.

typename Fn

Function type (const R&, const T&) -> R.

Parameters:
const R &initial

Initial accumulator value (passed by const reference).

Fn &&fn

Function that takes (accumulator, history_value) and returns new accumulator.

Returns:

Final accumulated result.

template<typename Fn>
inline Status forEachHistory(Fn &&fn) const

Iterate over all historical values.

Calls the provided function for each value in history from newest to oldest. The function receives the value and its index. This function is thread-safe: history is locked during entire iteration.

Since

v0.1.0

Signal<int, 1, std::not_equal_to<T>, 5> values(0);
values.set(10);
values.set(20);
values.set(30);

// Print all history
values.forEachHistory([](const int& value, size_t index) {
  Serial.printf("History[%d] = %d\n", index, value);
});
// Output:
// History[0] = 30  (newest)
// History[1] = 20
// History[2] = 10
// History[3] = 0   (oldest)

Note

Requires RXESP32_ENABLE_SIGNAL_HISTORY to be enabled.

Warning

Function must accept first parameter as const T& to avoid unnecessary copies.

Template Parameters:
typename Fn

Function type (const T&, size_t) -> void.

Parameters:
Fn &&fn

Function called for each history entry (value, index).

Returns:

Status::Ok on success, error code otherwise.

inline virtual const char *getName() const override

Get the name of this node.

Since

v0.1.0

Returns:

C-string name, or default “Signal” if not set.

struct Options

Configuration options for Signal construction.

Since

v0.1.0

Public Members

const char *name = nullptr

Optional name for debugging/logging.

bool skip_filter_check = false

Force propagation even if Filter returns false.

See Also