|
@@ -3,7 +3,8 @@
|
|
|
## blink with LED
|
|
|
|
|
|
This is platform-neural tutorial, although hardware implementation refers to
|
|
|
-arduino. The full code can be found at `examples/atmega328p/blink-led.cpp`
|
|
|
+arduino. The full code can be found at `examples/atmega328p/blink-led.cpp`. In
|
|
|
+this example the blinking with LED is performed using `rotor-light`.
|
|
|
|
|
|
Let's start from the inclusion of headers:
|
|
|
|
|
@@ -19,34 +20,30 @@ namespace rl = rotor_light;
|
|
|
|
|
|
In this example, there is only single message - `BlinkCommand`. It is good
|
|
|
practice to define messages in it's own namespace, as it is much more
|
|
|
-convenient to deail with messages later (handle and send).
|
|
|
+convenient to deal with messages later (handle and send).
|
|
|
|
|
|
```
|
|
|
namespace message {
|
|
|
|
|
|
-struct BlinkCommand : rl::Message {
|
|
|
- static constexpr auto type_id = __LINE__;
|
|
|
- using rl::Message::Message;
|
|
|
- rl::MessageTypeId get_type_id() const override { return type_id; }
|
|
|
-};
|
|
|
+struct BlinkCommand : rl::Message { // (1)
|
|
|
+ static constexpr auto type_id = __LINE__; // (2)
|
|
|
+ using rl::Message::Message; // (3)
|
|
|
+ rl::MessageTypeId get_type_id() const override { // (4)
|
|
|
+ return type_id; }
|
|
|
+ };
|
|
|
|
|
|
} // namespace message
|
|
|
```
|
|
|
|
|
|
-The `BlinkCommand` message have to be derived from `rotor_ligth` `Message`,
|
|
|
+The `BlinkCommand` message have to be derived from `rotor_ligth` `Message` (1),
|
|
|
and as the `BlinkCommand` has no payload, it just reuses its parent
|
|
|
-consturctor (`using rl::Message::Message`).
|
|
|
+constructor (`using rl::Message::Message`, (3)).
|
|
|
|
|
|
-As the `rotor_ligth` does not uses RTTI it should somehow identify
|
|
|
-message types at runtime. That's why overriden `get_type_id()` method
|
|
|
+As the `rotor_ligth` does not use RTTI it should somehow identify
|
|
|
+message types at runtime. That's why overridden `get_type_id()` method (4)
|
|
|
should be available on every message type. `rotor_light` does not care
|
|
|
how you, as the user, define your message type, the only requirement is the
|
|
|
-uniqueness of them. The easiest way to achieve that is just have line
|
|
|
-
|
|
|
-```
|
|
|
- static constexpr auto type_id = __LINE__;
|
|
|
-```
|
|
|
-
|
|
|
+uniqueness of them. The easiest way to achieve that is just have line (2)
|
|
|
in every message type definition, and group all used messages in a single
|
|
|
header file.
|
|
|
|
|
@@ -54,28 +51,444 @@ Let's move to the blinker actor code; as it is rather small, it is shown
|
|
|
entirely:
|
|
|
|
|
|
```
|
|
|
-struct Blinker : rl::Actor<2> {
|
|
|
-using Parent = Actor<2>;
|
|
|
+struct Blinker : rl::Actor<2> { // (5)
|
|
|
+using Parent = Actor<2>; // (6)
|
|
|
+
|
|
|
+void initialize() override { // (7)
|
|
|
+ subscribe(&Blinker::on_blink_command); // (8)
|
|
|
+ Parent::initialize(); // (9)
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+ void advance_start() override { // (10)
|
|
|
+ Parent::advance_start(); // (11)
|
|
|
+ blink(); // (12)
|
|
|
+ }
|
|
|
+
|
|
|
+ void blink() {
|
|
|
+ toggle_led(); // (13)
|
|
|
+ add_event(delay, [](void *data) { // (14)
|
|
|
+ auto self = static_cast<Blinker *>(data); // (15)
|
|
|
+ self->send<message::BlinkCommand>(0, self->id); // (16)
|
|
|
+ }, this);
|
|
|
+ }
|
|
|
+
|
|
|
+void on_blink_command(message::BlinkCommand &msg) { // (17)
|
|
|
+ blink();
|
|
|
+}
|
|
|
+
|
|
|
+rl::Duration delay; // (18)
|
|
|
+};
|
|
|
+```
|
|
|
+
|
|
|
+First, your own actor have to inherit from `rl::Actor` (5). The magic number `2` is
|
|
|
+used for preallocation of space for actor's message handlers: one handler from
|
|
|
+the base class defined by the framework (might be changed in the future), and the
|
|
|
+other one use the user-provided `on_blink_command`. The alias (6) makes it more
|
|
|
+convenient to refer the base class in the future.
|
|
|
+
|
|
|
+To react on the `BlinkCommand` message, the actor should subscribe to it (8). The
|
|
|
+proper place to do that is to override `initialize()` method (7) of the parent class.
|
|
|
+Of course, parent class **must** be initialized too, that's why don't forget to
|
|
|
+invoke its initialization (9).
|
|
|
+
|
|
|
+When actor is ready, its `advance_start()` (10) is invoked; as usually corresponding
|
|
|
+parent method should be called (11) and the initial blink (12) is performed. In
|
|
|
+the implementation of `blink()` the platform-dependent LED toggle is done at (13),
|
|
|
+and then delayed the same method invocation is scheduled (14-16). In (14) the
|
|
|
+capture-less lambda is scheduled to be invoked with `this` as the parameter when
|
|
|
+`delay` (18) amount of time passes. The lambda *must* be capture-less. After casting
|
|
|
+back `void*` to the actor class (15), it sends self a `BlinkCommand` message (16)
|
|
|
+using zero-priority queue. The `id` parameter is the destination actor address;
|
|
|
+which is the `Blinker` (self) address here. The destination address can also be
|
|
|
+multiple actors (i.e. it is mask).
|
|
|
+
|
|
|
+Each time the `BlinkCommand` is received (17), the `blink()` method is invoked, and
|
|
|
+the procedure above repeats again.
|
|
|
+
|
|
|
+Next, the application-wide compile-time configuration should be made:
|
|
|
+
|
|
|
+```
|
|
|
+using Storage = rl::traits::MessageStorage<rl::message::ChangeState, // (18)
|
|
|
+ rl::message::ChangeStateAck,
|
|
|
+ message::BlinkCommand>;
|
|
|
+using Queue = rl::Queue<Storage, 5>; /* upto 5 messages in 1 queue */ // (19)
|
|
|
+using Planner = rl::Planner<2>; /* upto 2 time events */ // (20)
|
|
|
+using Supervisor = rl::Supervisor< // (21)
|
|
|
+ rl::SupervisorBase::min_handlers_amount,
|
|
|
+ Blinker>;
|
|
|
+```
|
|
|
+
|
|
|
+`rotor-light` uses queues to store and process messages. There are no dynamic
|
|
|
+allocations, so, all sizes have to be known at compile-time, including the sizes
|
|
|
+of all used messages. The `Storage` helper (18) is used to determine the maximum
|
|
|
+allowed space size per message: here **all messages** in the application
|
|
|
+should be enumerated, i.e. messages from the framework and user-defined.
|
|
|
+
|
|
|
+Next, the application queue (or master queue) should be defined (19). It uses
|
|
|
+`Storage` and the magic number `5`, which pre-allocates space for queue with
|
|
|
+**zero-priority** for 5 messages. All `rotor-light` messages are put into that
|
|
|
+queue, but user-defined messages might use different queues. Queues with
|
|
|
+higher priorities are processed earlier than queues with lower priorities. So,
|
|
|
+to define master queue with two subqueues (priorities 0 and 1) with sizes
|
|
|
+enough to store 15 and 10 messages respectively, the following code should
|
|
|
+be used
|
|
|
+
|
|
|
+```
|
|
|
+using Queue = rl::Queue<Storage, 15, 10>;
|
|
|
+```
|
|
|
+
|
|
|
+What is the recommended queue sizes? Well, it is completely application defined, but
|
|
|
+for `rotor-light` messages there should be enough space to store `N * 2` messages,
|
|
|
+where `N` is the total number of actors (including supervisors).
|
|
|
+
|
|
|
+What happens, if a queue is full? The next send message is simply discarded, you'll
|
|
|
+be notified about that as the `send()` method returns `false`. If the discarded
|
|
|
+message is `rotor-light` message, then *the framework behavior is undefined*. So,
|
|
|
+either do not overload default queue (i.e. with zero-priority), or use different
|
|
|
+queues for user-messages. NB: there is a spike of `rotor-light` messages only
|
|
|
+during (re)starting supervisor; when everything is *operational* there is almost no
|
|
|
+framework messages.
|
|
|
+
|
|
|
+The Planner (20) is used to schedule future events, like it is shown at `add_event`
|
|
|
+(14). The number in angle braces defines planner capacity at compile-time. When event
|
|
|
+time arrives, it is removed from the planner. Please note, that `rotor-light` does
|
|
|
+not schedules any events into the planner, so its capacity and usage is completely
|
|
|
+controlled by the user code.
|
|
|
+
|
|
|
+How the framework interacts with the planner? If the root supervisor is used in
|
|
|
+*poll mode*, then, when there are no more messages left, the current time is
|
|
|
+requested, and all timed out event handlers are executed. Then the special message
|
|
|
+`refresh-time` is send to the supervisor and the procedure repeats endlessly.
|
|
|
+Indirectly, this is similar to the code like:
|
|
|
+
|
|
|
+```
|
|
|
+while (true) {
|
|
|
+ while (has_messages()) {
|
|
|
+ auto& message = get_front_message();
|
|
|
+ process(message);
|
|
|
+ pop_front_message();
|
|
|
+ }
|
|
|
+ // (22)
|
|
|
+ auto now = get_now();
|
|
|
+ while (has_expired_events(now)) {
|
|
|
+ auto& event = get_next_expired_event(now);
|
|
|
+ fire_event(event);
|
|
|
+ pop_event(event);
|
|
|
+ }
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+If the *await mode* is used, then user code should implement the whole `while` loop
|
|
|
+above and the event awaiting (22) code. This makes it possible to enter into
|
|
|
+power-save mode and wake up on external timer or interrupt. See `*-await.cpp`
|
|
|
+examples.
|
|
|
+
|
|
|
+In the line (21) the used supervisor is defined: the amount of handlers is specified
|
|
|
+(the same as for actor at (5), as each supervisor is an actor) and all its
|
|
|
+child actors (including other supervisors) are enumerated. The only `Blinker`
|
|
|
+child actor is specified in the current example.
|
|
|
+
|
|
|
+Let's move forward. In the following code `rotor-light` global variables are
|
|
|
+allocated:
|
|
|
+
|
|
|
+```
|
|
|
+Queue queue;
|
|
|
+Planner planner;
|
|
|
+rl::Context context{&queue, &planner, &get_now};
|
|
|
+Supervisor sup;
|
|
|
+```
|
|
|
+
|
|
|
+All of them can be allocated on the stack, but for the supervisor it is, probably,
|
|
|
+the bad idea, because most likely you'll need to access actors from some other
|
|
|
+global functions, i.e. ISR, and that can be done only via global supervisor
|
|
|
+instance. That might be not obvious, that the supervisor above allocates space for
|
|
|
+all its child actors, i.e. *supervisor contains and owns child actors*.
|
|
|
+
|
|
|
+The `get_now()` function pointer refers to a function with the following signature:
|
|
|
+
|
|
|
+```
|
|
|
+using TimePoint = int64_t;
|
|
|
+using NowFunction = TimePoint (*)();
|
|
|
+```
|
|
|
+
|
|
|
+It is the only link to the underlying hardware or system. The function should return
|
|
|
+current time; the meaning of "current time" (`TimePoint`) is completely user
|
|
|
+specific (i.e. it can point to seconds, milliseconds, microseconds etc.). The only
|
|
|
+requirement to the function, that it should be **monotonic**, i.e. do not decrease
|
|
|
+nor wrap each next `TimePoint` compared to the previous one. The implementation
|
|
|
+of the function is completely platform- or hardware-specific. For the arduino
|
|
|
+example it returns the number of microseconds passed since board boot.
|
|
|
|
|
|
-void initialize() override {
|
|
|
- subscribe(&Blinker::on_blink_command);
|
|
|
- Parent::initialize();
|
|
|
+The final piece of the current example is:
|
|
|
+
|
|
|
+```
|
|
|
+int main(int, char **) {
|
|
|
+ app_hw_init();
|
|
|
+
|
|
|
+ /* setup */
|
|
|
+ sup.bind(context); // (23)
|
|
|
+ auto blinker = sup.get_child<0>(); // (24)
|
|
|
+ blinker->delay = rl::Duration{250000}; // (25)
|
|
|
+ /* let it polls timer */
|
|
|
+ sup.start(true); // (26)
|
|
|
+
|
|
|
+ /* main cycle */
|
|
|
+ sup.process(); // (27)
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
-void advance_start() override {
|
|
|
- Parent::advance_start();
|
|
|
- add_event(
|
|
|
- delay, [](void *data) { static_cast<Blinker *>(data)->blink(); }, this);
|
|
|
+```
|
|
|
+
|
|
|
+The context binding (23) let the supervisor know pre-allocated queues, planner and
|
|
|
+the `get_now()` function. During context binding all actors get their unique ids,
|
|
|
+which can be used. After that individual actors can be accessed (24) with zero
|
|
|
+runtime overhead (as it uses `std::get` under the hood) and additional actors
|
|
|
+setup can be performed here (26), i.e. delay 1/4 of seconds between LED toggle.
|
|
|
+
|
|
|
+Please note, that setup phase (23-26) need to be performed **only once** despite
|
|
|
+of possible multiple actors restarts, i.e. actor identities are preserved during
|
|
|
+object lifetimes.
|
|
|
+
|
|
|
+Then, the whole machinery receive initial impulse (26). The `true` value here means
|
|
|
+usage of the "poll mode" (see above), i.e. endlessly send self a `refresh-timer`
|
|
|
+message, get time, and fire the timed-out events.
|
|
|
+
|
|
|
+In the line (27) supervisor actually starts processing messages, and probably never
|
|
|
+exits as soon as everything is going as expected. In the case of *failure
|
|
|
+escalation*, i.e. as it tried all possible restarts of actors to recover the
|
|
|
+failure, there is nothing left to do than exit and the whole board restart.
|
|
|
+
|
|
|
+## ping-pong messaging, poll timer
|
|
|
+
|
|
|
+In this example how to do messaging with `rotor-light` is demonstrated: `ping`
|
|
|
+message is sent from `pinger` actor to `ponger` actor. The `ponger` actor will
|
|
|
+reply back with `pong` message, then after some delay `pinger` actor repeats
|
|
|
+the same procedure. The full code can be found at
|
|
|
+`examples/atmega328p/ping-pong-poll.cpp`.
|
|
|
+
|
|
|
+First of all used messages should be defined:
|
|
|
+
|
|
|
+```
|
|
|
+namespace message {
|
|
|
+struct Ping : rl::Message {
|
|
|
+ using Message::Message;
|
|
|
+ static constexpr auto type_id = __LINE__;
|
|
|
+ rl::MessageTypeId get_type_id() const override { return type_id; }
|
|
|
+};
|
|
|
+
|
|
|
+struct Pong : rl::Message {
|
|
|
+ using Message::Message;
|
|
|
+ static constexpr auto type_id = __LINE__;
|
|
|
+ rl::MessageTypeId get_type_id() const override { return type_id; }
|
|
|
+};
|
|
|
+} // namespace message
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+The `ping` and `pong` messages are content-less, why there is need of them for all?
|
|
|
+Because there is need to demonstrate how to send and receive messages and
|
|
|
+distinguish them by type.
|
|
|
+
|
|
|
+The `Pinger` actor code is:
|
|
|
+
|
|
|
+```
|
|
|
+struct Pinger : rl::Actor<2> {
|
|
|
+ using Parent = Actor<2>;
|
|
|
+
|
|
|
+ void initialize() override {
|
|
|
+ subscribe(&Pinger::on_pong); // (28)
|
|
|
+ Parent::initialize();
|
|
|
+ }
|
|
|
+
|
|
|
+ void advance_start() override { // (29)
|
|
|
+ Parent::advance_start(); // (30)
|
|
|
+ ping(); // (31)
|
|
|
+ }
|
|
|
+
|
|
|
+ void ping() { // (32)
|
|
|
+ /* toggle led */
|
|
|
+ PORTB ^= (1 << LED);
|
|
|
+ send<message::Ping>(0, ponger_id); // (33)
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_pong(message::Pong &) { // (34)
|
|
|
+ add_event(500000, [](void *data) { // (35)
|
|
|
+ static_cast<Pinger *>(data)->ping();
|
|
|
+ }, this);
|
|
|
+ }
|
|
|
+
|
|
|
+ rl::ActorId ponger_id; // (36)
|
|
|
+};
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+As usually, the `initialize()` should be overridden to subscribe on `pong` messages
|
|
|
+(28). The `pinger` actor plays an *active role*, i.e. it sends initial `ping`
|
|
|
+message. This is performed in the overrisden `advance_start()` method (29), which is
|
|
|
+invoked as soon as the actor is ready: the default implementation machinery is
|
|
|
+invoked (30), and for convenience `ping()` (31) method is called. The `ping()` (32)
|
|
|
+method implementation is simple: after LED toggle, it sends the `ping` message (33).
|
|
|
+
|
|
|
+The `send` method parameters are: the message type (`message::Ping`) template
|
|
|
+parameter, message priority (aka destination queue) - `0`, and the destination
|
|
|
+actor(s) - ponger actor id, defined at (36). If there are additional message params,
|
|
|
+specified in the message type constructor, they should go here.
|
|
|
+
|
|
|
+As soon as `pong` reply is received (34), the ping procedure with LED toggle
|
|
|
+is rescheduled after 500000 microseconds (i.e. 0.5 second) at (35).
|
|
|
+
|
|
|
+The `ponger` actor code is rather trivial:
|
|
|
+
|
|
|
+```
|
|
|
+struct Ponger : rl::Actor<2> {
|
|
|
+ using Parent = Actor<2>;
|
|
|
+
|
|
|
+ void initialize() override {
|
|
|
+ subscribe(&Ponger::on_ping);
|
|
|
+ Parent::initialize();
|
|
|
+ }
|
|
|
+ void on_ping(message::Ping &) {
|
|
|
+ send<message::Pong>(0, pinger_id); // (37)
|
|
|
+ }
|
|
|
+ rl::ActorId pinger_id; // (38)
|
|
|
+};
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+as soon `ping` message is received, it replies back (37) with `pong` message. Please
|
|
|
+note, while conceptually it "replies back", technically it just sends a new `pong`
|
|
|
+message to pinger address, defined at (38). It is important, because there is no
|
|
|
+request-response pattern, i.e. it "knows" whom to send back the "reply".
|
|
|
+
|
|
|
+Lets move forward.
|
|
|
+
|
|
|
+```
|
|
|
+using Supervisor = rl::Supervisor<
|
|
|
+ rl::SupervisorBase::min_handlers_amount,
|
|
|
+ Pinger,
|
|
|
+ Ponger>;
|
|
|
+
|
|
|
+using Storage = rl::traits::MessageStorage<rl::message::ChangeState,
|
|
|
+ rl::message::ChangeStateAck,
|
|
|
+ message::Ping,
|
|
|
+ message::Pong>;
|
|
|
+```
|
|
|
+
|
|
|
+The standard supervisor is used; it owns `pinger` and `ponger` actors. The `Storage`
|
|
|
+allocates enough space for `Ping` and `Pong` messages.
|
|
|
+
|
|
|
+
|
|
|
+```
|
|
|
+int main(int, char **) {
|
|
|
+
|
|
|
+ app_hw_init();
|
|
|
+
|
|
|
+ /* setup */
|
|
|
+ sup.bind(context);
|
|
|
+ auto pinger = sup.get_child<0>(); // (39)
|
|
|
+ auto ponger = sup.get_child<1>(); // (40)
|
|
|
+ pinger->ponger_id = ponger->get_id(); // (41)
|
|
|
+ ponger->pinger_id = pinger->get_id(); // (42)
|
|
|
+ /* let it polls timer */
|
|
|
+ sup.start(true);
|
|
|
+
|
|
|
+ /* main cycle */
|
|
|
+ sup.process(); // (43)
|
|
|
+ return 0;
|
|
|
}
|
|
|
+```
|
|
|
+
|
|
|
+The most interesting part is the setup-phase (39-40). `pinger` and `ponger` actors
|
|
|
+should know each others addresses, and the addresses are available only after
|
|
|
+context binding.
|
|
|
+
|
|
|
+When everything is ready, it enters into main loop (43). In the loop it either
|
|
|
+delivers `rotor-light` messages or waits, until the next event time occurs. It uses
|
|
|
+**busy waiting** by actively polling (querying) timer, whether the next event time
|
|
|
+happend. As usually for busy waiting, it consumes 100% CPU time, which is common
|
|
|
+strategy for embedded/real-time applications.
|
|
|
+
|
|
|
+It is possible however to do something else, instead of endless timer polling, i.e.
|
|
|
+do CPU sleep (and consume less current and do less CO2 emission). See the
|
|
|
+next section.
|
|
|
+
|
|
|
+## ping-pong messaging, await the next event in power-save mode
|
|
|
+
|
|
|
+The code for this example is located at `examples/atmega328p/ping-pong-await.cpp`.
|
|
|
+From user point of view the example does the same as the previous one:
|
|
|
+it sends `ping` message, receives `pong` message, blinks with LED and after some
|
|
|
+delay the procedure repeats.
|
|
|
+
|
|
|
+However, the notable difference is in the delay: in the previous example it
|
|
|
+endlessly polls the timer whether the time arrives for the next event, burning
|
|
|
+CPU cycles, in the current example it does energy-efficient sleeping while
|
|
|
+there is noting to do (i.e. no messages) between events.
|
|
|
|
|
|
-void blink() {
|
|
|
- toggle_led();
|
|
|
- add_event(
|
|
|
- delay, [](void *data) { static_cast<Blinker *>(data)->blink(); }, this);
|
|
|
+As the main logic is the same, the actor-related code is also the same; only
|
|
|
+`main()` function differs.
|
|
|
+
|
|
|
+```
|
|
|
+int main(int, char **) {
|
|
|
+ app_hw_init();
|
|
|
+
|
|
|
+ /* allocate */
|
|
|
+ Queue queue;
|
|
|
+ Planner planner;
|
|
|
+ rl::Context context{&queue, &planner, &get_now};
|
|
|
+ Supervisor sup;
|
|
|
+
|
|
|
+ /* setup */
|
|
|
+ sup.bind(context);
|
|
|
+ auto pinger = sup.get_child<0>();
|
|
|
+ auto ponger = sup.get_child<1>();
|
|
|
+ pinger->ponger_id = ponger->get_id();
|
|
|
+ ponger->pinger_id = pinger->get_id();
|
|
|
+ /* let it polls timer */
|
|
|
+ sup.start(false); // (44)
|
|
|
+
|
|
|
+ /* main cycle */
|
|
|
+ while (true) { // (45)
|
|
|
+ sup.process(); // (46)
|
|
|
+ auto next_event_time = planner.next_event(); // (47)
|
|
|
+ if (next_event_time) { // (48)
|
|
|
+ perform_sleep(next_event_time); // (49)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
-void on_blink_command(message::BlinkCommand &msg) { blink(); }
|
|
|
+```
|
|
|
+
|
|
|
+Firstly, the timer poll via recursively sending self `refresh-timer` message should
|
|
|
+be disabled (44). Then the infinite loop (45) should be started, as the supervisor
|
|
|
+exits, when it has no more messages to process (46). The first nearby event should
|
|
|
+be extracted from planner (47), if any (48), and then perform platform-specific
|
|
|
+sleep until the event. The event handler will be actually processed upon the next
|
|
|
+loop iteration in (46).
|
|
|
+
|
|
|
+## integration
|
|
|
+
|
|
|
+Lets suppose, that there is a need to read a custom port on idle or perform some
|
|
|
+I/O activity, maybe indirectly via calling some other library on each iteration.
|
|
|
+How can that be done?
|
|
|
+
|
|
|
+One of the ways to accomplish that is shown above: disable timer poll and do
|
|
|
+the needed activity instead of entering into powersave mode, or in addition to
|
|
|
+the powersave mode.
|
|
|
+
|
|
|
+Then second way is to have a custom supervisor with `on_refhesh_timer` method
|
|
|
+overridden, like that:
|
|
|
+
|
|
|
+```
|
|
|
+struct MySupervisor : rl::Supervisor<3, MyActor1, MyActor2, ...> {
|
|
|
+using Parent = rl::Supervisor<3, MyActor1, MyActor2, ...>;
|
|
|
+
|
|
|
+void on_refhesh_timer(rl::message::RefreshTime &message) override {
|
|
|
+ Parent::on_refhesh_timer(message);
|
|
|
+ third_party_library_poll();
|
|
|
+}
|
|
|
|
|
|
-rl::Duration delay;
|
|
|
};
|
|
|
+
|
|
|
```
|