From f43c8536085c95eb2875bca9431c6a980d3dec6c Mon Sep 17 00:00:00 2001 From: rzeldent Date: Tue, 1 Sep 2026 00:23:51 +0200 Subject: [PATCH 01/10] Moved png to mermaid chart and added a link to the README.md --- README.md | 124 ++++++++++++++++++++++++++++++++++++- ReactiveArduino Legend.png | Bin 42199 -> 0 bytes 2 files changed, 123 insertions(+), 1 deletion(-) delete mode 100644 ReactiveArduino Legend.png diff --git a/README.md b/README.md index ab3591a..80b4c95 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,129 @@ More examples in Wiki/[Examples](https://github.com/luisllamasbinaburo/Arduino-R ### Observable, observers and operators legend More info about the Observables, Observers, and Operators available in the [Wiki](https://github.com/luisllamasbinaburo/Arduino-ReactiveArduino/wiki) -![alt text](https://github.com/luisllamasbinaburo/Arduino-ReactiveArduino/blob/master/ReactiveArduino%20Legend.png "Legend") +```mermaid +flowchart LR + subgraph OBS[Observables] + direction TB + Range + RangeDefer + Array + ArrayDefer + Property + AnalogInput + DigitalInput + TimerMillis + TimerMicros + IntervalMillis + IntervalMicros + SerialChar + SerialString + SerialInteger + SerialFloat + end + + subgraph OPR[Operators] + direction TB + subgraph OPO[Operators] + Where + Distinct + First + Last + Take + TakeAt + TakeFirst + TakeLast + TakeUntil + TakeWhile + Skip + SkipUntil + SkipWhile + Batch + TimeoutMillis + TimeoutMicros + ForEach + If + Loop + Repeat + Reset + NoReset + end + subgraph TRN[Transformations] + Select + Cast + Map + Reduce + Limit + Scale + ElapsedMicros + ElapsedMillis + Micros + Millis + Frequency + Threshold + Toggle + AdcToVoltage + Split + Join + Buffer + ToBool + ParseInt + ParseFloat + end + subgraph FLT[Filters] + OnRising + OnFalling + Median3 + Median5 + MovingAverage + MovingRMS + LowPass + HighPass + PassBand + StopBand + WindowMillis + WindowMicros + DebounceMillis + DebounceMicros + IsLessOrEqual + IsLess + IsGreaterOrEqual + IsGreater + IsNotEqual + IsEqual + IsZero + IsNotZero + end + subgraph AGG[Aggregates] + Count + Countdown + Sum + Min + Max + Average + Any + RMS + All + None + end + end + + subgraph OBV[Observers] + direction TB + Do + Finally + DoAndFinally + DoNothing + Property + Array + CircularBuffer + DigitalOutput + AnalogOutput + Serial + end + + OBS --> OPR --> OBV +``` ### Creating Observables Observables are generally generated through factory methods provided by the Reactive class. diff --git a/ReactiveArduino Legend.png b/ReactiveArduino Legend.png deleted file mode 100644 index c6335cfa777aef54deebaffc8b2b64240921fb74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42199 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ôv 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 From 70c796de103ba396d734d2db924bc5efc9b91fdd Mon Sep 17 00:00:00 2001 From: rzeldent Date: Tue, 1 Sep 2026 00:28:02 +0200 Subject: [PATCH 02/10] Add new observables and operators to the documentation, including ManualDefer and enhancements to existing examples --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 80b4c95..fecd5ef 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ More info about the Observables, Observers, and Operators available in the [Wiki flowchart LR subgraph OBS[Observables] direction TB + ManualDefer Range RangeDefer Array @@ -39,9 +40,11 @@ flowchart LR IntervalMillis IntervalMicros SerialChar + SerialByte SerialString SerialInteger SerialFloat + SerialDouble end subgraph OPR[Operators] @@ -76,6 +79,8 @@ flowchart LR Map Reduce Limit + LimitLower + LimitUpper Scale ElapsedMicros ElapsedMillis @@ -88,7 +93,10 @@ flowchart LR Split Join Buffer + StringBuffer ToBool + ToInt + ToFloat ParseInt ParseFloat end @@ -173,10 +181,19 @@ Hot observables emits the sequence when an observer subscribes to it. For exampl ```c++ FromArray(values, valuesLength) ``` -Cold observable does not emits any item when a observer subscribes to it. You have to explicitly call the `Next()` method whenever you want. For example, `FromArrayDefer(...)` +Cold observable does not emits any item when a observer subscribes to it. You have to explicitly call the `Next()` method whenever you want. For example, `FromArrayDefer(...)` or `ManualDefer(...)`. ```c++ FromArrayDefer(values, valuesLength) ``` + +With `ManualDefer(...)` you also have to explicitly call `Next()` to emit, and `Complete()` to finish the sequence. +```c++ +auto obs = ManualDefer(); +obs >> ToSerial(); +// ...later in code +obs.Next(); +obs.Complete(); +``` ### Dynamic memory considerations On many occasions we generate operators directly when we chain them, for example in the `Setup()`. However, creating an operator allocates dynamic memory. Therefore, you should avoid creating them in `Loop()`, or you could run out of memory. If you need to reuse (typically, call some operator method later in your code) set it as a global variable, and chain as normal. From bc38867f6f1f4fdfcff86fe7a999ad397775dcc8 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 21:57:06 +0200 Subject: [PATCH 03/10] Operators fix --- src/Operators/OperatorBatch.h | 10 ++++------ src/Operators/OperatorFirst.h | 2 ++ src/Operators/OperatorForEach.h | 1 + src/Operators/OperatorRepeat.h | 3 ++- src/Operators/OperatorSkip.h | 1 - src/Operators/OperatorTake.h | 14 +++++++++----- src/Operators/OperatorTakeFirst.h | 1 + src/Operators/OperatorTakeLast.h | 17 ++++++++++++++++- src/Operators/OperatorTimeoutMicros.h | 9 ++++----- src/Operators/OperatorTimeoutMillis.h | 7 +++---- src/ReactiveArduinoCore.h | 12 ++++++------ src/ReactiveArduinoLib.h | 8 ++++---- .../TransformationElapsedMicros.h | 10 +++++----- 13 files changed, 57 insertions(+), 38 deletions(-) diff --git a/src/Operators/OperatorBatch.h b/src/Operators/OperatorBatch.h index 53d07f8..98c9db6 100644 --- a/src/Operators/OperatorBatch.h +++ b/src/Operators/OperatorBatch.h @@ -32,16 +32,14 @@ OperatorBatch::OperatorBatch(size_t N) template void OperatorBatch::OnNext(T value) { - if (_index < _num_elements) - { - this->_childObservers.OnNext(value); - _index++; - } - else + if (_index >= _num_elements) { _index = 0; this->_childObservers.OnComplete(); } + + this->_childObservers.OnNext(value); + _index++; } #endif \ No newline at end of file diff --git a/src/Operators/OperatorFirst.h b/src/Operators/OperatorFirst.h index 838d175..b822ccc 100644 --- a/src/Operators/OperatorFirst.h +++ b/src/Operators/OperatorFirst.h @@ -45,6 +45,8 @@ void OperatorFirst::OnComplete() { if (_any) this->_childObservers.OnNext(_first); + + this->_childObservers.OnComplete(); } #endif \ No newline at end of file diff --git a/src/Operators/OperatorForEach.h b/src/Operators/OperatorForEach.h index fbacfd4..104c9ba 100644 --- a/src/Operators/OperatorForEach.h +++ b/src/Operators/OperatorForEach.h @@ -40,6 +40,7 @@ void OperatorForEach::OnNext(T value) template inline void OperatorForEach::OnComplete() { + this->_childObservers.OnComplete(); } #endif \ No newline at end of file diff --git a/src/Operators/OperatorRepeat.h b/src/Operators/OperatorRepeat.h index 8420d18..ebb6061 100644 --- a/src/Operators/OperatorRepeat.h +++ b/src/Operators/OperatorRepeat.h @@ -38,7 +38,8 @@ void OperatorRepeat::OnNext(T value) template void OperatorRepeat::OnComplete() { - _repetition--; + if (_repetition > 0) _repetition--; + if (_repetition > 0) { if (this->_parentObservable != nullptr) this->_parentObservable->Reset(); diff --git a/src/Operators/OperatorSkip.h b/src/Operators/OperatorSkip.h index 2a84db2..29ba640 100644 --- a/src/Operators/OperatorSkip.h +++ b/src/Operators/OperatorSkip.h @@ -21,7 +21,6 @@ class OperatorSkip : public Operator private: size_t _index = 0; size_t _num_elements = 0; - bool _completed = false; }; template diff --git a/src/Operators/OperatorTake.h b/src/Operators/OperatorTake.h index b2a5af5..c64e613 100644 --- a/src/Operators/OperatorTake.h +++ b/src/Operators/OperatorTake.h @@ -35,17 +35,21 @@ void OperatorTake::OnNext(T value) { if (_completed) return; - if (_index < _num_elements) - { - this->_childObservers.OnNext(value); - } - else + if (_num_elements == 0) { this->_childObservers.OnComplete(); _completed = true; + return; } + this->_childObservers.OnNext(value); _index++; + + if (_index >= _num_elements) + { + this->_childObservers.OnComplete(); + _completed = true; + } } #endif \ No newline at end of file diff --git a/src/Operators/OperatorTakeFirst.h b/src/Operators/OperatorTakeFirst.h index 017082a..c977632 100644 --- a/src/Operators/OperatorTakeFirst.h +++ b/src/Operators/OperatorTakeFirst.h @@ -33,6 +33,7 @@ void OperatorTakeFirst::OnNext(T value) if (_completed) return; this->_childObservers.OnNext(value); + this->_childObservers.OnComplete(); _completed = true; } diff --git a/src/Operators/OperatorTakeLast.h b/src/Operators/OperatorTakeLast.h index 36ba448..6906ffa 100644 --- a/src/Operators/OperatorTakeLast.h +++ b/src/Operators/OperatorTakeLast.h @@ -17,6 +17,11 @@ class OperatorTakeLast : public Operator OperatorTakeLast(); void OnNext(T value) override; + void OnComplete() override; + +private: + T _last = T(); + bool _any = false; }; template @@ -27,7 +32,17 @@ OperatorTakeLast::OperatorTakeLast() template void OperatorTakeLast::OnNext(T value) { - this->_childObservers.OnNext(value); + _last = value; + _any = true; +} + +template +void OperatorTakeLast::OnComplete() +{ + if (!_any) return; + + this->_childObservers.OnNext(_last); + this->_childObservers.OnComplete(); } #endif \ No newline at end of file diff --git a/src/Operators/OperatorTimeoutMicros.h b/src/Operators/OperatorTimeoutMicros.h index 6f5ea9d..721ad11 100644 --- a/src/Operators/OperatorTimeoutMicros.h +++ b/src/Operators/OperatorTimeoutMicros.h @@ -14,21 +14,21 @@ template class OperatorTimeoutMicros : public Operator { public: - OperatorTimeoutMicros(unsigned long interval, ReactiveAction action); + OperatorTimeoutMicros(unsigned long interval, ReactiveCallback action); void OnNext(T value) override; void OnComplete() override; void Update(); private: - ReactiveAction _doAction; + ReactiveCallback _doAction; unsigned long _starTime; unsigned long _interval; bool _completed = false; }; template -OperatorTimeoutMicros::OperatorTimeoutMicros(unsigned long interval, ReactiveAction action) +OperatorTimeoutMicros::OperatorTimeoutMicros(unsigned long interval, ReactiveCallback action) { _doAction = action; _interval = interval; @@ -38,7 +38,6 @@ OperatorTimeoutMicros::OperatorTimeoutMicros(unsigned long interval, Reactive template void OperatorTimeoutMicros::OnNext(T value) { - _doAction(value); _starTime = micros(); this->_childObservers.OnNext(value); @@ -56,7 +55,7 @@ inline void OperatorTimeoutMicros::Update() { if (_completed) return; - if (millis() - _starTime > _interval) + if (micros() - _starTime > _interval) { if (_doAction != nullptr) _doAction(); _completed = true; diff --git a/src/Operators/OperatorTimeoutMillis.h b/src/Operators/OperatorTimeoutMillis.h index d90e87e..9876389 100644 --- a/src/Operators/OperatorTimeoutMillis.h +++ b/src/Operators/OperatorTimeoutMillis.h @@ -14,21 +14,21 @@ template class OperatorTimeoutMillis : public Operator { public: - OperatorTimeoutMillis(unsigned long interval, ReactiveAction action); + OperatorTimeoutMillis(unsigned long interval, ReactiveCallback action); void OnNext(T value) override; void OnComplete() override; void Update(); private: - ReactiveAction _doAction; + ReactiveCallback _doAction; unsigned long _starTime; unsigned long _interval; bool _completed = false; }; template -OperatorTimeoutMillis::OperatorTimeoutMillis(unsigned long interval, ReactiveAction action) +OperatorTimeoutMillis::OperatorTimeoutMillis(unsigned long interval, ReactiveCallback action) { _doAction = action; _interval = interval; @@ -38,7 +38,6 @@ OperatorTimeoutMillis::OperatorTimeoutMillis(unsigned long interval, Reactive template void OperatorTimeoutMillis::OnNext(T value) { - _doAction(value); _starTime = millis(); this->_childObservers.OnNext(value); diff --git a/src/ReactiveArduinoCore.h b/src/ReactiveArduinoCore.h index 37e3ae6..f9263da 100644 --- a/src/ReactiveArduinoCore.h +++ b/src/ReactiveArduinoCore.h @@ -168,8 +168,8 @@ class Observable : IObservable, IResetable OperatorBatch& Batch(size_t num); OperatorIf& If(ReactivePredicate condition, ReactiveAction action); OperatorForEach& ForEach(ReactiveAction action); - OperatorTimeoutMillis& TimeoutMillis(ReactiveAction action); - OperatorTimeoutMicros& TimeoutMicros(ReactiveAction action); + OperatorTimeoutMillis& TimeoutMillis(unsigned long interval, ReactiveCallback action); + OperatorTimeoutMicros& TimeoutMicros(unsigned long interval, ReactiveCallback action); OperatorReset& DoReset(); OperatorNoReset& NotReset(); OperatorLoop& Loop(); @@ -385,17 +385,17 @@ auto Observable::ForEach(ReactiveAction action) -> OperatorForEach& } template -auto Observable::TimeoutMillis(ReactiveAction action) -> OperatorTimeoutMillis& +auto Observable::TimeoutMillis(unsigned long interval, ReactiveCallback action) -> OperatorTimeoutMillis& { - auto newOp = new OperatorTimeoutMillis(action); + auto newOp = new OperatorTimeoutMillis(interval, action); Compound(*this, *newOp); return *newOp; } template -auto Observable::TimeoutMicros(ReactiveAction action) -> OperatorTimeoutMicros& +auto Observable::TimeoutMicros(unsigned long interval, ReactiveCallback action) -> OperatorTimeoutMicros& { - auto newOp = new OperatorTimeoutMicros(action); + auto newOp = new OperatorTimeoutMicros(interval, action); Compound(*this, *newOp); return *newOp; } diff --git a/src/ReactiveArduinoLib.h b/src/ReactiveArduinoLib.h index 272cbcb..a7a2fa2 100644 --- a/src/ReactiveArduinoLib.h +++ b/src/ReactiveArduinoLib.h @@ -314,15 +314,15 @@ namespace Reactive } template - OperatorTimeoutMillis& TimeoutMillis(ReactiveAction action) + OperatorTimeoutMillis& TimeoutMillis(unsigned long interval, ReactiveCallback action) { - return *(new OperatorTimeoutMillis(action)); + return *(new OperatorTimeoutMillis(interval, action)); } template - OperatorTimeoutMicros& TimeoutMicros(ReactiveAction action) + OperatorTimeoutMicros& TimeoutMicros(unsigned long interval, ReactiveCallback action) { - return *(new OperatorTimeoutMicros(action)); + return *(new OperatorTimeoutMicros(interval, action)); } template diff --git a/src/Transformations/TransformationElapsedMicros.h b/src/Transformations/TransformationElapsedMicros.h index 4a83a99..14285ce 100644 --- a/src/Transformations/TransformationElapsedMicros.h +++ b/src/Transformations/TransformationElapsedMicros.h @@ -21,26 +21,26 @@ class TransformationElapsedMicros : public Operator void Reset() override; private: - unsigned long _starTime; + unsigned long _start_time; }; template TransformationElapsedMicros::TransformationElapsedMicros() { - _starTime = micros(); + _start_time = micros(); } template void TransformationElapsedMicros::Reset() { - _starTime = micros(); + _start_time = micros(); } template void TransformationElapsedMicros::OnNext(T value) { - this->_childObservers.OnNext(micros() - _starTime); - _starTime = micros(); + this->_childObservers.OnNext(micros() - _start_time); + _start_time = micros(); } #endif From 31bdd44a49e9b4eaa7788404bf5c36a48ad7f446 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 22:18:22 +0200 Subject: [PATCH 04/10] fixes Aggregates --- src/Aggregates/AggregateAll.h | 2 +- src/Aggregates/AggregateAverage.h | 2 +- src/Aggregates/AggregateCount.h | 2 +- src/Aggregates/AggregateCountdown.h | 6 +++++- src/Aggregates/AggregateNone.h | 2 +- src/Aggregates/AggregateRMS.h | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Aggregates/AggregateAll.h b/src/Aggregates/AggregateAll.h index c952ac0..1a6b2b6 100644 --- a/src/Aggregates/AggregateAll.h +++ b/src/Aggregates/AggregateAll.h @@ -33,7 +33,7 @@ AggregateAll::AggregateAll(ReactivePredicate condition) template void AggregateAll::OnNext(T value) { - if (_state && _condition(value)) _state = false; + if (!_condition(value)) _state = false; this->_childObservers.OnNext(_state); } diff --git a/src/Aggregates/AggregateAverage.h b/src/Aggregates/AggregateAverage.h index 47d11a5..d030f0c 100644 --- a/src/Aggregates/AggregateAverage.h +++ b/src/Aggregates/AggregateAverage.h @@ -33,7 +33,7 @@ void AggregateAverage::OnNext(T value) { _sum += value; _count++; - this->_childObservers.OnNext(_sum / _count); + this->_childObservers.OnNext(_sum / static_cast(_count)); } #endif \ No newline at end of file diff --git a/src/Aggregates/AggregateCount.h b/src/Aggregates/AggregateCount.h index 95b6d07..54ff497 100644 --- a/src/Aggregates/AggregateCount.h +++ b/src/Aggregates/AggregateCount.h @@ -19,7 +19,7 @@ class AggregateCount : public Operator void OnNext(T value) override; private: - int _count = false; + int _count = 0; }; template diff --git a/src/Aggregates/AggregateCountdown.h b/src/Aggregates/AggregateCountdown.h index edc4efc..e625727 100644 --- a/src/Aggregates/AggregateCountdown.h +++ b/src/Aggregates/AggregateCountdown.h @@ -19,7 +19,8 @@ class AggregateCountdown : public Operator void OnNext(T value) override; private: - int _count = false; + int _count = 0; + bool _completed = false; }; template @@ -31,11 +32,14 @@ AggregateCountdown::AggregateCountdown(int count) template void AggregateCountdown::OnNext(T value) { + if (_completed) return; + _count--; this->_childObservers.OnNext(_count); if (_count <= 0) { + _completed = true; this->_childObservers.OnComplete(); } } diff --git a/src/Aggregates/AggregateNone.h b/src/Aggregates/AggregateNone.h index f7bb0cc..655b282 100644 --- a/src/Aggregates/AggregateNone.h +++ b/src/Aggregates/AggregateNone.h @@ -33,7 +33,7 @@ AggregateNone::AggregateNone(ReactivePredicate condition) template void AggregateNone::OnNext(T value) { - if (!_condition(value)) _state = false; + if (_condition(value)) _state = false; this->_childObservers.OnNext(_state); } diff --git a/src/Aggregates/AggregateRMS.h b/src/Aggregates/AggregateRMS.h index 08c3506..7151558 100644 --- a/src/Aggregates/AggregateRMS.h +++ b/src/Aggregates/AggregateRMS.h @@ -33,7 +33,7 @@ void AggregateRMS::OnNext(T value) { _sumSqr += value * value; _count++; - this->_childObservers.OnNext(sqrt(_sumSqr / _count)); + this->_childObservers.OnNext(static_cast(sqrt(static_cast(_sumSqr) / static_cast(_count)))); } #endif \ No newline at end of file From b61c24f0af9abd638f99fb2d8048a7f24080b09b Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 22:28:33 +0200 Subject: [PATCH 05/10] _starTime to _startTime --- src/Observables/ObservableTimerMicros.h | 10 ++++++++-- src/Observables/ObservableTimerMillis.h | 8 +++++++- src/Operators/OperatorTimeoutMicros.h | 8 ++++---- src/Operators/OperatorTimeoutMillis.h | 8 ++++---- src/Transformations/TransformationElapsedMicros.h | 10 +++++----- src/Transformations/TransformationElapsedMillis.h | 10 +++++----- src/Transformations/TransformationFrequency.h | 10 +++++----- src/Transformations/TransformationTimestampMicros.h | 8 ++++---- src/Transformations/TransformationTimestampMillis.h | 8 ++++---- 9 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/Observables/ObservableTimerMicros.h b/src/Observables/ObservableTimerMicros.h index 703b076..58ed90c 100644 --- a/src/Observables/ObservableTimerMicros.h +++ b/src/Observables/ObservableTimerMicros.h @@ -46,6 +46,7 @@ template ObservableTimerMicros::ObservableTimerMicros(unsigned long interval, unsigned long delay) { _isActive = true; + _isExpired = false; _delay = delay; _offset = delay; _interval = interval; @@ -68,18 +69,21 @@ template void ObservableTimerMicros::Update() { if (_isActive == false) return; + if (_isExpired) return; auto elapsed = static_cast(micros() - _startTime); if (elapsed >= _interval + _offset) { this->_childObservers.OnNext(elapsed); + _isExpired = true; _offset = 0; } } template void ObservableTimerMicros::Reset() -{ +{isExpired = false; + _ _isActive = true; _offset = _delay; _startTime = micros(); @@ -118,7 +122,9 @@ unsigned long ObservableTimerMicros::GetElapsedTime() const template unsigned long ObservableTimerMicros::GetRemainingTime() const -{ +{unsigned long elapsed = micros() - _startTime; + if (elapsed >= _interval) return 0; + return _interval - elapsed return _interval - micros() + _startTime; } diff --git a/src/Observables/ObservableTimerMillis.h b/src/Observables/ObservableTimerMillis.h index 82bc412..9aaa3b5 100644 --- a/src/Observables/ObservableTimerMillis.h +++ b/src/Observables/ObservableTimerMillis.h @@ -46,6 +46,7 @@ template ObservableTimerMillis::ObservableTimerMillis(unsigned long interval, unsigned long delay) { _isActive = true; + _isExpired = false; _delay = delay; _offset = delay; _interval = interval; @@ -68,11 +69,13 @@ template void ObservableTimerMillis::Update() { if (_isActive == false) return; + if (_isExpired) return; auto elapsed = static_cast(millis() - _startTime); if (elapsed >= _interval + _offset) { this->_childObservers.OnNext(elapsed); + _isExpired = true; _offset = 0; } } @@ -81,6 +84,7 @@ template void ObservableTimerMillis::Reset() { _isActive = true; + _isExpired = false; _offset = _delay; _startTime = millis(); } @@ -119,7 +123,9 @@ unsigned long ObservableTimerMillis::GetElapsedTime() const template unsigned long ObservableTimerMillis::GetRemainingTime() const { - return _interval - millis() + _startTime; + unsigned long elapsed = millis() - _startTime; + if (elapsed >= _interval) return 0; + return _interval - elapsed; } template diff --git a/src/Operators/OperatorTimeoutMicros.h b/src/Operators/OperatorTimeoutMicros.h index 721ad11..36e006f 100644 --- a/src/Operators/OperatorTimeoutMicros.h +++ b/src/Operators/OperatorTimeoutMicros.h @@ -22,7 +22,7 @@ class OperatorTimeoutMicros : public Operator private: ReactiveCallback _doAction; - unsigned long _starTime; + unsigned long _startTime; unsigned long _interval; bool _completed = false; }; @@ -32,13 +32,13 @@ OperatorTimeoutMicros::OperatorTimeoutMicros(unsigned long interval, Reactive { _doAction = action; _interval = interval; - _starTime = micros(); + _startTime = micros(); } template void OperatorTimeoutMicros::OnNext(T value) { - _starTime = micros(); + _startTime = micros(); this->_childObservers.OnNext(value); } @@ -55,7 +55,7 @@ inline void OperatorTimeoutMicros::Update() { if (_completed) return; - if (micros() - _starTime > _interval) + if (micros() - _startTime > _interval) { if (_doAction != nullptr) _doAction(); _completed = true; diff --git a/src/Operators/OperatorTimeoutMillis.h b/src/Operators/OperatorTimeoutMillis.h index 9876389..58709fc 100644 --- a/src/Operators/OperatorTimeoutMillis.h +++ b/src/Operators/OperatorTimeoutMillis.h @@ -22,7 +22,7 @@ class OperatorTimeoutMillis : public Operator private: ReactiveCallback _doAction; - unsigned long _starTime; + unsigned long _startTime; unsigned long _interval; bool _completed = false; }; @@ -32,13 +32,13 @@ OperatorTimeoutMillis::OperatorTimeoutMillis(unsigned long interval, Reactive { _doAction = action; _interval = interval; - _starTime = millis(); + _startTime = millis(); } template void OperatorTimeoutMillis::OnNext(T value) { - _starTime = millis(); + _startTime = millis(); this->_childObservers.OnNext(value); } @@ -55,7 +55,7 @@ inline void OperatorTimeoutMillis::Update() { if (_completed) return; - if (millis() - _starTime > _interval) + if (millis() - _startTime > _interval) { if (_doAction != nullptr) _doAction(); _completed = true; diff --git a/src/Transformations/TransformationElapsedMicros.h b/src/Transformations/TransformationElapsedMicros.h index 14285ce..13e86ee 100644 --- a/src/Transformations/TransformationElapsedMicros.h +++ b/src/Transformations/TransformationElapsedMicros.h @@ -21,26 +21,26 @@ class TransformationElapsedMicros : public Operator void Reset() override; private: - unsigned long _start_time; + unsigned long _startTime; }; template TransformationElapsedMicros::TransformationElapsedMicros() { - _start_time = micros(); + _startTime = micros(); } template void TransformationElapsedMicros::Reset() { - _start_time = micros(); + _startTime = micros(); } template void TransformationElapsedMicros::OnNext(T value) { - this->_childObservers.OnNext(micros() - _start_time); - _start_time = micros(); + this->_childObservers.OnNext(micros() - _startTime); + _startTime = micros(); } #endif diff --git a/src/Transformations/TransformationElapsedMillis.h b/src/Transformations/TransformationElapsedMillis.h index 760b4de..494e301 100644 --- a/src/Transformations/TransformationElapsedMillis.h +++ b/src/Transformations/TransformationElapsedMillis.h @@ -21,26 +21,26 @@ class TransformationElapsedMillis : public Operator void Reset() override; private: - unsigned long _starTime; + unsigned long _startTime; }; template TransformationElapsedMillis::TransformationElapsedMillis() { - _starTime = millis(); + _startTime = millis(); } template void TransformationElapsedMillis::Reset() { - _starTime = millis(); + _startTime = millis(); } template void TransformationElapsedMillis::OnNext(T value) { - this->_childObservers.OnNext(millis() - _starTime); - _starTime = millis(); + this->_childObservers.OnNext(millis() - _startTime); + _startTime = millis(); } #endif \ No newline at end of file diff --git a/src/Transformations/TransformationFrequency.h b/src/Transformations/TransformationFrequency.h index 0229fc6..5a5e966 100644 --- a/src/Transformations/TransformationFrequency.h +++ b/src/Transformations/TransformationFrequency.h @@ -27,25 +27,25 @@ class TransformationFrequency : public Operator void Reset() override; private: - unsigned long _starTime; + unsigned long _startTime; }; template TransformationFrequency::TransformationFrequency() { - _starTime = millis(); + _startTime = millis(); } template void TransformationFrequency::Reset() { - _starTime = millis(); + _startTime = millis(); } template void TransformationFrequency::OnNext(T value) { - this->_childObservers.OnNext(1000.0 / (millis() - _starTime)); - _starTime = millis(); + this->_childObservers.OnNext(1000.0 / (millis() - _startTime)); + _startTime = millis(); } #endif diff --git a/src/Transformations/TransformationTimestampMicros.h b/src/Transformations/TransformationTimestampMicros.h index b9333a1..fd69ff1 100644 --- a/src/Transformations/TransformationTimestampMicros.h +++ b/src/Transformations/TransformationTimestampMicros.h @@ -21,24 +21,24 @@ class TransformationTimestampMicros : public Operator void Reset() override; private: - unsigned long _starTime; + unsigned long _startTime; }; template TransformationTimestampMicros::TransformationTimestampMicros() { - _starTime = micros(); + _startTime = micros(); } template void TransformationTimestampMicros::Reset() { - _starTime = micros(); + _startTime = micros(); } template void TransformationTimestampMicros::OnNext(T value) { - this->_childObservers.OnNext(micros() - _starTime); + this->_childObservers.OnNext(micros() - _startTime); } #endif \ No newline at end of file diff --git a/src/Transformations/TransformationTimestampMillis.h b/src/Transformations/TransformationTimestampMillis.h index 26e0775..43b375f 100644 --- a/src/Transformations/TransformationTimestampMillis.h +++ b/src/Transformations/TransformationTimestampMillis.h @@ -21,24 +21,24 @@ class TransformationTimestampMillis : public Operator void Reset() override; private: - unsigned long _starTime; + unsigned long _startTime; }; template TransformationTimestampMillis::TransformationTimestampMillis() { - _starTime = millis(); + _startTime = millis(); } template void TransformationTimestampMillis::Reset() { - _starTime = millis(); + _startTime = millis(); } template void TransformationTimestampMillis::OnNext(T value) { - this->_childObservers.OnNext(millis() - _starTime); + this->_childObservers.OnNext(millis() - _startTime); } #endif \ No newline at end of file From 01a7d77b33dc056327df5d66b548a66f7f94a116 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 22:33:40 +0200 Subject: [PATCH 06/10] fixes observables --- src/Observables/ObservableIntervalMicros.h | 5 +++-- src/Observables/ObservableIntervalMillis.h | 5 +++-- src/Observables/ObservableRange.h | 12 ++++++++++-- src/Observables/ObservableRangeDefer.h | 5 +++-- src/Observables/ObservableSerialByte.h | 4 ++-- src/Observables/ObservableSerialDouble.h | 2 +- src/Observables/ObservableTimerMicros.h | 10 +++++----- src/ReactiveArduinoLib.h | 2 +- 8 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/Observables/ObservableIntervalMicros.h b/src/Observables/ObservableIntervalMicros.h index 381f2fc..6ea0d81 100644 --- a/src/Observables/ObservableIntervalMicros.h +++ b/src/Observables/ObservableIntervalMicros.h @@ -33,7 +33,6 @@ class ObservableIntervalMicros : public Observable private: bool _isActive; - bool _isExpired; unsigned long _startTime; unsigned long _delay; unsigned long _offset; @@ -121,7 +120,9 @@ unsigned long ObservableIntervalMicros::GetElapsedTime() template unsigned long ObservableIntervalMicros::GetRemainingTime() { - return _interval - micros() + _startTime; + unsigned long elapsed = micros() - _startTime; + if (elapsed >= _interval) return 0; + return _interval - elapsed; } template diff --git a/src/Observables/ObservableIntervalMillis.h b/src/Observables/ObservableIntervalMillis.h index 0315774..5537e55 100644 --- a/src/Observables/ObservableIntervalMillis.h +++ b/src/Observables/ObservableIntervalMillis.h @@ -33,7 +33,6 @@ class ObservableIntervalMillis : public Observable private: bool _isActive; - bool _isExpired; unsigned long _startTime; unsigned long _delay; unsigned long _offset; @@ -121,7 +120,9 @@ unsigned long ObservableIntervalMillis::GetElapsedTime() template unsigned long ObservableIntervalMillis::GetRemainingTime() { - return _interval - millis() + _startTime; + unsigned long elapsed = millis() - _startTime; + if (elapsed >= _interval) return 0; + return _interval - elapsed; } template diff --git a/src/Observables/ObservableRange.h b/src/Observables/ObservableRange.h index 36cf0ee..2d9b3e0 100644 --- a/src/Observables/ObservableRange.h +++ b/src/Observables/ObservableRange.h @@ -54,8 +54,16 @@ void ObservableRange::UnSubscribe(IObserver &observer) template void ObservableRange::Run() { - for (auto i = _start; i <= _end; i += _step) - this->_childObservers.OnNext(i); + if (_step > 0) + { + for (auto i = _start; i <= _end; i += _step) + this->_childObservers.OnNext(i); + } + else if (_step < 0) + { + for (auto i = _start; i >= _end; i += _step) + this->_childObservers.OnNext(i); + } this->_childObservers.OnComplete(); } diff --git a/src/Observables/ObservableRangeDefer.h b/src/Observables/ObservableRangeDefer.h index 09cc17f..4d613f3 100644 --- a/src/Observables/ObservableRangeDefer.h +++ b/src/Observables/ObservableRangeDefer.h @@ -53,13 +53,14 @@ void ObservableRangeDefer::UnSubscribe(IObserver &observer) template void ObservableRangeDefer::Next() { - if (_value > _end) return; + if (_step > 0 && _value > _end) return; + if (_step < 0 && _value < _end) return; T value = _value; this->_childObservers.OnNext(value); _value += _step; - if (_value > _end) + if ((_step > 0 && _value > _end) || (_step < 0 && _value < _end)) this->_childObservers.OnComplete(); } diff --git a/src/Observables/ObservableSerialByte.h b/src/Observables/ObservableSerialByte.h index 0eaefef..1906d4e 100644 --- a/src/Observables/ObservableSerialByte.h +++ b/src/Observables/ObservableSerialByte.h @@ -15,8 +15,8 @@ class ObservableSerial : public Observable { public: ObservableSerial(); - void Subscribe(IObserver &observer); - void UnSubscribe(IObserver &observer); + void Subscribe(IObserver &observer) override; + void UnSubscribe(IObserver &observer) override; void Receive(); private: diff --git a/src/Observables/ObservableSerialDouble.h b/src/Observables/ObservableSerialDouble.h index 8664a10..0820168 100644 --- a/src/Observables/ObservableSerialDouble.h +++ b/src/Observables/ObservableSerialDouble.h @@ -23,7 +23,7 @@ class ObservableSerial : public Observable private: char _separator; - float _data = 0; + double _data = 0; int _dataReal = 0; int _dataDecimal = 0; int _dataPow = 1; diff --git a/src/Observables/ObservableTimerMicros.h b/src/Observables/ObservableTimerMicros.h index 58ed90c..e2d35d2 100644 --- a/src/Observables/ObservableTimerMicros.h +++ b/src/Observables/ObservableTimerMicros.h @@ -82,9 +82,9 @@ void ObservableTimerMicros::Update() template void ObservableTimerMicros::Reset() -{isExpired = false; - _ +{ _isActive = true; + _isExpired = false; _offset = _delay; _startTime = micros(); } @@ -122,10 +122,10 @@ unsigned long ObservableTimerMicros::GetElapsedTime() const template unsigned long ObservableTimerMicros::GetRemainingTime() const -{unsigned long elapsed = micros() - _startTime; +{ + unsigned long elapsed = micros() - _startTime; if (elapsed >= _interval) return 0; - return _interval - elapsed - return _interval - micros() + _startTime; + return _interval - elapsed; } template diff --git a/src/ReactiveArduinoLib.h b/src/ReactiveArduinoLib.h index a7a2fa2..cf1d479 100644 --- a/src/ReactiveArduinoLib.h +++ b/src/ReactiveArduinoLib.h @@ -111,7 +111,7 @@ namespace Reactive template auto FromSerial(char separator) -> ObservableSerial& { - return *(new ObservableSerial()); + return *(new ObservableSerial(separator)); } template <> From 9618d2db9e25ac1c568516bde644802fde98e822 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 22:37:11 +0200 Subject: [PATCH 07/10] fix observers --- src/Observers/ObserverAnalogOutput.h | 2 +- src/Observers/ObserverDigitalOutput.h | 6 +++--- src/Observers/ObserverDoNothing.h | 3 --- src/Observers/ObserverFinally.h | 2 +- src/Observers/ObserverSerial.h | 3 --- src/ReactiveArduinoCore.h | 4 ++-- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Observers/ObserverAnalogOutput.h b/src/Observers/ObserverAnalogOutput.h index d1fc677..2c96f8c 100644 --- a/src/Observers/ObserverAnalogOutput.h +++ b/src/Observers/ObserverAnalogOutput.h @@ -34,7 +34,7 @@ ObserverAnalogOutput::ObserverAnalogOutput(uint8_t pin) template void ObserverAnalogOutput::OnNext(T value) { - analogWrite(value); + analogWrite(_pin, value); } template diff --git a/src/Observers/ObserverDigitalOutput.h b/src/Observers/ObserverDigitalOutput.h index 86ca0c5..adbbbaa 100644 --- a/src/Observers/ObserverDigitalOutput.h +++ b/src/Observers/ObserverDigitalOutput.h @@ -4,12 +4,12 @@ #define _REACTIVEOBSERVERDIGITALOUTPUT_h template -class ObserverDigitalOutput : public IObserver +class ObserverDigitalOutput : public IObserver { public: ObserverDigitalOutput(uint8_t pin); - void OnNext(int value) override; + void OnNext(T value) override; void OnComplete() override; private: @@ -24,7 +24,7 @@ ObserverDigitalOutput::ObserverDigitalOutput(uint8_t pin) } template -void ObserverDigitalOutput::OnNext(int value) +void ObserverDigitalOutput::OnNext(T value) { digitalWrite(_pin, value); } diff --git a/src/Observers/ObserverDoNothing.h b/src/Observers/ObserverDoNothing.h index ac3e76a..c4b6502 100644 --- a/src/Observers/ObserverDoNothing.h +++ b/src/Observers/ObserverDoNothing.h @@ -18,9 +18,6 @@ class ObserverDoNothing : public IObserver void OnNext(T value) override; void OnComplete() override; - -private: - ReactiveAction _doAction; }; template diff --git a/src/Observers/ObserverFinally.h b/src/Observers/ObserverFinally.h index 8d36e74..d48d723 100644 --- a/src/Observers/ObserverFinally.h +++ b/src/Observers/ObserverFinally.h @@ -38,7 +38,7 @@ void ObserverFinally::OnNext(T value) template void ObserverFinally::OnComplete() { - _action(); + if (_action != nullptr) _action(); } #endif \ No newline at end of file diff --git a/src/Observers/ObserverSerial.h b/src/Observers/ObserverSerial.h index 88813bf..2fcb8f4 100644 --- a/src/Observers/ObserverSerial.h +++ b/src/Observers/ObserverSerial.h @@ -14,9 +14,6 @@ template class ObserverSerial : public IObserver { public: - - -private: void OnNext(T value) override; void OnComplete() override; }; diff --git a/src/ReactiveArduinoCore.h b/src/ReactiveArduinoCore.h index f9263da..1d35ce1 100644 --- a/src/ReactiveArduinoCore.h +++ b/src/ReactiveArduinoCore.h @@ -237,7 +237,7 @@ class Observable : IObservable, IResetable AggregateAll& All(ReactivePredicate condition); AggregateNone& None(ReactivePredicate condition); - ObserverSerial ToSerial(); + ObserverSerial& ToSerial(); ObserverDo& Do(ReactiveAction action); ObserverFinally& Finally(ReactiveCallback action); ObserverDoAndFinally& DoAndFinally(ReactiveAction doAction, ReactiveCallback finallyAction); @@ -912,7 +912,7 @@ auto Observable::None(ReactivePredicate condition) -> AggregateNone& } template -auto Observable::ToSerial() -> ObserverSerial +auto Observable::ToSerial() -> ObserverSerial& { auto newOp = new ObserverSerial(); Subscribe(*newOp); From 3d15f5cd2a1f0b252f362b79fa023a2b4a5b3f1b Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 22:49:11 +0200 Subject: [PATCH 08/10] fix transformations --- src/ReactiveArduinoCore.h | 8 ++++---- src/ReactiveArduinoLib.h | 2 +- src/Transformations/TransformationAdcToVoltage.h | 10 +++++----- src/Transformations/TransformationFrequency.h | 3 ++- src/Transformations/TransformationScale.h | 6 ++++++ src/Transformations/TransformationSplit.h | 6 +----- src/Transformations/TransformationToggle.h | 8 ++++---- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/ReactiveArduinoCore.h b/src/ReactiveArduinoCore.h index 1d35ce1..2ab32e3 100644 --- a/src/ReactiveArduinoCore.h +++ b/src/ReactiveArduinoCore.h @@ -192,8 +192,8 @@ class Observable : IObservable, IResetable TransformationFrequency& Frequency(); TransformationThreshold& Threshold(T threshold, int state = LOW); TransformationThreshold& DoubleThreshold(T lowThreshold, T highThreshold, int state = LOW); - TransformationToggle& Toggle(int state = LOW); - TransformationAdcToVoltage& AdcToVoltage(T input_max = 1023, T output_max = 5.0); + TransformationToggle& Toggle(bool state = false); + TransformationAdcToVoltage& AdcToVoltage(float input_max = 1023.0f, float output_max = 5.0f); TransformationSplit& Split(char separator = ','); TransformationJoin& Join(char separator = ','); TransformationStringBuffer & StringBuffer(); @@ -576,7 +576,7 @@ auto Observable::DoubleThreshold(T lowThreshold, T highThreshold, int state) } template -auto Observable::Toggle(int state) -> TransformationToggle& +auto Observable::Toggle(bool state) -> TransformationToggle& { auto newOp = new TransformationToggle(state); Compound(*this, *newOp); @@ -584,7 +584,7 @@ auto Observable::Toggle(int state) -> TransformationToggle& } template -auto Observable::AdcToVoltage(T input_max, T output_max) -> TransformationAdcToVoltage& +auto Observable::AdcToVoltage(float input_max, float output_max) -> TransformationAdcToVoltage& { auto newOp = new TransformationAdcToVoltage(input_max, output_max); Compound(*this, *newOp); diff --git a/src/ReactiveArduinoLib.h b/src/ReactiveArduinoLib.h index cf1d479..dc8b78f 100644 --- a/src/ReactiveArduinoLib.h +++ b/src/ReactiveArduinoLib.h @@ -473,7 +473,7 @@ namespace Reactive } template - TransformationAdcToVoltage& AdcToVoltage(T input_max = 1023, T output_max = 5.0) + TransformationAdcToVoltage& AdcToVoltage(float input_max = 1023.0f, float output_max = 5.0f) { return *(new TransformationAdcToVoltage(input_max, output_max)); } diff --git a/src/Transformations/TransformationAdcToVoltage.h b/src/Transformations/TransformationAdcToVoltage.h index fa9d0f3..785ee99 100644 --- a/src/Transformations/TransformationAdcToVoltage.h +++ b/src/Transformations/TransformationAdcToVoltage.h @@ -14,17 +14,17 @@ template class TransformationAdcToVoltage : public Operator { public: - TransformationAdcToVoltage(T input_max = 1023, T output_max = 5.0); + TransformationAdcToVoltage(float input_max = 1023.0f, float output_max = 5.0f); void OnNext(T value) override; private: - T _input_max = T(); - T _output_max = T(); + float _input_max = 0.0f; + float _output_max = 0.0f; }; template -TransformationAdcToVoltage::TransformationAdcToVoltage(T input_max, T output_max) +TransformationAdcToVoltage::TransformationAdcToVoltage(float input_max, float output_max) { _input_max = input_max; _output_max = output_max; @@ -34,7 +34,7 @@ TransformationAdcToVoltage::TransformationAdcToVoltage(T input_max, T output_ template void TransformationAdcToVoltage::OnNext(T value) { - this->_childObservers.OnNext((value * _output_max) / _input_max); + this->_childObservers.OnNext((static_cast(value) * _output_max) / _input_max); } #endif \ No newline at end of file diff --git a/src/Transformations/TransformationFrequency.h b/src/Transformations/TransformationFrequency.h index 5a5e966..b2c415b 100644 --- a/src/Transformations/TransformationFrequency.h +++ b/src/Transformations/TransformationFrequency.h @@ -45,7 +45,8 @@ void TransformationFrequency::Reset() template void TransformationFrequency::OnNext(T value) { - this->_childObservers.OnNext(1000.0 / (millis() - _startTime)); + unsigned long elapsed = millis() - _startTime; + this->_childObservers.OnNext(elapsed > 0 ? 1000.0f / elapsed : 0.0f); _startTime = millis(); } #endif diff --git a/src/Transformations/TransformationScale.h b/src/Transformations/TransformationScale.h index 7ffa872..c08e11e 100644 --- a/src/Transformations/TransformationScale.h +++ b/src/Transformations/TransformationScale.h @@ -38,6 +38,12 @@ TransformationScale::TransformationScale(T input_min, T input_max, T output_m template void TransformationScale::OnNext(T value) { + if (_input_max == _input_min) + { + this->_childObservers.OnNext(_output_min); + return; + } + T scaled = (value - _input_min) * (_output_max - _output_min) / (_input_max - _input_min) + _output_min; this->_childObservers.OnNext(scaled); } diff --git a/src/Transformations/TransformationSplit.h b/src/Transformations/TransformationSplit.h index 804d584..3d19cf9 100644 --- a/src/Transformations/TransformationSplit.h +++ b/src/Transformations/TransformationSplit.h @@ -14,8 +14,6 @@ template class TransformationSplit : public Operator { public: - ReactiveFunction _function; - TransformationSplit(char separator = ','); void OnNext(T value) override; @@ -34,15 +32,13 @@ TransformationSplit::TransformationSplit(char separator) template void TransformationSplit::OnNext(T value) { - size_t counter = 0; size_t lastIndex = 0; _buffer = value; for (size_t index = 0; index < _buffer.length(); index++) { - if (_buffer.substring(index, index + 1) == ",") + if (_buffer.charAt(index) == _separator) { this->_childObservers.OnNext(_buffer.substring(lastIndex, index)); lastIndex = index + 1; - counter++; } if (index == _buffer.length() - 1) diff --git a/src/Transformations/TransformationToggle.h b/src/Transformations/TransformationToggle.h index 4c9d969..c73baf4 100644 --- a/src/Transformations/TransformationToggle.h +++ b/src/Transformations/TransformationToggle.h @@ -14,18 +14,18 @@ template class TransformationToggle : public Operator { public: - TransformationToggle(int state = LOW); + TransformationToggle(bool state = false); void OnNext(T value) override; private: - int _state = false; + bool _state = false; }; template -TransformationToggle::TransformationToggle(int state) +TransformationToggle::TransformationToggle(bool state) { - _state = false; + _state = state; } template From 99e59d096824b525aa93025ce8e892e71efbfb36 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 23:35:22 +0200 Subject: [PATCH 09/10] Added unittests --- platformio.ini | 19 ++ src/Filters/FilterWindowMicros.h | 2 - src/Filters/FilterWindowMillis.h | 2 - src/Observables/ObservableSerialString.h | 2 - src/Operators/OperatorRepeat.h | 2 - src/Operators/OperatorTakeUntil.h | 2 - src/Operators/OperatorTakeWhile.h | 2 - src/ReactiveArduinoCore.h | 8 +- src/ReactiveArduinoLib.h | 2 +- src/Transformations/TransformationJoin.h | 2 - src/Transformations/TransformationThreshold.h | 25 +- test/TestHelpers.h | 35 +++ test/WProgram.h | 107 ++++++++ test/test_aggregates.cpp | 128 +++++++++ test/test_filters.cpp | 170 ++++++++++++ test/test_main.cpp | 215 +++++++++++++++ test/test_observables.cpp | 137 ++++++++++ test/test_observers.cpp | 106 ++++++++ test/test_operators.cpp | 239 +++++++++++++++++ test/test_transformations.cpp | 244 ++++++++++++++++++ 20 files changed, 1415 insertions(+), 34 deletions(-) create mode 100644 platformio.ini create mode 100644 test/TestHelpers.h create mode 100644 test/WProgram.h create mode 100644 test/test_aggregates.cpp create mode 100644 test/test_filters.cpp create mode 100644 test/test_main.cpp create mode 100644 test/test_observables.cpp create mode 100644 test/test_observers.cpp create mode 100644 test/test_operators.cpp create mode 100644 test/test_transformations.cpp diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..979410b --- /dev/null +++ b/platformio.ini @@ -0,0 +1,19 @@ +; PlatformIO Unit Testing configuration for the ReactiveArduino library. +; +; Run the tests with: +; pio test -e native +; +; The `native` environment compiles and runs the tests on the host machine. +; Arduino hardware functions (millis, micros, pinMode, digitalWrite, analogWrite, +; analogRead, digitalRead, String, Serial) are stubbed in test/WProgram.h so the +; header-only library can be exercised without a board. + +[platformio] +default_envs = native + +[env:native] +platform = native +test_framework = unity +build_flags = + -Isrc + -Itest diff --git a/src/Filters/FilterWindowMicros.h b/src/Filters/FilterWindowMicros.h index 363bdc1..179505e 100644 --- a/src/Filters/FilterWindowMicros.h +++ b/src/Filters/FilterWindowMicros.h @@ -32,9 +32,7 @@ void FilterWindowMicros::OnNext(T value) } if (_started && static_cast(micros() - _lastTrigger) <= _interval) - { this->_childObservers.OnNext(value); - } } #endif \ No newline at end of file diff --git a/src/Filters/FilterWindowMillis.h b/src/Filters/FilterWindowMillis.h index 020d061..ee4fbea 100644 --- a/src/Filters/FilterWindowMillis.h +++ b/src/Filters/FilterWindowMillis.h @@ -39,9 +39,7 @@ void FilterWindowMillis::OnNext(T value) } if (_started && static_cast(millis() - _lastTrigger) <= _interval) - { this->_childObservers.OnNext(value); - } } #endif \ No newline at end of file diff --git a/src/Observables/ObservableSerialString.h b/src/Observables/ObservableSerialString.h index 6c1ce38..67c501b 100644 --- a/src/Observables/ObservableSerialString.h +++ b/src/Observables/ObservableSerialString.h @@ -47,9 +47,7 @@ inline void ObservableSerial::Receive() { const char newChar = Serial.read(); if (newChar != _separator) - { _buffer.concat(newChar); - } else { _childObservers.OnNext(_buffer); diff --git a/src/Operators/OperatorRepeat.h b/src/Operators/OperatorRepeat.h index ebb6061..3ffc4fa 100644 --- a/src/Operators/OperatorRepeat.h +++ b/src/Operators/OperatorRepeat.h @@ -45,9 +45,7 @@ void OperatorRepeat::OnComplete() if (this->_parentObservable != nullptr) this->_parentObservable->Reset(); } else - { this->_childObservers.OnComplete(); - } } #endif \ No newline at end of file diff --git a/src/Operators/OperatorTakeUntil.h b/src/Operators/OperatorTakeUntil.h index dfbd10b..60d057c 100644 --- a/src/Operators/OperatorTakeUntil.h +++ b/src/Operators/OperatorTakeUntil.h @@ -36,9 +36,7 @@ void OperatorTakeUntil::OnNext(T value) if (_completed) return; if (!this->_condition(value)) - { this->_childObservers.OnNext(value); - } else { _completed = true; diff --git a/src/Operators/OperatorTakeWhile.h b/src/Operators/OperatorTakeWhile.h index e5782a7..21f90e3 100644 --- a/src/Operators/OperatorTakeWhile.h +++ b/src/Operators/OperatorTakeWhile.h @@ -36,9 +36,7 @@ void OperatorTakeWhile::OnNext(T value) if (_completed) return; if (this->_condition(value)) - { this->_childObservers.OnNext(value); - } else { _completed = true; diff --git a/src/ReactiveArduinoCore.h b/src/ReactiveArduinoCore.h index 2ab32e3..438c992 100644 --- a/src/ReactiveArduinoCore.h +++ b/src/ReactiveArduinoCore.h @@ -190,8 +190,8 @@ class Observable : IObservable, IResetable TransformationElapsedMillis& ElapsedMillis(); TransformationElapsedMicros& ElapsedMicros(); TransformationFrequency& Frequency(); - TransformationThreshold& Threshold(T threshold, int state = LOW); - TransformationThreshold& DoubleThreshold(T lowThreshold, T highThreshold, int state = LOW); + TransformationThreshold& Threshold(T threshold, bool state = false); + TransformationThreshold& DoubleThreshold(T lowThreshold, T highThreshold, bool state = false); TransformationToggle& Toggle(bool state = false); TransformationAdcToVoltage& AdcToVoltage(float input_max = 1023.0f, float output_max = 5.0f); TransformationSplit& Split(char separator = ','); @@ -560,7 +560,7 @@ auto Observable::Frequency() -> TransformationFrequency& } template -auto Observable::Threshold(T threshold, int state) -> TransformationThreshold& +auto Observable::Threshold(T threshold, bool state) -> TransformationThreshold& { auto newOp = new TransformationThreshold(threshold, state); Compound(*this, *newOp); @@ -568,7 +568,7 @@ auto Observable::Threshold(T threshold, int state) -> TransformationThreshold } template -auto Observable::DoubleThreshold(T lowThreshold, T highThreshold, int state) -> TransformationThreshold& +auto Observable::DoubleThreshold(T lowThreshold, T highThreshold, bool state) -> TransformationThreshold& { auto newOp = new TransformationThreshold(lowThreshold, highThreshold, state); Compound(*this, *newOp); diff --git a/src/ReactiveArduinoLib.h b/src/ReactiveArduinoLib.h index dc8b78f..d88da47 100644 --- a/src/ReactiveArduinoLib.h +++ b/src/ReactiveArduinoLib.h @@ -457,7 +457,7 @@ namespace Reactive template TransformationThreshold& DoubleThreshold(T lowThreshold, T highThreshold) { - return *(new TransformationThreshold(lowThreshold, highThreshold)); + return *(new TransformationThreshold(lowThreshold, highThreshold, false)); } template diff --git a/src/Transformations/TransformationJoin.h b/src/Transformations/TransformationJoin.h index 6ef4067..2569ccd 100644 --- a/src/Transformations/TransformationJoin.h +++ b/src/Transformations/TransformationJoin.h @@ -40,9 +40,7 @@ void TransformationJoin::OnNext(T value) _isFirst = false; } else - { _buffer = _buffer + _separator + String(value); - } this->_childObservers.OnNext(_buffer); } diff --git a/src/Transformations/TransformationThreshold.h b/src/Transformations/TransformationThreshold.h index b83ebc5..65a023d 100644 --- a/src/Transformations/TransformationThreshold.h +++ b/src/Transformations/TransformationThreshold.h @@ -14,21 +14,20 @@ template class TransformationThreshold : public Operator { public: - TransformationThreshold(T threshold) : TransformationThreshold(threshold, threshold, LOW) {} - TransformationThreshold(T threshold, int state) : TransformationThreshold(threshold, threshold, state) {} - TransformationThreshold(T lowThreshold, T highThreshold) : TransformationThreshold(lowThreshold, highThreshold, LOW) {} - TransformationThreshold(T lowThreshold, T highThreshold, int state); + TransformationThreshold(T threshold) : TransformationThreshold(threshold, threshold, false) {} + TransformationThreshold(T threshold, bool state) : TransformationThreshold(threshold, threshold, state) {} + TransformationThreshold(T lowThreshold, T highThreshold, bool state); void OnNext(T value) override; private: T _fallThreshold = T(); T _riseThreshold = T(); - int _state; + bool _state = false; }; template -TransformationThreshold::TransformationThreshold(T threshold1, T threshold2, int state) +TransformationThreshold::TransformationThreshold(T threshold1, T threshold2, bool state) { _fallThreshold = threshold1 <= threshold2 ? threshold1 : threshold2; _riseThreshold = threshold1 > threshold2 ? threshold1 : threshold2; @@ -38,15 +37,11 @@ TransformationThreshold::TransformationThreshold(T threshold1, T threshold2, template void TransformationThreshold::OnNext(T value) { - if (_state == LOW && value > _riseThreshold) - { - _state = HIGH; - } - - if (_state == HIGH && value < _fallThreshold) - { - _state = LOW; - } + if (!_state && value > _riseThreshold) + _state = true; + + if (_state && value < _fallThreshold) + _state = false; this->_childObservers.OnNext(_state); } diff --git a/test/TestHelpers.h b/test/TestHelpers.h new file mode 100644 index 0000000..fd24501 --- /dev/null +++ b/test/TestHelpers.h @@ -0,0 +1,35 @@ +// Shared helpers for the ReactiveArduino unit tests. +#ifndef TEST_HELPERS_H +#define TEST_HELPERS_H + +#include "WProgram.h" +#include + +// A terminal observer that records every value and completion it receives. +template +struct Sink : IObserver +{ + std::vector values; + int completeCount = 0; + + void OnNext(T v) override { values.push_back(v); } + void OnComplete() override { completeCount++; } +}; + +// Reset the Arduino mock state before each test. +inline void resetMocks() +{ + g_millis = 0; + g_micros = 0; + g_pinModePin = 0xFF; + g_pinModeMode = 0xFF; + g_digitalPin = 0xFF; + g_digitalValue = 0xFF; + g_analogPin = 0xFF; + g_analogValue = -1; + g_analogRead = 0; + g_digitalRead = 0; + Serial.printCount = 0; +} + +#endif diff --git a/test/WProgram.h b/test/WProgram.h new file mode 100644 index 0000000..af13a97 --- /dev/null +++ b/test/WProgram.h @@ -0,0 +1,107 @@ +// Minimal Arduino API stub used by the native PlatformIO unit tests. +// ReactiveArduinoLib.h includes "WProgram.h" when ARDUINO is not defined, +// so this file is picked up from the `test` include path. +#ifndef WPROGRAM_STUB_H +#define WPROGRAM_STUB_H + +#include +#include +#include +#include +#include +#include +#include + +typedef unsigned char byte; + +// ---- Minimal Arduino String ---- +class String +{ +public: + String() {} + String(const char* s) { if (s) _s = s; } + String(char c) { _s.assign(1, c); } + String(int v) { char b[16]; snprintf(b, sizeof(b), "%d", v); _s = b; } + String(unsigned int v) { char b[16]; snprintf(b, sizeof(b), "%u", v); _s = b; } + String(long v) { char b[24]; snprintf(b, sizeof(b), "%ld", v); _s = b; } + String(unsigned long v) { char b[24]; snprintf(b, sizeof(b), "%lu", v); _s = b; } + String(float v) { char b[32]; snprintf(b, sizeof(b), "%f", v); _s = b; } + String(double v) { char b[40]; snprintf(b, sizeof(b), "%f", v); _s = b; } + + const char* c_str() const { return _s.c_str(); } + unsigned int length() const { return (unsigned int)_s.size(); } + char charAt(unsigned int i) const { return (i < _s.size()) ? _s[i] : '\0'; } + + String substring(unsigned int from, unsigned int to) const + { + if (from >= _s.size()) return String(); + if (to > _s.size()) to = (unsigned int)_s.size(); + if (to < from) to = from; + return String(_s.substr(from, to - from).c_str()); + } + + void concat(char c) { _s += c; } + void concat(const char* s) { if (s) _s += s; } + int toInt() const { return (int)atoi(_s.c_str()); } + float toFloat() const { return (float)atof(_s.c_str()); } + + String operator+(const String& o) const { String r(_s.c_str()); r._s += o._s; return r; } + String& operator=(const String& o) { _s = o._s; return *this; } + String& operator=(const char* o) { _s = (o ? o : ""); return *this; } + bool operator==(const String& o) const { return _s == o._s; } + bool operator==(const char* o) const { return _s == (o ? o : ""); } + +private: + std::string _s; +}; + +// ---- Controllable time ---- +inline unsigned long g_millis = 0; +inline unsigned long g_micros = 0; +inline unsigned long millis() { return g_millis; } +inline unsigned long micros() { return g_micros; } + +// ---- Pin I/O capture ---- +inline uint8_t g_pinModePin = 0xFF; +inline uint8_t g_pinModeMode = 0xFF; +inline void pinMode(uint8_t pin, uint8_t mode) { g_pinModePin = pin; g_pinModeMode = mode; } + +inline uint8_t g_digitalPin = 0xFF; +inline uint8_t g_digitalValue = 0xFF; +inline void digitalWrite(uint8_t pin, uint8_t value) { g_digitalPin = pin; g_digitalValue = value; } + +inline uint8_t g_analogPin = 0xFF; +inline int g_analogValue = -1; +inline void analogWrite(uint8_t pin, int value) { g_analogPin = pin; g_analogValue = value; } + +inline int g_analogRead = 0; +inline int analogRead(uint8_t) { return g_analogRead; } + +inline int g_digitalRead = 0; +inline int digitalRead(uint8_t) { return g_digitalRead; } + +// ---- Serial capture ---- +struct SerialStub +{ + int printCount = 0; + int available() { return 0; } + int read() { return -1; } + void begin(unsigned long) {} + template void println(const U&) { printCount++; } +}; +inline SerialStub Serial; + +#ifndef LOW +#define LOW 0 +#endif +#ifndef HIGH +#define HIGH 1 +#endif +#ifndef INPUT +#define INPUT 0 +#endif +#ifndef OUTPUT +#define OUTPUT 1 +#endif + +#endif diff --git a/test/test_aggregates.cpp b/test/test_aggregates.cpp new file mode 100644 index 0000000..00bc470 --- /dev/null +++ b/test/test_aggregates.cpp @@ -0,0 +1,128 @@ +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +static bool isPositive(int v) { return v > 0; } + +void test_count(void) +{ + int arr[4] = {10, 20, 30, 40}; + ObservableArray src(arr, 4); + auto& agg = src.Count(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_EQUAL_INT(4, s.values.size()); + TEST_ASSERT_EQUAL_INT(4, s.values.back()); +} + +void test_countdown(void) +{ + int arr[4] = {0, 0, 0, 0}; + ObservableArray src(arr, 4); + auto& agg = src.CountDown(3); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); // 2, 1, 0 + TEST_ASSERT_EQUAL_INT(0, s.values.back()); +} + +void test_sum(void) +{ + int arr[4] = {1, 2, 3, 4}; + ObservableArray src(arr, 4); + auto& agg = src.Sum(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_EQUAL_INT(10, s.values.back()); +} + +void test_min(void) +{ + int arr[5] = {5, 2, 8, 1, 3}; + ObservableArray src(arr, 5); + auto& agg = src.Min(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.back()); +} + +void test_max(void) +{ + int arr[5] = {5, 2, 8, 1, 3}; + ObservableArray src(arr, 5); + auto& agg = src.Max(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_EQUAL_INT(8, s.values.back()); +} + +void test_average_float(void) +{ + float arr[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + ObservableArray src(arr, 4); + auto& agg = src.Average(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 2.5f, s.values.back()); +} + +void test_rms_float(void) +{ + float arr[2] = {3.0f, 4.0f}; + ObservableArray src(arr, 2); + auto& agg = src.RMS(); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 3.5355339f, s.values.back()); +} + +void test_any(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& agg = src.Any(isPositive); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_TRUE(s.values.back()); +} + +void test_all_true(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& agg = src.All(isPositive); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_TRUE(s.values.back()); +} + +void test_all_detects_failure(void) +{ + int arr[3] = {1, -2, 3}; + ObservableArray src(arr, 3); + auto& agg = src.All(isPositive); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_FALSE(s.values.back()); +} + +void test_none(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& agg = src.None(isPositive); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_FALSE(s.values.back()); +} + +void test_none_detects_match(void) +{ + int arr[3] = {-1, 2, -3}; + ObservableArray src(arr, 3); + auto& agg = src.None(isPositive); + Sink s; + agg.Subscribe(s); + TEST_ASSERT_FALSE(s.values.back()); +} diff --git a/test/test_filters.cpp b/test/test_filters.cpp new file mode 100644 index 0000000..d5dc485 --- /dev/null +++ b/test/test_filters.cpp @@ -0,0 +1,170 @@ +#include +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +void test_median3_bruteforce(void) +{ + int p[3] = {0, 1, 2}; + do + { + ObservableArray src(p, 3); + auto& f = src.Median3(); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.back()); + } while (std::next_permutation(p, p + 3)); +} + +void test_median5_bruteforce(void) +{ + int p[5] = {0, 1, 2, 3, 4}; + do + { + ObservableArray src(p, 5); + auto& f = src.Median5(); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, s.values.back()); + } while (std::next_permutation(p, p + 5)); +} + +void test_moving_average(void) +{ + float seq[5] = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f}; + ObservableArray src(seq, 5); + auto& f = src.MovingAverage(3); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(5, s.values.size()); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 2.0f, s.values[2]); // (1+2+3)/3 + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 4.0f, s.values[4]); // (3+4+5)/3 +} + +void test_moving_rms(void) +{ + float seq[2] = {3.0f, 4.0f}; + ObservableArray src(seq, 2); + auto& f = src.MovingRMS(2); + Sink s; + f.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 3.5355339f, s.values.back()); // sqrt((9+16)/2) +} + +void test_on_rising(void) +{ + int arr[4] = {1, 2, 1, 0}; + ObservableArray src(arr, 4); + auto& f = src.OnRising(); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(2, s.values[0]); +} + +void test_on_falling(void) +{ + int arr[4] = {1, 2, 1, 0}; + ObservableArray src(arr, 4); + auto& f = src.OnFalling(); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); + TEST_ASSERT_EQUAL_INT(0, s.values[1]); +} + +void test_low_pass(void) +{ + float arr[1] = {1.0f}; + ObservableArray src(arr, 1); + auto& f = src.LowPass(0.5); + Sink s; + f.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 0.5f, s.values[0]); +} + +void test_high_pass(void) +{ + float arr[1] = {1.0f}; + ObservableArray src(arr, 1); + auto& f = src.HighPass(0.5); + Sink s; + f.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 0.5f, s.values[0]); +} + +void test_pass_stop_band(void) +{ + float arr[1] = {1.0f}; + ObservableArray src(arr, 1); + auto& pb = src.PassBand(0.1, 0.9); + Sink sp; + pb.Subscribe(sp); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 0.8f, sp.values[0]); // high(0.9) - low(0.1) + + auto& sb = src.StopBand(0.1, 0.9); + Sink ss; + sb.Subscribe(ss); + TEST_ASSERT_FLOAT_WITHIN(0.0001f, 0.2f, ss.values[0]); // 1.0 - 0.8 +} + +void test_is_equal(void) +{ + int arr[4] = {1, 2, 3, 0}; + ObservableArray src(arr, 4); + auto& f = src.IsEqual(2); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(2, s.values[0]); +} + +void test_is_less(void) +{ + int arr[4] = {1, 2, 3, 0}; + ObservableArray src(arr, 4); + auto& f = src.IsLess(3); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); // 1, 2, 0 are < 3 +} + +void test_is_zero(void) +{ + int arr[4] = {1, 2, 3, 0}; + ObservableArray src(arr, 4); + auto& f = src.IsZero(); + Sink s; + f.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(0, s.values[0]); +} + +void test_debounce(void) +{ + ObservableProperty src; + auto& f = src.DebounceMillis(10); + Sink s; + f.Subscribe(s); + g_millis = 0; src = 1; // elapsed 0 -> dropped + g_millis = 20; src = 2; // elapsed 20 -> emitted + g_millis = 25; src = 3; // elapsed 5 -> dropped + g_millis = 35; src = 4; // elapsed 15 -> emitted + TEST_ASSERT_EQUAL_INT(2, s.values.size()); + TEST_ASSERT_EQUAL_INT(2, s.values[0]); + TEST_ASSERT_EQUAL_INT(4, s.values[1]); +} + +void test_window(void) +{ + ObservableProperty src; + auto& f = src.WindowMillis(100); + Sink s; + f.Subscribe(s); + g_millis = 0; src = 1; // opens window, elapsed 0 -> emitted + g_millis = 50; src = 2; // elapsed 50 -> emitted + g_millis = 200; src = 3; // elapsed 200 -> dropped + TEST_ASSERT_EQUAL_INT(2, s.values.size()); +} diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 0000000..0ac623e --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,215 @@ +// Single Unity test runner for the ReactiveArduino unit tests. +// +// PlatformIO links every *.cpp file in `test/` into a single test binary, so +// there must be exactly one main(), setUp() and tearDown(). Test functions live +// in the per-topic test_*.cpp files and are declared/run here. +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +void setUp(void) { resetMocks(); } +void tearDown(void) {} + +// ---- test_aggregates.cpp ---- +void test_count(void); +void test_countdown(void); +void test_sum(void); +void test_min(void); +void test_max(void); +void test_average_float(void); +void test_rms_float(void); +void test_any(void); +void test_all_true(void); +void test_all_detects_failure(void); +void test_none(void); +void test_none_detects_match(void); + +// ---- test_filters.cpp ---- +void test_median3_bruteforce(void); +void test_median5_bruteforce(void); +void test_moving_average(void); +void test_moving_rms(void); +void test_on_rising(void); +void test_on_falling(void); +void test_low_pass(void); +void test_high_pass(void); +void test_pass_stop_band(void); +void test_is_equal(void); +void test_is_less(void); +void test_is_zero(void); +void test_debounce(void); +void test_window(void); + +// ---- test_observables.cpp ---- +void test_range_ascending(void); +void test_range_descending(void); +void test_range_defer(void); +void test_array(void); +void test_array_defer(void); +void test_property(void); +void test_manual_defer(void); +void test_timer_one_shot(void); +void test_timer_rearm(void); +void test_interval_periodic(void); +void test_analog_input(void); +void test_digital_input(void); + +// ---- test_observers.cpp ---- +void test_do(void); +void test_do_nothing(void); +void test_finally(void); +void test_do_and_finally(void); +void test_to_property(void); +void test_to_array(void); +void test_to_circular_buffer(void); +void test_digital_output(void); +void test_analog_output(void); +void test_serial_output(void); + +// ---- test_operators.cpp ---- +void test_where(void); +void test_distinct(void); +void test_first(void); +void test_last(void); +void test_skip(void); +void test_take(void); +void test_take_at(void); +void test_take_first(void); +void test_take_last(void); +void test_take_until(void); +void test_take_while(void); +void test_skip_until(void); +void test_skip_while(void); +void test_batch(void); +void test_foreach(void); +void test_if(void); +void test_timeout_millis(void); +void test_repeat(void); +void test_do_reset(void); +void test_not_reset(void); + +// ---- test_transformations.cpp ---- +void test_select(void); +void test_map(void); +void test_cast(void); +void test_reduce(void); +void test_limit(void); +void test_limit_upper(void); +void test_limit_lower(void); +void test_scale(void); +void test_abs(void); +void test_adc_to_voltage(void); +void test_toggle(void); +void test_threshold(void); +void test_elapsed_millis(void); +void test_timestamp_millis(void); +void test_frequency(void); +void test_to_bool(void); +void test_string_buffer(void); +void test_split(void); +void test_join(void); +void test_parse_int(void); +void test_parse_float(void); + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_count); + RUN_TEST(test_countdown); + RUN_TEST(test_sum); + RUN_TEST(test_min); + RUN_TEST(test_max); + RUN_TEST(test_average_float); + RUN_TEST(test_rms_float); + RUN_TEST(test_any); + RUN_TEST(test_all_true); + RUN_TEST(test_all_detects_failure); + RUN_TEST(test_none); + RUN_TEST(test_none_detects_match); + + RUN_TEST(test_median3_bruteforce); + RUN_TEST(test_median5_bruteforce); + RUN_TEST(test_moving_average); + RUN_TEST(test_moving_rms); + RUN_TEST(test_on_rising); + RUN_TEST(test_on_falling); + RUN_TEST(test_low_pass); + RUN_TEST(test_high_pass); + RUN_TEST(test_pass_stop_band); + RUN_TEST(test_is_equal); + RUN_TEST(test_is_less); + RUN_TEST(test_is_zero); + RUN_TEST(test_debounce); + RUN_TEST(test_window); + + RUN_TEST(test_range_ascending); + RUN_TEST(test_range_descending); + RUN_TEST(test_range_defer); + RUN_TEST(test_array); + RUN_TEST(test_array_defer); + RUN_TEST(test_property); + RUN_TEST(test_manual_defer); + RUN_TEST(test_timer_one_shot); + RUN_TEST(test_timer_rearm); + RUN_TEST(test_interval_periodic); + RUN_TEST(test_analog_input); + RUN_TEST(test_digital_input); + + RUN_TEST(test_do); + RUN_TEST(test_do_nothing); + RUN_TEST(test_finally); + RUN_TEST(test_do_and_finally); + RUN_TEST(test_to_property); + RUN_TEST(test_to_array); + RUN_TEST(test_to_circular_buffer); + RUN_TEST(test_digital_output); + RUN_TEST(test_analog_output); + RUN_TEST(test_serial_output); + + RUN_TEST(test_where); + RUN_TEST(test_distinct); + RUN_TEST(test_first); + RUN_TEST(test_last); + RUN_TEST(test_skip); + RUN_TEST(test_take); + RUN_TEST(test_take_at); + RUN_TEST(test_take_first); + RUN_TEST(test_take_last); + RUN_TEST(test_take_until); + RUN_TEST(test_take_while); + RUN_TEST(test_skip_until); + RUN_TEST(test_skip_while); + RUN_TEST(test_batch); + RUN_TEST(test_foreach); + RUN_TEST(test_if); + RUN_TEST(test_timeout_millis); + RUN_TEST(test_repeat); + RUN_TEST(test_do_reset); + RUN_TEST(test_not_reset); + + RUN_TEST(test_select); + RUN_TEST(test_map); + RUN_TEST(test_cast); + RUN_TEST(test_reduce); + RUN_TEST(test_limit); + RUN_TEST(test_limit_upper); + RUN_TEST(test_limit_lower); + RUN_TEST(test_scale); + RUN_TEST(test_abs); + RUN_TEST(test_adc_to_voltage); + RUN_TEST(test_toggle); + RUN_TEST(test_threshold); + RUN_TEST(test_elapsed_millis); + RUN_TEST(test_timestamp_millis); + RUN_TEST(test_frequency); + RUN_TEST(test_to_bool); + RUN_TEST(test_string_buffer); + RUN_TEST(test_split); + RUN_TEST(test_join); + RUN_TEST(test_parse_int); + RUN_TEST(test_parse_float); + + return UNITY_END(); +} diff --git a/test/test_observables.cpp b/test/test_observables.cpp new file mode 100644 index 0000000..e85ca66 --- /dev/null +++ b/test/test_observables.cpp @@ -0,0 +1,137 @@ +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +void test_range_ascending(void) +{ + ObservableRange src(1, 5); + Sink s; + src.Subscribe(s); + TEST_ASSERT_EQUAL_INT(5, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); + TEST_ASSERT_EQUAL_INT(5, s.values.back()); +} + +void test_range_descending(void) +{ + ObservableRange src(5, 1, -1); + Sink s; + src.Subscribe(s); + TEST_ASSERT_EQUAL_INT(5, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.values.back()); +} + +void test_range_defer(void) +{ + ObservableRangeDefer src(1, 3); + Sink s; + src.Subscribe(s); + src.Next(); + src.Next(); + src.Next(); + src.Next(); // past the end + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.completeCount); +} + +void test_array(void) +{ + int arr[3] = {10, 20, 30}; + ObservableArray src(arr, 3); + Sink s; + src.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(30, s.values.back()); +} + +void test_array_defer(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArrayDefer src(arr, 3); + Sink s; + src.Subscribe(s); + src.Next(); + src.Next(); + src.Next(); + src.Next(); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); +} + +void test_property(void) +{ + ObservableProperty src; + Sink s; + src.Subscribe(s); + src = 7; + src = 8; + TEST_ASSERT_EQUAL_INT(2, s.values.size()); + src.Finish(); + src = 9; // ignored after completion + TEST_ASSERT_EQUAL_INT(2, s.values.size()); +} + +void test_manual_defer(void) +{ + ObservableManualDefer src; + Sink s; + src.Subscribe(s); + src.Next(); + src.Next(); + TEST_ASSERT_EQUAL_INT(2, s.values.size()); +} + +void test_timer_one_shot(void) +{ + ObservableTimerMillis t(1000); + Sink s; + t.Subscribe(s); + g_millis = 500; t.Update(); // not yet + g_millis = 1000; t.Update(); // fire + g_millis = 2000; t.Update(); // already expired + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_TRUE(s.values[0] == 1000UL); +} + +void test_timer_rearm(void) +{ + ObservableTimerMillis t(1000); + Sink s; + t.Subscribe(s); + g_millis = 1000; t.Update(); // fire + t.Reset(); // re-arm (startTime = 1000) + g_millis = 1500; t.Update(); // not yet + g_millis = 2000; t.Update(); // fire again + TEST_ASSERT_EQUAL_INT(2, s.values.size()); +} + +void test_interval_periodic(void) +{ + ObservableIntervalMillis iv(1000); + Sink s; + iv.Subscribe(s); + g_millis = 1000; iv.Update(); // fire + g_millis = 1500; iv.Update(); // not yet + g_millis = 2000; iv.Update(); // fire + TEST_ASSERT_EQUAL_INT(2, s.values.size()); +} + +void test_analog_input(void) +{ + ObservableAnalogInput src(0); + Sink s; + src.Subscribe(s); + g_analogRead = 42; + src.Next(); + TEST_ASSERT_EQUAL_INT(42, s.values.back()); +} + +void test_digital_input(void) +{ + ObservableDigitalInput src(1); + Sink s; + src.Subscribe(s); + g_digitalRead = 1; + src.Next(); + TEST_ASSERT_EQUAL_INT(1, s.values.back()); +} diff --git a/test/test_observers.cpp b/test/test_observers.cpp new file mode 100644 index 0000000..c6f264e --- /dev/null +++ b/test/test_observers.cpp @@ -0,0 +1,106 @@ +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +static int g_actionCount = 0; +static void countAction(int) { g_actionCount++; } +static int g_cbCount = 0; +static void countCb() { g_cbCount++; } + +void test_do(void) +{ + g_actionCount = 0; + g_cbCount = 0; + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + src.Do(countAction); + TEST_ASSERT_EQUAL_INT(3, g_actionCount); +} + +void test_do_nothing(void) +{ + int arr[2] = {1, 2}; + ObservableArray src(arr, 2); + src.DoNothing(); // must not crash + TEST_ASSERT_TRUE(true); +} + +void test_finally(void) +{ + g_actionCount = 0; + g_cbCount = 0; + int arr[2] = {1, 2}; + ObservableArray src(arr, 2); + src.Finally(countCb); + TEST_ASSERT_EQUAL_INT(1, g_cbCount); +} + +void test_do_and_finally(void) +{ + g_actionCount = 0; + g_cbCount = 0; + int arr[2] = {1, 2}; + ObservableArray src(arr, 2); + src.DoAndFinally(countAction, countCb); + TEST_ASSERT_EQUAL_INT(2, g_actionCount); + TEST_ASSERT_EQUAL_INT(1, g_cbCount); +} + +void test_to_property(void) +{ + int arr[2] = {10, 20}; + ObservableArray src(arr, 2); + int out = 0; + src.ToProperty(out); + TEST_ASSERT_EQUAL_INT(20, out); +} + +void test_to_array(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + int out[3] = {0, 0, 0}; + auto& a = src.ToArray(out, 3); + TEST_ASSERT_EQUAL_INT(3, out[2]); + TEST_ASSERT_EQUAL_INT(3, a.GetIndex()); +} + +void test_to_circular_buffer(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + int out[3] = {0, 0, 0}; + src.ToCircularBuffer(out, 3); + // circular overwrite of a 3-slot buffer with 5 values -> {4, 5, 3} + TEST_ASSERT_EQUAL_INT(4, out[0]); + TEST_ASSERT_EQUAL_INT(5, out[1]); + TEST_ASSERT_EQUAL_INT(3, out[2]); +} + +void test_digital_output(void) +{ + int arr[1] = {1}; + ObservableArray src(arr, 1); + src.ToDigitalOutput(13); + TEST_ASSERT_EQUAL_INT(OUTPUT, (int)g_pinModeMode); + TEST_ASSERT_EQUAL_INT(13, (int)g_digitalPin); + TEST_ASSERT_EQUAL_INT(1, (int)g_digitalValue); +} + +void test_analog_output(void) +{ + int arr[1] = {128}; + ObservableArray src(arr, 1); + src.ToAnalogOutput(9); + TEST_ASSERT_EQUAL_INT(9, (int)g_analogPin); + TEST_ASSERT_EQUAL_INT(128, g_analogValue); +} + +void test_serial_output(void) +{ + int arr[1] = {42}; + ObservableArray src(arr, 1); + src.ToSerial(); + TEST_ASSERT_EQUAL_INT(1, Serial.printCount); +} diff --git a/test/test_operators.cpp b/test/test_operators.cpp new file mode 100644 index 0000000..d5e1f47 --- /dev/null +++ b/test/test_operators.cpp @@ -0,0 +1,239 @@ +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +static bool isEven(int v) { return v % 2 == 0; } +static bool lessThan3(int v) { return v < 3; } +static bool greaterThan3(int v) { return v > 3; } +static int g_actionCount = 0; +static void countAction(int) { g_actionCount++; } +static int g_cbCount = 0; +static void countCb() { g_cbCount++; } + +void test_where(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.Where(isEven); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(6, s.values.back()); +} + +void test_distinct(void) +{ + int arr[6] = {1, 1, 2, 2, 3, 3}; + ObservableArray src(arr, 6); + auto& op = src.Distinct(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); +} + +void test_first(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + auto& op = src.First(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); + TEST_ASSERT_EQUAL_INT(1, s.completeCount); +} + +void test_last(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + auto& op = src.Last(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(5, s.values[0]); +} + +void test_skip(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.Skip(2); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(4, s.values.size()); + TEST_ASSERT_EQUAL_INT(3, s.values[0]); +} + +void test_take(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.Take(3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(3, s.values.back()); +} + +void test_take_at(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + auto& op = src.TakeAt(2); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(3, s.values[0]); +} + +void test_take_first(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + auto& op = src.TakeFirst(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); +} + +void test_take_last(void) +{ + int arr[5] = {1, 2, 3, 4, 5}; + ObservableArray src(arr, 5); + auto& op = src.TakeLast(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values.size()); + TEST_ASSERT_EQUAL_INT(5, s.values[0]); +} + +void test_take_until(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.TakeUntil(greaterThan3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); // 1, 2, 3 +} + +void test_take_while(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.TakeWhile(lessThan3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, s.values.size()); // 1, 2 +} + +void test_skip_until(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.SkipUntil(greaterThan3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); // 4, 5, 6 + TEST_ASSERT_EQUAL_INT(4, s.values[0]); +} + +void test_skip_while(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.SkipWhile(lessThan3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(4, s.values.size()); // 3, 4, 5, 6 + TEST_ASSERT_EQUAL_INT(3, s.values[0]); +} + +void test_batch(void) +{ + int arr[6] = {1, 2, 3, 4, 5, 6}; + ObservableArray src(arr, 6); + auto& op = src.Batch(3); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(6, s.values.size()); // no values dropped at boundaries +} + +void test_foreach(void) +{ + g_actionCount = 0; + g_cbCount = 0; + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& op = src.ForEach(countAction); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, g_actionCount); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); +} + +void test_if(void) +{ + g_actionCount = 0; + g_cbCount = 0; + int arr[4] = {1, 2, 3, 4}; + ObservableArray src(arr, 4); + auto& op = src.If(isEven, countAction); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, g_actionCount); // only for 2 and 4 + TEST_ASSERT_EQUAL_INT(4, s.values.size()); +} + +void test_timeout_millis(void) +{ + g_actionCount = 0; + g_cbCount = 0; + ObservableProperty src; + auto& op = src.TimeoutMillis(1000, countCb); + Sink s; + op.Subscribe(s); + src = 1; // resets the timer + g_millis = 500; op.Update(); // not expired + TEST_ASSERT_EQUAL_INT(0, g_cbCount); + g_millis = 1500; op.Update(); // expired + TEST_ASSERT_EQUAL_INT(1, g_cbCount); + TEST_ASSERT_EQUAL_INT(1, s.completeCount); +} + +void test_repeat(void) +{ + int arr[2] = {1, 2}; + ObservableArray src(arr, 2); + auto& op = src.Repeat(2); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(4, s.values.size()); // 1, 2, 1, 2 + TEST_ASSERT_EQUAL_INT(1, s.values[2]); + TEST_ASSERT_EQUAL_INT(2, s.values[3]); +} + +void test_do_reset(void) +{ + int arr[2] = {1, 2}; + ObservableArray src(arr, 2); + auto& op = src.DoReset(); + Sink s; + op.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, s.values.size()); + op.Reset(); // resets the parent -> re-runs + TEST_ASSERT_EQUAL_INT(4, s.values.size()); +} + +void test_not_reset(void) +{ + ObservableManualDefer src; + auto& op = src.NotReset(); + Sink s; + op.Subscribe(s); + op.Reset(); // completes children without resetting the parent + TEST_ASSERT_EQUAL_INT(1, s.completeCount); +} diff --git a/test/test_transformations.cpp b/test/test_transformations.cpp new file mode 100644 index 0000000..09de901 --- /dev/null +++ b/test/test_transformations.cpp @@ -0,0 +1,244 @@ +#include +#include "ReactiveArduinoLib.h" +using namespace Reactive; +#include "TestHelpers.h" + +static int addOne(int v) { return v + 1; } +static float half(int v) { return (float)v / 2.0f; } +static int sumInt(int acc, int v) { return acc + v; } + +void test_select(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& t = src.Select(addOne); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(4, s.values.back()); +} + +void test_map(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& t = src.Map(half); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 1.5f, s.values.back()); +} + +void test_cast(void) +{ + int arr[3] = {1, 2, 3}; + ObservableArray src(arr, 3); + auto& t = src.Cast(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 3.0f, s.values.back()); +} + +void test_reduce(void) +{ + int arr[4] = {1, 2, 3, 4}; + ObservableArray src(arr, 4); + auto& t = src.Reduce(sumInt, 0); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(10, s.values.back()); +} + +void test_limit(void) +{ + int arr[5] = {0, 1, 5, 9, 10}; + ObservableArray src(arr, 5); + auto& t = src.Limit(1, 9); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); + TEST_ASSERT_EQUAL_INT(9, s.values.back()); +} + +void test_limit_upper(void) +{ + int arr[4] = {0, 5, 10, 15}; + ObservableArray src(arr, 4); + auto& t = src.LimitUpper(10); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(10, s.values.back()); +} + +void test_limit_lower(void) +{ + int arr[4] = {0, 5, 10, 15}; + ObservableArray src(arr, 4); + auto& t = src.LimitLower(5); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(5, s.values[0]); +} + +void test_scale(void) +{ + float arr[3] = {0.0f, 5.0f, 10.0f}; + ObservableArray src(arr, 3); + auto& t = src.Scale(0.0f, 10.0f, 0.0f, 100.0f); + Sink s; + t.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 50.0f, s.values[1]); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 100.0f, s.values.back()); +} + +void test_abs(void) +{ + // TransformationAbs is not exposed through a fluent method, so it is + // exercised directly through its public Operator interface. + TransformationAbs t; + Sink s; + t._childObservers.Add(&s); + t.OnNext(-3); + t.OnNext(0); + t.OnNext(3); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_EQUAL_INT(3, s.values[0]); + TEST_ASSERT_EQUAL_INT(3, s.values[2]); +} + +void test_adc_to_voltage(void) +{ + int arr[2] = {0, 512}; + ObservableArray src(arr, 2); + auto& t = src.AdcToVoltage(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.001f, (512.0f * 5.0f) / 1023.0f, s.values.back()); +} + +void test_toggle(void) +{ + int arr[3] = {1, 1, 1}; + ObservableArray src(arr, 3); + auto& t = src.Toggle(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(1, s.values[0]); // first toggle -> HIGH + TEST_ASSERT_EQUAL_INT(0, s.values[1]); + TEST_ASSERT_EQUAL_INT(1, s.values[2]); +} + +void test_threshold(void) +{ + int arr[4] = {0, 2, 8, 10}; + ObservableArray src(arr, 4); + auto& t = src.Threshold(5); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(0, s.values[1]); // 2 is still LOW + TEST_ASSERT_EQUAL_INT(1, s.values.back()); // 8 and 10 are HIGH +} + +void test_elapsed_millis(void) +{ + ObservableProperty src; + auto& t = src.ElapsedMillis(); + Sink s; + t.Subscribe(s); + g_millis = 100; src = 1; + g_millis = 250; src = 2; + TEST_ASSERT_TRUE(s.values[0] == 100UL); + TEST_ASSERT_TRUE(s.values[1] == 150UL); +} + +void test_timestamp_millis(void) +{ + ObservableProperty src; + auto& t = src.Millis(); + Sink s; + t.Subscribe(s); + g_millis = 100; src = 1; + g_millis = 250; src = 2; + TEST_ASSERT_TRUE(s.values[0] == 100UL); + TEST_ASSERT_TRUE(s.values[1] == 250UL); +} + +void test_frequency(void) +{ + ObservableProperty src; + auto& t = src.Frequency(); + Sink s; + t.Subscribe(s); + g_millis = 100; src = 1; // 1000 / 100 = 10 Hz + g_millis = 200; src = 2; // 1000 / 100 = 10 Hz + TEST_ASSERT_FLOAT_WITHIN(0.01f, 10.0f, s.values[0]); + TEST_ASSERT_FLOAT_WITHIN(0.01f, 10.0f, s.values[1]); +} + +void test_to_bool(void) +{ + int arr[3] = {0, 1, 2}; + ObservableArray src(arr, 3); + auto& t = src.ToBool(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_FALSE(s.values[0]); + TEST_ASSERT_TRUE(s.values[1]); + TEST_ASSERT_TRUE(s.values[2]); +} + +void test_string_buffer(void) +{ + String sarr[2] = {String("ab"), String("cd")}; + ObservableArray src(sarr, 2); + auto& t = src.StringBuffer(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(2, s.values.size()); + TEST_ASSERT_TRUE(strcmp(s.values[1].c_str(), "abcd") == 0); +} + +void test_split(void) +{ + String sarr[1] = {String("a;b;c")}; + ObservableArray src(sarr, 1); + auto& t = src.Split(';'); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_TRUE(strcmp(s.values[0].c_str(), "a") == 0); + TEST_ASSERT_TRUE(strcmp(s.values[2].c_str(), "c") == 0); +} + +void test_join(void) +{ + String sarr[3] = {String("a"), String("b"), String("c")}; + ObservableArray src(sarr, 3); + auto& t = src.Join('-'); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(3, s.values.size()); + TEST_ASSERT_TRUE(strcmp(s.values[2].c_str(), "a-b-c") == 0); +} + +void test_parse_int(void) +{ + String sarr[2] = {String("123"), String("45")}; + ObservableArray src(sarr, 2); + auto& t = src.ParseInt(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_EQUAL_INT(123, s.values[0]); + TEST_ASSERT_EQUAL_INT(45, s.values[1]); +} + +void test_parse_float(void) +{ + String sarr[2] = {String("3.5"), String("2.25")}; + ObservableArray src(sarr, 2); + auto& t = src.ParseFloat(); + Sink s; + t.Subscribe(s); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 3.5f, s.values[0]); + TEST_ASSERT_FLOAT_WITHIN(0.001f, 2.25f, s.values[1]); +} From d07de9bcfc41b9f844f0c48e1398b4b3864bcde1 Mon Sep 17 00:00:00 2001 From: rzeldent Date: Wed, 30 Sep 2026 23:36:33 +0200 Subject: [PATCH 10/10] Add unit testing job to CI workflow --- .github/workflows/main.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d482c4..ae8cbeb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,3 +19,18 @@ jobs: run: python -m pip install platformio - name: Build firmware run: pio ci --lib="." --board ${{matrix.pio-env}} "examples/${{matrix.example}}" + + test: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + architecture: 'x64' + - name: Install PlatformIO + run: python -m pip install platformio + - name: Run unit tests + run: pio test -e native