diff options
Diffstat (limited to 'pw_rpc_transport/docs.rst')
-rw-r--r-- | pw_rpc_transport/docs.rst | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/pw_rpc_transport/docs.rst b/pw_rpc_transport/docs.rst new file mode 100644 index 000000000..8e4eb2ed1 --- /dev/null +++ b/pw_rpc_transport/docs.rst @@ -0,0 +1,249 @@ +.. _module-pw_rpc_transport: + +.. warning:: + This is an experimental module currently under development. APIs and + functionality may change at any time. + +================ +pw_rpc_transport +================ +The ``pw_rpc_transport`` provides a transport layer for ``pw_rpc``. + +``pw_rpc`` provides a system for defining and invoking remote procedure calls +(RPCs) on a device. It does not include any transports for sending these RPC +calls. On a real device there could be multiple ways of inter-process and/or +inter-core communication: hardware mailboxes, shared memory, network sockets, +Unix domain sockets. ``pw_rpc_transport`` provides means to implement various +transports and integrate them with ``pw_rpc`` services. + +``pw_rpc_transport`` relies on the assumption that a ``pw_rpc`` channel ID +uniquely identifies both sides of an RPC conversation. It allows developers to +define transports, egresses and ingresses for various channel IDs and choose +what framing will be used to send RPC packets over those transports. + +RpcFrame +-------- +Framed RPC data ready to be sent via ``RpcFrameSender``. Consists of a header +and a payload. Some RPC transport encodings may not require a header and put +all of the framed data into the payload (in which case the header can be +an empty span). + +A single RPC packet can be split into multiple ``RpcFrame``'s depending on the +MTU of the transport. + +All frames for an RPC packet are expected to be sent and received in order +without being interleaved by other packets' frames. + +RpcFrameSender +-------------- +Sends RPC frames over some communication channel (e.g. a hardware mailbox, +shared memory, or a socket). It exposes its MTU size and generally only knows +how to send an ``RpcFrame`` of a size that doesn't exceed that MTU. + +RpcPacketEncoder / RpcPacketDecoder +----------------------------------- +``RpcPacketEncoder`` is used to split and frame an RPC packet. +``RpcPacketDecoder`` then does the opposite e.g. stitches together received +frames and removes any framing added by the encoder. + +RpcEgressHandler +---------------- +Provides means of sending an RPC packet to its destination. Typically it ties +together an ``RpcPacketEncoder`` and ``RpcFrameSender``. + +RpcIngressHandler +----------------- +Provides means of receiving RPC packets over some transport. Typically it has +logic for reading RPC frames from some transport (a network connection, +shared memory, or a hardware mailbox), stitching and decoding them with +``RpcPacketDecoder`` and passing full RPC packets to their intended processor +via ``RpcPacketProcessor``. + +RpcPacketProcessor +------------------ +Used by ``RpcIngressHandler`` to send the received RPC packet to its intended +handler (e.g. a pw_rpc ``Service``). + +-------------------- +Creating a transport +-------------------- +RPC transports implement ``pw::rpc::RpcFrameSender``. The transport exposes its +maximum transmission unit (MTU) and only knows how to send packets of up to the +size of that MTU. + +.. code-block:: cpp + + class MyRpcTransport : public RpcFrameSender { + public: + size_t mtu() const override { return 128; } + + Status Send(RpcFrame frame) override { + // Send the frame via mailbox, shared memory or some other mechanism... + } + }; + +-------------------------- +Integration with pw_stream +-------------------------- +An RpcFrameSender implementaion that wraps a ``pw::stream::Writer`` is provided +by ``pw::rpc::StreamRpcFrameSender``. As the stream interface doesn't know +about MTU's, it's up to the user to select one. + +.. code-block:: cpp + + stream::SysIoWriter writer; + StreamRpcFrameSender<kMtu> sender(writer); + +A thread to feed data to a ``pw::rpc::RpcIngressHandler`` from a +``pw::stream::Reader`` is provided by ``pw::rpc::StreamRpcDispatcher``. + +.. code-block:: cpp + + rpc::HdlcRpcIngress<kMaxRpcPacketSize> hdlc_ingress(...); + stream::SysIoReader reader; + + // Feed Hdlc ingress with bytes from sysio. + rpc::StreamRpcDispatcher<kMaxSysioRead> sysio_dispatcher(reader, + hdlc_ingress); + + thread::DetachedThread(SysioDispatcherThreadOptions(), + sysio_dispatcher); + +------------------------------------------- +Using transports: a sample three-node setup +------------------------------------------- + +A transport must be properly registered in order for ``pw_rpc`` to correctly +route its packets. Below is an example of using a ``SocketRpcTransport`` and +a (hypothetical) ``SharedMemoryRpcTransport`` to set up RPC connectivity between +three endpoints. + +Node A runs ``pw_rpc`` clients who want to talk to nodes B and C using +``kChannelAB`` and ``kChannelAC`` respectively. However there is no direct +connectivity from A to C: only B can talk to C over shared memory while A can +talk to B over a socket connection. Also, some services on A are self-hosted +and accessed from the same process on ``kChannelAA``: + +.. code-block:: cpp + + // Set up A->B transport over a network socket where B is a server + // and A is a client. + SocketRpcTransport<kSocketReadBufferSize> a_to_b_transport( + SocketRpcTransport<kSocketReadBufferSize>::kAsClient, "localhost", + kNodeBPortNumber); + + // LocalRpcEgress handles RPC packets received from other nodes and destined + // to this node. + LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress; + // HdlcRpcEgress applies HDLC framing to all packets outgoing over the A->B + // transport. + HdlcRpcEgress<kMaxPacketSize> a_to_b_egress("a->b", a_to_b_transport); + + // List of channels for all packets originated locally at A. + std::array tx_channels = { + // Self-destined packets go directly to local egress. + Channel::Create<kChannelAA>(&local_egress), + // Packets to B and C go over A->B transport. + Channel::Create<kChannelAB>(&a_to_b_egress), + Channel::Create<kChannelAC>(&a_to_b_egress), + }; + + // Here we list all egresses for the packets _incoming_ from B. + std::array b_rx_channels = { + // Packets on both AB and AC channels are destined locally; hence sending + // to the local egress. + ChannelEgress{kChannelAB, local_egress}, + ChannelEgress{kChannelAC, local_egress}, + }; + + // HdlcRpcIngress complements HdlcRpcEgress: all packets received on + // `b_rx_channels` are assumed to have HDLC framing. + HdlcRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels); + + // Local egress needs to know how to send received packets to their target + // pw_rpc service. + ServiceRegistry registry(tx_channels); + local_egress.set_packet_processor(registry); + // Socket transport needs to be aware of what ingress it's handling. + a_to_b_transport.set_ingress(b_ingress); + + // Both RpcSocketTransport and LocalRpcEgress are ThreadCore's and + // need to be started in order for packet processing to start. + DetachedThread(/*...*/, a_to_b_transport); + DetachedThread(/*...*/, local_egress); + +Node B setup is the most complicated since it needs to deal with egress +and ingress from both A and B and needs to support two kinds of transports. Note +that A is unaware of which transport and framing B is using when talking to C: + +.. code-block:: cpp + + // This is the server counterpart to A's client socket. + SocketRpcTransport<kSocketReadBufferSize> b_to_a_transport( + SocketRpcTransport<kSocketReadBufferSize>::kAsServer, "localhost", + kNodeBPortNumber); + + SharedMemoryRpcTransport b_to_c_transport(/*...*/); + + LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress; + HdlcRpcEgress<kMaxPacketSize> b_to_a_egress("b->a", b_to_a_transport); + // SimpleRpcEgress applies a very simple length-prefixed framing to B->C + // traffic (because HDLC adds unnecessary overhead over shared memory). + SimpleRpcEgress<kMaxPacketSize> b_to_c_egress("b->c", b_to_c_transport); + + // List of channels for all packets originated locally at B (note that in + // this example B doesn't need to talk to C directly; it only proxies for A). + std::array tx_channels = { + Channel::Create<kChannelAB>(&b_to_a_egress), + }; + + // Here we list all egresses for the packets _incoming_ from A. + std::array a_rx_channels = { + ChannelEgress{kChannelAB, local_egress}, + ChannelEgress{kChannelAC, b_to_c_egress}, + }; + + // Here we list all egresses for the packets _incoming_ from C. + std::array c_rx_channels = { + ChannelEgress{kChannelAC, b_to_a_egress}, + }; + + HdlcRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels); + SimpleRpcIngress<kMaxPacketSize> c_ingress(c_rx_channels); + + ServiceRegistry registry(tx_channels); + local_egress.set_packet_processor(registry); + + b_to_a_transport.set_ingress(a_ingress); + b_to_c_transport.set_ingress(c_ingress); + + DetachedThread({}, b_to_a_transport); + DetachedThread({}, b_to_c_transport); + DetachedThread({}, local_egress); + +Node C setup is straightforward since it only needs to handle ingress from B: + +.. code-block:: cpp + + SharedMemoryRpcTransport c_to_b_transport(/*...*/); + LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress; + SimpleRpcEgress<kMaxPacketSize> c_to_b_egress("c->b", c_to_b_transport); + + std::array tx_channels = { + Channel::Create<kChannelAC>(&c_to_b_egress), + }; + + // Here we list all egresses for the packets _incoming_ from B. + std::array b_rx_channels = { + ChannelEgress{kChannelAC, local_egress}, + }; + + SimpleRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels); + + ServiceRegistry registry(tx_channels); + local_egress.set_packet_processor(registry); + + c_to_b_transport.set_ingress(b_ingress); + + DetachedThread(/*...*/, c_to_b_transport); + DetachedThread(/*...*/, local_egress); |