|
@@ -4,57 +4,53 @@
|
|
|
|
|
|
This is platform-neural tutorial, although hardware implementation refers to
|
|
|
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`.
|
|
|
+this example the blinking with LED is performed using
|
|
|
+[rotor-light](https://notabug.org/basiliscos/cpp-rotor-light).
|
|
|
|
|
|
Let's start from the inclusion of headers:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
#include <rotor-light.hpp>
|
|
|
-```
|
|
|
+~~~
|
|
|
|
|
|
All further interactions with the framework is done via `rl` namespace
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
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 deal with messages later (handle and send).
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
namespace message {
|
|
|
|
|
|
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` (1),
|
|
|
+The `BlinkCommand` message have to be derived from `rotor_light` `Message` (1),
|
|
|
and as the `BlinkCommand` has no payload, it just reuses its parent
|
|
|
constructor (`using rl::Message::Message`, (3)).
|
|
|
|
|
|
-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 (2)
|
|
|
-in every message type definition, and group all used messages in a single
|
|
|
-header file.
|
|
|
+As the `rotor_light` does not use RTTI it should somehow identify
|
|
|
+message types at runtime, the `type_id` (2) field helps with that.
|
|
|
+`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 (2) in every message type definition, and
|
|
|
+group all used messages in a single header file.
|
|
|
|
|
|
Let's move to the blinker actor code; as it is rather small, it is shown
|
|
|
entirely:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
struct Blinker : rl::Actor<2> { // (5)
|
|
|
-using Parent = Actor<2>; // (6)
|
|
|
+using Parent = Actor<2>; // (6)
|
|
|
|
|
|
-void initialize() override { // (7)
|
|
|
+void initialize() override { // (7)
|
|
|
subscribe(&Blinker::on_blink_command); // (8)
|
|
|
Parent::initialize(); // (9)
|
|
|
}
|
|
@@ -66,10 +62,10 @@ void initialize() override { // (7)
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
+ toggle_led(); // (13)
|
|
|
+ add_event<rl::ctx::thread>(delay, [](void *data) { // (14)
|
|
|
+ auto self = static_cast<Blinker *>(data); // (15)
|
|
|
+ self->send<rl::ctx::thread, message::BlinkCommand>(0, self->id); // (16)
|
|
|
}, this);
|
|
|
}
|
|
|
|
|
@@ -79,7 +75,7 @@ void on_blink_command(message::BlinkCommand &msg) { // (17)
|
|
|
|
|
|
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
|
|
@@ -103,12 +99,18 @@ 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).
|
|
|
|
|
|
+The `rl::ctx::thread` in (13) and (16) gives hints, wether interrupts should
|
|
|
+(`ctx::thread`) or not (`ctx::interrupt`) be masked during methods invokations.
|
|
|
+The `ctx::thread` masks and then unmasks all interrupts, so, generally it
|
|
|
+should not be called from interrupt context. The last one (`ctx::interrupt`)
|
|
|
+does not touches CPU interrupt enablence flag.
|
|
|
+
|
|
|
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:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
using Storage = rl::traits::MessageStorage<rl::message::ChangeState, // (18)
|
|
|
rl::message::ChangeStateAck,
|
|
|
message::BlinkCommand>;
|
|
@@ -117,44 +119,48 @@ 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.
|
|
|
+[rotor-light](https://notabug.org/basiliscos/cpp-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
|
|
|
-
|
|
|
-```
|
|
|
+**zero-priority** for 5 messages. All [rotor-light](https://notabug.org/basiliscos/cpp-rotor-light)
|
|
|
+messages are put into that queue by default, 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
|
|
|
+
|
|
|
+~~~{.cpp}
|
|
|
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).
|
|
|
+for [rotor-light](https://notabug.org/basiliscos/cpp-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.
|
|
|
+message is [rotor-light](https://notabug.org/basiliscos/cpp-rotor-light) message,
|
|
|
+then *the framework behavior is undefined* (more strictly speaking, overridable
|
|
|
+`void rotor_light::on_queue_full()` method is invoked). 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](https://notabug.org/basiliscos/cpp-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.
|
|
|
+(13). The number in angle braces (20) defines planner capacity at compile-time. When
|
|
|
+event time arrives, it is removed from the planner. Please note, that
|
|
|
+[rotor-light](https://notabug.org/basiliscos/cpp-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
|
|
@@ -162,7 +168,7 @@ requested, and all timed out event handlers are executed. Then the special messa
|
|
|
`refresh-time` is send to the supervisor and the procedure repeats endlessly.
|
|
|
Indirectly, this is similar to the code like:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
while (true) {
|
|
|
while (has_messages()) {
|
|
|
auto& message = get_front_message();
|
|
@@ -177,7 +183,7 @@ while (true) {
|
|
|
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
|
|
@@ -189,15 +195,15 @@ In the line (21) the used supervisor is defined: the amount of handlers is speci
|
|
|
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:
|
|
|
+Let's move forward. In the following code [rotor-light](https://notabug.org/basiliscos/cpp-rotor-light)
|
|
|
+global variables are allocated:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
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
|
|
@@ -207,12 +213,12 @@ 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:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
using TimePoint = int64_t;
|
|
|
using NowFunction = TimePoint (*)();
|
|
|
-```
|
|
|
+~~~
|
|
|
|
|
|
-It is the only link to the underlying hardware or system. The function should return
|
|
|
+It is a 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
|
|
@@ -222,7 +228,7 @@ example it returns the number of microseconds passed since board boot.
|
|
|
|
|
|
The final piece of the current example is:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
int main(int, char **) {
|
|
|
app_hw_init();
|
|
|
|
|
@@ -237,8 +243,7 @@ int main(int, char **) {
|
|
|
sup.process(); // (27)
|
|
|
return 0;
|
|
|
}
|
|
|
-
|
|
|
-```
|
|
|
+~~~
|
|
|
|
|
|
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,
|
|
@@ -261,38 +266,35 @@ 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
|
|
|
+In this example how to do messaging with [rotor-light](https://notabug.org/basiliscos/cpp-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:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
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.
|
|
|
+distinguish them **by type**.
|
|
|
|
|
|
The `Pinger` actor code is:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
struct Pinger : rl::Actor<2> {
|
|
|
using Parent = Actor<2>;
|
|
|
|
|
@@ -306,26 +308,24 @@ struct Pinger : rl::Actor<2> {
|
|
|
ping(); // (31)
|
|
|
}
|
|
|
|
|
|
- void ping() { // (32)
|
|
|
- /* toggle led */
|
|
|
- PORTB ^= (1 << LED);
|
|
|
- send<message::Ping>(0, ponger_id); // (33)
|
|
|
+ void ping() { // (32)
|
|
|
+ Board::toggle_led();
|
|
|
+ send<rl::ctx::thread, message::Ping>(0, ponger_id); // (33)
|
|
|
}
|
|
|
|
|
|
- void on_pong(message::Pong &) { // (34)
|
|
|
- add_event(500000, [](void *data) { // (35)
|
|
|
+ void on_pong(message::Pong &) { // (34)
|
|
|
+ add_event<rl::ctx::thread>(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
|
|
|
+message. This is performed in the overridden `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).
|
|
@@ -340,7 +340,7 @@ is rescheduled after 500000 microseconds (i.e. 0.5 second) at (35).
|
|
|
|
|
|
The `ponger` actor code is rather trivial:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
struct Ponger : rl::Actor<2> {
|
|
|
using Parent = Actor<2>;
|
|
|
|
|
@@ -349,12 +349,11 @@ struct Ponger : rl::Actor<2> {
|
|
|
Parent::initialize();
|
|
|
}
|
|
|
void on_ping(message::Ping &) {
|
|
|
- send<message::Pong>(0, pinger_id); // (37)
|
|
|
+ send<rl::ctx::thread, 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`
|
|
@@ -363,7 +362,7 @@ request-response pattern, i.e. it "knows" whom to send back the "reply".
|
|
|
|
|
|
Lets move forward.
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
using Supervisor = rl::Supervisor<
|
|
|
rl::SupervisorBase::min_handlers_amount,
|
|
|
Pinger,
|
|
@@ -373,13 +372,13 @@ 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.
|
|
|
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
int main(int, char **) {
|
|
|
|
|
|
app_hw_init();
|
|
@@ -397,19 +396,20 @@ int main(int, char **) {
|
|
|
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.
|
|
|
+delivers [rotor-light](https://notabug.org/basiliscos/cpp-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.
|
|
|
+It is possible however to do something else, instead of endless timer polling, e.g.
|
|
|
do CPU sleep (and consume less current and do less CO2 emission). See the
|
|
|
next section.
|
|
|
|
|
@@ -423,12 +423,12 @@ 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.
|
|
|
+there is nothing to do (i.e. no messages) between events.
|
|
|
|
|
|
As the main logic is the same, the actor-related code is also the same; only
|
|
|
`main()` function differs.
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
int main(int, char **) {
|
|
|
app_hw_init();
|
|
|
|
|
@@ -452,13 +452,12 @@ int main(int, char **) {
|
|
|
sup.process(); // (46)
|
|
|
auto next_event_time = planner.next_event(); // (47)
|
|
|
if (next_event_time) { // (48)
|
|
|
- perform_sleep(next_event_time); // (49)
|
|
|
+ Board::sleep(next_event_time); // (49)
|
|
|
}
|
|
|
}
|
|
|
return 0;
|
|
|
}
|
|
|
-
|
|
|
-```
|
|
|
+~~~
|
|
|
|
|
|
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
|
|
@@ -480,7 +479,7 @@ the powersave mode.
|
|
|
Then second way is to have a custom supervisor with `on_refhesh_timer` method
|
|
|
overridden, like that:
|
|
|
|
|
|
-```
|
|
|
+~~~{.cpp}
|
|
|
struct MySupervisor : rl::Supervisor<3, MyActor1, MyActor2, ...> {
|
|
|
using Parent = rl::Supervisor<3, MyActor1, MyActor2, ...>;
|
|
|
|
|
@@ -490,5 +489,4 @@ void on_refhesh_timer(rl::message::RefreshTime &message) override {
|
|
|
}
|
|
|
|
|
|
};
|
|
|
-
|
|
|
-```
|
|
|
+~~~
|