TypeScript client#
pw_rpc: Efficient, low-code-size RPC system for embedded devices
Pigweed TypeScript client provides two ways to call RPCs. The Device API is easier to work with if you are using the RPC via HDLC over WebSerial.
If the device abstraction is not a good fit, Pigweed provides the pw_rpc module,
which makes it possible to call Pigweed RPCs from TypeScript. The module includes
a client library to facilitate handling RPCs.
Creating an RPC Client#
The RPC client is instantiated from a list of channels and a set of protos.
function savePacket(packetBytes: Uint8Array): void {
const packet = RpcPacket.deserializeBinary(packetBytes);
// Forward packet to physical transport:
// transport.send(packet.serializeBinary());
console.log(`Sending packet to channel ${packet.getChannelId()}`);
}
const channels = [new Channel(1, savePacket), new Channel(5)];
const client = Client.fromProtoSet(channels, new ProtoCollection());
To generate a ProtoSet/ProtoCollection from your own .proto files, use
pw_proto_compiler in your package.json:
"scripts": {
"build-protos": "pw_proto_compiler -p protos/rpc1.proto -p protos/rpc2.proto --out dist/protos"
}
This will generate a collection.js file which can be passed to Client.fromProtoSet.
Finding an RPC Method#
Once the client is instantiated with the correct proto library, the target RPC
method is found by searching based on the full name:
{packageName}.{serviceName}.{methodName}
const channel = client.channel()!;
const unaryStub = channel.methodStub(
'pw.rpc.test1.TheTestService.SomeUnary',
) as UnaryMethodStub;
The four possible RPC stubs are UnaryMethodStub,
ServerStreamingMethodStub, ClientStreamingMethodStub, and
BidirectionalStreamingMethodStub. Note that channel.methodStub()
returns a general stub. Since each stub type has different invoke
parameters, the general stub should be typecast before using.
Invoke an RPC with callbacks#
All RPC methods can be invoked with a set of callbacks that are triggered when either a response is received, the RPC is completed, or an error occurs. The example below demonstrates registering these callbacks on a Bidirectional RPC:
const bidiStub = client
.channel()!
.methodStub(
'pw.rpc.test1.TheTestService.SomeBidi',
) as BidirectionalStreamingMethodStub;
// Configure callback functions:
const onNext = (response: Message) => {
console.log(`Received message: ${response}`);
};
const onComplete = (status: Status) => {
console.log(`RPC completed with status: ${status}`);
};
const onError = (error: Error) => {
console.error(`RPC error: ${error}`);
};
const request = new bidiStub.method.requestType();
bidiStub.invoke(request, onNext, onComplete, onError);
Server streaming and bidirectional streaming methods can receive many responses
from the server. The client limits the maximum number of responses it stores for
a single RPC call to avoid unbounded memory usage in long-running streams. Once
the limit is reached, the oldest responses will be replaced as new ones arrive.
By default, the limit is set to DEFAULT_MAX_STREAM_RESPONSES (=16384), but
this can be configured on a per-call basis.
Open an RPC: ignore initial errors#
open allows you to start and register an RPC without throwing on initial errors. This
is useful for starting an RPC before the server is ready (for instance, starting
a logging RPC while the device is booting):
open(request?: Message,
onNext: Callback = () => {},
onCompleted: Callback = () => {},
onError: Callback = () => {}): Call
Blocking RPCs: promise API#
Each MethodStub type provides a call() / finishAndWait() method that allows
sending requests and awaiting responses through promises. The timeout field is optional;
if no timeout is specified, the RPC will wait indefinitely.
Unary RPC#
async function callUnary(): Promise<void> {
const unaryRpc = client
.channel()!
.methodStub('pw.rpc.test1.TheTestService.SomeUnary') as UnaryMethodStub;
const req = new unaryRpc.method.requestType();
const timeoutMs = 2000;
const [status, response] = await unaryRpc.call(req, timeoutMs);
console.log(`Status: ${status}, Response: ${response}`);
}
Server Streaming RPC#
async function callServerStreaming(): Promise<void> {
const serverStreamRpc = client
.channel()!
.methodStub(
'pw.rpc.test1.TheTestService.SomeServerStreaming',
) as ServerStreamingMethodStub;
const req = new serverStreamRpc.method.requestType();
const call = serverStreamRpc.invoke(req);
const timeoutMs = 2000;
// Stream responses as they arrive:
for await (const response of call.getResponses(2, timeoutMs)) {
console.log(response);
}
// Await remaining responses until stream completion:
const responses = call.getResponses();
while (!responses.done) {
console.log(await responses.value());
}
}
Client Streaming RPC#
async function callClientStreaming(): Promise<void> {
const clientStreamRpc = client
.channel()!
.methodStub(
'pw.rpc.test1.TheTestService.SomeClientStreaming',
) as ClientStreamingMethodStub;
const stream = clientStreamRpc.invoke();
const req = new clientStreamRpc.method.requestType();
// Send request messages to the stream:
stream.send(req);
// Complete stream and await final unary response:
const timeoutMs = 2000;
stream
.finishAndWait([req, req], timeoutMs)
.then(([status, response]) => {
console.log(`Finished: ${status}, Response: ${response}`);
})
.catch((reason) => {
console.error(`Stream error: ${reason}`);
});
}
Bidirectional Streaming RPC#
async function callBidiStreaming(): Promise<void> {
const bidiStreamingRpc = client
.channel()!
.methodStub(
'pw.rpc.test1.TheTestService.SomeBidiStreaming',
) as BidirectionalStreamingMethodStub;
const stream = bidiStreamingRpc.invoke();
const req = new bidiStreamingRpc.method.requestType();
// Send requests to device:
stream.send(req);
// Receive stream responses:
const timeoutMs = 2000;
for await (const response of stream.getResponses(1, timeoutMs)) {
console.log(response);
}
// Finish sending and await completion:
stream
.finishAndWait([req], timeoutMs)
.then(([status]) => {
console.log(`Bidirectional stream finished with status: ${status}`);
})
.catch((reason) => {
console.error(`Bidirectional stream error: ${reason}`);
});
}