C++ with Nanopb#
pw_rpc can generate services which encode/decode RPC requests and responses
as nanopb message structs.
Usage#
To enable nanopb code generation, the build argument
dir_pw_third_party_nanopb must be set to point to a local nanopb
installation. Nanopb 0.4 is recommended, but Nanopb 0.3 is also supported.
Define a pw_proto_library containing the .proto file defining your service
(and optionally other related protos), then depend on the nanopb_rpc
version of that library in the code implementing the service.
# chat/BUILD.gn
import("$dir_pw_build/target_types.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
pw_proto_library("chat_protos") {
sources = [ "chat_protos/chat_service.proto" ]
}
# Library that implements the Chat service.
pw_source_set("chat_service") {
sources = [
"chat_service.cc",
"chat_service.h",
]
public_deps = [ ":chat_protos.nanopb_rpc" ]
}
A C++ header file is generated for each input .proto file, with the .proto
extension replaced by .rpc.pb.h. For example, given the input file
chat_protos/chat_service.proto, the generated header file will be placed
at the include path "chat_protos/chat_service.rpc.pb.h".
Generated code API#
All examples in this document use the following RPC service definition.
syntax = "proto3";
package chat;
service Chat {
// Returns information about a chatroom.
rpc GetRoomInformation(RoomInfoRequest) returns (RoomInfoResponse);
// Lists all of the users in a chatroom.
rpc ListUsersInRoom(ListUsersRequest) returns (stream ListUsersResponse);
// Uploads a file, in chunks, to a chatroom.
rpc UploadFile(stream UploadFileRequest) returns (UploadFileResponse);
// Sends messages to a chatroom while receiving messages from other users.
rpc SendMessage(stream ChatMessage) returns (stream ChatMessage);
}
message RoomInfoRequest {
string room = 1;
}
message RoomInfoResponse {
string room = 1;
uint32 users = 2;
}
message ListUsersRequest {
string room = 1;
}
message ListUsersResponse {
string user = 1;
}
message UploadFileRequest {
bytes chunk = 1;
}
message UploadFileResponse {
uint32 bytes_received = 1;
}
message ChatMessage {
string msg = 1;
uint32 timestamp = 2;
}
Server-side#
A C++ class is generated for each service in the .proto file. The class is
located within a special pw_rpc::nanopb sub-namespace of the file’s package.
The generated class is a base class which must be derived to implement the service’s methods. The base class is templated on the derived class.
class ChatService final
: public ::chat::pw_rpc::nanopb::Chat::Service<ChatService> {
public:
// 1. Unary RPC
pw::Status GetRoomInformation(const chat_RoomInfoRequest& request,
chat_RoomInfoResponse& response) {
PW_LOG_INFO("Room requested: %s", request.room);
std::strncpy(response.room, request.room, sizeof(response.room));
response.users = 42;
return pw::OkStatus();
}
// 2. Server Streaming RPC
void ListUsersInRoom(
const chat_ListUsersRequest& request,
pw::rpc::NanopbServerWriter<chat_ListUsersResponse>& writer) {
PW_LOG_INFO("Listing users in room: %s", request.room);
chat_ListUsersResponse user1{.user = "Alice"};
static_cast<void>(writer.Write(user1));
chat_ListUsersResponse user2{.user = "Bob"};
static_cast<void>(writer.Write(user2));
static_cast<void>(writer.Finish(pw::OkStatus()));
}
// 3. Client Streaming RPC
void UploadFile(
pw::rpc::NanopbServerReader<chat_UploadFileRequest,
chat_UploadFileResponse>& reader) {
upload_reader_ = std::move(reader);
upload_reader_.set_on_next([this](const chat_UploadFileRequest& request) {
total_bytes_ += request.chunk.size;
if (request.chunk.size == 0) {
// Upload finished: complete the call with total bytes transferred
chat_UploadFileResponse response{
.bytes_received = static_cast<uint32_t>(total_bytes_)};
static_cast<void>(upload_reader_.Finish(response, pw::OkStatus()));
}
});
}
// 4. Bidirectional Streaming RPC
void SendMessage(
pw::rpc::NanopbServerReaderWriter<chat_ChatMessage, chat_ChatMessage>&
stream) {
chat_stream_ = std::move(stream);
chat_stream_.set_on_next([this](const chat_ChatMessage& message) {
chat_ChatMessage reply{.msg = "Echo", .timestamp = message.timestamp};
static_cast<void>(chat_stream_.Write(reply));
});
}
private:
pw::rpc::NanopbServerReader<chat_UploadFileRequest, chat_UploadFileResponse>
upload_reader_;
pw::rpc::NanopbServerReaderWriter<chat_ChatMessage, chat_ChatMessage>
chat_stream_;
size_t total_bytes_ = 0;
};
The writer and reader helper APIs provide methods to stream and finish calls:
-
Status NanopbServerWriter::Write(const Response &response)#
Writes a single response message to the stream. The returned status indicates whether the write was successful.
-
Status NanopbServerWriter::Finish(Status status = OkStatus())#
Closes the stream and sends back the RPC’s overall status to the client.
-
Status NanopbServerWriter::TryFinish(Status status = OkStatus())#
Closes the stream and sends back the RPC’s overall status to the client only if the final packet is successfully sent.
Attention
Make sure to use std::move when passing the NanopbServerWriter around to
avoid accidentally closing it and ending the RPC.
-
Status NanopbServerReader::Finish(const Response &response, Status status = OkStatus())#
Sends the final unary response message and status to the client, closing the stream.
-
void NanopbServerReader::set_on_next(Function<void(const Request&)> &&on_next)#
Sets the callback invoked when a new request message arrives from the client.
-
Status NanopbServerReaderWriter::Write(const Response &response)#
Writes a single response message to the stream.
-
Status NanopbServerReaderWriter::Finish(Status status = OkStatus())#
Closes the stream and sends back the RPC’s overall status to the client.
-
void NanopbServerReaderWriter::set_on_next(Function<void(const Request&)> &&on_next)#
Sets the callback invoked when an incoming request message arrives from the client.
Client-side#
A corresponding client class is generated for every service defined in the proto
file. To allow multiple types of clients to exist, it is placed under the
pw_rpc::nanopb namespace. The Client class is nested under
pw_rpc::nanopb::ServiceName. For example, the Chat service would create
chat::pw_rpc::nanopb::Chat::Client.
Service clients are instantiated with a reference to the RPC client through which they will send requests, and the channel ID they will use.
Callback invocation
RPC callbacks are invoked synchronously from Client::ProcessPacket.
Unary RPC#
A unary RPC call takes the request struct and a callback to invoke when a response is received. The callback receives the RPC’s status and response struct.
Server streaming RPC#
A server streaming RPC call takes the initial request struct and two callbacks. The first is invoked on every stream response received, and the second is invoked once the stream is complete with its overall status.
Client streaming RPC#
A client streaming RPC call returns a NanopbClientWriter object used to send
a stream of requests, and takes a callback invoked when the server’s final
response arrives:
[[maybe_unused]] void StartUpload() {
ChatClient chat_client(client, 1);
auto writer = chat_client.UploadFile(
[](const chat_UploadFileResponse& response, pw::Status status) {
if (status.ok()) {
PW_LOG_INFO("Uploaded %u bytes",
static_cast<unsigned>(response.bytes_received));
}
});
chat_UploadFileRequest chunk{.chunk = {.size = 0, .bytes = {0}}};
static_cast<void>(writer.Write(chunk));
static_cast<void>(writer.RequestCompletion());
}
Bidirectional streaming RPC#
A bidirectional streaming RPC call returns a NanopbClientReaderWriter object
used to send requests, and takes callbacks for incoming stream responses and stream
completion:
[[maybe_unused]] void StartChat() {
ChatClient chat_client(client, 1);
auto stream = chat_client.SendMessage(
[](const chat_ChatMessage& msg) {
PW_LOG_INFO("Message from room: %s", msg.msg);
},
[](pw::Status status) { PW_LOG_INFO("Chat closed: %s", status.str()); });
chat_ChatMessage msg{.msg = "Hello!", .timestamp = 0};
static_cast<void>(stream.Write(msg));
static_cast<void>(stream.RequestCompletion());
}
Example usage#
The following example demonstrates how to call an RPC method using a nanopb service client and receive the response.
using ChatClient = ::chat::pw_rpc::nanopb::Chat::Client;
void LogRoomInformation(const chat_RoomInfoResponse& response,
pw::Status status) {
if (status.ok()) {
PW_LOG_INFO("Room %s has %u users",
response.room,
static_cast<unsigned>(response.users));
}
}
[[maybe_unused]] void InvokeSomeRpcs() {
ChatClient chat_client(client, 1);
// Unary call
auto call =
chat_client.GetRoomInformation({.room = "pigweed"}, LogRoomInformation);
if (!call.active()) {
return;
}
}
Zephyr#
To enable pw_rpc.nanopb.* for Zephyr add CONFIG_PIGWEED_RPC_NANOPB=y to
the project’s configuration. This will enable the Kconfig menu for the
following:
pw_rpc.nanopb.methodwhich can be enabled viaCONFIG_PIGWEED_RPC_NANOPB_METHOD=y.pw_rpc.nanopb.method_unionwhich can be enabled viaCONFIG_PIGWEED_RPC_NANOPB_METHOD_UNION=y.pw_rpc.nanopb.clientwhich can be enabled viaCONFIG_PIGWEED_RPC_NANOPB_CLIENT=y.pw_rpc.nanopb.commonwhich can be enabled viaCONFIG_PIGWEED_RPC_NANOPB_COMMON=y.pw_rpc.nanopb.echo_servicewhich can be enabled viaCONFIG_PIGWEED_RPC_NANOPB_ECHO_SERVICE=y.