Skip to content

Commit a48c188

Browse files
authored
Merge pull request #703 from SignalK/new_transforms
Implement new merging and repeating transforms
2 parents 4020558 + ead67b7 commit a48c188

File tree

9 files changed

+959
-95
lines changed

9 files changed

+959
-95
lines changed

examples/join_and_zip.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @file join_and_zip.cpp
3+
* @brief Example of Join and Zip transforms.
4+
*
5+
* Join and Zip transforms combine multiple input values into a single output.
6+
*
7+
* Try running this code with the serial monitor open ("Upload and Monitor"
8+
* in PlatformIO menu). The program will produce capital letters every second,
9+
* lowercase letters every 3 seconds, and integers every 10 seconds. Comment
10+
* out and enable Join and zip transforms to see how they affect the
11+
* output.
12+
*
13+
*/
14+
15+
#include "sensesp.h"
16+
17+
#include <math.h>
18+
19+
#include "sensesp/sensors/sensor.h"
20+
#include "sensesp/transforms/join.h"
21+
#include "sensesp/transforms/lambda_transform.h"
22+
#include "sensesp/transforms/zip.h"
23+
#include "sensesp_minimal_app_builder.h"
24+
25+
using namespace sensesp;
26+
27+
ReactESP app;
28+
29+
SensESPMinimalApp* sensesp_app;
30+
31+
void setup() {
32+
SetupLogging();
33+
34+
// Note: SensESPMinimalAppBuilder is used to build the app. This creates
35+
// a minimal app with no networking or other bells and whistles which
36+
// would be distracting in this example. In normal use, this is not what
37+
// you would use. Unless, of course, you know that is what you want.
38+
SensESPMinimalAppBuilder builder;
39+
40+
sensesp_app = builder.get_app();
41+
42+
// Produce capital letters every second
43+
auto sensor_A = new RepeatSensor<char>(1000, []() {
44+
static char value = 'Z';
45+
if (value == 'Z') {
46+
value = 'A';
47+
} else {
48+
value += 1;
49+
}
50+
return value;
51+
});
52+
53+
sensor_A->connect_to(new LambdaConsumer<char>(
54+
[](char value) { ESP_LOGD("App", " %c", value); }));
55+
56+
// Produce lowercase letters every 3 seconds
57+
auto sensor_a = new RepeatSensor<char>(3000, []() {
58+
static char value = 'z';
59+
if (value == 'z') {
60+
value = 'a';
61+
} else {
62+
value += 1;
63+
}
64+
return value;
65+
});
66+
67+
sensor_a->connect_to(new LambdaConsumer<char>(
68+
[](char value) { ESP_LOGD("App", " %c", value); }));
69+
70+
// Produce integers every 10 seconds
71+
auto sensor_int = new RepeatSensor<int>(10000, []() {
72+
static int value = 0;
73+
value += 1;
74+
return value;
75+
});
76+
77+
sensor_int->connect_to(new LambdaConsumer<int>(
78+
[](int value) { ESP_LOGD("App", " %d", value); }));
79+
80+
// Join the three producer outputs into one tuple. A tuple is a data
81+
// structure that can hold multiple values of different types. The resulting
82+
// tuple can be consumed by consumers to process the values together.
83+
84+
auto* merged = new Join3<char, char, int>(5000);
85+
86+
// The Join transform will emit a tuple whenever any of the producers emit a
87+
// new value, as long as all values are less than max_age milliseconds old.
88+
89+
// Once an integer is produced, the Join transform produces tuples for all
90+
// new letter input until the last integer value is over 5000 milliseconds
91+
// old.
92+
93+
// Next, try commenting out the Join transform and enabling the Zip transform
94+
// below to see how it affects the output.
95+
96+
// auto* merged = new Zip3<char, char, int>(5000);
97+
98+
// The Zip transform will emit a tuple only when all producers have emitted a
99+
// new value within max_age milliseconds. This has the effect of synchronizing
100+
// the producers' outputs, at the cost of potentially waiting for all
101+
// producers to emit a new value.
102+
103+
// Below, the sensors are connected to the consumers of the Join/Zip
104+
// transform. The syntax here is a bit more complex and warrants some
105+
// explanation.
106+
107+
// `merged` is our variable holding a pointer to the Join or Zip transform.
108+
// The `consumers` member of the transform is a tuple of LambdaConsumers
109+
// that consume and process the values of the producers. Subscripts [] can
110+
// only be used to access elements of a same type, but our LambdaConsumers
111+
// are of potentially different types - hence the tuple. The `std::get<>()`
112+
// function is used to access the elements of the tuple. The first argument
113+
// is the index of the element in the tuple, starting from 0.
114+
115+
// `connect_to()` expects a pointer to a `ValueConsumer`, but `std::get`
116+
// returns a reference to the tuple element. The `&` operator is used to
117+
// get the address of the tuple element, which is then passed to
118+
// `connect_to()`.
119+
120+
// TL;DR: We connect each sensor to the corresponding consumer Join or
121+
// Zip transform.
122+
123+
sensor_A->connect_to(&(std::get<0>(merged->consumers)));
124+
sensor_a->connect_to(&(std::get<1>(merged->consumers)));
125+
sensor_int->connect_to(&(std::get<2>(merged->consumers)));
126+
127+
// Here, we have a LambdaTransform that takes the tuple of values produced
128+
// by the Join/Zip transform and converts it into a string. Note the template
129+
// arguments: the transform input is a tuple of char, char, and int, and the
130+
// output is a String. The same input type needs to be defined in our lambda
131+
// function, starting with [].
132+
133+
auto merged_string = new LambdaTransform<std::tuple<char, char, int>, String>(
134+
[](std::tuple<char, char, int> values) {
135+
return String(std::get<0>(values)) + " " + String(std::get<1>(values)) +
136+
" " + String(std::get<2>(values));
137+
});
138+
139+
// Remember to connect the Join/Zip transform to the LambdaTransform:
140+
141+
merged->connect_to(merged_string);
142+
143+
// Finally, we connect the LambdaTransform to a consumer that will print the
144+
// merged values to the console.
145+
146+
merged_string->connect_to(new LambdaConsumer<String>(
147+
[](String value) {
148+
ESP_LOGD("App", "Merged: %s", value.c_str());
149+
}));
150+
}
151+
152+
void loop() { app.tick(); }

examples/repeat_transform.cpp

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @file repeat_transform.cpp
3+
* @brief Example of different Repeat transforms.
4+
*
5+
* Repeat transforms transmit their input value at regular intervals, ensuring
6+
* output even at the absence of input.
7+
*
8+
* Try running this code with the serial monitor open ("Upload and Monitor"
9+
* in PlatformIO menu). The program will produce capital letters every second,
10+
* lowercase letters every 3 seconds, and integers every 10 seconds. Comment
11+
* out and enable different Repeat transform variants to see how they affect the
12+
* output.
13+
*
14+
*/
15+
16+
#include "sensesp.h"
17+
18+
#include <math.h>
19+
20+
#include "sensesp/sensors/sensor.h"
21+
#include "sensesp/system/lambda_consumer.h"
22+
#include "sensesp/transforms/repeat.h"
23+
#include "sensesp_minimal_app_builder.h"
24+
25+
using namespace sensesp;
26+
27+
ReactESP app;
28+
29+
SensESPMinimalApp* sensesp_app;
30+
31+
void setup() {
32+
SetupLogging();
33+
34+
// Note: SensESPMinimalAppBuilder is used to build the app. This creates
35+
// a minimal app with no networking or other bells and whistles which
36+
// would be distracting in this example. In normal use, this is not what
37+
// you would use. Unless, of course, you know that is what you want.
38+
SensESPMinimalAppBuilder builder;
39+
40+
sensesp_app = builder.get_app();
41+
42+
// Produce capital letters every second
43+
auto sensor_A = new RepeatSensor<char>(1000, []() {
44+
static char value = 'Z';
45+
if (value == 'Z') {
46+
value = 'A';
47+
} else {
48+
value += 1;
49+
}
50+
return value;
51+
});
52+
53+
sensor_A->connect_to(new LambdaConsumer<char>(
54+
[](char value) { ESP_LOGD("App", " %c", value); }));
55+
56+
// Produce lowercase letters every 3 seconds
57+
auto sensor_a = new RepeatSensor<char>(3000, []() {
58+
static char value = 'z';
59+
if (value == 'z') {
60+
value = 'a';
61+
} else {
62+
value += 1;
63+
}
64+
return value;
65+
});
66+
67+
sensor_a->connect_to(new LambdaConsumer<char>(
68+
[](char value) { ESP_LOGD("App", " %c", value); }));
69+
70+
// Produce integers every 10 seconds
71+
auto sensor_int = new RepeatSensor<int>(10000, []() {
72+
static int value = 0;
73+
value += 1;
74+
return value;
75+
});
76+
77+
sensor_int->connect_to(new LambdaConsumer<int>(
78+
[](int value) { ESP_LOGD("App", " %d", value); }));
79+
80+
// Repeat the values every 2 seconds
81+
82+
auto repeat_A = new Repeat<char>(2000);
83+
auto repeat_a = new Repeat<char>(2000);
84+
auto repeat_int = new Repeat<int>(2000);
85+
86+
// Pay attention to the individual columns of the program console output.
87+
// Capital letters are produced every second. Repeat gets always triggered as
88+
// a result, but never on its own because the repeat timer always is reset
89+
// before it expires. Lowercase letters are produced every 3 seconds. Repeat
90+
// gets triggered immediately and then again after 2 seconds. Integers are
91+
// produced every 10 seconds. Repeat gets triggered immediately and then
92+
// again after every 2 seconds until the next integer is produced.
93+
94+
// Try commenting out the Repeat lines above and uncommenting the
95+
// RepeatStopping lines below.
96+
97+
// auto repeat_A = new RepeatStopping<char>(2000, 5000);
98+
// auto repeat_a = new RepeatStopping<char>(2000, 5000);
99+
// auto repeat_int = new RepeatStopping<int>(2000, 5000);
100+
101+
// The maximum age is set to 5 seconds. Both the capital and lowercase
102+
// letters are produced like before because their repetition rates are
103+
// faster than the expiration time. However, the integers are produced
104+
// only every 10 seconds, and they stop being repeated after 5 seconds
105+
// until a new integer is produced.
106+
107+
// auto repeat_A = new RepeatExpiring<char>(2000, 5000, '?');
108+
// auto repeat_a = new RepeatExpiring<char>(2000, 5000, '?');
109+
// auto repeat_int = new RepeatExpiring<int>(2000, 5000, -1);
110+
111+
// The expiration time is set to 5 seconds. Both the capital and lowercase
112+
// letters are produced like before because their repetition rates are
113+
// faster than the expiration time. However, the integers are produced
114+
// only every 10 seconds, and the value does expire after 5 seconds, indicated
115+
// by the -1 value that gets output after the expiry, until a new integer is
116+
// produced.
117+
118+
// Finally, try commenting out the RepeatExpiring lines above and uncommenting
119+
// the RepeatConstantRate lines below.
120+
121+
// auto repeat_A = new RepeatConstantRate<char>(2000, 5000, '?');
122+
// auto repeat_a = new RepeatConstantRate<char>(2000, 5000, '?');
123+
// auto repeat_int = new RepeatConstantRate<int>(2000, 5000, -1);
124+
125+
// Notice how the repetitions are no longer triggered by the sensors but
126+
// are produced at a constant rate, in clusters. The letters still
127+
// never expire, but the integers do.
128+
129+
sensor_A->connect_to(repeat_A);
130+
sensor_a->connect_to(repeat_a);
131+
sensor_int->connect_to(repeat_int);
132+
133+
repeat_A->connect_to(new LambdaConsumer<char>(
134+
[](char value) { ESP_LOGD("App", "Repeat: %c", value); }));
135+
136+
repeat_a->connect_to(new LambdaConsumer<char>(
137+
[](char value) { ESP_LOGD("App", "Repeat: %c", value); }));
138+
139+
repeat_int->connect_to(new LambdaConsumer<int>(
140+
[](int value) { ESP_LOGD("App", "Repeat: %d", value); }));
141+
}
142+
143+
void loop() { app.tick(); }

src/sensesp/system/observable.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ void Observable::notify() {
1111
}
1212

1313
void Observable::attach(std::function<void()> observer) {
14-
observers.push_front(observer);
14+
// First iterate to the last element
15+
auto before_end = observers.before_begin();
16+
for (auto& _ : observers) {
17+
++before_end;
18+
}
19+
// Then insert the new observer
20+
observers.insert_after(before_end, observer);
1521
}
1622

1723
} // namespace sensesp

0 commit comments

Comments
 (0)