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_HISTORYto be enabled.- Template Parameters:¶
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});
-
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:¶
-
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 regardlessNote
If signal is frozen, change is deferred until unfreeze().
All dependents are notified in a single dispatcher wake.
- Parameters:¶
- Returns:¶
Status::Okon success,Status::Unchangedif 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:¶
- Returns:¶
Status::Okon success,Status::Unchangedif 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:¶
- Returns:¶
Status::Okon 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.
-
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 });See also
- 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: 5Note
History is still updated (if enabled) and Filter check is bypassed.
- Parameters:¶
- Returns:¶
Status::Okon 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 dependentsNote
Value changes are still tracked and stored.
- Returns:¶
Status::Okon 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_notifyis 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 sentNote
Only one notification is sent even if multiple changes occurred.
Use
should_notify = falseto discard accumulated changes.
- Parameters:¶
- bool should_notify = true¶
If true (default), notify dependents of accumulated changes.
- Returns:¶
Status::Okon 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_HISTORYis defined and template argumentHistorySize > 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); // 10Note
Requires
RXESP32_ENABLE_SIGNAL_HISTORYto be enabled.History is stored in circular buffer, oldest entries are overwritten.
-
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_HISTORYto 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
HistorySizetemplate 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_HISTORYto 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_HISTORYto be enabled.Warning
Function must accept both parameters as const references (
const R&,const T&) to avoid unnecessary copies for large types.
-
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_HISTORYto be enabled.Warning
Function must accept first parameter as
const T&to avoid unnecessary copies.
See Also¶
Reactive Nodes - Nodes Overview