Actors

Drogue Actor is an actor framework for microcontrollers. An actor system is a framework that allows for isolating state within narrow contexts, making it easier to reason about system. Within a actor system, the primary component is an Actor, which represents the boundary of state usage. Each actor has exclusive access to its own state and only communicates with other actors through message-passing.

Actor model example

Drogue Actor Model

Actor Model

Async

Each actor is single-threaded, able to process a single message at a time, allowing for lock-free processing of each event. As embedded processors are globally single-threaded, supporting multiple actors requires the usage of async and .await within the Rust ecosystem. Each actor can therefore process each message either synchronously if its logic is non-blocking or using an async block if complex processing is required.

Each event is fully processed, in the order in which it is received, before the next event is considered.

While processing an event, an actor may send a message to another actor, which itself is an asynchronous action, allowing the system to continue to make progress with actors that are able to.

Messages

All messages are sent using async channels attached to each actor. The channel depth is configurable based on const generics. Upon starting, the actor is given a handle to an Inbox from which it can await messages to arrive.

Addresses

Each actor within the system has its own unique Address which is used to communicate with the actor (through it’s FIFO). There is an async request(msg) method on each address to send a message asynchronously to the actor, which may only be used from another async context, as the sender must .await the response. There is also a sync notify(msg) method on each address to send a message without awaiting a response.

Specifically, the Address for a given actor may implement user provided traits to provide fluent APIs for communicating with the underlying actor. For instance, the Socket type wraps an Address for actors that implement the TcpSocket trait.

Lifecycle

Each actor is wrapped in an ActorContext object which is stored as a static (global). When the application starts, each context is `mount(…​)`ed, and provided an instance of the actor to "run".

During mount, the actor system will initialize the channels, and spawn the underlying embassy task. Once spawned, the actors Address<…​> is made available.

Bootstrap

As with any embassy application, the entry point is specified using an async function marked with #[embassy::main], and it will be passed a Spawner that is passed to each actor context mount(…​).

Packages

In some cases, it may be desirable to have two or more actors involve in a single semantic component or package. The Package trait may be implemented for any type.

Writing an Actor

An actor must implement the Actor trait, which will allow it to be spawned by the Embassy executor. An actor providing an atomic counter using the above driver is shown below:

#![feature(type_alias_impl_trait)]
#![feature(generic_associated_types)]

use drogue_device::*;

pub struct AtomicCounter {
    counter: MyCounter
}

pub enum CounterMessage {
    Increment,
    Add(u32),
}

#[actor]
impl Actor for AtomicCounter {
    type Message<'m> = CounterMessage;

    async fn on_mount<M>(&mut self, _: Address<CounterMessage>, mut inbox: M)
    where
        M: Inbox<CounterMessage>
    {
        loop {
            if let Some(mut m) = inbox.next().await {
                match *m.message() {
                    CounterMessage::Increment => self.counter.increment().await,
                    CounterMessage::Add(value) => self.counter.add(*value).await,
                }
            }
        }
    }
}

Notice the actor attribute, which handles a lot of the boilerplate of writing actors. This actor expands the code to the following:

impl Actor for AtomicCounter {
    type Message<'m> = CounterMessage;

    type OnMountFuture<'m> = impl Future<Output = ()> + 'm;
    fn on_mount<'m, M>(
        &'m mut self,
        _: Address<CounterMessage>,
        inbox: &'m mut M,
    ) -> Self::OnMountFuture<'m, M>
    where
        M: Inbox<Self> + 'm
    {
        async move {
            loop {
                if let Some(mut m) = inbox.next().await {
                    match *m.message() {
                        CounterMessage::Increment => self.counter.increment().await,
                        CounterMessage::Add(value) => self.counter.add(*value).await,
                    }
                }
            }
        }
    }
}

Once Rust has native support for async traits, the attribute will no longer be necessary.

Although a very complex way to implement an atomic counter, it gives you an idea of how actors provide exclusive access to a resource.