#serde #framing #cobs

no-std postcard-rpc

A no_std + serde compatible RPC library for Rust

8 unstable releases (3 breaking)

new 0.4.2 Apr 23, 2024
0.4.1 Apr 23, 2024
0.4.0 Feb 23, 2024
0.3.1 Nov 26, 2023
0.1.2 Nov 15, 2023

#484 in Embedded development

Download history 71/week @ 2024-02-17 146/week @ 2024-02-24 7/week @ 2024-03-02 18/week @ 2024-03-09 15/week @ 2024-03-16 2/week @ 2024-03-23 9/week @ 2024-03-30 8/week @ 2024-04-06 11/week @ 2024-04-13 190/week @ 2024-04-20

218 downloads per month
Used in erdnuss-comms

MIT/Apache

70KB
1K SLoC

Postcard RPC

A host (PC) and client (MCU) library for handling RPC-style request-response types.

See overview.md for documentation.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.


lib.rs:

The goal of postcard-rpc is to make it easier for a host PC to talk to a constrained device, like a microcontroller.

See the repo for examples, and the overview for more details on how to use this crate.

Defining a schema

Typically, you will define your "wire types" in a shared schema crate. This crate essentially defines the protocol used between two or more devices.

A schema consists of a couple of necessary items:

Wire types

We will need to define all of the types that we will use within our protocol. We specify normal Rust types, which will need to implement or derive three important traits:

  • serde's Serialize trait - which defines how we can convert a type into bytes on the wire
  • serde's Deserialize trait - which defines how we can convert bytes on the wire into a type
  • postcard's Schema trait - which generates a reflection-style schema value for a given type.

Here's an example of three types we'll use in future examples:

// Consider making your shared "wire types" crate conditionally no-std,
// if you want to use it with no-std embedded targets! This makes it no_std
// except for testing and when the "use-std" feature is active.
//
// You may need to also ensure that `std`/`use-std` features are not active
// in any dependencies as well.
#![cfg_attr(not(any(test, feature = "use-std")), no_std)]

use serde::{Serialize, Deserialize};
use postcard::experimental::schema::Schema;

#[derive(Serialize, Deserialize, Schema)]
pub struct Alpha {
    pub one: u8,
    pub two: i64,
}

#[derive(Serialize, Deserialize, Schema)]
pub enum Beta {
    Bib,
    Bim(i16),
    Bap,
}

#[derive(Serialize, Deserialize, Schema)]
pub struct Delta(pub [u8; 32]);

#[derive(Serialize, Deserialize, Schema)]
pub enum WireError {
    ALittleBad,
    VeryBad,
}

Endpoints

Now that we have some basic types that will be used on the wire, we need to start building our protocol. The first thing we can build are [Endpoint]s, which represent a bidirectional "Request"/"Response" relationship. One of our devices will act as a Client (who makes a request, and receives a response), and the other device will act as a Server (who receives a request, and sends a response). Every request should be followed (eventually) by exactly one response.

An endpoint consists of:

  • The type of the Request
  • The type of the Response
  • A string "path", like an HTTP URI that uniquely identifies the endpoint.

The easiest way to define an Endpoint is to use the [endpoint!][endpoint] macro.

#
#
#
use postcard_rpc::endpoint;

// Define an endpoint
endpoint!(
    // This is the name of a marker type that represents our Endpoint,
    // and implements the `Endpoint` trait.
    FirstEndpoint,
    // This is the request type for this endpoint
    Alpha,
    // This is the response type for this endpoint
    Beta,
    // This is the path/URI of the endpoint
    "endpoints/first",
);

Topics

Sometimes, you would just like to send data in a single direction, with no response. This could be for reasons like asynchronous logging, blindly sending sensor data periodically, or any other reason you can think of.

Topics have no "client" or "server" role, either device may decide to send a message on a given topic.

A topic consists of:

  • The type of the Message
  • A string "path", like an HTTP URI that uniquely identifies the topic.

The easiest way to define a Topic is to use the [topic!][topic] macro.

#
#
use postcard_rpc::topic;

// Define a topic
topic!(
    // This is the name of a marker type that represents our Topic,
    // and implements `Topic` trait.
    FirstTopic,
    // This is the message type for the endpoint (note there is no
    // response type!)
    Delta,
    // This is the path/URI of the topic
    "topics/first",
);

Using a schema

At the moment, this library is primarily oriented around:

  • A single Client, usually a PC, with access to std
  • A single Server, usually an MCU, without access to std

For Client facilities, check out the host_client module, particularly the HostClient struct. This is only available with the use-std feature active.

A serial-port transport using cobs encoding is available with the cobs-serial feature. This feature will add the new_serial_cobs constructor to HostClient.

For Server facilities, check out the Dispatch struct. This is available with or without the standard library.

Dependencies

~2–34MB
~509K SLoC