blob: f599cfdbf25ee4aef1aba7e6ea4dac4b81fa07a2 [file] [log] [blame]
.. _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);