https://github.com/petekubiak/post-haste
A no-std async Rust library for creating modular projects
https://github.com/petekubiak/post-haste
async bare-metal messaging modular-architecture no-std rust
Last synced: 5 months ago
JSON representation
A no-std async Rust library for creating modular projects
- Host: GitHub
- URL: https://github.com/petekubiak/post-haste
- Owner: petekubiak
- License: gpl-3.0
- Created: 2025-04-01T17:17:04.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-11-26T11:43:46.000Z (7 months ago)
- Last Synced: 2025-11-29T10:06:40.122Z (7 months ago)
- Topics: async, bare-metal, messaging, modular-architecture, no-std, rust
- Language: Rust
- Homepage:
- Size: 150 KB
- Stars: 7
- Watchers: 1
- Forks: 3
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- rust-embedded - post-haste - free async Rust library for creating modular projects. (no-std crates / WIP)
- awesome-embedded-rust - post-haste - free async Rust library for creating modular projects. (no-std crates / WIP)
README
# Post-haste
A no_std, alloc-free async Rust library for creating modular projects.
## Summary
The goal of this library is to provide a framework for a highly modularised code base.
There are two core components around which this framework is based: the Agent and the Postmaster.
## Agents
Functionality of the application is divided up between a number of modules, dubbed "Agents".
Each Agent is expected to have a single responsibility.
Post-haste provides the Agent trait, which defines the interface for Agents and gives them the ability to be integrated into the rest of the system.
The defining features of an Agent are an instantiation function: `create()`, a task loop: `run()`, and an inbox.
The `create()` function is called automatically when the Agent is "registered" (see [Registering Agents](#registering-agents) below).
The body of this function should instantiate the Agent and populate it with any configuration it requires.
The `Config` associated type is used to assist in this.
The `run()` function is the Agent's main loop.
This is spawned as a standalone task in the executor, and is not expected to return (i.e. the lifetime of Agents is expected to be the same as the lifetime of the application).
When an Agent is registered, it is assigned a mailbox.
The receiving end of the mailbox (the inbox) is passed in as an argument to the `run()` function.
In the vast majority of cases, the core logic of the Agent's loop will be to await messages arriving in its inbox and perform actions based on what is received.
## The Postmaster
The postmaster provides the mechanism by which Agents are able to communicate, and by which data moves around the system.
### Initialisation
In order to allow the Postmaster to be `no_std` and `alloc`-free, its logic requires knowledge about the project to function.
Specifically, it needs to know the number of Agents which will be running and the payload structures which the messages will contain.
To achieve this, the Postmaster logic must be written at compile-time by the `init_postmaster!()` macro.
The two arguments to the macro are of course the `Address` type and the `Payload` type, both defined by your project.
The `init_postmaster!()` macro takes an optional third argument, the default timeout that the Postmaster should use when sending messages in microseconds.
If this optional argument is left out, the Postmaster will use a timeout of 1 ms (1000 us).
For more information on message sending timeout, see [Communicating with Agents](#communicating-with-agents) below.
The output of the macro is a `postmater` module, containing the Postmaster's public interface.
### Registering Agents
Once you have defined an Agent type as described above, it is instantiated using the `postmaster::register_agent!()` macro.
This macro takes the following arguments:
- A handle to the executor's Spawner (only in Embassy)
- The Address to which the instance will be registered
- The type of Agent being instantiated
- Config for the Agent in the form of an instance of its associated `Config` type
- (Optional) The size of the Agent's message queue
Within this macro, the Agent's message queue is created, the Agent instance is created and a task is spawned for its main loop.
The Agent can be considered active and ready to receive messages immediately following its registration.
### Communicating with Agents
The standard way to communicate with an Agent is by sending it messages using the Postmaster.
The `postmaster` module generated by `init_postmaster!()` provides a set of functions for this purpose.
`postmaster::message()` takes source and destination addresses, plus a message payload, and returns a `MessageBuilder`.
The `MessageBuilder` allows further configuration of how the message is sent (explained further below).
Once configured, the message is sent by calling the `MessageBuilder`'s `send()` function.
When sending a message, it may be configured with a "timeout" and a "delay".
Upon attempting to send a message it may not be possible to immediately push the message onto the recipient's queue, for example if said queue is already full.
This is the purpose of the timeout: the `send()` function returns a future which will resolve either when the message has been successfully posted, or when the timeout expires.
By default, the timeout is 1 ms.
Sending a message with a "delay" means that the `send()` function will immediately return, but the message will only be added to the recipient's queue after the delay is complete.
The `postmaster` module also contains a couple of shortcut functions for sending messages:
- `postmaster::send()` which will attempt to send the message immediately with the default timeout of 1 ms.
- `postmaster::try_send()` which will attempt to send the message immediately, but will not wait: it will return immediately.
In all cases, what the recipient receives when it accesses its inbox is a `postmaster::Message` struct, which contains the source address and the message payload.
Please note: the `Message` and `Address` associated types in the `Agent` trait correspond to the auto-generated `Message` type and the user-provided `Address` list respectively.
### Other features
A high level overview of the Postmaster's diagnostics can be obtained using the `postmaster::get_diagnostics()` function.
Currently this just contains a tally of the number of messages successfully sent, and the number of send failures since boot.
It is also possible to register a standalone mailbox on the system, without associating it with an Agent, using `postmaster::register()`.
This might for example be used to communicate back to the main task of the project, or to provide a "debug" address for debug messages to be sent.
The default timeout used by the Postmaster when a message is sent with no specific timeout configuration can be changed using `postmaster::set_timeout()`, taking a value in microseconds.
### Advanced configuration
#### Delayed message pool (Embassy only)
When using post-haste on bare metal targets with Embassy, delayed messages are held in a finite pool while they await the expiry of their delay duration.
By default, the size of this pool is 8.
If at any point the pool is full, any attempt to send a delayed message will result in a `DelayedMessagePoolFull` error, and the message will not be sent.
The size of the pool can be modified by setting the `DELAYED_MESSAGE_POOL_SIZE` environment variable.
Please note however that increasing the pool size will increase static memory usage.
## Example usage
The following forms the core of the code layout for a baremetal project built upon post_haste (excluding any architecture-specific code and dependencies):
```rust
#![no_std]
// NOTE: This feature is currently required in order to generate the correct number of mailboxes based on the number of provided addresses (avoiding alloc)
// Therefore, the project must be compiled with the nightly compiler
#![feature(variant_count)]
use embassy_executor::Spawner;
use post_haste::agent::Agent;
use post_haste::init_postmaster;
/// The list of Agent addresses, used to identify the source and destination for messages.
/// Each Agent must have a unique address
enum Address {
AgentA,
AgentB,
// ...
}
/// Top-level definition of messages used in the system.
enum Payloads {
General(GeneralPayloads),
// ...
}
/// A sub-category of messages (this heirachical ordering isn't necessary, but is highly recommended for organisation)
enum GeneralPayloads {
Hello,
// ...
}
// Generates the postmaster logic and initialises the postmaster for use within the project
init_postmaster!(Address, Payloads);
struct MyAgent {
address: Address
}
impl Agent for MyAgent {
type Address = Address;
type Message = postmaster::Message;
type Config = ();
async fn create(address: Self::Address, _: Self::Config) -> Self {
// Initialisation code goes here...
Self { address }
}
async fn run(self, inbox: post_haste::agent::Inbox) -> ! {
loop {
let received_message = inbox.recv().await.unwrap();
match received_message {
Payloads::Hello => postmaster::send(received_message.source, self.address, Payloads::Hello).await.unwrap();
// ...
}
}
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
const QUEUE_SIZE: usize = 8;
postmaster::register_agent!(spawner, AgentA, MyAgent, ());
postmaster::register_agent!(spawner, AgentB, MyAgent, (), QUEUE_SIZE);
loop {
// ...
}
}
```
While the framework was originally developed for no_std baremetal environments, it is also fully compatible with tokio.
- [tokio_basic.rs](examples/tokio_basic.rs) gives a very simple example of two Agents exchanging messages.
- [showcase.rs](examples/showcase.rs) follows the same concept, but aims to demonstrate some useful patterns within the framework.