diff options
Diffstat (limited to 'pw_rpc/docs.rst')
-rw-r--r-- | pw_rpc/docs.rst | 206 |
1 files changed, 167 insertions, 39 deletions
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst index 2e62c31c8..daece792f 100644 --- a/pw_rpc/docs.rst +++ b/pw_rpc/docs.rst @@ -6,11 +6,19 @@ pw_rpc The ``pw_rpc`` module provides a system for defining and invoking remote procedure calls (RPCs) on a device. +This document discusses the ``pw_rpc`` protocol and its C++ implementation. +``pw_rpc`` implementations for other languages are described in their own +documents: + +.. toctree:: + :maxdepth: 1 + + py/docs + .. admonition:: Try it out! For a quick intro to ``pw_rpc``, see the - :ref:`module-pw_hdlc_lite-rpc-example` in the :ref:`module-pw_hdlc_lite` - module. + :ref:`module-pw_hdlc-rpc-example` in the :ref:`module-pw_hdlc` module. .. attention:: @@ -56,21 +64,99 @@ This protocol buffer is declared in a ``BUILD.gn`` file as follows: sources = [ "foo_bar/the_service.proto" ] } -2. RPC service definition -------------------------- -``pw_rpc`` generates a C++ base class for each RPC service declared in a .proto -file. The serivce class is implemented by inheriting from this generated base -and defining a method for each RPC. +.. admonition:: proto2 or proto3 syntax? + + Always use proto3 syntax rather than proto2 for new protocol buffers. Proto2 + protobufs can be compiled for ``pw_rpc``, but they are not as well supported + as proto3. Specifically, ``pw_rpc`` lacks support for non-zero default values + in proto2. When using Nanopb with ``pw_rpc``, proto2 response protobufs with + non-zero field defaults should be manually initialized to the default struct. + + In the past, proto3 was sometimes avoided because it lacked support for field + presence detection. Fortunately, this has been fixed: proto3 now supports + ``optional`` fields, which are equivalent to proto2 ``optional`` fields. + + If you need to distinguish between a default-valued field and a missing field, + mark the field as ``optional``. The presence of the field can be detected + with a ``HasField(name)`` or ``has_<field>`` member, depending on the library. + + Optional fields have some overhead --- default-valued fields are included in + the encoded proto, and, if using Nanopb, the proto structs have a + ``has_<field>`` flag for each optional field. Use plain fields if field + presence detection is not needed. + + .. code-block:: protobuf + + syntax = "proto3"; -A service named ``TheService`` in package ``foo.bar`` will generate the -following class: + message MyMessage { + // Leaving this field unset is equivalent to setting it to 0. + int32 number = 1; + + // Setting this field to 0 is different from leaving it unset. + optional int32 other_number = 2; + } + +2. RPC code generation +---------------------- +``pw_rpc`` generates a C++ header file for each ``.proto`` file. This header is +generated in the build output directory. Its exact location varies by build +system and toolchain, but the C++ include path always matches the sources +declaration in the ``pw_proto_library``. The ``.proto`` extension is replaced +with an extension corresponding to the protobuf library in use. + +================== =============== =============== ============= +Protobuf libraries Build subtarget Protobuf header pw_rpc header +================== =============== =============== ============= +Raw only .raw_rpc (none) .raw_rpc.pb.h +Nanopb or raw .nanopb_rpc .pb.h .rpc.pb.h +pw_protobuf or raw .pwpb_rpc .pwpb.h .rpc.pwpb.h +================== =============== =============== ============= + +For example, the generated RPC header for ``"foo_bar/the_service.proto"`` is +``"foo_bar/the_service.rpc.pb.h"`` for Nanopb or +``"foo_bar/the_service.raw_rpc.pb.h"`` for raw RPCs. + +The generated header defines a base class for each RPC service declared in the +``.proto`` file. A service named ``TheService`` in package ``foo.bar`` would +generate the following base class: .. cpp:class:: template <typename Implementation> foo::bar::generated::TheService +3. RPC service definition +------------------------- +The serivce class is implemented by inheriting from the generated RPC service +base class and defining a method for each RPC. The methods must match the name +and function signature for one of the supported protobuf implementations. +Services may mix and match protobuf implementations within one service. + +.. tip:: + + The generated code includes RPC service implementation stubs. You can + reference or copy and paste these to get started with implementing a service. + These stub classes are generated at the bottom of the pw_rpc proto header. + + To use the stubs, do the following: + + #. Locate the generated RPC header in the build directory. For example: + + .. code-block:: sh + + find out/ -name <proto_name>.rpc.pb.h + + #. Scroll to the bottom of the generated RPC header. + #. Copy the stub class declaration to a header file. + #. Copy the member function definitions to a source file. + #. Rename the class or change the namespace, if desired. + #. List these files in a build target with a dependency on the + ``pw_proto_library``. + A Nanopb implementation of this service would be as follows: .. code-block:: cpp + #include "foo_bar/the_service.rpc.pb.h" + namespace foo::bar { class TheService : public generated::TheService<TheService> { @@ -79,7 +165,7 @@ A Nanopb implementation of this service would be as follows: const foo_bar_Request& request, foo_bar_Response& response) { // implementation - return pw::Status::OK; + return pw::OkStatus(); } void MethodTwo(ServerContext& ctx, @@ -103,7 +189,7 @@ The Nanopb implementation would be declared in a ``BUILD.gn``: pw_source_set("the_service") { public_configs = [ ":public" ] public = [ "public/foo_bar/service.h" ] - public_deps = [ ":the_service_proto_nanopb_rpc" ] + public_deps = [ ":the_service_proto.nanopb_rpc" ] } .. attention:: @@ -111,9 +197,9 @@ The Nanopb implementation would be declared in a ``BUILD.gn``: pw_rpc's generated classes will support using ``pw_protobuf`` or raw buffers (no protobuf library) in the future. -3. Register the service with a server +4. Register the service with a server ------------------------------------- -This example code sets up an RPC server with an :ref:`HDLC<module-pw_hdlc_lite>` +This example code sets up an RPC server with an :ref:`HDLC<module-pw_hdlc>` channel output and the example service. .. code-block:: cpp @@ -123,7 +209,7 @@ channel output and the example service. // adapt this as necessary. pw::stream::SysIoWriter writer; pw::rpc::RpcChannelOutput<kMaxTransmissionUnit> hdlc_channel_output( - writer, pw::hdlc_lite::kDefaultRpcAddress, "HDLC output"); + writer, pw::hdlc::kDefaultRpcAddress, "HDLC output"); pw::rpc::Channel channels[] = { pw::rpc::Channel::Create<1>(&hdlc_channel_output)}; @@ -148,10 +234,37 @@ channel output and the example service. std::array<std::byte, kMaxTransmissionUnit> input_buffer; PW_LOG_INFO("Starting pw_rpc server"); - pw::hdlc_lite::ReadAndProcessPackets( + pw::hdlc::ReadAndProcessPackets( server, hdlc_channel_output, input_buffer); } +Channels +======== +``pw_rpc`` sends all of its packets over channels. These are logical, +application-layer routes used to tell the RPC system where a packet should go. + +Channels over a client-server connection must all have a unique ID, which can be +assigned statically at compile time or dynamically. + +.. code-block:: cpp + + // Creating a channel with the static ID 3. + pw::rpc::Channel static_channel = pw::rpc::Channel::Create<3>(&output); + + // Grouping channel IDs within an enum can lead to clearer code. + enum ChannelId { + kUartChannel = 1, + kSpiChannel = 2, + }; + + // Creating a channel with a static ID defined within an enum. + pw::rpc::Channel another_static_channel = + pw::rpc::Channel::Create<ChannelId::kUartChannel>(&output); + + // Creating a channel with a dynamic ID (note that no output is provided; it + // will be set when the channel is used. + pw::rpc::Channel dynamic_channel; + Services ======== A service is a logical grouping of RPCs defined within a .proto file. ``pw_rpc`` @@ -175,9 +288,9 @@ Testing a pw_rpc integration ============================ After setting up a ``pw_rpc`` server in your project, you can test that it is working as intended by registering the provided ``EchoService``, defined in -``pw_rpc_protos/echo.proto``, which echoes back a message that it receives. +``echo.proto``, which echoes back a message that it receives. -.. literalinclude:: pw_rpc_protos/echo.proto +.. literalinclude:: echo.proto :language: protobuf :lines: 14- @@ -209,9 +322,9 @@ Packet format ------------- Pigweed RPC packets consist of a type and a set of fields. The packets are encoded as protocol buffers. The full packet format is described in -``pw_rpc/pw_rpc_protos/packet.proto``. +``pw_rpc/pw_rpc/internal/packet.proto``. -.. literalinclude:: pw_rpc_protos/packet.proto +.. literalinclude:: internal/packet.proto :language: protobuf :lines: 14- @@ -604,26 +717,18 @@ The Method class ^^^^^^^^^^^^^^^^ The RPC Server depends on the ``pw::rpc::internal::Method`` class. ``Method`` serves as the bridge between the ``pw_rpc`` server library and the user-defined -RPC functions. ``Method`` takes an RPC packet, decodes it using a protobuf -library (if applicable), and calls the RPC function. Since ``Method`` interacts -directly with the protobuf library, it must be implemented separately for each -protobuf library. - -``pw::rpc::internal::Method`` is not implemented as a facade with different -backends. Instead, there is a separate instance of the ``pw_rpc`` server library -for each ``Method`` implementation. There are a few reasons for this. - -* ``Method`` is entirely internal to ``pw_rpc``. Users will never implement a - custom backend. Exposing a facade would unnecessarily expose implementation - details and make ``pw_rpc`` more difficult to use. -* There is no common interface between ``pw_rpc`` / ``Method`` implementations. - It's not possible to swap between e.g. a Nanopb and a ``pw_protobuf`` RPC - server because the interface for the user-implemented RPCs changes completely. - This nullifies the primary benefit of facades. -* The different ``Method`` implementations can be built easily alongside one - another in a cross-platform way. This makes testing simpler, since the tests - build with any backend configuration. Users can select which ``Method`` - implementation to use simply by depending on the corresponding server library. +RPC functions. Each supported protobuf implementation extends ``Method`` to +implement its request and response proto handling. The ``pw_rpc`` server +calls into the ``Method`` implementation through the base class's ``Invoke`` +function. + +``Method`` implementations store metadata about each method, including a +function pointer to the user-defined method implementation. They also provide +``static constexpr`` functions for creating each type of method. ``Method`` +implementations must satisfy the ``MethodImplTester`` test class in +``pw_rpc_private/method_impl_tester.h``. + +See ``pw_rpc/internal/method.h`` for more details about ``Method``. Packet flow ^^^^^^^^^^^ @@ -743,3 +848,26 @@ interfaces for working with RPCs. The RPC server stores a list of all of active ``ClientCall`` objects. When an incoming packet is recieved, it dispatches to one of its active calls, which then decodes the payload and presents it to the user. + +ClientServer +============ +Sometimes, a device needs to both process RPCs as a server, as well as making +calls to another device as a client. To do this, both a client and server must +be set up, and incoming packets must be sent to both of them. + +Pigweed simplifies this setup by providing a ``ClientServer`` class which wraps +an RPC client and server with the same set of channels. + +.. code-block:: cpp + + pw::rpc::Channel channels[] = { + pw::rpc::Channel::Create<1>(&channel_output)}; + + // Creates both a client and a server. + pw::rpc::ClientServer client_server(channels); + + void ProcessRpcData(pw::ConstByteSpan packet) { + // Calls into both the client and the server, sending the packet to the + // appropriate one. + client_server.ProcessPacket(packet, output); + } |