aboutsummaryrefslogtreecommitdiff
path: root/pw_rpc
diff options
context:
space:
mode:
Diffstat (limited to 'pw_rpc')
-rw-r--r--pw_rpc/Android.bp360
-rw-r--r--pw_rpc/BUILD.bazel86
-rw-r--r--pw_rpc/BUILD.gn83
-rw-r--r--pw_rpc/CMakeLists.txt18
-rw-r--r--pw_rpc/Kconfig20
-rw-r--r--pw_rpc/benchmark.cc37
-rw-r--r--pw_rpc/bidirectional_streaming_rpc.svg86
-rw-r--r--pw_rpc/bidirectional_streaming_rpc_cancelled.svg94
-rw-r--r--pw_rpc/call.cc77
-rw-r--r--pw_rpc/call_test.cc41
-rw-r--r--pw_rpc/callback_test.cc23
-rw-r--r--pw_rpc/client.cc5
-rw-r--r--pw_rpc/client_call.cc7
-rw-r--r--pw_rpc/client_integration_test.cc61
-rw-r--r--pw_rpc/client_streaming_rpc.svg69
-rw-r--r--pw_rpc/client_streaming_rpc_cancelled.svg76
-rw-r--r--pw_rpc/docs.rst1475
-rw-r--r--pw_rpc/fake_channel_output.cc2
-rw-r--r--pw_rpc/fuzz/BUILD.gn12
-rw-r--r--pw_rpc/fuzz/alarm_timer_test.cc6
-rw-r--r--pw_rpc/fuzz/argparse.cc4
-rw-r--r--pw_rpc/fuzz/argparse_test.cc7
-rw-r--r--pw_rpc/fuzz/engine.cc2
-rw-r--r--pw_rpc/fuzz/engine_test.cc32
-rw-r--r--pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h1
-rw-r--r--pw_rpc/internal/packet.proto8
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java40
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java6
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java145
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel14
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java12
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java12
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java6
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java70
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java8
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java21
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java7
-rw-r--r--pw_rpc/method_test.cc2
-rw-r--r--pw_rpc/nanopb/Android.bp6
-rw-r--r--pw_rpc/nanopb/BUILD.bazel25
-rw-r--r--pw_rpc/nanopb/BUILD.gn27
-rw-r--r--pw_rpc/nanopb/CMakeLists.txt45
-rw-r--r--pw_rpc/nanopb/Kconfig32
-rw-r--r--pw_rpc/nanopb/callback_test.cc269
-rw-r--r--pw_rpc/nanopb/client_reader_writer_test.cc78
-rw-r--r--pw_rpc/nanopb/client_server_context_test.cc146
-rw-r--r--pw_rpc/nanopb/client_server_context_threaded_test.cc163
-rw-r--r--pw_rpc/nanopb/docs.rst38
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h27
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h36
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h40
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h21
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h3
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h14
-rw-r--r--pw_rpc/packet.cc22
-rw-r--r--pw_rpc/packet_meta.cc2
-rw-r--r--pw_rpc/packet_meta_test.cc29
-rw-r--r--pw_rpc/packet_test.cc36
-rw-r--r--pw_rpc/public/pw_rpc/benchmark.h11
-rw-r--r--pw_rpc/public/pw_rpc/channel.h2
-rw-r--r--pw_rpc/public/pw_rpc/internal/call.h85
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_call.h13
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing.h37
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h16
-rw-r--r--pw_rpc/public/pw_rpc/internal/config.h50
-rw-r--r--pw_rpc/public/pw_rpc/internal/fake_channel_output.h4
-rw-r--r--pw_rpc/public/pw_rpc/internal/hash.h6
-rw-r--r--pw_rpc/public/pw_rpc/internal/method_lookup.h6
-rw-r--r--pw_rpc/public/pw_rpc/internal/packet.h5
-rw-r--r--pw_rpc/public/pw_rpc/internal/server_call.h65
-rw-r--r--pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h200
-rw-r--r--pw_rpc/public/pw_rpc/internal/test_method_context.h2
-rw-r--r--pw_rpc/public/pw_rpc/packet_meta.h9
-rw-r--r--pw_rpc/public/pw_rpc/server.h5
-rw-r--r--pw_rpc/public/pw_rpc/synchronous_call.h460
-rw-r--r--pw_rpc/public/pw_rpc/writer.h2
-rw-r--r--pw_rpc/pw_rpc_private/fake_server_reader_writer.h6
-rw-r--r--pw_rpc/pw_rpc_private/test_method.h (renamed from pw_rpc/public/pw_rpc/internal/test_method.h)20
-rw-r--r--pw_rpc/pwpb/Android.bp48
-rw-r--r--pw_rpc/pwpb/BUILD.bazel21
-rw-r--r--pw_rpc/pwpb/BUILD.gn11
-rw-r--r--pw_rpc/pwpb/CMakeLists.txt31
-rw-r--r--pw_rpc/pwpb/client_reader_writer_test.cc77
-rw-r--r--pw_rpc/pwpb/client_server_context_test.cc119
-rw-r--r--pw_rpc/pwpb/client_server_context_threaded_test.cc139
-rw-r--r--pw_rpc/pwpb/codegen_test.cc46
-rw-r--r--pw_rpc/pwpb/docs.rst54
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h150
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h35
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h37
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h2
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h3
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h6
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h6
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h14
-rw-r--r--pw_rpc/pwpb/synchronous_call_test.cc75
-rw-r--r--pw_rpc/py/Android.bp15
-rw-r--r--pw_rpc/py/BUILD.bazel18
-rw-r--r--pw_rpc/py/BUILD.gn28
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/call.py9
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/impl.py40
-rw-r--r--pw_rpc/py/pw_rpc/client.py61
-rw-r--r--pw_rpc/py/pw_rpc/codegen.py45
-rw-r--r--pw_rpc/py/pw_rpc/codegen_nanopb.py99
-rw-r--r--pw_rpc/py/pw_rpc/codegen_pwpb.py103
-rw-r--r--pw_rpc/py/pw_rpc/codegen_raw.py8
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/console.py7
-rw-r--r--pw_rpc/py/pw_rpc/descriptors.py26
-rw-r--r--pw_rpc/py/pw_rpc/packets.py67
-rw-r--r--pw_rpc/py/pw_rpc/testing.py2
-rwxr-xr-xpw_rpc/py/tests/callback_client_test.py343
-rwxr-xr-xpw_rpc/py/tests/client_test.py117
-rwxr-xr-xpw_rpc/py/tests/packets_test.py56
-rwxr-xr-xpw_rpc/py/tests/python_client_cpp_server_test.py49
-rw-r--r--pw_rpc/raw/Android.bp4
-rw-r--r--pw_rpc/raw/BUILD.bazel13
-rw-r--r--pw_rpc/raw/BUILD.gn17
-rw-r--r--pw_rpc/raw/client_reader_writer_test.cc81
-rw-r--r--pw_rpc/raw/client_test.cc182
-rw-r--r--pw_rpc/raw/codegen_test.cc2
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h23
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_testing.h3
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h8
-rw-r--r--pw_rpc/raw/synchronous_call_test.cc241
-rw-r--r--pw_rpc/request_packets.svg42
-rw-r--r--pw_rpc/response_packets.svg42
-rw-r--r--pw_rpc/server.cc41
-rw-r--r--pw_rpc/server_call.cc7
-rw-r--r--pw_rpc/server_streaming_rpc.svg58
-rw-r--r--pw_rpc/server_streaming_rpc_cancelled.svg77
-rw-r--r--pw_rpc/server_test.cc89
-rw-r--r--pw_rpc/size_report/base.cc4
-rw-r--r--pw_rpc/size_report/server_only.cc6
-rw-r--r--pw_rpc/system_server/BUILD.bazel4
-rw-r--r--pw_rpc/ts/call.ts26
-rw-r--r--pw_rpc/ts/call_test.ts12
-rw-r--r--pw_rpc/ts/client.ts36
-rw-r--r--pw_rpc/ts/client_test.ts99
-rw-r--r--pw_rpc/ts/descriptors.ts19
-rw-r--r--pw_rpc/ts/descriptors_test.ts14
-rw-r--r--pw_rpc/ts/method.ts122
-rw-r--r--pw_rpc/ts/packets.ts59
-rw-r--r--pw_rpc/ts/packets_test.ts8
-rw-r--r--pw_rpc/ts/queue.ts2
-rw-r--r--pw_rpc/ts/rpc_classes.ts10
-rw-r--r--pw_rpc/unary_rpc.svg47
-rw-r--r--pw_rpc/unary_rpc_cancelled.svg59
149 files changed, 5759 insertions, 2767 deletions
diff --git a/pw_rpc/Android.bp b/pw_rpc/Android.bp
index e62b96157..516fb0a8c 100644
--- a/pw_rpc/Android.bp
+++ b/pw_rpc/Android.bp
@@ -75,7 +75,6 @@ filegroup {
cc_library_headers {
name: "pw_rpc_include_dirs",
- cpp_std: "c++20",
export_include_dirs: [
"public",
],
@@ -99,7 +98,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -109,8 +108,8 @@ cc_defaults {
name: "pw_rpc_defaults",
cpp_std: "c++20",
header_libs: [
- "fuschia_sdk_lib_fit",
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_fit",
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_function_headers",
@@ -126,8 +125,8 @@ cc_defaults {
"pw_toolchain",
],
export_header_lib_headers: [
- "fuschia_sdk_lib_fit",
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_fit",
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_function_headers",
@@ -167,23 +166,21 @@ cc_defaults {
"pw_rpc_internal_packet_pwpb_h",
],
srcs: [
- ":pw_rpc_src_files"
+ ":pw_rpc_src_files",
],
- host_supported: true,
- vendor_available: true,
}
genrule {
name: "pw_rpc_internal_packet_pwpb_h",
srcs: ["internal/packet.proto"],
cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--out-dir=$$(dirname $(location pw_rpc/internal/packet.pwpb.h)) " +
- "--plugin-path=$(location pw_protobuf_plugin_py) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--sources $(in) " +
- "--language pwpb " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc) ",
+ "--out-dir=$$(dirname $(location pw_rpc/internal/packet.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
out: [
"pw_rpc/internal/packet.pwpb.h",
],
@@ -198,13 +195,13 @@ genrule {
name: "pw_rpc_internal_packet_py",
srcs: ["internal/packet.proto"],
cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--sources $(in) " +
- "--language python " +
- "--no-generate-type-hints " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language python " +
+ "--no-generate-type-hints " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
out: [
"packet_pb2.py",
],
@@ -218,14 +215,45 @@ genrule {
// The output file names are based on the srcs file name with a .pb.c / .pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_nanopb_proto",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location protoc-gen-nanopb) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language nanopb " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location protoc-gen-nanopb) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "protoc-gen-nanopb",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Same as pw_rpc_generate_nanopb_proto but the proto files are compiled with a
+// single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_nanopb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_proto_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location protoc-gen-nanopb) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"protoc-gen-nanopb",
@@ -237,14 +265,45 @@ genrule_defaults {
// The output file name is based on the srcs file name with a .rpc.pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_nanopb_rpc_header",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language nanopb_rpc " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_nanopb_py",
+ ],
+}
+
+// Same as pw_rpc_generate_nanopb_rpc_header but the proto files are compiled
+// with a single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_nanopb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_rpc_header_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb_rpc " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"pw_protobuf_compiler_py",
@@ -256,14 +315,15 @@ genrule_defaults {
// The output file name is based on the srcs file name with a .raw_rpc.pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_raw_rpc_header",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language raw_rpc " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language raw_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"pw_protobuf_compiler_py",
@@ -271,6 +331,212 @@ genrule_defaults {
],
}
+// Generate header pwpb files.
+// The output file names are based on the srcs file name with a .pwpb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_proto",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Same as pw_rpc_generate_pwpb_proto but the proto files are compiled with a
+// single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_pwpb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_proto_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Generate the header pwpb RPC file.
+// The output file name is based on the srcs file name with a .rpc.pwpb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_rpc_header",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_pwpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_pwpb_py",
+ ],
+}
+
+// Same as pw_rpc_generate_pwpb_rpc_header but the proto files are compiled
+// with a single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_pwpb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_rpc_header_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_pwpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb_rpc " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_pwpb_py",
+ ],
+}
+
+// Copies the proto files to a prefix directory to add the prefix to the
+// compiled proto. The prefix is taken from the directory name of the first
+// item listen in out.
+genrule_defaults {
+ name: "pw_rpc_add_prefix_to_proto",
+ cmd: "out_files=($(out)); prefix=$$(dirname $${out_files[0]}); " +
+ "mkdir -p $${prefix}; cp -t $${prefix} $(in);",
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_with_prefix",
+ defaults: ["pw_rpc_add_prefix_to_proto"],
+ srcs: [
+ "echo.options",
+ "echo.proto",
+ ],
+ out: [
+ "pw_rpc/echo.options",
+ "pw_rpc/echo.proto",
+ ],
+}
+
+genrule {
+ name: "pw_rpc_echo_rpc_header",
+ defaults: ["pw_rpc_generate_nanopb_rpc_header_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.rpc.pb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_header",
+ defaults: ["pw_rpc_generate_nanopb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_source",
+ defaults: ["pw_rpc_generate_nanopb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pb.c"],
+}
+
+// This is a copy of the echo.pb.h header, since the generated echo.pb.c
+// includes it by file name, while pw_rpc/nanopb/echo_service_nanopb.h includes
+// it with a prefix.
+// Soong makes it very hard to add include directories when they don't come from
+// modules, so this is a kludge to add an include directory path without a
+// prefix.
+genrule {
+ name: "pw_rpc_echo_proto_header_copy",
+ cmd: "cp $(in) $(out)",
+ srcs: [":pw_rpc_echo_proto_header"],
+ out: ["echo.pb.h"],
+}
+
+cc_library_static {
+ name: "pw_rpc_echo_service_nanopb",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ export_include_dirs: ["public/pw_rpc"],
+ generated_headers: [
+ "pw_rpc_echo_proto_header",
+ "pw_rpc_echo_proto_header_copy",
+ "pw_rpc_echo_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_rpc_echo_proto_header",
+ "pw_rpc_echo_proto_header_copy",
+ "pw_rpc_echo_rpc_header",
+ ],
+ generated_sources: ["pw_rpc_echo_proto_source"],
+ static_libs: ["libprotobuf-c-nano"],
+}
+
+genrule {
+ name: "pw_rpc_echo_pwpb_rpc_header",
+ defaults: ["pw_rpc_generate_pwpb_rpc_header_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.rpc.pwpb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_pwpb_proto_header",
+ defaults: ["pw_rpc_generate_pwpb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pwpb.h"],
+}
+
+cc_library_static {
+ name: "pw_rpc_echo_service_pwpb",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ export_include_dirs: ["public/pw_rpc"],
+ generated_headers: [
+ "pw_rpc_echo_pwpb_proto_header",
+ "pw_rpc_echo_pwpb_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_rpc_echo_pwpb_proto_header",
+ "pw_rpc_echo_pwpb_rpc_header",
+ ],
+}
+
python_library_host {
name: "pw_rpc_internal_packet_py_lib",
srcs: [
diff --git a/pw_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index 8574e9f53..dca4569dd 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -12,12 +12,10 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
-load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("@rules_proto_grpc//:defs.bzl", "proto_plugin")
+load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -46,7 +44,7 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Build this as a cc_binary and use it in integration tests.
+# TODO: b/242059613 - Build this as a cc_binary and use it in integration tests.
filegroup(
name = "test_rpc_server",
srcs = ["test_rpc_server.cc"],
@@ -131,6 +129,7 @@ pw_cc_library(
pw_cc_library(
name = "synchronous_client_api",
+ srcs = ["public/pw_rpc/internal/synchronous_call_impl.h"],
hdrs = [
"public/pw_rpc/synchronous_call.h",
"public/pw_rpc/synchronous_call_result.h",
@@ -201,11 +200,11 @@ pw_cc_library(
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
"public/pw_rpc/internal/method_info_tester.h",
- "public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
"public/pw_rpc/payloads_view.h",
"pw_rpc_private/fake_server_reader_writer.h",
+ "pw_rpc_private/test_method.h",
],
includes = [
".",
@@ -242,11 +241,11 @@ pw_cc_library(
"//pw_log",
"//pw_stream:socket_stream",
"//pw_unit_test",
- "//pw_unit_test:logging_event_handler",
+ "//pw_unit_test:logging",
],
)
-# TODO(b/242059613): Add the client integration test to the build.
+# TODO: b/242059613 - Add the client integration test to the build.
filegroup(
name = "client_integration_test",
srcs = ["client_integration_test.cc"],
@@ -271,10 +270,10 @@ pw_cc_test(
":pw_rpc_test_cc.raw_rpc",
"//pw_rpc/raw:client_testing",
"//pw_sync:binary_semaphore",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:yield",
- "//pw_thread_stl:test_threads",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -303,6 +302,7 @@ pw_cc_test(
],
deps = [
":pw_rpc",
+ "//pw_fuzzer:fuzztest",
],
)
@@ -313,6 +313,7 @@ pw_cc_test(
],
deps = [
":pw_rpc",
+ "//pw_fuzzer:fuzztest",
],
)
@@ -356,11 +357,20 @@ pw_cc_test(
deps = [":internal_test_utils"],
)
-# TODO(b/234874064): Add test_helpers_test when it is possible to use .options
-# in bazel build.
-filegroup(
+pw_cc_test(
name = "test_helpers_test",
srcs = ["test_helpers_test.cc"],
+ deps = [
+ ":test_helpers",
+ "//pw_result",
+ "//pw_rpc/pwpb:client_testing",
+ "//pw_rpc/pwpb:echo_service",
+ "//pw_rpc/pwpb:server_api",
+ "//pw_status",
+ "//pw_sync:interrupt_spin_lock",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:timed_thread_notification",
+ ],
)
proto_library(
@@ -398,54 +408,6 @@ pw_proto_library(
deps = [":pw_rpc_test_proto"],
)
-proto_plugin(
- name = "pw_cc_plugin_raw",
- outputs = [
- "{protopath}.raw_rpc.pb.h",
- ],
- protoc_plugin_name = "raw_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_raw",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "pw_cc_plugin_nanopb_rpc",
- outputs = [
- "{protopath}.rpc.pb.h",
- ],
- protoc_plugin_name = "nanopb_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_nanopb",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "nanopb_plugin",
- options = [
- "--library-include-format='#include\"%s\"'",
- ],
- outputs = [
- "{protopath}.pb.h",
- "{protopath}.pb.c",
- ],
- separate_options_flag = True,
- tool = "@com_github_nanopb_nanopb//:bazel_generator",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "pw_cc_plugin_pwpb_rpc",
- outputs = [
- "{protopath}.rpc.pwpb.h",
- ],
- protoc_plugin_name = "pwpb_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_pwpb",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
pw_proto_filegroup(
name = "echo_proto_and_options",
srcs = ["echo.proto"],
@@ -465,6 +427,4 @@ py_proto_library(
pw_proto_library(
name = "echo_cc",
deps = [":echo_proto"],
- # TODO(tpudlik): We should provide echo.options to nanopb here, but the
- # current proto codegen implementation provides no mechanism for doing so.
)
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 18c8778e3..97a32cec4 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
@@ -59,6 +60,16 @@ group("use_global_mutex") {
public_configs = [ ":global_mutex_config" ]
}
+config("dynamic_allocation_config") {
+ defines = [ "PW_RPC_DYNAMIC_ALLOCATION=1" ]
+ visibility = [ ":*" ]
+}
+
+# Use this for pw_rpc_CONFIG to enable dynamic allocation.
+pw_source_set("use_dynamic_allocation") {
+ public_configs = [ ":dynamic_allocation_config" ]
+}
+
pw_source_set("config") {
sources = [ "public/pw_rpc/internal/config.h" ]
public_configs = [ ":public_include_path" ]
@@ -146,6 +157,7 @@ pw_source_set("synchronous_client_api") {
"public/pw_rpc/synchronous_call.h",
"public/pw_rpc/synchronous_call_result.h",
]
+ sources = [ "public/pw_rpc/internal/synchronous_call_impl.h" ]
}
# Classes shared by the server and client.
@@ -290,10 +302,10 @@ pw_source_set("test_utils") {
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
"public/pw_rpc/internal/method_info_tester.h",
- "public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
"pw_rpc_private/fake_server_reader_writer.h",
+ "pw_rpc_private/test_method.h",
]
public_configs = [ ":public_include_path" ]
public_deps = [
@@ -309,6 +321,7 @@ pw_source_set("test_utils") {
}
pw_source_set("integration_testing") {
+ testonly = pw_unit_test_TESTONLY
public = [
"public/pw_rpc/integration_test_socket_client.h",
"public/pw_rpc/integration_testing.h",
@@ -319,7 +332,7 @@ pw_source_set("integration_testing") {
"$dir_pw_hdlc:pw_rpc",
"$dir_pw_hdlc:rpc_channel_output",
"$dir_pw_stream:socket_stream",
- "$dir_pw_unit_test:logging_event_handler",
+ "$dir_pw_unit_test:logging",
dir_pw_assert,
dir_pw_function,
dir_pw_unit_test,
@@ -339,6 +352,7 @@ pw_executable("test_rpc_server") {
}
pw_executable("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "client_integration_test.cc" ]
deps = [
":client",
@@ -356,7 +370,8 @@ pw_executable("client_integration_test") {
}
}
-pw_python_action("cpp_client_server_integration_test") {
+pw_python_action_test("cpp_client_server_integration_test") {
+ testonly = pw_unit_test_TESTONLY
script = "py/pw_rpc/testing.py"
args = [
"--server",
@@ -370,8 +385,7 @@ pw_python_action("cpp_client_server_integration_test") {
":client_integration_test",
":test_rpc_server",
]
-
- stamp = true
+ tags = [ "integration" ]
}
pw_proto_library("protos") {
@@ -397,16 +411,6 @@ pw_doc_group("docs") {
"benchmark.proto",
"echo.proto",
"internal/packet.proto",
- "unary_rpc.svg",
- "unary_rpc_cancelled.svg",
- "server_streaming_rpc.svg",
- "server_streaming_rpc_cancelled.svg",
- "client_streaming_rpc.svg",
- "client_streaming_rpc_cancelled.svg",
- "bidirectional_streaming_rpc.svg",
- "bidirectional_streaming_rpc_cancelled.svg",
- "request_packets.svg",
- "response_packets.svg",
]
group_deps = [
"nanopb:docs",
@@ -477,6 +481,9 @@ pw_test("call_test") {
":test_utils",
]
sources = [ "call_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("callback_test") {
@@ -486,13 +493,16 @@ pw_test("callback_test") {
":server",
":test_protos.raw_rpc",
"$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:sleep",
- "$dir_pw_thread:test_threads",
"$dir_pw_thread:yield",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"raw:client_testing",
]
sources = [ "callback_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("channel_test") {
@@ -501,6 +511,9 @@ pw_test("channel_test") {
":test_utils",
]
sources = [ "channel_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_python_action("generate_ids_test") {
@@ -515,28 +528,36 @@ pw_python_action("generate_ids_test") {
}
pw_test("ids_test") {
- deps = [
- ":generate_ids_test",
- ":server",
- ]
- sources = get_target_outputs(":generate_ids_test")
+ deps = [ ":server" ]
+ source_gen_deps = [ ":generate_ids_test" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("packet_test") {
deps = [
":server",
+ "$dir_pw_fuzzer:fuzztest",
dir_pw_bytes,
dir_pw_protobuf,
]
sources = [ "packet_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("packet_meta_test") {
deps = [
":server",
+ "$dir_pw_fuzzer:fuzztest",
dir_pw_bytes,
]
sources = [ "packet_meta_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("service_test") {
@@ -546,6 +567,9 @@ pw_test("service_test") {
dir_pw_assert,
]
sources = [ "service_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("client_server_test") {
@@ -555,6 +579,9 @@ pw_test("client_server_test") {
"raw:server_api",
]
sources = [ "client_server_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("method_test") {
@@ -563,6 +590,9 @@ pw_test("method_test") {
":test_utils",
]
sources = [ "method_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("server_test") {
@@ -573,11 +603,17 @@ pw_test("server_test") {
dir_pw_assert,
]
sources = [ "server_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("fake_channel_output_test") {
deps = [ ":test_utils" ]
sources = [ "fake_channel_output_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("test_helpers_test") {
@@ -596,4 +632,7 @@ pw_test("test_helpers_test") {
enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
pw_sync_COUNTING_SEMAPHORE_BACKEND != "" &&
pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index 37b7cb0c9..4d95c180d 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -61,9 +61,6 @@ pw_add_library(pw_rpc.server STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_SERVER)
- zephyr_link_libraries(pw_rpc.server)
-endif()
pw_add_library(pw_rpc.client STATIC
HEADERS
@@ -83,9 +80,6 @@ pw_add_library(pw_rpc.client STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT)
- zephyr_link_libraries(pw_rpc.client)
-endif()
pw_add_library(pw_rpc.client_server STATIC
HEADERS
@@ -98,14 +92,12 @@ pw_add_library(pw_rpc.client_server STATIC
SOURCES
client_server.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT_SERVER)
- zephyr_link_libraries(pw_rpc.client_server)
-endif()
pw_add_library(pw_rpc.synchronous_client_api INTERFACE
HEADERS
public/pw_rpc/synchronous_call.h
public/pw_rpc/synchronous_call_result.h
+ public/pw_rpc/internal/synchronous_call_impl.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
@@ -170,10 +162,6 @@ if(NOT "${pw_thread.yield_BACKEND}" STREQUAL "")
pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.yield)
endif()
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_COMMON)
- zephyr_link_libraries(pw_rpc.common)
-endif()
-
pw_add_library(pw_rpc.fake_channel_output STATIC
HEADERS
public/pw_rpc/internal/fake_channel_output.h
@@ -253,10 +241,10 @@ pw_add_library(pw_rpc.test_utils INTERFACE
public/pw_rpc/internal/fake_channel_output.h
public/pw_rpc/internal/method_impl_tester.h
public/pw_rpc/internal/method_info_tester.h
- public/pw_rpc/internal/test_method.h
public/pw_rpc/internal/test_method_context.h
public/pw_rpc/internal/test_utils.h
pw_rpc_private/fake_server_reader_writer.h
+ pw_rpc_private/test_method.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
@@ -343,6 +331,7 @@ pw_add_test(pw_rpc.packet_test
packet_test.cc
PRIVATE_DEPS
pw_bytes
+ pw_fuzzer.fuzztest
pw_protobuf
pw_rpc.server
GROUPS
@@ -355,6 +344,7 @@ pw_add_test(pw_rpc.packet_meta_test
packet_meta_test.cc
PRIVATE_DEPS
pw_bytes
+ pw_fuzzer.fuzztest
pw_rpc.server
GROUPS
modules
diff --git a/pw_rpc/Kconfig b/pw_rpc/Kconfig
index f01276ff7..d199dc852 100644
--- a/pw_rpc/Kconfig
+++ b/pw_rpc/Kconfig
@@ -12,26 +12,34 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_rpc"
+
rsource "nanopb/Kconfig"
config PIGWEED_RPC_SERVER
- bool "Enable Pigweed RPC server library (pw_rpc.server)"
+ bool "Link pw_rpc.server library"
select PIGWEED_RPC_COMMON
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_CLIENT
- bool "Enable Pigweed RPC client library (pw_rpc.client)"
+ bool "Link pw_rpc.client library"
select PIGWEED_RPC_COMMON
select PIGWEED_RESULT
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_CLIENT_SERVER
- bool "Enable Pigweed RPC client-server library (pw_rpc.client_server)"
+ bool "Link pw_rpc.client_server library"
select PIGWEED_RPC_CLIENT
select PIGWEED_RPC_SERVER
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_COMMON
- bool "Enable Pigweed RPC common library (pw_rpc.common)"
+ bool "Link pw_rpc.common library"
select PIGWEED_ASSERT
select PIGWEED_BYTES
select PIGWEED_CONTAINERS
@@ -40,3 +48,7 @@ config PIGWEED_RPC_COMMON
select PIGWEED_STATUS
select PIGWEED_LOG
select PIGWEED_SYNC_MUTEX
+ help
+ See :ref:`module-pw_rpc` for module details.
+
+endmenu
diff --git a/pw_rpc/benchmark.cc b/pw_rpc/benchmark.cc
index dfa11b64e..a8cec9522 100644
--- a/pw_rpc/benchmark.cc
+++ b/pw_rpc/benchmark.cc
@@ -39,17 +39,36 @@ void BenchmarkService::UnaryEcho(ConstByteSpan request,
.IgnoreError();
}
+BenchmarkService::ReaderWriterId BenchmarkService::AllocateReaderWriterId() {
+ return next_reader_writer_id_++;
+}
+
void BenchmarkService::BidirectionalEcho(
RawServerReaderWriter& new_reader_writer) {
- reader_writer_ = std::move(new_reader_writer);
-
- reader_writer_.set_on_next([this](ConstByteSpan request) {
- Status status = reader_writer_.Write(request);
- if (!status.ok()) {
- reader_writer_.Finish(status)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
- }
- });
+ auto id = AllocateReaderWriterId();
+
+ struct Captures {
+ BenchmarkService* self;
+ ReaderWriterId id;
+ };
+
+ auto captures = std::make_unique<Captures>(Captures{.self = this, .id = id});
+ new_reader_writer.set_on_next(
+ [captures = std::move(captures)](ConstByteSpan request) {
+ auto& reader_writers = captures->self->reader_writers_;
+ auto rw_id = captures->id;
+ auto reader_writer = reader_writers.find(rw_id);
+ if (reader_writer == reader_writers.end()) {
+ return;
+ }
+ Status status = reader_writer->second.Write(request);
+ if (!status.ok()) {
+ reader_writer->second.Finish(status)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
+ reader_writers.erase(rw_id);
+ }
+ });
+ reader_writers_.insert({id, std::move(new_reader_writer)});
}
} // namespace pw::rpc
diff --git a/pw_rpc/bidirectional_streaming_rpc.svg b/pw_rpc/bidirectional_streaming_rpc.svg
deleted file mode 100644
index fb18ea96c..000000000
--- a/pw_rpc/bidirectional_streaming_rpc.svg
+++ /dev/null
@@ -1,86 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="806.3000000000001" viewBox="0 0 624 733" width="686.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Bidirectional Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="558" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="231" y="153"></rect>
-<rect fill="rgb(0,0,0)" height="488" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="423" y="153"></rect>
-<polygon fill="rgb(0,0,0)" points="87,126 211,126 219,134 219,180 87,180 87,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="51,220 211,220 219,228 219,287 51,287 51,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,407 603,407 611,415 611,474 443,474 443,407" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="27,514 211,514 219,522 219,568 27,568 27,514" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,608 573,608 581,616 581,675 443,675 443,608" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="171" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="363" y="46"></rect>
-<path d="M 232 80 L 232 321" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<path d="M 232 329 L 232 353" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="2 8"></path>
-<path d="M 232 361 L 232 721" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="558" stroke="rgb(0,0,0)" width="8" x="228" y="147"></rect>
-<path d="M 424 80 L 424 321" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<path d="M 424 329 L 424 353" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="2 8"></path>
-<path d="M 424 361 L 424 721" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="488" stroke="rgb(0,0,0)" width="8" x="420" y="147"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="168" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="232.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="360" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="424.0" y="66">server</text>
-<path d="M 240 147 L 416 147" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,143 416,147 408,151" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="84,120 208,120 216,128 216,174 84,174 84,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 120 L 208 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 128 L 216 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="146.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="119.0" y="172">method ID</text>
-<path d="M 240 247 L 416 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="408,243 416,247 408,251" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="48,214 208,214 216,222 216,281 48,281 48,214" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 214 L 208 222" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 222 L 216 222" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="128.0" y="227">PacketType.CLIENT_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="240">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="253">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="83.0" y="266">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="77.0" y="279">payload</text>
-<path d="M 240 434 L 416 434" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="248,430 240,434 248,438" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,401 600,401 608,409 608,468 440,468 440,401" stroke="rgb(0,0,0)"></polygon>
-<path d="M 600 401 L 600 409" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 600 409 L 608 409" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="520.0" y="414">PacketType.SERVER_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="427">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="440">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="453">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="469.0" y="466">payload</text>
-<path d="M 240 535 L 416 535" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,531 416,535 408,539" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,508 208,508 216,516 216,562 24,562 24,508" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 508 L 208 516" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 516 L 216 516" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="168" x="116.0" y="521">PacketType.CLIENT_STREAM_END</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="534">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="547">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="560">method ID</text>
-<path d="M 240 635 L 416 635" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="248,631 240,635 248,639" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,602 570,602 578,610 578,669 440,669 440,602" stroke="rgb(0,0,0)"></polygon>
-<path d="M 570 602 L 570 610" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 570 610 L 578 610" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="505.0" y="615">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="628">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="641">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="654">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="466.0" y="667">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="30" x="259.0" y="145">start</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="313.0" y="245">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="347.0" y="432">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="256.0" y="533">done</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="404.0" y="633">done</text>
-<rect fill="white" height="19" stroke="white" width="158" x="249" y="331"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="328.0" y="346">(messages in any order)</text>
-</svg>
diff --git a/pw_rpc/bidirectional_streaming_rpc_cancelled.svg b/pw_rpc/bidirectional_streaming_rpc_cancelled.svg
deleted file mode 100644
index a6e6b7f3b..000000000
--- a/pw_rpc/bidirectional_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 604 559" width="604px" height="559px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"/>
- </filter>
- </defs>
- <title>Cancelled Bidirectional Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "start",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID"
- ];
-
- client --&gt; server [
- noactivate,
- label = "messages (zero or more)",
- leftnote = "PacketType.CLIENT_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client &lt;-- server [
- noactivate,
- label = "messages (zero or more)",
- rightnote = "PacketType.SERVER_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="209" y="153"/>
- <rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="401" y="153"/>
- <polygon fill="rgb(0,0,0)" points="64,126 189,126 197,134 197,180 64,180 64,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="27,220 189,220 197,228 197,287 27,287 27,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="421,327 583,327 591,335 591,394 421,394 421,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="33,434 189,434 197,442 197,501 33,501 33,434" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="149" y="46"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="341" y="46"/>
- <path d="M 210 80 L 210 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="206" y="147"/>
- <path d="M 402 80 L 402 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="398" y="147"/>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="146" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="210.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="338" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="402.5" y="66">server</text>
- <path d="M 218 147 L 394 147" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,143 394,147 386,151" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="61,120 186,120 194,128 194,174 61,174 61,120" stroke="rgb(0,0,0)"/>
- <path d="M 186 120 L 186 128" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 128 L 194 128" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="123.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="96.5" y="172">method ID</text>
- <path d="M 218 247 L 394 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="386,243 394,247 386,251" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="24,214 186,214 194,222 194,281 24,281 24,214" stroke="rgb(0,0,0)"/>
- <path d="M 186 214 L 186 222" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 222 L 194 222" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="105.0" y="227">PacketType.CLIENT_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="240">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="266">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="53.5" y="279">payload</text>
- <path d="M 218 354 L 394 354" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="226,350 218,354 226,358" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="418,321 580,321 588,329 588,388 418,388 418,321" stroke="rgb(0,0,0)"/>
- <path d="M 580 321 L 580 329" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 580 329 L 588 329" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="499.0" y="334">PacketType.SERVER_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="347">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="360">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="453.5" y="373">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="447.5" y="386">payload</text>
- <path d="M 218 461 L 394 461" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,457 394,461 386,465" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="30,428 186,428 194,436 194,495 30,495 30,428" stroke="rgb(0,0,0)"/>
- <path d="M 186 428 L 186 436" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 436 L 194 436" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="108.0" y="441">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="68.5" y="454">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="68.5" y="467">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="65.5" y="480">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="86.5" y="493">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="31" x="237.5" y="145">start</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="292.0" y="245">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="324.0" y="352">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="240.5" y="459">cancel</text>
-</svg>
diff --git a/pw_rpc/call.cc b/pw_rpc/call.cc
index ffb94150b..1083f7769 100644
--- a/pw_rpc/call.cc
+++ b/pw_rpc/call.cc
@@ -83,9 +83,8 @@ Call::Call(LockedEndpoint& endpoint_ref,
id_(call_id),
service_id_(service_id),
method_id_(method_id),
- state_(kActive | (HasClientStream(properties.method_type())
- ? static_cast<uint8_t>(kClientStreamActive)
- : 0u)),
+ // Note: Bit kActive set to 1 and kClientRequestedCompletion is set to 0.
+ state_(kActive),
awaiting_cleanup_(OkStatus().code()),
callbacks_executing_(0),
properties_(properties) {
@@ -96,20 +95,22 @@ Call::Call(LockedEndpoint& endpoint_ref,
endpoint().RegisterCall(*this);
}
-Call::~Call() {
- // Note: this explicit deregistration is necessary to ensure that
- // modifications to the endpoint call list occur while holding rpc_lock.
- // Removing this explicit registration would result in unsynchronized
- // modification of the endpoint call list via the destructor of the
- // superclass `IntrusiveList<Call>::Item`.
+void Call::DestroyServerCall() {
RpcLockGuard lock;
+ // Any errors are logged in Channel::Send.
+ CloseAndSendResponseLocked(OkStatus()).IgnoreError();
+ WaitForCallbacksToComplete();
+ state_ |= kHasBeenDestroyed;
+}
- // This `active_locked()` guard is necessary to ensure that `endpoint()` is
- // still valid.
- if (active_locked()) {
- endpoint().UnregisterCall(*this);
- }
+void Call::DestroyClientCall() {
+ RpcLockGuard lock;
+ CloseClientCall();
+ WaitForCallbacksToComplete();
+ state_ |= kHasBeenDestroyed;
+}
+void Call::WaitForCallbacksToComplete() {
do {
int iterations = 0;
while (CallbacksAreRunning()) {
@@ -118,9 +119,6 @@ Call::~Call() {
}
} while (CleanUpIfRequired());
-
- // Help prevent dangling references in callbacks by waiting for callbacks to
- // complete before deleting this call.
}
void Call::MoveFrom(Call& other) {
@@ -284,6 +282,19 @@ void Call::HandlePayload(ConstByteSpan payload) {
endpoint_->CleanUpCalls();
}
+void Call::CloseClientCall() {
+ // When a client call is closed, for bidirectional and client streaming RPCs,
+ // the server may be waiting for client stream messages, so we need to notify
+ // the server that the client has requested for completion and no further
+ // requests should be expected from the client. For unary and server streaming
+ // RPCs, since the client is not sending messages, server does not need to be
+ // notified.
+ if (has_client_stream() && !client_requested_completion()) {
+ RequestCompletionLocked().IgnoreError();
+ }
+ UnregisterAndMarkClosed();
+}
+
void Call::UnregisterAndMarkClosed() {
if (active_locked()) {
endpoint().UnregisterCall(*this);
@@ -291,4 +302,36 @@ void Call::UnregisterAndMarkClosed() {
}
}
+void Call::DebugLog() const PW_NO_LOCK_SAFETY_ANALYSIS {
+ PW_LOG_INFO(
+ "Call %p\n"
+ "\tEndpoint: %p\n"
+ "\tCall ID: %8u\n"
+ "\tChannel: %8u\n"
+ "\tService: %08x\n"
+ "\tMethod: %08x\n"
+ "\tState: %8x\n"
+ "\tCleanup: %8s\n"
+ "\tBusy CBs: %8x\n"
+ "\tType: %8d\n"
+ "\tClient: %8d\n"
+ "\tWrapped: %8d\n"
+ "\ton_error: %8d\n"
+ "\ton_next: %8d\n",
+ static_cast<const void*>(this),
+ static_cast<const void*>(endpoint_),
+ static_cast<unsigned>(id_),
+ static_cast<unsigned>(channel_id_),
+ static_cast<unsigned>(service_id_),
+ static_cast<unsigned>(method_id_),
+ static_cast<int>(state_),
+ Status(static_cast<Status::Code>(awaiting_cleanup_)).str(),
+ static_cast<int>(callbacks_executing_),
+ static_cast<int>(properties_.method_type()),
+ static_cast<int>(properties_.call_type()),
+ static_cast<int>(hold_lock_while_invoking_callback_with_payload()),
+ static_cast<int>(on_error_ == nullptr),
+ static_cast<int>(on_next_ == nullptr));
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/call_test.cc b/pw_rpc/call_test.cc
index b2cb44bfb..c3ae8be98 100644
--- a/pw_rpc/call_test.cc
+++ b/pw_rpc/call_test.cc
@@ -21,10 +21,10 @@
#include <optional>
#include "gtest/gtest.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/internal/test_utils.h"
#include "pw_rpc/service.h"
#include "pw_rpc_private/fake_server_reader_writer.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc {
@@ -188,13 +188,14 @@ TEST_F(ServerWriterTest, DefaultConstructor_NoClientStream) {
FakeServerWriter writer;
RpcLockGuard lock;
EXPECT_FALSE(writer.as_server_call().has_client_stream());
- EXPECT_FALSE(writer.as_server_call().client_stream_open());
+ EXPECT_FALSE(writer.as_server_call().client_requested_completion());
}
TEST_F(ServerWriterTest, Open_NoClientStream) {
RpcLockGuard lock;
EXPECT_FALSE(writer_.as_server_call().has_client_stream());
- EXPECT_FALSE(writer_.as_server_call().client_stream_open());
+ EXPECT_TRUE(writer_.as_server_call().has_server_stream());
+ EXPECT_FALSE(writer_.as_server_call().client_requested_completion());
}
class ServerReaderTest : public Test {
@@ -210,41 +211,41 @@ class ServerReaderTest : public Test {
FakeServerReader reader_;
};
-TEST_F(ServerReaderTest, DefaultConstructor_ClientStreamClosed) {
+TEST_F(ServerReaderTest, DefaultConstructor_StreamClosed) {
FakeServerReader reader;
EXPECT_FALSE(reader.as_server_call().active());
RpcLockGuard lock;
- EXPECT_FALSE(reader.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader.as_server_call().client_requested_completion());
}
TEST_F(ServerReaderTest, Open_ClientStreamStartsOpen) {
RpcLockGuard lock;
EXPECT_TRUE(reader_.as_server_call().has_client_stream());
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
}
-TEST_F(ServerReaderTest, Close_ClosesClientStream) {
+TEST_F(ServerReaderTest, Close_ClosesStream) {
EXPECT_TRUE(reader_.as_server_call().active());
rpc_lock().lock();
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
rpc_lock().unlock();
EXPECT_EQ(OkStatus(),
reader_.as_server_call().CloseAndSendResponse(OkStatus()));
EXPECT_FALSE(reader_.as_server_call().active());
RpcLockGuard lock;
- EXPECT_FALSE(reader_.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.as_server_call().client_requested_completion());
}
-TEST_F(ServerReaderTest, EndClientStream_OnlyClosesClientStream) {
+TEST_F(ServerReaderTest, RequestCompletion_OnlyMakesClientNotReady) {
EXPECT_TRUE(reader_.active());
rpc_lock().lock();
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
- reader_.as_server_call().HandleClientStreamEnd();
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
+ reader_.as_server_call().HandleClientRequestedCompletion();
EXPECT_TRUE(reader_.active());
RpcLockGuard lock;
- EXPECT_FALSE(reader_.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.as_server_call().client_requested_completion());
}
class ServerReaderWriterTest : public Test {
@@ -264,33 +265,31 @@ TEST_F(ServerReaderWriterTest, Move_MaintainsClientStream) {
FakeServerReaderWriter destination;
rpc_lock().lock();
- EXPECT_FALSE(destination.as_server_call().client_stream_open());
+ EXPECT_FALSE(destination.as_server_call().client_requested_completion());
rpc_lock().unlock();
destination = std::move(reader_writer_);
RpcLockGuard lock;
EXPECT_TRUE(destination.as_server_call().has_client_stream());
- EXPECT_TRUE(destination.as_server_call().client_stream_open());
+ EXPECT_FALSE(destination.as_server_call().client_requested_completion());
}
TEST_F(ServerReaderWriterTest, Move_MovesCallbacks) {
int calls = 0;
reader_writer_.set_on_error([&calls](Status) { calls += 1; });
reader_writer_.set_on_next([&calls](ConstByteSpan) { calls += 1; });
-
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- reader_writer_.set_on_client_stream_end([&calls]() { calls += 1; });
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ reader_writer_.set_on_completion_requested_if_enabled(
+ [&calls]() { calls += 1; });
FakeServerReaderWriter destination(std::move(reader_writer_));
rpc_lock().lock();
destination.as_server_call().HandlePayload({});
rpc_lock().lock();
- destination.as_server_call().HandleClientStreamEnd();
+ destination.as_server_call().HandleClientRequestedCompletion();
rpc_lock().lock();
destination.as_server_call().HandleError(Status::Unknown());
- EXPECT_EQ(calls, 2 + PW_RPC_CLIENT_STREAM_END_CALLBACK);
+ EXPECT_EQ(calls, 2 + PW_RPC_COMPLETION_REQUEST_CALLBACK);
}
TEST_F(ServerReaderWriterTest, Move_ClearsCallAndChannelId) {
diff --git a/pw_rpc/callback_test.cc b/pw_rpc/callback_test.cc
index 76f15a2cc..80e0cb4cc 100644
--- a/pw_rpc/callback_test.cc
+++ b/pw_rpc/callback_test.cc
@@ -16,8 +16,8 @@
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread/thread.h"
#include "pw_thread/yield.h"
@@ -28,6 +28,12 @@ using namespace std::chrono_literals;
using test::pw_rpc::raw::TestService;
+// These tests cover interactions between a thread moving or destroying an RPC
+// call object and a thread running callbacks for that call. In order to test
+// that the first thread waits for callbacks to complete when trying to move or
+// destroy the call, it is necessary to have the callback thread yield to the
+// other thread. There isn't a good way to synchronize these threads without
+// changing the code under test.
void YieldToOtherThread() {
// Sleep for a while and then yield just to be sure the other thread runs.
this_thread::sleep_for(100ms);
@@ -38,6 +44,8 @@ class CallbacksTest : public ::testing::Test {
protected:
CallbacksTest()
: callback_thread_(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with
+ // TestThreadContext.
thread::test::TestOptionsThread0(),
[](void* arg) {
static_cast<CallbacksTest*>(arg)->SendResponseAfterSemaphore();
@@ -58,7 +66,7 @@ class CallbacksTest : public ::testing::Test {
thread::Thread callback_thread_;
- // Must be set to true by the RPC callback in each test.
+ // Must be incremented exactly once by the RPC callback in each test.
volatile int callback_executed_ = 0;
// Variables optionally used by tests. These are in this object so lambads
@@ -81,11 +89,11 @@ class CallbacksTest : public ::testing::Test {
};
TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
- // Skip this test if locks are disabled because the thread can't yield.
if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
callback_thread_sem_.release();
callback_thread_.join();
- GTEST_SKIP();
+ GTEST_SKIP()
+ << "Skipping because locks are disabled, so this thread cannot yield.";
}
{
@@ -126,11 +134,11 @@ TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
}
TEST_F(CallbacksTest, MoveActiveCall_WaitsForCallbackToComplete) {
- // Skip this test if locks are disabled because the thread can't yield.
if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
callback_thread_sem_.release();
callback_thread_.join();
- GTEST_SKIP();
+ GTEST_SKIP()
+ << "Skipping because locks are disabled, so this thread cannot yield.";
}
call_1_ = TestService::TestBidirectionalStreamRpc(
@@ -231,7 +239,8 @@ TEST_F(CallbacksTest, PacketDroppedIfOnNextIsBusy) {
main_thread_sem_.acquire(); // Confirm that the callback is running
- // Handle a few packets for this call, which should be dropped.
+ // Handle a few packets for this call, which should be dropped since on_next
+ // is busy. callback_executed_ should remain at 1.
for (int i = 0; i < 5; ++i) {
context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
{}, call_1_.id());
diff --git a/pw_rpc/client.cc b/pw_rpc/client.cc
index 006264220..4cf26dca6 100644
--- a/pw_rpc/client.cc
+++ b/pw_rpc/client.cc
@@ -42,7 +42,8 @@ Status Client::ProcessPacket(ConstByteSpan data) {
if (channel == nullptr) {
internal::rpc_lock().unlock();
- PW_LOG_WARN("RPC client received a packet for an unregistered channel");
+ PW_LOG_WARN("RPC client received a packet for an unregistered channel: %lu",
+ static_cast<unsigned long>(packet.channel_id()));
return Status::Unavailable();
}
@@ -88,7 +89,7 @@ Status Client::ProcessPacket(ConstByteSpan data) {
case PacketType::REQUEST:
case PacketType::CLIENT_STREAM:
case PacketType::CLIENT_ERROR:
- case PacketType::CLIENT_STREAM_END:
+ case PacketType::CLIENT_REQUEST_COMPLETION:
default:
internal::rpc_lock().unlock();
PW_LOG_WARN("pw_rpc client unable to handle packet of type %u",
diff --git a/pw_rpc/client_call.cc b/pw_rpc/client_call.cc
index 70ca4344a..612f13c99 100644
--- a/pw_rpc/client_call.cc
+++ b/pw_rpc/client_call.cc
@@ -16,13 +16,6 @@
namespace pw::rpc::internal {
-void ClientCall::CloseClientCall() {
- if (client_stream_open()) {
- CloseClientStreamLocked().IgnoreError();
- }
- UnregisterAndMarkClosed();
-}
-
void ClientCall::MoveClientCallFrom(ClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
WaitUntilReadyForMove(*this, other);
diff --git a/pw_rpc/client_integration_test.cc b/pw_rpc/client_integration_test.cc
index 53bff084a..8f1021176 100644
--- a/pw_rpc/client_integration_test.cc
+++ b/pw_rpc/client_integration_test.cc
@@ -14,6 +14,8 @@
#include <sys/socket.h>
+#include <algorithm>
+#include <array>
#include <cstring>
#include "gtest/gtest.h"
@@ -47,28 +49,35 @@ Benchmark::Client kServiceClient(pw::rpc::integration_test::client(),
class StringReceiver {
public:
const char* Wait() {
- PW_CHECK(sem_.try_acquire_for(1500ms));
- return buffer_;
+ PW_CHECK(sem_.try_acquire_for(10s));
+ return reinterpret_cast<const char*>(buffer_.begin());
}
Function<void(ConstByteSpan, Status)> UnaryOnCompleted() {
- return [this](ConstByteSpan data, Status) { CopyPayload(data); };
+ return [this](ConstByteSpan data, Status) { CopyStringPayload(data); };
}
Function<void(ConstByteSpan)> OnNext() {
- return [this](ConstByteSpan data) { CopyPayload(data); };
+ return [this](ConstByteSpan data) { CopyStringPayload(data); };
}
- private:
- void CopyPayload(ConstByteSpan data) {
- std::memset(buffer_, 0, sizeof(buffer_));
- PW_CHECK_UINT_LE(data.size(), sizeof(buffer_));
- std::memcpy(buffer_, data.data(), data.size());
+ void CopyStringPayload(ConstByteSpan data) {
+ std::memset(buffer_.data(), 0, buffer_.size());
+ PW_CHECK_UINT_LE(data.size(), buffer_.size());
+ std::copy(data.begin(), data.end(), buffer_.begin());
sem_.release();
}
+ void ReverseCopyStringPayload(ConstByteSpan data) {
+ std::memset(buffer_.data(), 0, buffer_.size());
+ PW_CHECK_UINT_LE(data.size(), buffer_.size());
+ std::reverse_copy(data.begin(), data.end() - 1, buffer_.begin());
+ sem_.release();
+ }
+
+ private:
pw::sync::BinarySemaphore sem_;
- char buffer_[64];
+ std::array<std::byte, 64> buffer_;
};
TEST(RawRpcIntegrationTest, Unary) {
@@ -96,6 +105,38 @@ TEST(RawRpcIntegrationTest, BidirectionalStreaming) {
}
}
+// This test sometimes fails due to a server stream packet being dropped.
+// TODO: b/290048137 - Enable this test after the flakiness is fixed.
+TEST(RawRpcIntegrationTest, DISABLED_OnNextOverwritesItsOwnCall) {
+ for (int i = 0; i < kIterations; ++i) {
+ struct {
+ StringReceiver receiver;
+ pw::rpc::RawClientReaderWriter call;
+ } ctx;
+
+ // Chain together three calls. The first and third copy the string in normal
+ // order, while the second copies the string in reverse order.
+ ctx.call = kServiceClient.BidirectionalEcho([&ctx](ConstByteSpan data) {
+ ctx.call = kServiceClient.BidirectionalEcho([&ctx](ConstByteSpan data) {
+ ctx.receiver.ReverseCopyStringPayload(data);
+ ctx.call = kServiceClient.BidirectionalEcho(ctx.receiver.OnNext());
+ });
+ ctx.receiver.CopyStringPayload(data);
+ });
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Window"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "Window");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Door"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "rooD");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Roof"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "Roof");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Cancel());
+ }
+}
+
} // namespace
} // namespace rpc_test
diff --git a/pw_rpc/client_streaming_rpc.svg b/pw_rpc/client_streaming_rpc.svg
deleted file mode 100644
index 37155fdae..000000000
--- a/pw_rpc/client_streaming_rpc.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="614.9000000000001" viewBox="0 0 594 559" width="653.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Client Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="231" y="153"></rect>
-<rect fill="rgb(0,0,0)" height="308" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="423" y="153"></rect>
-<polygon fill="rgb(0,0,0)" points="87,126 211,126 219,134 219,180 87,180 87,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="51,220 211,220 219,228 219,287 51,287 51,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="27,327 211,327 219,335 219,381 27,381 27,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,421 573,421 581,429 581,501 443,501 443,421" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="171" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="363" y="46"></rect>
-<path d="M 232 80 L 232 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="228" y="147"></rect>
-<path d="M 424 80 L 424 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="308" stroke="rgb(0,0,0)" width="8" x="420" y="147"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="168" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="232.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="360" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="424.0" y="66">server</text>
-<path d="M 240 147 L 416 147" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,143 416,147 408,151" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="84,120 208,120 216,128 216,174 84,174 84,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 120 L 208 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 128 L 216 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="146.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="119.0" y="172">method ID</text>
-<path d="M 240 247 L 416 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="408,243 416,247 408,251" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="48,214 208,214 216,222 216,281 48,281 48,214" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 214 L 208 222" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 222 L 216 222" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="128.0" y="227">PacketType.CLIENT_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="240">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="253">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="83.0" y="266">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="77.0" y="279">payload</text>
-<path d="M 240 348 L 416 348" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,344 416,348 408,352" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,321 208,321 216,329 216,375 24,375 24,321" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 321 L 208 329" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 329 L 216 329" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="168" x="116.0" y="334">PacketType.CLIENT_STREAM_END</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="347">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="360">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="373">method ID</text>
-<path d="M 240 455 L 416 455" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="248,451 240,455 248,459" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,415 570,415 578,423 578,495 440,495 440,415" stroke="rgb(0,0,0)"></polygon>
-<path d="M 570 415 L 570 423" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 570 423 L 578 423" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="505.0" y="428">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="441">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="454">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="467">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="469.0" y="480">payload</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="466.0" y="493">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="30" x="259.0" y="145">start</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="313.0" y="245">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="256.0" y="346">done</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="48" x="392.0" y="453">response</text>
-</svg>
diff --git a/pw_rpc/client_streaming_rpc_cancelled.svg b/pw_rpc/client_streaming_rpc_cancelled.svg
deleted file mode 100644
index 9542ed2df..000000000
--- a/pw_rpc/client_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,76 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 598 452" width="598px" height="452px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"/>
- </filter>
- </defs>
- <title>Cancelled Client Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "start",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID"
- ];
-
- client --&gt; server [
- noactivate,
- label = "messages (zero or more)",
- leftnote = "PacketType.CLIENT_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- rightnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="277" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="209" y="153"/>
- <rect fill="rgb(0,0,0)" height="277" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="401" y="153"/>
- <polygon fill="rgb(0,0,0)" points="64,126 189,126 197,134 197,180 64,180 64,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="27,220 189,220 197,228 197,287 27,287 27,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="421,327 577,327 585,335 585,394 421,394 421,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="149" y="46"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="341" y="46"/>
- <path d="M 210 80 L 210 440" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="277" stroke="rgb(0,0,0)" width="8" x="206" y="147"/>
- <path d="M 402 80 L 402 440" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="277" stroke="rgb(0,0,0)" width="8" x="398" y="147"/>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="146" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="210.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="338" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="402.5" y="66">server</text>
- <path d="M 218 147 L 394 147" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,143 394,147 386,151" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="61,120 186,120 194,128 194,174 61,174 61,120" stroke="rgb(0,0,0)"/>
- <path d="M 186 120 L 186 128" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 128 L 194 128" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="123.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="96.5" y="172">method ID</text>
- <path d="M 218 247 L 394 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="386,243 394,247 386,251" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="24,214 186,214 194,222 194,281 24,281 24,214" stroke="rgb(0,0,0)"/>
- <path d="M 186 214 L 186 222" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 222 L 194 222" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="105.0" y="227">PacketType.CLIENT_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="240">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="266">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="53.5" y="279">payload</text>
- <path d="M 218 354 L 394 354" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,350 394,354 386,358" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="418,321 574,321 582,329 582,388 418,388 418,321" stroke="rgb(0,0,0)"/>
- <path d="M 574 321 L 574 329" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 574 329 L 582 329" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="496.0" y="334">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="347">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="360">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="453.5" y="373">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="474.5" y="386">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="31" x="237.5" y="145">start</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="292.0" y="245">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="240.5" y="352">cancel</text>
-</svg>
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 83bd6d558..093e52a78 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -11,20 +11,20 @@ This document discusses the ``pw_rpc`` protocol and its C++ implementation.
documents:
.. toctree::
- :maxdepth: 1
+ :maxdepth: 1
- py/docs
- ts/docs
+ py/docs
+ ts/docs
.. admonition:: Try it out!
- For a quick intro to ``pw_rpc``, see the
- :ref:`module-pw_hdlc-rpc-example` in the :ref:`module-pw_hdlc` module.
+ For a quick intro to ``pw_rpc``, see the
+ :ref:`module-pw_hdlc-rpc-example` in the :ref:`module-pw_hdlc` module.
.. warning::
- This documentation is under construction. Many sections are outdated or
- incomplete. The content needs to be reorgnanized.
+ This documentation is under construction. Many sections are outdated or
+ incomplete. The content needs to be reorgnanized.
---------------
Implementations
@@ -56,6 +56,14 @@ Pigweed provides several client and server implementations of ``pw_rpc``.
-
- in development
+.. warning::
+
+ ``pw_protobuf`` and ``nanopb`` RPC services cannot currently coexist within the
+ same RPC server. Unless you are running multiple RPC servers, you cannot
+ incrementally migrate services from one protobuf implementation to another,
+ or otherwise mix and match. See
+ `Issue 234874320 <https://issues.pigweed.dev/issues/234874320>`_.
+
-------------
RPC semantics
-------------
@@ -68,6 +76,7 @@ In ``pw_rpc``, an RPC begins when the client sends an initial packet. The server
receives the packet, looks up the relevant service method, then calls into the
RPC function. The RPC is considered active until the server sends a status to
finish the RPC. The client may terminate an ongoing RPC by cancelling it.
+Multiple RPC requests to the same method may be made simultaneously.
Depending the type of RPC, the client and server exchange zero or more protobuf
request or response payloads. There are four RPC types:
@@ -96,10 +105,10 @@ The key events in the RPC lifecycle are:
only one response and it is handled when the RPC completes.
* **Error**. The server or client terminates the RPC abnormally with a status.
The receiving endpoint calls a callback.
-* **Client stream end**. The client sends a message that it has finished sending
- requests. The server calls a callback when it receives it. Some servers may
- ignore the client stream end. The client stream end is only relevant for
- client and bidirectional streaming RPCs.
+* **Request Completion**. The client sends a message that it would like to
+ request call completion. The server calls a callback when it receives it. Some
+ servers may ignore the request completion message. In client and bidirectional
+ streaming RPCs, this also indicates that client has finished sending requests.
Status codes
============
@@ -153,65 +162,65 @@ Pigweed RPCs are declared in a protocol buffer service definition.
.. code-block:: protobuf
- syntax = "proto3";
+ syntax = "proto3";
- package foo.bar;
+ package foo.bar;
- message Request {}
+ message Request {}
- message Response {
- int32 number = 1;
- }
+ message Response {
+ int32 number = 1;
+ }
- service TheService {
- rpc MethodOne(Request) returns (Response) {}
- rpc MethodTwo(Request) returns (stream Response) {}
- }
+ service TheService {
+ rpc MethodOne(Request) returns (Response) {}
+ rpc MethodTwo(Request) returns (stream Response) {}
+ }
This protocol buffer is declared in a ``BUILD.gn`` file as follows:
.. code-block:: python
- import("//build_overrides/pigweed.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("//build_overrides/pigweed.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- pw_proto_library("the_service_proto") {
- sources = [ "foo_bar/the_service.proto" ]
- }
+ pw_proto_library("the_service_proto") {
+ sources = [ "foo_bar/the_service.proto" ]
+ }
.. 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.
+ 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.
+ 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 ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
- depending on the library.
+ 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 ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
+ depending on the library.
- Optional fields have some overhead --- if using Nanopb, default-valued fields
- are included in the encoded proto, and the proto structs have a
- ``has_<field>`` flag for each optional field. Use plain fields if field
- presence detection is not needed.
+ Optional fields have some overhead --- if using Nanopb, default-valued fields
+ are included in the encoded proto, and 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
+ .. code-block:: protobuf
- syntax = "proto3";
+ syntax = "proto3";
- message MyMessage {
- // Leaving this field unset is equivalent to setting it to 0.
- int32 number = 1;
+ 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;
- }
+ // Setting this field to 0 is different from leaving it unset.
+ optional int32 other_number = 2;
+ }
2. RPC code generation
======================
@@ -248,64 +257,64 @@ 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.
+ 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:
+ To use the stubs, do the following:
- #. Locate the generated RPC header in the build directory. For example:
+ #. Locate the generated RPC header in the build directory. For example:
- .. code-block:: sh
+ .. code-block:: sh
- find out/ -name <proto_name>.rpc.pwpb.h
+ find out/ -name <proto_name>.rpc.pwpb.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``.
+ #. 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 pw_protobuf implementation of this service would be as follows:
.. code-block:: cpp
- #include "foo_bar/the_service.rpc.pwpb.h"
+ #include "foo_bar/the_service.rpc.pwpb.h"
- namespace foo::bar {
+ namespace foo::bar {
- class TheService : public pw_rpc::pwpb::TheService::Service<TheService> {
- public:
- pw::Status MethodOne(const Request::Message& request,
- Response::Message& response) {
- // implementation
- response.number = 123;
- return pw::OkStatus();
- }
+ class TheService : public pw_rpc::pwpb::TheService::Service<TheService> {
+ public:
+ pw::Status MethodOne(const Request::Message& request,
+ Response::Message& response) {
+ // implementation
+ response.number = 123;
+ return pw::OkStatus();
+ }
- void MethodTwo(const Request::Message& request,
- ServerWriter<Response::Message>& response) {
- // implementation
- response.Write({.number = 123});
- }
- };
+ void MethodTwo(const Request::Message& request,
+ ServerWriter<Response::Message>& response) {
+ // implementation
+ response.Write({.number = 123});
+ }
+ };
- } // namespace foo::bar
+ } // namespace foo::bar
The pw_protobuf implementation would be declared in a ``BUILD.gn``:
.. code-block:: python
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_build/target_types.gni")
- pw_source_set("the_service") {
- public_configs = [ ":public" ]
- public = [ "public/foo_bar/service.h" ]
- public_deps = [ ":the_service_proto.pwpb_rpc" ]
- }
+ pw_source_set("the_service") {
+ public_configs = [ ":public" ]
+ public = [ "public/foo_bar/service.h" ]
+ public_deps = [ ":the_service_proto.pwpb_rpc" ]
+ }
4. Register the service with a server
=====================================
@@ -314,40 +323,40 @@ channel output and the example service.
.. code-block:: cpp
- // Set up the output channel for the pw_rpc server to use. This configures the
- // pw_rpc server to use HDLC over UART; projects not using UART and HDLC must
- // adapt this as necessary.
- pw::stream::SysIoWriter writer;
- pw::rpc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
- writer, pw::hdlc::kDefaultRpcAddress, "HDLC output");
+ // Set up the output channel for the pw_rpc server to use. This configures the
+ // pw_rpc server to use HDLC over UART; projects not using UART and HDLC must
+ // adapt this as necessary.
+ pw::stream::SysIoWriter writer;
+ pw::rpc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ writer, pw::hdlc::kDefaultRpcAddress, "HDLC output");
- // Allocate an array of channels for the server to use. If dynamic allocation
- // is enabled (PW_RPC_DYNAMIC_ALLOCATION=1), the server can be initialized
- // without any channels, and they can be added later.
- pw::rpc::Channel channels[] = {
- pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+ // Allocate an array of channels for the server to use. If dynamic allocation
+ // is enabled (PW_RPC_DYNAMIC_ALLOCATION=1), the server can be initialized
+ // without any channels, and they can be added later.
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
- // Declare the pw_rpc server with the HDLC channel.
- pw::rpc::Server server(channels);
+ // Declare the pw_rpc server with the HDLC channel.
+ pw::rpc::Server server(channels);
- foo::bar::TheService the_service;
- pw::rpc::SomeOtherService some_other_service;
+ foo::bar::TheService the_service;
+ pw::rpc::SomeOtherService some_other_service;
- void RegisterServices() {
- // Register the foo.bar.TheService example service and another service.
- server.RegisterService(the_service, some_other_service);
- }
+ void RegisterServices() {
+ // Register the foo.bar.TheService example service and another service.
+ server.RegisterService(the_service, some_other_service);
+ }
- int main() {
- // Set up the server.
- RegisterServices();
+ int main() {
+ // Set up the server.
+ RegisterServices();
- // Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+ // Declare a buffer for decoding incoming HDLC frames.
+ std::array<std::byte, kMaxTransmissionUnit> input_buffer;
- PW_LOG_INFO("Starting pw_rpc server");
- pw::hdlc::ReadAndProcessPackets(server, input_buffer);
- }
+ PW_LOG_INFO("Starting pw_rpc server");
+ pw::hdlc::ReadAndProcessPackets(server, input_buffer);
+ }
--------
Channels
@@ -360,22 +369,22 @@ 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);
+ // 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,
- };
+ // 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 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;
+ // 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;
Sometimes, the ID and output of a channel are not known at compile time as they
depend on information stored on the physical device. To support this use case, a
@@ -384,13 +393,13 @@ output.
.. code-block:: cpp
- // Create a dynamic channel without a compile-time ID or output.
- pw::rpc::Channel dynamic_channel;
+ // Create a dynamic channel without a compile-time ID or output.
+ pw::rpc::Channel dynamic_channel;
- void Init() {
- // Called during boot to pull the channel configuration from the system.
- dynamic_channel.Configure(GetChannelId(), some_output);
- }
+ void Init() {
+ // Called during boot to pull the channel configuration from the system.
+ dynamic_channel.Configure(GetChannelId(), some_output);
+ }
Adding and removing channels
============================
@@ -407,9 +416,9 @@ with the ``ABORTED`` status.
.. code-block:: cpp
- // When a channel is closed, any pending calls will receive
- // on_error callbacks with ABORTED status.
- client->CloseChannel(1);
+ // When a channel is closed, any pending calls will receive
+ // on_error callbacks with ABORTED status.
+ client->CloseChannel(1);
--------
Services
@@ -432,10 +441,10 @@ Protobuf library APIs
---------------------
.. toctree::
- :maxdepth: 1
+ :maxdepth: 1
- pwpb/docs
- nanopb/docs
+ pwpb/docs
+ nanopb/docs
----------------------------
Testing a pw_rpc integration
@@ -445,35 +454,35 @@ working as intended by registering the provided ``EchoService``, defined in
``echo.proto``, which echoes back a message that it receives.
.. literalinclude:: echo.proto
- :language: protobuf
- :lines: 14-
+ :language: protobuf
+ :lines: 14-
For example, in C++ with pw_protobuf:
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/server.h"
+ #include "pw_rpc/server.h"
- // Include the apporpriate header for your protobuf library.
- #include "pw_rpc/echo_service_pwpb.h"
+ // Include the apporpriate header for your protobuf library.
+ #include "pw_rpc/echo_service_pwpb.h"
- constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
- static pw::rpc::Server server(kChannels);
+ constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
+ static pw::rpc::Server server(kChannels);
- static pw::rpc::EchoService echo_service;
+ static pw::rpc::EchoService echo_service;
- void Init() {
- server.RegisterService(echo_service);
- }
+ void Init() {
+ server.RegisterService(echo_service);
+ }
Benchmarking and stress testing
===============================
.. toctree::
- :maxdepth: 1
- :hidden:
+ :maxdepth: 1
+ :hidden:
- benchmark
+ benchmark
``pw_rpc`` provides an RPC service and Python module for stress testing and
benchmarking a ``pw_rpc`` deployment. See :ref:`module-pw_rpc-benchmark`.
@@ -507,27 +516,27 @@ For example, a service for accessing a file system should simply be named
.. code-block:: protobuf
- // file.proto
- package pw.file;
+ // file.proto
+ package pw.file;
- service FileSystem {
- rpc List(ListRequest) returns (stream ListResponse);
- }
+ service FileSystem {
+ rpc List(ListRequest) returns (stream ListResponse);
+ }
The C++ service implementation class may append "Service" to the name.
.. code-block:: cpp
- // file_system_service.h
- #include "pw_file/file.raw_rpc.pb.h"
+ // file_system_service.h
+ #include "pw_file/file.raw_rpc.pb.h"
- namespace pw::file {
+ namespace pw::file {
- class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
- void List(ConstByteSpan request, RawServerWriter& writer);
- };
+ class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
+ void List(ConstByteSpan request, RawServerWriter& writer);
+ };
- } // namespace pw::file
+ } // namespace pw::file
For upstream Pigweed services, this naming style is a requirement. Note that
some services created before this was established may use non-compliant
@@ -553,22 +562,22 @@ message payload size.
.. code-block:: cpp
- #include "pw_file/file.raw_rpc.pb.h"
- #include "pw_rpc/channel.h"
+ #include "pw_file/file.raw_rpc.pb.h"
+ #include "pw_rpc/channel.h"
- namespace pw::file {
+ namespace pw::file {
- class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
- public:
- void List(ConstByteSpan request, RawServerWriter& writer);
+ class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
+ public:
+ void List(ConstByteSpan request, RawServerWriter& writer);
- private:
- // Allocate a buffer for building proto responses.
- static constexpr size_t kEncodeBufferSize = pw::rpc::MaxSafePayloadSize();
- std::array<std::byte, kEncodeBufferSize> encode_buffer_;
- };
+ private:
+ // Allocate a buffer for building proto responses.
+ static constexpr size_t kEncodeBufferSize = pw::rpc::MaxSafePayloadSize();
+ std::array<std::byte, kEncodeBufferSize> encode_buffer_;
+ };
- } // namespace pw::file
+ } // namespace pw::file
--------------------
Protocol description
@@ -584,8 +593,8 @@ encoded as protocol buffers. The full packet format is described in
``pw_rpc/pw_rpc/internal/packet.proto``.
.. literalinclude:: internal/packet.proto
- :language: protobuf
- :lines: 14-
+ :language: protobuf
+ :lines: 14-
The packet type and RPC type determine which fields are present in a Pigweed RPC
packet. Each packet type is only sent by either the client or the server.
@@ -593,53 +602,53 @@ These tables describe the meaning of and fields included with each packet type.
Client-to-server packets
------------------------
-+-------------------+-------------------------------------+
-| packet type | description |
-+===================+=====================================+
-| REQUEST | Invoke an RPC |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - payload |
-| | (unary & server streaming only) |
-| | - call_id (optional) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_STREAM | Message in a client stream |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - payload |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_STREAM_END | Client stream is complete |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_ERROR | Abort an ongoing RPC |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - status |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
++---------------------------+-------------------------------------+
+| packet type | description |
++===========================+=====================================+
+| REQUEST | Invoke an RPC |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - payload |
+| | (unary & server streaming only) |
+| | - call_id (optional) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_STREAM | Message in a client stream |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - payload |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_REQUEST_COMPLETION | Client requested stream completion |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_ERROR | Abort an ongoing RPC |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - status |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
**Client errors**
@@ -730,14 +739,14 @@ client streaming, and bidirectional streaming.
The basic flow for all RPC invocations is as follows:
- * Client sends a ``REQUEST`` packet. Includes a payload for unary & server
- streaming RPCs.
- * For client and bidirectional streaming RPCs, the client may send any number
- of ``CLIENT_STREAM`` packets with payloads.
- * For server and bidirectional streaming RPCs, the server may send any number
- of ``SERVER_STREAM`` packets.
- * The server sends a ``RESPONSE`` packet. Includes a payload for unary &
- client streaming RPCs. The RPC is complete.
+* Client sends a ``REQUEST`` packet. Includes a payload for unary & server
+ streaming RPCs.
+* For client and bidirectional streaming RPCs, the client may send any number of
+ ``CLIENT_STREAM`` packets with payloads.
+* For server and bidirectional streaming RPCs, the server may send any number of
+ ``SERVER_STREAM`` packets.
+* The server sends a ``RESPONSE`` packet. Includes a payload for unary & client
+ streaming RPCs. The RPC is complete.
The client may cancel an ongoing RPC at any time by sending a ``CLIENT_ERROR``
packet with status ``CANCELLED``. The server may finish an ongoing RPC at any
@@ -748,59 +757,183 @@ Unary RPC
In a unary RPC, the client sends a single request and the server sends a single
response.
-.. image:: unary_rpc.svg
+.. mermaid::
+ :alt: Unary RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S->>-C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
The client may attempt to cancel a unary RPC by sending a ``CLIENT_ERROR``
packet with status ``CANCELLED``. The server sends no response to a cancelled
RPC. If the server processes the unary RPC synchronously (the handling thread
sends the response), it may not be possible to cancel the RPC.
-.. image:: unary_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Unary RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note left of C: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
+
Server streaming RPC
--------------------
In a server streaming RPC, the client sends a single request and the server
sends any number of ``SERVER_STREAM`` packets followed by a ``RESPONSE`` packet.
-.. image:: server_streaming_rpc.svg
+.. mermaid::
+ :alt: Server Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S->>-C: done
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>status
+
The client may terminate a server streaming RPC by sending a ``CLIENT_STREAM``
packet with status ``CANCELLED``. The server sends no response.
-.. image:: server_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Server Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note left of C: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
+
Client streaming RPC
--------------------
In a client streaming RPC, the client starts the RPC by sending a ``REQUEST``
packet with no payload. It then sends any number of messages in
-``CLIENT_STREAM`` packets, followed by a ``CLIENT_STREAM_END``. The server sends
+``CLIENT_STREAM`` packets, followed by a ``CLIENT_REQUEST_COMPLETION``. The server sends
a single ``RESPONSE`` to finish the RPC.
-.. image:: client_streaming_rpc.svg
+.. mermaid::
+ :alt: Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
The server may finish the RPC at any time by sending its ``RESPONSE`` packet,
-even if it has not yet received the ``CLIENT_STREAM_END`` packet. The client may
+even if it has not yet received the ``CLIENT_REQUEST_COMPLETION`` packet. The client may
terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
``CANCELLED``.
-.. image:: client_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note right of S: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
Bidirectional streaming RPC
---------------------^^^^^^^
+---------------------------
In a bidirectional streaming RPC, the client sends any number of requests and
the server sends any number of responses. The client invokes the RPC by sending
-a ``REQUEST`` with no payload. It sends a ``CLIENT_STREAM_END`` packet when it
+a ``REQUEST`` with no payload. It sends a ``CLIENT_REQUEST_COMPLETION`` packet when it
has finished sending requests. The server sends a ``RESPONSE`` packet to finish
the RPC.
-.. image:: bidirectional_streaming_rpc.svg
+.. mermaid::
+ :alt: Bidirectional Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->S: (messages in any order)
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: done
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>status
+
The server may finish the RPC at any time by sending the ``RESPONSE`` packet,
-even if it has not received the ``CLIENT_STREAM_END`` packet. The client may
+even if it has not received the ``CLIENT_REQUEST_COMPLETION`` packet. The client may
terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
``CANCELLED``.
-.. image:: bidirectional_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
-------
C++ API
@@ -812,7 +945,7 @@ Declare an instance of ``rpc::Server`` and register services with it.
.. admonition:: TODO
- Document the public interface
+ Document the public interface
Size report
-----------
@@ -851,12 +984,47 @@ Packet flow
Requests
........
-.. image:: request_packets.svg
+.. mermaid::
+ :alt: Request Packet Flow
+
+ flowchart LR
+ packets[Packets]
+
+ subgraph pw_rpc [pw_rpc Library]
+ direction TB
+ internalMethod[[internal::Method]]
+ Server --> Service --> internalMethod
+ end
+
+ packets --> Server
+
+ generatedServices{{generated services}}
+ userDefinedRPCs(user-defined RPCs)
+
+ generatedServices --> userDefinedRPCs
+ internalMethod --> generatedServices
Responses
.........
-.. image:: response_packets.svg
+.. mermaid::
+ :alt: Request Packet Flow
+
+ flowchart LR
+ generatedServices{{generated services}}
+ userDefinedRPCs(user-defined RPCs)
+
+ subgraph pw_rpc [pw_rpc Library]
+ direction TB
+ internalMethod[[internal::Method]]
+ internalMethod --> Server --> Channel
+ end
+
+ packets[Packets]
+ Channel --> packets
+
+ userDefinedRPCs --> generatedServices
+ generatedServices --> internalMethod
RPC client
==========
@@ -872,22 +1040,22 @@ clients cannot use the same channels.
To send incoming RPC packets from the transport layer to be processed by a
client, the client's ``ProcessPacket`` function is called with the packet data.
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/client.h"
+ #include "pw_rpc/client.h"
- namespace {
+ namespace {
- pw::rpc::Channel my_channels[] = {
- pw::rpc::Channel::Create<1>(&my_channel_output)};
- pw::rpc::Client my_client(my_channels);
+ pw::rpc::Channel my_channels[] = {
+ pw::rpc::Channel::Create<1>(&my_channel_output)};
+ pw::rpc::Client my_client(my_channels);
- } // namespace
+ } // namespace
- // Called when the transport layer receives an RPC packet.
- void ProcessRpcPacket(ConstByteSpan packet) {
- my_client.ProcessPacket(packet);
- }
+ // Called when the transport layer receives an RPC packet.
+ void ProcessRpcPacket(ConstByteSpan packet) {
+ my_client.ProcessPacket(packet);
+ }
Note that client processing such as callbacks will be invoked within
the body of ``ProcessPacket``.
@@ -915,54 +1083,55 @@ the ongoing RPC call, and can be used to manage it. An RPC call is only active
as long as its call object is alive.
.. tip::
- Use ``std::move`` when passing around call objects to keep RPCs alive.
+
+ Use ``std::move`` when passing around call objects to keep RPCs alive.
Example
^^^^^^^
.. code-block:: c++
- #include "pw_rpc/echo_service_nanopb.h"
-
- namespace {
- // Generated clients are namespaced with their proto library.
- using EchoClient = pw_rpc::nanopb::EchoService::Client;
-
- // RPC channel ID on which to make client calls. RPC calls cannot be made on
- // channel 0 (Channel::kUnassignedChannelId).
- constexpr uint32_t kDefaultChannelId = 1;
-
- pw::rpc::NanopbUnaryReceiver<pw_rpc_EchoMessage> echo_call;
-
- // Callback invoked when a response is received. This is called synchronously
- // from Client::ProcessPacket.
- void EchoResponse(const pw_rpc_EchoMessage& response,
- pw::Status status) {
- if (status.ok()) {
- PW_LOG_INFO("Received echo response: %s", response.msg);
- } else {
- PW_LOG_ERROR("Echo failed with status %d",
- static_cast<int>(status.code()));
- }
- }
-
- } // namespace
-
- void CallEcho(const char* message) {
- // Create a client to call the EchoService.
- EchoClient echo_client(my_rpc_client, kDefaultChannelId);
-
- pw_rpc_EchoMessage request{};
- pw::string::Copy(message, request.msg);
-
- // By assigning the returned call to the global echo_call, the RPC
- // call is kept alive until it completes. When a response is received, it
- // will be logged by the handler function and the call will complete.
- echo_call = echo_client.Echo(request, EchoResponse);
- if (!echo_call.active()) {
- // The RPC call was not sent. This could occur due to, for example, an
- // invalid channel ID. Handle if necessary.
- }
- }
+ #include "pw_rpc/echo_service_nanopb.h"
+
+ namespace {
+ // Generated clients are namespaced with their proto library.
+ using EchoClient = pw_rpc::nanopb::EchoService::Client;
+
+ // RPC channel ID on which to make client calls. RPC calls cannot be made on
+ // channel 0 (Channel::kUnassignedChannelId).
+ constexpr uint32_t kDefaultChannelId = 1;
+
+ pw::rpc::NanopbUnaryReceiver<pw_rpc_EchoMessage> echo_call;
+
+ // Callback invoked when a response is received. This is called synchronously
+ // from Client::ProcessPacket.
+ void EchoResponse(const pw_rpc_EchoMessage& response,
+ pw::Status status) {
+ if (status.ok()) {
+ PW_LOG_INFO("Received echo response: %s", response.msg);
+ } else {
+ PW_LOG_ERROR("Echo failed with status %d",
+ static_cast<int>(status.code()));
+ }
+ }
+
+ } // namespace
+
+ void CallEcho(const char* message) {
+ // Create a client to call the EchoService.
+ EchoClient echo_client(my_rpc_client, kDefaultChannelId);
+
+ pw_rpc_EchoMessage request{};
+ pw::string::Copy(message, request.msg);
+
+ // By assigning the returned call to the global echo_call, the RPC
+ // call is kept alive until it completes. When a response is received, it
+ // will be logged by the handler function and the call will complete.
+ echo_call = echo_client.Echo(request, EchoResponse);
+ if (!echo_call.active()) {
+ // The RPC call was not sent. This could occur due to, for example, an
+ // invalid channel ID. Handle if necessary.
+ }
+ }
Call objects
============
@@ -974,23 +1143,23 @@ server or client.
The public call types are as follows:
.. list-table::
- :header-rows: 1
-
- * - RPC Type
- - Server call
- - Client call
- * - Unary
- - ``(Raw|Nanopb|Pwpb)UnaryResponder``
- - ``(Raw|Nanopb|Pwpb)UnaryReceiver``
- * - Server streaming
- - ``(Raw|Nanopb|Pwpb)ServerWriter``
- - ``(Raw|Nanopb|Pwpb)ClientReader``
- * - Client streaming
- - ``(Raw|Nanopb|Pwpb)ServerReader``
- - ``(Raw|Nanopb|Pwpb)ClientWriter``
- * - Bidirectional streaming
- - ``(Raw|Nanopb|Pwpb)ServerReaderWriter``
- - ``(Raw|Nanopb|Pwpb)ClientReaderWriter``
+ :header-rows: 1
+
+ * - RPC Type
+ - Server call
+ - Client call
+ * - Unary
+ - ``(Raw|Nanopb|Pwpb)UnaryResponder``
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver``
+ * - Server streaming
+ - ``(Raw|Nanopb|Pwpb)ServerWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReader``
+ * - Client streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReader``
+ - ``(Raw|Nanopb|Pwpb)ClientWriter``
+ * - Bidirectional streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReaderWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter``
Client call API
---------------
@@ -998,67 +1167,68 @@ Client call objects provide a few common methods.
.. cpp:class:: pw::rpc::ClientCallType
- The ``ClientCallType`` will be one of the following types:
+ The ``ClientCallType`` will be one of the following types:
- - ``(Raw|Nanopb|Pwpb)UnaryReceiver`` for unary
- - ``(Raw|Nanopb|Pwpb)ClientReader`` for server streaming
- - ``(Raw|Nanopb|Pwpb)ClientWriter`` for client streaming
- - ``(Raw|Nanopb|Pwpb)ClientReaderWriter`` for bidirectional streaming
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver`` for unary
+ - ``(Raw|Nanopb|Pwpb)ClientReader`` for server streaming
+ - ``(Raw|Nanopb|Pwpb)ClientWriter`` for client streaming
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter`` for bidirectional streaming
- .. cpp:function:: bool active() const
+ .. cpp:function:: bool active() const
- Returns true if the call is active.
+ Returns true if the call is active.
- .. cpp:function:: uint32_t channel_id() const
+ .. cpp:function:: uint32_t channel_id() const
- Returns the channel ID of this call, which is 0 if the call is inactive.
+ Returns the channel ID of this call, which is 0 if the call is inactive.
- .. cpp:function:: uint32_t id() const
+ .. cpp:function:: uint32_t id() const
- Returns the call ID, a unique identifier for this call.
+ Returns the call ID, a unique identifier for this call.
- .. cpp:function:: void Write(RequestType)
+ .. cpp:function:: void Write(RequestType)
- Only available on client and bidirectional streaming calls. Sends a stream
- request. Returns:
+ Only available on client and bidirectional streaming calls. Sends a stream
+ request. Returns:
- - ``OK`` - the request was successfully sent
- - ``FAILED_PRECONDITION`` - the writer is closed
- - ``INTERNAL`` - pw_rpc was unable to encode message; does not apply to raw
- calls
- - other errors - the :cpp:class:`ChannelOutput` failed to send the packet;
- the error codes are determined by the :cpp:class:`ChannelOutput`
- implementation
+ - ``OK`` - the request was successfully sent
+ - ``FAILED_PRECONDITION`` - the writer is closed
+ - ``INTERNAL`` - pw_rpc was unable to encode message; does not apply to
+ raw calls
+ - other errors - the :cpp:class:`ChannelOutput` failed to send the packet;
+ the error codes are determined by the :cpp:class:`ChannelOutput`
+ implementation
- .. cpp:function:: pw::Status CloseClientStream()
+ .. cpp:function:: pw::Status RequestCompletion()
- Only available on client and bidirectional streaming calls. Notifies the
- server that no further client stream messages will be sent.
+ Notifies the server that client has requested for call completion. On
+ client and bidirectional streaming calls no further client stream messages
+ will be sent.
- .. cpp:function:: pw::Status Cancel()
+ .. cpp:function:: pw::Status Cancel()
- Cancels this RPC. Closes the call and sends a ``CANCELLED`` error to the
- server. Return statuses are the same as :cpp:func:`Write`.
+ Cancels this RPC. Closes the call and sends a ``CANCELLED`` error to the
+ server. Return statuses are the same as :cpp:func:`Write`.
- .. cpp:function:: void Abandon()
+ .. cpp:function:: void Abandon()
- Closes this RPC locally. Sends a ``CLIENT_STREAM_END``, but no cancellation
- packet. Future packets for this RPC are dropped, and the client sends a
- ``FAILED_PRECONDITION`` error in response because the call is not active.
+ Closes this RPC locally. Sends a ``CLIENT_REQUEST_COMPLETION``, but no cancellation
+ packet. Future packets for this RPC are dropped, and the client sends a
+ ``FAILED_PRECONDITION`` error in response because the call is not active.
- .. cpp:function:: void set_on_completed(pw::Function<void(ResponseTypeIfUnaryOnly, pw::Status)>)
+ .. cpp:function:: void set_on_completed(pw::Function<void(ResponseTypeIfUnaryOnly, pw::Status)>)
- Sets the callback that is called when the RPC completes normally. The
- signature depends on whether the call has a unary or stream response.
+ Sets the callback that is called when the RPC completes normally. The
+ signature depends on whether the call has a unary or stream response.
- .. cpp:function:: void set_on_error(pw::Function<void(pw::Status)>)
+ .. cpp:function:: void set_on_error(pw::Function<void(pw::Status)>)
- Sets the callback that is called when the RPC is terminated due to an error.
+ Sets the callback that is called when the RPC is terminated due to an error.
- .. cpp:function:: void set_on_next(pw::Function<void(ResponseType)>)
+ .. cpp:function:: void set_on_next(pw::Function<void(ResponseType)>)
- Only available on server and bidirectional streaming calls. Sets the callback
- that is called for each stream response.
+ Only available on server and bidirectional streaming calls. Sets the callback
+ that is called for each stream response.
Callbacks
---------
@@ -1066,33 +1236,33 @@ The C++ call objects allow users to set callbacks that are invoked when RPC
`events`_ occur.
.. list-table::
- :header-rows: 1
-
- * - Name
- - Stream signature
- - Non-stream signature
- - Server
- - Client
- * - ``on_error``
- - ``void(pw::Status)``
- - ``void(pw::Status)``
- - ✅
- - ✅
- * - ``on_next``
- - n/a
- - ``void(const PayloadType&)``
- - ✅
- - ✅
- * - ``on_completed``
- - ``void(pw::Status)``
- - ``void(const PayloadType&, pw::Status)``
- -
- - ✅
- * - ``on_client_stream_end``
- - ``void()``
- - n/a
- - ✅ (:c:macro:`optional <PW_RPC_CLIENT_STREAM_END_CALLBACK>`)
- -
+ :header-rows: 1
+
+ * - Name
+ - Stream signature
+ - Non-stream signature
+ - Server
+ - Client
+ * - ``on_error``
+ - ``void(pw::Status)``
+ - ``void(pw::Status)``
+ - ✅
+ - ✅
+ * - ``on_next``
+ - n/a
+ - ``void(const PayloadType&)``
+ - ✅
+ - ✅
+ * - ``on_completed``
+ - ``void(pw::Status)``
+ - ``void(const PayloadType&, pw::Status)``
+ -
+ - ✅
+ * - ``on_client_requested_completion``
+ - ``void()``
+ - n/a
+ - ✅ (:c:macro:`optional <PW_RPC_COMPLETION_REQUEST_CALLBACK>`)
+ -
Limitations and restrictions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1146,9 +1316,9 @@ Example warning for a dropped stream packet:
.. code-block:: text
- WRN Received stream packet for 1:cc0f6de0/31e616ce before the callback for
- a previous packet completed! This packet will be dropped. This can be
- avoided by handling packets for a particular RPC on only one thread.
+ WRN Received stream packet for 1:cc0f6de0/31e616ce before the callback for
+ a previous packet completed! This packet will be dropped. This can be
+ avoided by handling packets for a particular RPC on only one thread.
RPC calls introspection
=======================
@@ -1165,110 +1335,97 @@ We have an RPC service ``SpecialService`` with ``MyMethod`` method:
.. code-block:: protobuf
- package some.package;
- service SpecialService {
- rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
- }
+ package some.package;
+ service SpecialService {
+ rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
+ }
We also have a templated Storage type alias:
.. code-block:: c++
- template <auto kMethod>
- using Storage =
- std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
+ template <auto kMethod>
+ using Storage =
+ std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
``Storage<some::package::pw_rpc::pwpb::SpecialService::MyMethod>`` will
instantiate as:
.. code-block:: c++
- std::pair<some::package::MyMethodRequest::Message,
- some::package::MyMethodResponse::Message>;
+ std::pair<some::package::MyMethodRequest::Message,
+ some::package::MyMethodResponse::Message>;
.. note::
- Only nanopb and pw_protobuf have real types as
- ``MethodRequestType<RpcMethod>``/``MethodResponseType<RpcMethod>``. Raw has
- them both set as ``void``. In reality, they are ``pw::ConstByteSpan``. Any
- helper/trait that wants to use this types for raw methods should do a custom
- implementation that copies the bytes under the span instead of copying just
- the span.
+ Only nanopb and pw_protobuf have real types as
+ ``MethodRequestType<RpcMethod>``/``MethodResponseType<RpcMethod>``. Raw has
+ them both set as ``void``. In reality, they are ``pw::ConstByteSpan``. Any
+ helper/trait that wants to use this types for raw methods should do a custom
+ implementation that copies the bytes under the span instead of copying just
+ the span.
-Client Synchronous Call wrappers
-================================
-If synchronous behavior is desired when making client calls, users can use one
-of the ``SynchronousCall<RpcMethod>`` wrapper functions to make their RPC call.
-These wrappers effectively wrap the asynchronous Client RPC call with a timed
-thread notification and return once a result is known or a timeout has occurred.
-These return a ``SynchronousCallResult<Response>`` object, which can be queried
-to determine whether any error scenarios occurred and, if not, access the
-response.
-
-``SynchronousCall<RpcMethod>`` will block indefinitely, whereas
-``SynchronousCallFor<RpcMethod>`` and ``SynchronousCallUntil<RpcMethod>`` will
-block for a given timeout or until a deadline, respectively. All wrappers work
-with both the standalone static RPC functions and the generated Client member
-methods.
+.. _module-pw_rpc-client-sync-call-wrappers:
-.. note:: Use of the SynchronousCall wrappers requires a TimedThreadNotification
- backend.
-.. note:: Only nanopb and pw_protobuf Unary RPC methods are supported.
+Client synchronous call wrappers
+================================
+.. doxygenfile:: pw_rpc/synchronous_call.h
+ :sections: detaileddescription
Example
-------
.. code-block:: c++
- #include "pw_rpc/synchronous_call.h"
-
- void InvokeUnaryRpc() {
- pw::rpc::Client client;
- pw::rpc::Channel channel;
-
- RoomInfoRequest request;
- SynchronousCallResult<RoomInfoResponse> result =
- SynchronousCall<Chat::GetRoomInformation>(client, channel.id(), request);
-
- if (result.is_rpc_error()) {
- ShutdownClient(client);
- } else if (result.is_server_error()) {
- HandleServerError(result.status());
- } else if (result.is_timeout()) {
- // SynchronousCall will block indefinitely, so we should never get here.
- PW_UNREACHABLE();
- }
- HandleRoomInformation(std::move(result).response());
- }
-
- void AnotherExample() {
- pw_rpc::nanopb::Chat::Client chat_client(client, channel);
- constexpr auto kTimeout = pw::chrono::SystemClock::for_at_least(500ms);
-
- RoomInfoRequest request;
- auto result = SynchronousCallFor<Chat::GetRoomInformation>(
- chat_client, request, kTimeout);
-
- if (result.is_timeout()) {
- RetryRoomRequest();
- } else {
- ...
- }
- }
-
-The ``SynchronousCallResult<Response>`` is also compatible with the PW_TRY
-family of macros, but users should be aware that their use will lose information
-about the type of error. This should only be used if the caller will handle all
-error scenarios the same.
+ #include "pw_rpc/synchronous_call.h"
+
+ void InvokeUnaryRpc() {
+ pw::rpc::Client client;
+ pw::rpc::Channel channel;
+
+ RoomInfoRequest request;
+ SynchronousCallResult<RoomInfoResponse> result =
+ SynchronousCall<Chat::GetRoomInformation>(client, channel.id(), request);
+
+ if (result.is_rpc_error()) {
+ ShutdownClient(client);
+ } else if (result.is_server_error()) {
+ HandleServerError(result.status());
+ } else if (result.is_timeout()) {
+ // SynchronousCall will block indefinitely, so we should never get here.
+ PW_UNREACHABLE();
+ }
+ HandleRoomInformation(std::move(result).response());
+ }
+
+ void AnotherExample() {
+ pw_rpc::nanopb::Chat::Client chat_client(client, channel);
+ constexpr auto kTimeout = pw::chrono::SystemClock::for_at_least(500ms);
+
+ RoomInfoRequest request;
+ auto result = SynchronousCallFor<Chat::GetRoomInformation>(
+ chat_client, request, kTimeout);
+
+ if (result.is_timeout()) {
+ RetryRoomRequest();
+ } else {
+ ...
+ }
+ }
+
+The ``SynchronousCallResult<Response>`` is also compatible with the
+:c:macro:`PW_TRY` family of macros, but users should be aware that their use
+will lose information about the type of error. This should only be used if the
+caller will handle all error scenarios the same.
.. code-block:: c++
- pw::Status SyncRpc() {
- const RoomInfoRequest request;
- PW_TRY_ASSIGN(const RoomInfoResponse& response,
- SynchronousCall<Chat::GetRoomInformation>(client, request));
- HandleRoomInformation(response);
- return pw::OkStatus();
- }
+ pw::Status SyncRpc() {
+ const RoomInfoRequest request;
+ PW_TRY_ASSIGN(const RoomInfoResponse& response,
+ SynchronousCall<Chat::GetRoomInformation>(client, request));
+ HandleRoomInformation(response);
+ return pw::OkStatus();
+ }
ClientServer
============
@@ -1281,17 +1438,17 @@ 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)};
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&channel_output)};
- // Creates both a client and a server.
- pw::rpc::ClientServer client_server(channels);
+ // 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);
- }
+ void ProcessRpcData(pw::ConstByteSpan packet) {
+ // Calls into both the client and the server, sending the packet to the
+ // appropriate one.
+ client_server.ProcessPacket(packet);
+ }
Testing
=======
@@ -1305,7 +1462,7 @@ are supported. Code that uses the raw API may be tested with the raw test
helpers, and vice versa. The Nanopb and Pwpb APIs also provides a test helper
with a real client-server pair that supports testing of asynchronous messaging.
-To test sychronous code that invokes RPCs, declare a ``RawClientTestContext``,
+To test synchronous code that invokes RPCs, declare a ``RawClientTestContext``,
``PwpbClientTestContext``, or ``NanopbClientTestContext``. These test context
objects provide a preconfigured RPC client, channel, server fake, and buffer for
encoding packets.
@@ -1322,42 +1479,42 @@ the expected data was sent and then simulates a response from the server.
.. code-block:: cpp
- #include "pw_rpc/raw/client_testing.h"
+ #include "pw_rpc/raw/client_testing.h"
- class ClientUnderTest {
- public:
- // To support injecting an RPC client for testing, classes that make RPC
- // calls should take an RPC client and channel ID or an RPC service client
- // (e.g. pw_rpc::raw::MyService::Client).
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+ class ClientUnderTest {
+ public:
+ // To support injecting an RPC client for testing, classes that make RPC
+ // calls should take an RPC client and channel ID or an RPC service client
+ // (e.g. pw_rpc::raw::MyService::Client).
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
- void DoSomethingThatInvokesAnRpc();
+ void DoSomethingThatInvokesAnRpc();
- bool SetToTrueWhenRpcCompletes();
- };
+ bool SetToTrueWhenRpcCompletes();
+ };
- TEST(TestAThing, InvokesRpcAndHandlesResponse) {
- RawClientTestContext context;
- ClientUnderTest thing(context.client(), context.channel().id());
+ TEST(TestAThing, InvokesRpcAndHandlesResponse) {
+ RawClientTestContext context;
+ ClientUnderTest thing(context.client(), context.channel().id());
- // Execute the code that invokes the MyService.TheMethod RPC.
- things.DoSomethingThatInvokesAnRpc();
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ things.DoSomethingThatInvokesAnRpc();
- // Find and verify the payloads sent for the MyService.TheMethod RPC.
- auto msgs = context.output().payloads<pw_rpc::raw::MyService::TheMethod>();
- ASSERT_EQ(msgs.size(), 1u);
+ // Find and verify the payloads sent for the MyService.TheMethod RPC.
+ auto msgs = context.output().payloads<pw_rpc::raw::MyService::TheMethod>();
+ ASSERT_EQ(msgs.size(), 1u);
- VerifyThatTheExpectedMessageWasSent(msgs.back());
+ VerifyThatTheExpectedMessageWasSent(msgs.back());
- // Send the response packet from the server and verify that the class reacts
- // accordingly.
- EXPECT_FALSE(thing.SetToTrueWhenRpcCompletes());
+ // Send the response packet from the server and verify that the class reacts
+ // accordingly.
+ EXPECT_FALSE(thing.SetToTrueWhenRpcCompletes());
- context_.server().SendResponse<pw_rpc::raw::MyService::TheMethod>(
- final_message, OkStatus());
+ context_.server().SendResponse<pw_rpc::raw::MyService::TheMethod>(
+ final_message, OkStatus());
- EXPECT_TRUE(thing.SetToTrueWhenRpcCompletes());
- }
+ EXPECT_TRUE(thing.SetToTrueWhenRpcCompletes());
+ }
To test client code that uses asynchronous responses, encapsulates multiple
rpc calls to one or more services, or uses a custom service implemenation,
@@ -1379,47 +1536,62 @@ response is received. It verifies that expected data was both sent and received.
.. code-block:: cpp
- #include "my_library_protos/my_service.rpc.pb.h"
- #include "pw_rpc/nanopb/client_server_testing_threaded.h"
- #include "pw_thread_stl/options.h"
-
- class ClientUnderTest {
- public:
- // To support injecting an RPC client for testing, classes that make RPC
- // calls should take an RPC client and channel ID or an RPC service client
- // (e.g. pw_rpc::raw::MyService::Client).
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
-
- Status BlockOnResponse(uint32_t value);
- };
-
-
- class TestService final : public MyService<TestService> {
- public:
- Status TheMethod(const pw_rpc_test_TheMethod& request,
- pw_rpc_test_TheMethod& response) {
- response.value = request.integer + 1;
- return pw::OkStatus();
- }
- };
-
- TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
- NanopbClientServerTestContextThreaded<> ctx(pw::thread::stl::Options{});
- TestService service;
- ctx.server().RegisterService(service);
- ClientUnderTest client(ctx.client(), ctx.channel().id());
-
- // Execute the code that invokes the MyService.TheMethod RPC.
- constexpr uint32_t value = 1;
- const auto result = client.BlockOnResponse(value);
- const auto request = ctx.request<MyService::TheMethod>(0);
- const auto response = ctx.resonse<MyService::TheMethod>(0);
-
- // Verify content of messages
- EXPECT_EQ(result, pw::OkStatus());
- EXPECT_EQ(request.value, value);
- EXPECT_EQ(response.value, value + 1);
- }
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing_threaded.h"
+ #include "pw_thread_stl/options.h"
+
+ class ClientUnderTest {
+ public:
+ // To support injecting an RPC client for testing, classes that make RPC
+ // calls should take an RPC client and channel ID or an RPC service client
+ // (e.g. pw_rpc::raw::MyService::Client).
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+
+ Status BlockOnResponse(uint32_t value);
+ };
+
+
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
+
+ TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
+ NanopbClientServerTestContextThreaded<> ctx(pw::thread::stl::Options{});
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
+
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.BlockOnResponse(value);
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.resonse<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
+
+Use the context's
+``response(uint32_t index, Response<kMethod>& response)`` to decode messages
+into a provided response object. You would use this version if decoder callbacks
+are needed to fully decode a message. For instance if it uses ``repeated``
+fields.
+
+.. code-block:: cpp
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
Synchronous versions of these test contexts also exist that may be used on
non-threaded systems ``NanopbClientServerTestContext`` and
@@ -1433,45 +1605,79 @@ to with a test service implemenation.
.. code-block:: cpp
- #include "my_library_protos/my_service.rpc.pb.h"
- #include "pw_rpc/nanopb/client_server_testing.h"
-
- class ClientUnderTest {
- public:
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
-
- Status SendRpcCall(uint32_t value);
- };
-
-
- class TestService final : public MyService<TestService> {
- public:
- Status TheMethod(const pw_rpc_test_TheMethod& request,
- pw_rpc_test_TheMethod& response) {
- response.value = request.integer + 1;
- return pw::OkStatus();
- }
- };
-
- TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
- NanopbClientServerTestContext<> ctx();
- TestService service;
- ctx.server().RegisterService(service);
- ClientUnderTest client(ctx.client(), ctx.channel().id());
-
- // Execute the code that invokes the MyService.TheMethod RPC.
- constexpr uint32_t value = 1;
- const auto result = client.SendRpcCall(value);
- // Needed after ever RPC call to trigger forward of packets
- ctx.ForwardNewPackets();
- const auto request = ctx.request<MyService::TheMethod>(0);
- const auto response = ctx.resonse<MyService::TheMethod>(0);
-
- // Verify content of messages
- EXPECT_EQ(result, pw::OkStatus());
- EXPECT_EQ(request.value, value);
- EXPECT_EQ(response.value, value + 1);
- }
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing.h"
+
+ class ClientUnderTest {
+ public:
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+
+ Status SendRpcCall(uint32_t value);
+ };
+
+
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
+
+ TEST(TestServiceTest, ReceivesUnaryRpcResponse) {
+ NanopbClientServerTestContext<> ctx();
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
+
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.SendRpcCall(value);
+ // Needed after ever RPC call to trigger forward of packets
+ ctx.ForwardNewPackets();
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.response<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
+
+Custom packet processing for ClientServerTestContext
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Optional constructor arguments for nanopb/pwpb ``*ClientServerTestContext`` and
+``*ClientServerTestContextThreaded`` allow allow customized packet processing.
+By default the only thing is done is ``ProcessPacket()`` call on the
+``ClientServer`` instance.
+
+For cases when additional instrumentation or offloading to separate thread is
+needed, separate client and server processors can be passed to context
+constructors. A packet processor is a function that returns ``pw::Status`` and
+accepts two arguments: ``pw::rpc::ClientServer&`` and ``pw::ConstByteSpan``.
+Default packet processing is equivalent to the next processor:
+
+.. code-block:: cpp
+
+ [](ClientServer& client_server, pw::ConstByteSpan packet) -> pw::Status {
+ return client_server.ProcessPacket(packet);
+ };
+
+The Server processor will be applied to all packets sent to the server (i.e.
+requests) and client processor will be applied to all packets sent to the client
+(i.e. responses).
+
+.. note::
+
+ The packet processor MUST call ``ClientServer::ProcessPacket()`` method.
+ Otherwise the packet won't be processed.
+
+.. note::
+
+ If the packet processor offloads processing to the separate thread, it MUST
+ copy the ``packet``. After the packet processor returns, the underlying array
+ can go out of scope or be reused for other purposes.
SendResponseIfCalled() helper
-----------------------------
@@ -1479,23 +1685,23 @@ SendResponseIfCalled() helper
have a call for the specified method and then responses to it. It supports
timeout for the waiting part (default timeout is 100ms).
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/test_helpers.h"
+ #include "pw_rpc/test_helpers.h"
- pw::rpc::PwpbClientTestContext client_context;
- other::pw_rpc::pwpb::OtherService::Client other_service_client(
- client_context.client(), client_context.channel().id());
+ pw::rpc::PwpbClientTestContext client_context;
+ other::pw_rpc::pwpb::OtherService::Client other_service_client(
+ client_context.client(), client_context.channel().id());
- PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
- context(other_service_client);
- context.call({});
+ PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
+ context(other_service_client);
+ context.call({});
- ASSERT_OK(pw::rpc::test::SendResponseIfCalled<
- other::pw_rpc::pwpb::OtherService::GetPart>(
- client_context, {.value = 42}));
+ ASSERT_OK(pw::rpc::test::SendResponseIfCalled<
+ other::pw_rpc::pwpb::OtherService::GetPart>(
+ client_context, {.value = 42}));
- // At this point MyService::GetData handler received the GetPartResponse.
+ // At this point MyService::GetData handler received the GetPartResponse.
Integration testing with ``pw_rpc``
-----------------------------------
@@ -1509,16 +1715,16 @@ defined in ``pw_rpc/integration_testing.h``:
.. cpp:var:: constexpr uint32_t kChannelId
- The RPC channel for integration test RPCs.
+ The RPC channel for integration test RPCs.
.. cpp:function:: pw::rpc::Client& pw::rpc::integration_test::Client()
- Returns the global RPC client for integration test use.
+ Returns the global RPC client for integration test use.
.. cpp:function:: pw::Status pw::rpc::integration_test::InitializeClient(int argc, char* argv[], const char* usage_args = "PORT")
- Initializes logging and the global RPC client for integration testing. Starts
- a background thread that processes incoming.
+ Initializes logging and the global RPC client for integration testing. Starts
+ a background thread that processes incoming.
Module Configuration Options
============================
@@ -1528,7 +1734,7 @@ this module, see the
more details.
.. doxygenfile:: pw_rpc/public/pw_rpc/internal/config.h
- :sections: define
+ :sections: define
Sharing server and client code
==============================
@@ -1570,43 +1776,44 @@ interface.
.. _module-pw_rpc-ChannelOutput:
.. cpp:class:: pw::rpc::ChannelOutput
- ``pw_rpc`` endpoints use :cpp:class:`ChannelOutput` instances to send packets.
- Systems that integrate pw_rpc must use one or more :cpp:class:`ChannelOutput`
- instances.
+ ``pw_rpc`` endpoints use :cpp:class:`ChannelOutput` instances to send
+ packets. Systems that integrate pw_rpc must use one or more
+ :cpp:class:`ChannelOutput` instances.
- .. cpp:member:: static constexpr size_t kUnlimited = std::numeric_limits<size_t>::max()
+ .. cpp:member:: static constexpr size_t kUnlimited = std::numeric_limits<size_t>::max()
- Value returned from :cpp:func:`MaximumTransmissionUnit` to indicate an
- unlimited MTU.
+ Value returned from :cpp:func:`MaximumTransmissionUnit` to indicate an
+ unlimited MTU.
- .. cpp:function:: virtual size_t MaximumTransmissionUnit()
+ .. cpp:function:: virtual size_t MaximumTransmissionUnit()
- Returns the size of the largest packet the :cpp:class:`ChannelOutput` can
- send. :cpp:class:`ChannelOutput` implementations should only override this
- function if they impose a limit on the MTU. The default implementation
- returns :cpp:member:`kUnlimited`, which indicates that there is no MTU
- limit.
+ Returns the size of the largest packet the :cpp:class:`ChannelOutput` can
+ send. :cpp:class:`ChannelOutput` implementations should only override this
+ function if they impose a limit on the MTU. The default implementation
+ returns :cpp:member:`kUnlimited`, which indicates that there is no MTU
+ limit.
- .. cpp:function:: virtual pw::Status Send(span<std::byte> packet)
+ .. cpp:function:: virtual pw::Status Send(span<std::byte> packet)
- Sends an encoded RPC packet. Returns OK if further packets may be sent, even
- if the current packet could not be sent. Returns any other status if the
- Channel is no longer able to send packets.
+ Sends an encoded RPC packet. Returns OK if further packets may be sent,
+ even if the current packet could not be sent. Returns any other status if
+ the Channel is no longer able to send packets.
- The RPC system's internal lock is held while this function is called. Avoid
- long-running operations, since these will delay any other users of the RPC
- system.
+ The RPC system's internal lock is held while this function is
+ called. Avoid long-running operations, since these will delay any other
+ users of the RPC system.
- .. danger::
+ .. danger::
- No ``pw_rpc`` APIs may be accessed in this function! Implementations MUST
- NOT access any RPC endpoints (:cpp:class:`pw::rpc::Client`,
- :cpp:class:`pw::rpc::Server`) or call objects
- (:cpp:class:`pw::rpc::ServerReaderWriter`,
- :cpp:class:`pw::rpc::ClientReaderWriter`, etc.) inside the :cpp:func:`Send`
- function or any descendent calls. Doing so will result in deadlock! RPC APIs
- may be used by other threads, just not within :cpp:func:`Send`.
+ No ``pw_rpc`` APIs may be accessed in this function! Implementations
+ MUST NOT access any RPC endpoints (:cpp:class:`pw::rpc::Client`,
+ :cpp:class:`pw::rpc::Server`) or call objects
+ (:cpp:class:`pw::rpc::ServerReaderWriter`,
+ :cpp:class:`pw::rpc::ClientReaderWriter`, etc.) inside the
+ :cpp:func:`Send` function or any descendent calls. Doing so will result
+ in deadlock! RPC APIs may be used by other threads, just not within
+ :cpp:func:`Send`.
- The buffer provided in ``packet`` must NOT be accessed outside of this
- function. It must be sent immediately or copied elsewhere before the
- function returns.
+ The buffer provided in ``packet`` must NOT be accessed outside of this
+ function. It must be sent immediately or copied elsewhere before the
+ function returns.
diff --git a/pw_rpc/fake_channel_output.cc b/pw_rpc/fake_channel_output.cc
index 6404e68d6..3ef04346a 100644
--- a/pw_rpc/fake_channel_output.cc
+++ b/pw_rpc/fake_channel_output.cc
@@ -79,7 +79,7 @@ Status FakeChannelOutput::HandlePacket(span<const std::byte> buffer) {
packet.status().str());
return OkStatus();
case pwpb::PacketType::SERVER_STREAM:
- case pwpb::PacketType::CLIENT_STREAM_END:
+ case pwpb::PacketType::CLIENT_REQUEST_COMPLETION:
return OkStatus();
}
PW_CRASH("Unhandled PacketType %d", static_cast<int>(result.value().type()));
diff --git a/pw_rpc/fuzz/BUILD.gn b/pw_rpc/fuzz/BUILD.gn
index bec42fe44..b2fd728b7 100644
--- a/pw_rpc/fuzz/BUILD.gn
+++ b/pw_rpc/fuzz/BUILD.gn
@@ -65,7 +65,10 @@ pw_source_set("argparse") {
pw_test("argparse_test") {
sources = [ "argparse_test.cc" ]
- deps = [ ":argparse" ]
+ deps = [
+ ":argparse",
+ "$dir_pw_string:builder",
+ ]
}
pw_source_set("engine") {
@@ -95,15 +98,17 @@ pw_test("engine_test") {
sources = [ "engine_test.cc" ]
deps = [
":engine",
+ "$dir_pw_rpc:client_server_testing",
"$dir_pw_rpc:client_server_testing_threaded",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
dir_pw_log,
pw_chrono_SYSTEM_TIMER_BACKEND,
]
}
pw_executable("client_fuzzer") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "client_fuzzer.cc" ]
deps = [
":argparse",
@@ -114,6 +119,7 @@ pw_executable("client_fuzzer") {
}
pw_python_action("cpp_client_server_fuzz_test") {
+ testonly = pw_unit_test_TESTONLY
script = "../py/pw_rpc/testing.py"
args = [
"--server",
diff --git a/pw_rpc/fuzz/alarm_timer_test.cc b/pw_rpc/fuzz/alarm_timer_test.cc
index ce8395a6b..a64156996 100644
--- a/pw_rpc/fuzz/alarm_timer_test.cc
+++ b/pw_rpc/fuzz/alarm_timer_test.cc
@@ -37,7 +37,7 @@ TEST(AlarmTimerTest, Restart) {
timer.Start(50ms);
for (size_t i = 0; i < 10; ++i) {
timer.Restart();
- EXPECT_FALSE(sem.try_acquire_for(10us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(10us)));
}
sem.acquire();
}
@@ -47,7 +47,7 @@ TEST(AlarmTimerTest, Cancel) {
AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
timer.Start(50ms);
timer.Cancel();
- EXPECT_FALSE(sem.try_acquire_for(100us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(100us)));
}
TEST(AlarmTimerTest, Destroy) {
@@ -57,7 +57,7 @@ TEST(AlarmTimerTest, Destroy) {
[&sem](chrono::SystemClock::time_point) { sem.release(); });
timer.Start(50ms);
}
- EXPECT_FALSE(sem.try_acquire_for(100us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(100us)));
}
} // namespace
diff --git a/pw_rpc/fuzz/argparse.cc b/pw_rpc/fuzz/argparse.cc
index 39a5dd694..7df102a20 100644
--- a/pw_rpc/fuzz/argparse.cc
+++ b/pw_rpc/fuzz/argparse.cc
@@ -183,7 +183,7 @@ ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
result = kParsedTwo;
}
char* endptr;
- auto value = strtoull(arg0.data(), &endptr, 0);
+ unsigned long long value = strtoull(arg0.data(), &endptr, 0);
if (*endptr) {
PW_LOG_ERROR("Failed to parse number from '%s'", arg0.data());
return kParseFailure;
@@ -192,7 +192,7 @@ ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
PW_LOG_ERROR("Parsed value is too large: %llu", value);
return kParseFailure;
}
- set_value(value);
+ set_value(static_cast<uint64_t>(value));
return result;
}
diff --git a/pw_rpc/fuzz/argparse_test.cc b/pw_rpc/fuzz/argparse_test.cc
index 6f0ab3889..93adea4d6 100644
--- a/pw_rpc/fuzz/argparse_test.cc
+++ b/pw_rpc/fuzz/argparse_test.cc
@@ -18,6 +18,7 @@
#include <limits>
#include "gtest/gtest.h"
+#include "pw_string/string_builder.h"
namespace pw::rpc::fuzz {
namespace {
@@ -132,17 +133,17 @@ void CheckArgs(Vector<ArgParserVariant>& parsers,
bool verbose,
size_t runs,
uint16_t port) {
- bool actual_verbose;
+ bool actual_verbose = false;
EXPECT_EQ(GetArg(parsers, "--verbose", &actual_verbose), OkStatus());
EXPECT_EQ(verbose, actual_verbose);
EXPECT_EQ(ResetArg(parsers, "--verbose"), OkStatus());
- size_t actual_runs;
+ size_t actual_runs = 0u;
EXPECT_EQ(GetArg(parsers, "--runs", &actual_runs), OkStatus());
EXPECT_EQ(runs, actual_runs);
EXPECT_EQ(ResetArg(parsers, "--runs"), OkStatus());
- uint16_t actual_port;
+ uint16_t actual_port = 0u;
EXPECT_EQ(GetArg(parsers, "port", &actual_port), OkStatus());
EXPECT_EQ(port, actual_port);
EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
diff --git a/pw_rpc/fuzz/engine.cc b/pw_rpc/fuzz/engine.cc
index b19dfa398..3c46b299b 100644
--- a/pw_rpc/fuzz/engine.cc
+++ b/pw_rpc/fuzz/engine.cc
@@ -57,7 +57,7 @@ struct CloseClientStreamVisitor final {
result_type operator()(std::monostate&) {}
result_type operator()(pw::rpc::RawUnaryReceiver&) {}
result_type operator()(pw::rpc::RawClientReaderWriter& call) {
- call.CloseClientStream().IgnoreError();
+ call.RequestCompletion().IgnoreError();
}
};
diff --git a/pw_rpc/fuzz/engine_test.cc b/pw_rpc/fuzz/engine_test.cc
index 5ac1a49a1..b8720fcb4 100644
--- a/pw_rpc/fuzz/engine_test.cc
+++ b/pw_rpc/fuzz/engine_test.cc
@@ -20,9 +20,10 @@
#include "pw_containers/vector.h"
#include "pw_log/log.h"
#include "pw_rpc/benchmark.h"
+#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/internal/fake_channel_output.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc::fuzz {
namespace {
@@ -56,7 +57,11 @@ using FuzzerChannelOutputBase =
/// Channel output that can be waited on by the server.
class FuzzerChannelOutput : public FuzzerChannelOutputBase {
public:
- FuzzerChannelOutput() : FuzzerChannelOutputBase() {}
+ explicit FuzzerChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : FuzzerChannelOutputBase(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
};
using FuzzerContextBase =
@@ -68,7 +73,13 @@ class FuzzerContext : public FuzzerContextBase {
public:
static constexpr uint32_t kChannelId = 1;
- FuzzerContext() : FuzzerContextBase(thread::test::TestOptionsThread0()) {}
+ explicit FuzzerContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
+ : FuzzerContextBase(thread::test::TestOptionsThread0(),
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
};
class RpcFuzzTestingTest : public testing::Test {
@@ -92,13 +103,16 @@ class RpcFuzzTestingTest : public testing::Test {
fuzzer.Run(actions_);
}
+ void TearDown() override { context_.server().UnregisterService(service_); }
+
private:
FuzzerContext context_;
BenchmarkService service_;
Vector<uint32_t, Fuzzer::kMaxActions> actions_;
};
-TEST_F(RpcFuzzTestingTest, SequentialRequests) {
+// TODO: b/274437709 - Re-enable.
+TEST_F(RpcFuzzTestingTest, DISABLED_SequentialRequests) {
// Callback thread
Add(Action::kWriteStream, 1, 'B', 1);
Add(Action::kSkip, 0, 0);
@@ -126,7 +140,7 @@ TEST_F(RpcFuzzTestingTest, SequentialRequests) {
Run();
}
-// TODO(b/274437709): Re-enable.
+// TODO: b/274437709 - Re-enable.
TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
// Callback thread
NextThread();
@@ -149,7 +163,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
@@ -177,7 +191,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
@@ -205,7 +219,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
@@ -232,7 +246,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_DestroyedRequests) {
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
index 05a7633e6..e8e4b6f3d 100644
--- a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
@@ -57,6 +57,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include <string_view>
#include <variant>
diff --git a/pw_rpc/internal/packet.proto b/pw_rpc/internal/packet.proto
index 606efb9ef..104966357 100644
--- a/pw_rpc/internal/packet.proto
+++ b/pw_rpc/internal/packet.proto
@@ -27,14 +27,16 @@ enum PacketType {
REQUEST = 0;
// A message in a client stream. Always sent after a REQUEST and before a
- // CLIENT_STREAM_END.
+ // CLIENT_REQUEST_COMPLETION.
CLIENT_STREAM = 2;
// The client received a packet for an RPC it did not request.
CLIENT_ERROR = 4;
- // A client stream has completed.
- CLIENT_STREAM_END = 8;
+ // Client has requested for call completion. In client streaming and
+ // bi-directional streaming RPCs, this also indicates that the client is done
+ // with sending requests.
+ CLIENT_REQUEST_COMPLETION = 8;
// Server-to-client packets
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
index cdbfc07de..5e682fba1 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
@@ -188,7 +188,7 @@ public class Client {
throw new InvalidRpcServiceException(serviceId);
}
- Method method = service.methods().get(methodId);
+ Method method = service.method(methodId);
if (method == null) {
throw new InvalidRpcServiceMethodException(service, methodId);
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
index ff80cbb61..c306147f2 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
@@ -20,7 +20,6 @@ import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import com.google.protobuf.Parser;
-import java.lang.reflect.InvocationTargetException;
/**
* Information about an RPC service method.
@@ -37,9 +36,17 @@ public abstract class Method {
public abstract Type type();
- public abstract Class<? extends MessageLite> request();
+ abstract Parser<? extends MessageLite> requestParser();
- public abstract Class<? extends MessageLite> response();
+ abstract Parser<? extends MessageLite> responseParser();
+
+ public final Class<? extends MessageLite> request() {
+ return getProtobufClass(requestParser());
+ }
+
+ public final Class<? extends MessageLite> response() {
+ return getProtobufClass(responseParser());
+ }
final int id() {
return Ids.calculate(name());
@@ -57,11 +64,11 @@ public abstract class Method {
return createFullName(service().name(), name());
}
- public static String createFullName(String serviceName, String methodName) {
+ static String createFullName(String serviceName, String methodName) {
return serviceName + '/' + methodName;
}
- /* package */ static Builder builder() {
+ static Builder builder() {
return new AutoValue_Method.Builder();
}
@@ -79,29 +86,26 @@ public abstract class Method {
abstract Builder setName(String value);
- abstract Builder setRequest(Class<? extends MessageLite> value);
+ abstract Builder setRequestParser(Parser<? extends MessageLite> parser);
- abstract Builder setResponse(Class<? extends MessageLite> value);
+ abstract Builder setResponseParser(Parser<? extends MessageLite> parser);
abstract Method build();
}
/** Decodes a response payload according to the method's response type. */
final MessageLite decodeResponsePayload(ByteString data) throws InvalidProtocolBufferException {
- return decodeProtobuf(response(), data);
+ return responseParser().parseFrom(data);
}
- /** Deserializes a protobuf using the provided class. */
- @SuppressWarnings("unchecked")
- static MessageLite decodeProtobuf(Class<? extends MessageLite> messageType, ByteString data)
- throws InvalidProtocolBufferException {
+ private static Class<? extends MessageLite> getProtobufClass(
+ Parser<? extends MessageLite> parser) {
try {
- Parser<? extends MessageLite> parser =
- (Parser<? extends MessageLite>) messageType.getMethod("parser").invoke(null);
- return parser.parseFrom(data);
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- throw new LinkageError(
- "Method was created with a protobuf class with an invalid or missing parser() method", e);
+ return parser.parseFrom(ByteString.EMPTY).getClass();
+ } catch (InvalidProtocolBufferException e) {
+ throw new AssertionError("Failed to parse zero bytes as a protobuf! "
+ + "It was assumed that zero bytes is always a valid protobuf.",
+ e);
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
index 6d7fb2b42..3a5b57a53 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
@@ -26,9 +26,9 @@ import javax.annotation.Nullable;
*
* Invoking an RPC with a method client may throw exceptions:
*
- * TODO(hepler): This class should be split into four types -- one for each method type. The call
- * type checks should be done when the object is created. Also, the client should be typed on
- * the request/response.
+ * TODO: b/301644223 - This class should be split into four types -- one for each method type. The
+ * call type checks should be done when the object is created. Also, the client should be typed
+ * on the request/response.
*/
public class MethodClient {
private final Client client;
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
index 3260bb13f..44e89b6e0 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
@@ -69,7 +69,7 @@ import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
public static byte[] clientStreamEnd(PendingRpc rpc) {
return RpcPacket.newBuilder()
- .setType(PacketType.CLIENT_STREAM_END)
+ .setType(PacketType.CLIENT_REQUEST_COMPLETION)
.setChannelId(rpc.channel().id())
.setServiceId(rpc.service().id())
.setMethodId(rpc.method().id())
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
index eeffd2af8..a239f10ef 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
@@ -14,8 +14,11 @@
package dev.pigweed.pw_rpc;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.stream.Collectors;
@@ -34,20 +37,26 @@ public class Service {
.collect(Collectors.toMap(Method::id, m -> m)));
}
- public String name() {
+ /** Returns the fully qualified name of this service (package.Service). */
+ public final String name() {
return name;
}
- int id() {
+ /** Returns the methods in this service. */
+ public final ImmutableCollection<Method> getMethods() {
+ return methods.values();
+ }
+
+ final int id() {
return id;
}
- public ImmutableMap<Integer, Method> methods() {
- return methods;
+ final Method method(String name) {
+ return methods.get(Ids.calculate(name));
}
- public final Method method(String name) {
- return methods().get(Ids.calculate(name));
+ final Method method(int id) {
+ return methods.get(id);
}
@Override
@@ -55,39 +64,139 @@ public class Service {
return name();
}
+ // TODO: b/293361955 - Remove deprecated methods.
+
+ /**
+ * Declares a unary service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
public static Method.Builder unaryMethod(
- String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.UNARY)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder serverStreamingMethod(
+ /**
+ * Declares a unary service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder unaryMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return unaryMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a server streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder serverStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.SERVER_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder clientStreamingMethod(
+ /**
+ * Declares a server streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder serverStreamingMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return serverStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a client streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder clientStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.CLIENT_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder bidirectionalStreamingMethod(
+ /**
+ * Declares a client streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder clientStreamingMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return clientStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a bidirectional streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder bidirectionalStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.BIDIRECTIONAL_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
+ }
+
+ /**
+ * Declares a bidirectional streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder bidirectionalStreamingMethod(
+ String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return bidirectionalStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Gets the Parser from a protobuf class using reflection.
+ *
+ * This function is provided for backwards compatibility with the deprecated service API that
+ * takes a class object instead of a protobuf parser object.
+ */
+ @SuppressWarnings("unchecked")
+ private static Parser<? extends MessageLite> getParser(Class<? extends MessageLite> messageType) {
+ try {
+ return (Parser<? extends MessageLite>) messageType.getMethod("parser").invoke(null);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new LinkageError(
+ String.format(
+ "Service method created with %s is missing parser() method, likely optimized out. "
+ + "Pass MyMessage.parser() instead of MyMessage.class in service declarations. "
+ + "The class-based API is deprecated and will be removed. "
+ + "See b/293361955.",
+ messageType),
+ e);
+ }
}
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
index 532255da9..5e16cc71e 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
@@ -109,6 +109,20 @@ java_test(
)
java_test(
+ name = "ServiceTest",
+ size = "small",
+ srcs = ["ServiceTest.java"],
+ test_class = "dev.pigweed.pw_rpc.ServiceTest",
+ deps = [
+ ":test_proto_java_proto_lite",
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "@com_google_protobuf//java/lite",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+java_test(
name = "StreamObserverCallTest",
size = "small",
srcs = ["StreamObserverCallTest.java"],
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
index b3e31984b..8fb4b9d55 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
@@ -43,11 +43,13 @@ public final class ClientTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidiStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidiStreaming", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method UNARY_METHOD = SERVICE.method("SomeUnary");
private static final Method SERVER_STREAMING_METHOD = SERVICE.method("SomeServerStreaming");
@@ -135,7 +137,7 @@ public final class ClientTest {
InvalidRpcServiceException.class, () -> client.method(CHANNEL_ID, "abc.Service/Method"));
Service service = new Service("throwaway.NotRealService",
- Service.unaryMethod("NotAnRpc", SomeMessage.class, AnotherMessage.class));
+ Service.unaryMethod("NotAnRpc", SomeMessage.parser(), AnotherMessage.parser()));
assertThrows(InvalidRpcServiceException.class,
() -> client.method(CHANNEL_ID, service.method("NotAnRpc")));
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
index 91722ee70..041f69398 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
@@ -36,11 +36,13 @@ public final class EndpointTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, SomeMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, SomeMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, SomeMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), SomeMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), SomeMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), SomeMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidiStreaming", SomeMessage.class, SomeMessage.class));
+ "SomeBidiStreaming", SomeMessage.parser(), SomeMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
@@ -195,7 +197,7 @@ public final class EndpointTest {
.build()))
.isTrue();
assertThat(endpoint.processClientPacket(call.rpc().method(),
- packetBuilder().setType(PacketType.CLIENT_STREAM_END).build()))
+ packetBuilder().setType(PacketType.CLIENT_REQUEST_COMPLETION).build()))
.isTrue();
assertThat(endpoint.processClientPacket(call.rpc().method(),
packetBuilder()
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
index a1f11997a..ca7196287 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
@@ -41,10 +41,10 @@ public final class FutureCallTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClient", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod("SomeClient", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectional", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectional", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
private static final int CHANNEL_ID = 555;
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java
new file mode 100644
index 000000000..1d0ee0607
--- /dev/null
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java
@@ -0,0 +1,70 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package dev.pigweed.pw_rpc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public final class ServiceTest {
+ private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidiStreaming", SomeMessage.parser(), AnotherMessage.parser()));
+
+ private static final Method METHOD_1 = SERVICE.method("SomeUnary");
+ private static final Method METHOD_2 = SERVICE.method("SomeServerStreaming");
+ private static final Method METHOD_3 = SERVICE.method("SomeClientStreaming");
+ private static final Method METHOD_4 = SERVICE.method("SomeBidiStreaming");
+
+ @Test
+ public void getMethods_includesAllMethods() {
+ assertThat(SERVICE.getMethods()).containsExactly(METHOD_1, METHOD_2, METHOD_3, METHOD_4);
+ }
+
+ @Test
+ public void serviceDeclaration_deprecatedClassBasedEquivalentToParserBased() {
+ final Service declaredWithClassObjects = new Service(SERVICE.name(),
+ Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidiStreaming", SomeMessage.class, AnotherMessage.class));
+
+ assertThat(declaredWithClassObjects.method("SomeUnary").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeUnary").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeServerStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeServerStreaming").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeClientStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeClientStreaming").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeBidiStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeBidiStreaming").responseParser());
+
+ assertThat(declaredWithClassObjects.method("SomeUnary").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeUnary").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeServerStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeServerStreaming").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeClientStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeClientStreaming").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeBidiStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeBidiStreaming").requestParser());
+ }
+}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
index 76d86c162..d1260de4d 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
@@ -32,10 +32,10 @@ public final class StreamObserverCallTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClient", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod("SomeClient", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectional", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectional", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
private static final int CHANNEL_ID = 555;
@@ -130,7 +130,7 @@ public final class StreamObserverCallTest {
streamObserverCall.finish();
verify(mockOutput)
- .send(packetBuilder().setType(PacketType.CLIENT_STREAM_END).build().toByteArray());
+ .send(packetBuilder().setType(PacketType.CLIENT_REQUEST_COMPLETION).build().toByteArray());
}
@Test
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
index 36102f447..a1bc59c64 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
@@ -34,11 +34,13 @@ import org.mockito.junit.MockitoRule;
public final class StreamObserverMethodClientTest {
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectionalStreaming", SomeMessage.parser(), AnotherMessage.parser()));
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -212,7 +214,8 @@ public final class StreamObserverMethodClientTest {
@Test
public void invalidService_throwsException() {
Service otherService = new Service("something.Else",
- Service.clientStreamingMethod("ClientStream", SomeMessage.class, AnotherMessage.class));
+ Service.clientStreamingMethod(
+ "ClientStream", SomeMessage.parser(), AnotherMessage.parser()));
MethodClient methodClient = new MethodClient(
client, channel.id(), otherService.method("ClientStream"), defaultObserver);
@@ -222,13 +225,13 @@ public final class StreamObserverMethodClientTest {
@Test
public void invalidMethod_throwsException() {
Service serviceWithDifferentUnaryMethod = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", AnotherMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", AnotherMessage.parser(), AnotherMessage.parser()),
Service.serverStreamingMethod(
- "SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.clientStreamingMethod(
- "SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectionalStreaming", SomeMessage.parser(), AnotherMessage.parser()));
MethodClient methodClient = new MethodClient(
client, 999, serviceWithDifferentUnaryMethod.method("SomeUnary"), defaultObserver);
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
index a58d51901..5e989a76b 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
@@ -175,9 +175,10 @@ public class TestClient {
private <T extends MessageLite> T parseRequestPayload(Class<T> payloadType, RpcPacket packet) {
try {
- return payloadType.cast(Method.decodeProtobuf(
- client.method(CHANNEL_ID, packet.getServiceId(), packet.getMethodId()).method().request(),
- packet.getPayload()));
+ return payloadType.cast(client.method(CHANNEL_ID, packet.getServiceId(), packet.getMethodId())
+ .method()
+ .requestParser()
+ .parseFrom(packet.getPayload()));
} catch (InvalidProtocolBufferException e) {
throw new AssertionError("Decoding sent packet payload failed", e);
}
diff --git a/pw_rpc/method_test.cc b/pw_rpc/method_test.cc
index b1403d8b0..39cd87c6c 100644
--- a/pw_rpc/method_test.cc
+++ b/pw_rpc/method_test.cc
@@ -18,9 +18,9 @@
#include "gtest/gtest.h"
#include "pw_rpc/internal/packet.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/method_type.h"
#include "pw_rpc/server.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc::internal {
namespace {
diff --git a/pw_rpc/nanopb/Android.bp b/pw_rpc/nanopb/Android.bp
index da2de4d04..c210b2998 100644
--- a/pw_rpc/nanopb/Android.bp
+++ b/pw_rpc/nanopb/Android.bp
@@ -59,7 +59,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -80,6 +80,4 @@ cc_defaults {
srcs: [
":pw_rpc_nanopb_src_files",
],
- host_supported: true,
- vendor_available: true,
-} \ No newline at end of file
+}
diff --git a/pw_rpc/nanopb/BUILD.bazel b/pw_rpc/nanopb/BUILD.bazel
index 32c0573bb..e28f6bf59 100644
--- a/pw_rpc/nanopb/BUILD.bazel
+++ b/pw_rpc/nanopb/BUILD.bazel
@@ -131,7 +131,22 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Enable this library when logging_event_handler can be used.
+pw_cc_test(
+ name = "callback_test",
+ srcs = ["callback_test.cc"],
+ deps = [
+ ":client_testing",
+ "//pw_rpc",
+ "//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread:sleep",
+ "//pw_thread:yield",
+ "//pw_thread_stl:non_portable_test_thread_options",
+ ],
+)
+
+# TODO: b/242059613 - Enable this library when logging_event_handler can be used.
filegroup(
name = "client_integration_test",
srcs = [
@@ -178,6 +193,7 @@ pw_cc_test(
":client_api",
":client_server_testing",
"//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_sync:mutex",
],
)
@@ -192,8 +208,9 @@ pw_cc_test(
":client_server_testing_threaded",
"//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
"//pw_sync:binary_semaphore",
- "//pw_thread:test_threads_header",
- "//pw_thread_stl:test_threads",
+ "//pw_sync:mutex",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -267,7 +284,7 @@ pw_cc_test(
],
)
-# TODO(b/234874064): Requires nanopb options file support to compile.
+# TODO: b/234874064 - Requires nanopb options file support to compile.
filegroup(
name = "echo_service_test",
srcs = ["echo_service_test.cc"],
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index d04fa8923..1fb9852d2 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -135,6 +135,7 @@ pw_source_set("echo_service") {
}
pw_source_set("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
public_configs = [ ":public" ]
public_deps = [
"$dir_pw_sync:binary_semaphore",
@@ -152,6 +153,7 @@ pw_doc_group("docs") {
pw_test_group("tests") {
tests = [
+ ":callback_test",
":client_call_test",
":client_reader_writer_test",
":client_server_context_test",
@@ -171,6 +173,23 @@ pw_test_group("tests") {
]
}
+pw_test("callback_test") {
+ enable_if = dir_pw_third_party_nanopb != "" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ deps = [
+ ":client_testing",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:yield",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
+ "..:client",
+ "..:server",
+ "..:test_protos.nanopb_rpc",
+ ]
+ sources = [ "callback_test.cc" ]
+}
+
pw_test("client_call_test") {
deps = [
":client_api",
@@ -196,10 +215,11 @@ pw_test("client_server_context_test") {
deps = [
":client_api",
":client_server_testing",
+ "$dir_pw_sync:mutex",
"..:test_protos.nanopb_rpc",
]
sources = [ "client_server_context_test.cc" ]
- enable_if = dir_pw_third_party_nanopb != ""
+ enable_if = dir_pw_third_party_nanopb != "" && pw_sync_MUTEX_BACKEND != ""
}
_stl_threading_and_nanopb_enabled =
@@ -212,8 +232,9 @@ pw_test("client_server_context_threaded_test") {
":client_api",
":client_server_testing_threaded",
"$dir_pw_sync:binary_semaphore",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"..:test_protos.nanopb_rpc",
]
sources = [ "client_server_context_threaded_test.cc" ]
diff --git a/pw_rpc/nanopb/CMakeLists.txt b/pw_rpc/nanopb/CMakeLists.txt
index a3a73e5db..b2665c382 100644
--- a/pw_rpc/nanopb/CMakeLists.txt
+++ b/pw_rpc/nanopb/CMakeLists.txt
@@ -35,18 +35,12 @@ pw_add_library(pw_rpc.nanopb.server_api STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD)
- zephyr_link_libraries(pw_rpc.nanopb.method)
-endif()
# TODO(hepler): Deprecate this once no one depends on it.
pw_add_library(pw_rpc.nanopb.method_union INTERFACE
PUBLIC_DEPS
pw_rpc.nanopb.server_api
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD_UNION)
- zephyr_link_libraries(pw_rpc.nanopb.method_union)
-endif()
pw_add_library(pw_rpc.nanopb.client_api INTERFACE
HEADERS
@@ -58,9 +52,6 @@ pw_add_library(pw_rpc.nanopb.client_api INTERFACE
pw_rpc.client
pw_rpc.nanopb.common
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_CLIENT)
- zephyr_link_libraries(pw_rpc.nanopb.client_api)
-endif()
pw_add_library(pw_rpc.nanopb.common STATIC
HEADERS
@@ -78,9 +69,6 @@ pw_add_library(pw_rpc.nanopb.common STATIC
pw_rpc.client
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_COMMON)
- zephyr_link_libraries(pw_rpc.nanopb.common)
-endif()
pw_add_library(pw_rpc.nanopb.test_method_context INTERFACE
HEADERS
@@ -142,9 +130,6 @@ pw_add_library(pw_rpc.nanopb.echo_service INTERFACE
PUBLIC_DEPS
pw_rpc.protos.nanopb_rpc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_ECHO_SERVICE)
- zephyr_link_libraries(pw_rpc.nanopb.echo_service)
-endif()
pw_add_library(pw_rpc.nanopb.client_integration_test STATIC
SOURCES
@@ -182,17 +167,20 @@ pw_add_test(pw_rpc.nanopb.client_reader_writer_test
pw_rpc.nanopb
)
-pw_add_test(pw_rpc.nanopb.client_server_context_test
- SOURCES
- client_server_context_test.cc
- PRIVATE_DEPS
- pw_rpc.nanopb.client_api
- pw_rpc.nanopb.client_server_testing
- pw_rpc.test_protos.nanopb_rpc
- GROUPS
- modules
- pw_rpc.nanopb
-)
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+ pw_add_test(pw_rpc.nanopb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.client_server_testing
+ pw_rpc.test_protos.nanopb_rpc
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_rpc.nanopb
+ )
+endif()
if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
@@ -205,7 +193,8 @@ if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
pw_rpc.nanopb.client_server_testing_threaded
pw_rpc.test_protos.nanopb_rpc
pw_sync.binary_semaphore
- pw_thread.test_threads
+ pw_sync.mutex
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_thread_stl.test_threads
GROUPS
@@ -351,7 +340,7 @@ pw_add_test(pw_rpc.nanopb.stub_generation_test
pw_rpc.nanopb
)
-# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+# TODO: b/231950909 - Test disabled as pw_work_queue lacks CMakeLists.txt
if((TARGET pw_work_queue.pw_work_queue) AND
("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL ""))
diff --git a/pw_rpc/nanopb/Kconfig b/pw_rpc/nanopb/Kconfig
index 649707fbb..3b7ea3dcf 100644
--- a/pw_rpc/nanopb/Kconfig
+++ b/pw_rpc/nanopb/Kconfig
@@ -12,42 +12,44 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_RPC_NANOPB
- bool "Pigweed RPC nanobp"
-
-if PIGWEED_RPC_NANOPB
-
-config PIGWEED_RPC_NANOPB_DIR
- string "Optional 3rd party directory for nanopb"
- help
- The directory for the custom nanopb build rules to integrate with pigweed.
+menu "nanopb"
config PIGWEED_RPC_NANOPB_METHOD
- bool "Enable Pigweed RPC/Nanopb method library (pw_rpc.nanopb.method)"
+ bool "Link pw_rpc.nanopb.method library"
select PIGWEED_RPC_NANOPB_COMMON
select PIGWEED_RPC_SERVER
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_METHOD_UNION
- bool "Enable Pigweed RPC/Nanopb method union library (pw_rpc.nanopb.method_union)"
+ bool "Link pw_rpc.nanopb.method_union library"
select PIGWEED_RPC_NANOPB_METHOD
select PIGWEED_RPC_RAW
select PIGWEED_RPC_SERVER
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_CLIENT
- bool "Enable Pigweed RPC/Nanopb client library (pw_rpc.nanopb.client)"
+ bool "Link pw_rpc.nanopb.client library"
select PIGWEED_FUNCTION
select PIGWEED_RPC_NANOPB_COMMON
select PIGWEED_RPC_COMMON
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_COMMON
- bool "Enable Pigweed RPC/Nanopb common library (pw_rpc.nanopb.common)"
+ bool "Link pw_rpc.nanopb.common library"
select PIGWEED_BYTES
select PIGWEED_LOG
select PIGWEED_RPC_COMMON
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_ECHO_SERVICE
- bool "Enable Pigweed RPC/Nanopb echo service library (pw_rpc.nanopb.echo_service)"
+ bool "Link pw_rpc.nanopb.echo_service library"
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
-endif # PIGWEED_RPC_NANOPB \ No newline at end of file
+endmenu
diff --git a/pw_rpc/nanopb/callback_test.cc b/pw_rpc/nanopb/callback_test.cc
new file mode 100644
index 000000000..bfd5088f6
--- /dev/null
+++ b/pw_rpc/nanopb/callback_test.cc
@@ -0,0 +1,269 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+#include "pw_rpc/nanopb/client_testing.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/non_portable_test_thread_options.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/yield.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+using test::pw_rpc::nanopb::TestService;
+
+using ClientReaderWriter =
+ NanopbClientReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>;
+
+// These tests cover interactions between a thread moving or destroying an RPC
+// call object and a thread running callbacks for that call. In order to test
+// that the first thread waits for callbacks to complete when trying to move or
+// destroy the call, it is necessary to have the callback thread yield to the
+// other thread. There isn't a good way to synchronize these threads without
+// changing the code under test.
+void YieldToOtherThread() {
+ // Sleep for a while and then yield just to be sure the other thread runs.
+ this_thread::sleep_for(100ms);
+ this_thread::yield();
+}
+
+class CallbacksTest : public ::testing::Test {
+ protected:
+ CallbacksTest()
+ : callback_thread_(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with
+ // TestThreadContext.
+ thread::test::TestOptionsThread0(),
+ [](void* arg) {
+ static_cast<CallbacksTest*>(arg)->SendResponseAfterSemaphore();
+ },
+ this) {}
+
+ ~CallbacksTest() override {
+ EXPECT_FALSE(callback_thread_.joinable()); // Tests must join the thread!
+ }
+
+ void RespondToCall(const ClientReaderWriter& call) {
+ respond_to_call_ = &call;
+ }
+
+ NanopbClientTestContext<> context_;
+ sync::BinarySemaphore callback_thread_sem_;
+ sync::BinarySemaphore main_thread_sem_;
+
+ thread::Thread callback_thread_;
+
+ // Must be incremented exactly once by the RPC callback in each test.
+ volatile int callback_executed_ = 0;
+
+ // Variables optionally used by tests. These are in this object so lambads
+ // only need to capture [this] to access them.
+ volatile bool call_is_in_scope_ = false;
+
+ ClientReaderWriter call_1_;
+ ClientReaderWriter call_2_;
+
+ private:
+ void SendResponseAfterSemaphore() {
+ // Wait until the main thread says to send the response.
+ callback_thread_sem_.acquire();
+
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, respond_to_call_->id());
+ }
+
+ const ClientReaderWriter* respond_to_call_ = &call_1_;
+};
+
+TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ {
+ ClientReaderWriter local_call = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id());
+ RespondToCall(local_call);
+
+ call_is_in_scope_ = true;
+
+ local_call.set_on_next([this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release();
+
+ // Wait for a while so the main thread tries to destroy the call.
+ YieldToOtherThread();
+
+ // Now, make sure the call is still in scope. The main thread should
+ // block in the call's destructor until this callback completes.
+ EXPECT_TRUE(call_is_in_scope_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Wait until the callback thread starts.
+ main_thread_sem_.acquire();
+ }
+
+ // The callback thread will sleep for a bit. Meanwhile, let the call go out
+ // of scope, and mark it as such.
+ call_is_in_scope_ = false;
+
+ // Wait for the callback thread to finish.
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+TEST_F(CallbacksTest, MoveActiveCall_WaitsForCallbackToComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ YieldToOtherThread();
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Confirm that the callback thread started.
+ main_thread_sem_.acquire();
+
+ // Move the call object. This thread should wait until the on_completed
+ // callback is done.
+ EXPECT_TRUE(call_1_.active());
+ call_2_ = std::move(call_1_);
+
+ // The callback should already have finished. This thread should have waited
+ // for it to finish during the move.
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ callback_thread_.join();
+}
+
+TEST_F(CallbacksTest, MoveOtherCallIntoOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ call_1_ = std::move(call_2_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, MoveOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ // Cancel this call first, or the move will deadlock, since the moving
+ // thread will wait for the callback thread (both this thread) to
+ // terminate if the call is active.
+ EXPECT_EQ(OkStatus(), call_1_.Cancel());
+ call_2_ = std::move(call_1_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, PacketDroppedIfOnNextIsBusy) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ callback_thread_sem_.acquire(); // Wait for the main thread to release
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread.
+ callback_thread_sem_.release();
+
+ main_thread_sem_.acquire(); // Confirm that the callback is running
+
+ // Handle a few packets for this call, which should be dropped since on_next
+ // is busy. callback_executed_ should remain at 1.
+ for (int i = 0; i < 5; ++i) {
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, call_1_.id());
+ }
+
+ // Wait for the callback thread to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/client_reader_writer_test.cc b/pw_rpc/nanopb/client_reader_writer_test.cc
index 2e8b8fccf..66da9ce43 100644
--- a/pw_rpc/nanopb/client_reader_writer_test.cc
+++ b/pw_rpc/nanopb/client_reader_writer_test.cc
@@ -59,7 +59,7 @@ TEST(NanopbClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
call.set_on_error([](Status) {});
@@ -72,6 +72,7 @@ TEST(NanopbClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -88,7 +89,75 @@ TEST(NanopbClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientWriter, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientWriter<pw_rpc_test_TestRequest, pw_rpc_test_TestStreamResponse>
+ call = TestService::TestClientStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnCompletedCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientReader, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientReader<pw_rpc_test_TestStreamResponse> call =
+ TestService::TestServerStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientReaderWriter, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>
+ call = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -130,7 +199,7 @@ TEST(NanopbClientWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
call.set_on_error([](Status) {});
@@ -152,6 +221,7 @@ TEST(NanopbClientReader, Closed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -175,7 +245,7 @@ TEST(NanopbClientReaderWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
diff --git a/pw_rpc/nanopb/client_server_context_test.cc b/pw_rpc/nanopb/client_server_context_test.cc
index b023fdf4f..710efb311 100644
--- a/pw_rpc/nanopb/client_server_context_test.cc
+++ b/pw_rpc/nanopb/client_server_context_test.cc
@@ -11,10 +11,12 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+#include <array>
#include "gtest/gtest.h"
#include "pw_rpc/nanopb/client_server_testing.h"
#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_sync/mutex.h"
namespace pw::rpc {
namespace {
@@ -29,8 +31,28 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
- NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+ Status TestAnotherUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ typedef std::array<uint32_t, 3> ArgType;
+ // The values array needs to be kept in memory until after this method call
+ // returns since the response is not encoded until after returning from this
+ // method.
+ static const ArgType values = {7, 8, 9};
+ response.repeated_field.funcs.encode = +[](pb_ostream_t* stream,
+ const pb_field_t* field,
+ void* const* arg) -> bool {
+ // Note: nanopb passes the pointer to the repeated_filed.arg member as
+ // arg, not its contents.
+ for (auto elem : *static_cast<const ArgType*>(*arg)) {
+ if (!pb_encode_tag_for_field(stream, field) ||
+ !pb_encode_varint(stream, elem))
+ return false;
+ }
+ return true;
+ };
+ response.repeated_field.arg = const_cast<ArgType*>(&values);
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(
const pw_rpc_test_TestRequest&,
@@ -44,7 +66,7 @@ class TestService final : public GeneratedService::Service<TestService> {
pw_rpc_test_TestStreamResponse>&) {}
};
-TEST(NanopbClientServerTestContext, ReceivesUnaryRpcReponse) {
+TEST(NanopbClientServerTestContext, ReceivesUnaryRpcResponse) {
NanopbClientServerTestContext<> ctx;
TestService service;
ctx.server().RegisterService(service);
@@ -70,7 +92,7 @@ TEST(NanopbClientServerTestContext, ReceivesUnaryRpcReponse) {
EXPECT_EQ(request.integer, sent_request.integer);
}
-TEST(NanopbClientServerTestContext, ReceivesMultipleReponses) {
+TEST(NanopbClientServerTestContext, ReceivesMultipleResponses) {
NanopbClientServerTestContext<> ctx;
TestService service;
ctx.server().RegisterService(service);
@@ -112,5 +134,121 @@ TEST(NanopbClientServerTestContext, ReceivesMultipleReponses) {
EXPECT_EQ(request2.integer, sent_request2.integer);
}
+TEST(NanopbClientServerTestContext,
+ ReceivesMultipleResponsesWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ NanopbClientServerTestContext<> ctx(server_processor, client_processor);
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ pw_rpc_test_TestResponse response1 pw_rpc_test_TestResponse_init_default;
+ pw_rpc_test_TestResponse response2 pw_rpc_test_TestResponse_init_default;
+ auto handler1 = [&response1](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response2 = server_response; };
+
+ pw_rpc_test_TestRequest request1{.integer = 1,
+ .status_code = OkStatus().code()};
+ pw_rpc_test_TestRequest request2{.integer = 2,
+ .status_code = OkStatus().code()};
+ const auto call1 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(NanopbClientServerTestContext, ResponseWithCallbacks) {
+ NanopbClientServerTestContext<> ctx;
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ const auto call = GeneratedService::TestAnotherUnaryRpc(
+ ctx.client(), ctx.channel().id(), pw_rpc_test_TestRequest_init_default);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ // To decode a response object that requires to set pb_callback_t members,
+ // pass it to the response() method as a parameter.
+ constexpr size_t kMaxNumValues = 4;
+ struct DecoderContext {
+ uint32_t num_calls = 0;
+ uint32_t values[kMaxNumValues];
+ bool failed = false;
+ } decoder_context;
+
+ pw_rpc_test_TestResponse response = pw_rpc_test_TestResponse_init_default;
+ response.repeated_field.funcs.decode = +[](pb_istream_t* stream,
+ const pb_field_t* /* field */,
+ void** arg) -> bool {
+ DecoderContext* dec_ctx = static_cast<DecoderContext*>(*arg);
+ uint64_t value;
+ if (!pb_decode_varint(stream, &value)) {
+ dec_ctx->failed = true;
+ return false;
+ }
+ if (dec_ctx->num_calls < kMaxNumValues) {
+ dec_ctx->values[dec_ctx->num_calls] = value;
+ }
+ dec_ctx->num_calls++;
+ return true;
+ };
+ response.repeated_field.arg = &decoder_context;
+ ctx.response<GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_FALSE(decoder_context.failed);
+ EXPECT_EQ(3u, decoder_context.num_calls);
+ EXPECT_EQ(7u, decoder_context.values[0]);
+ EXPECT_EQ(8u, decoder_context.values[1]);
+ EXPECT_EQ(9u, decoder_context.values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/client_server_context_threaded_test.cc b/pw_rpc/nanopb/client_server_context_threaded_test.cc
index 68be69960..0ebefa48d 100644
--- a/pw_rpc/nanopb/client_server_context_threaded_test.cc
+++ b/pw_rpc/nanopb/client_server_context_threaded_test.cc
@@ -12,11 +12,14 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <atomic>
+
#include "gtest/gtest.h"
#include "pw_rpc/nanopb/client_server_testing_threaded.h"
#include "pw_rpc_test_protos/test.rpc.pb.h"
#include "pw_sync/binary_semaphore.h"
-#include "pw_thread/test_threads.h"
+#include "pw_sync/mutex.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc {
namespace {
@@ -31,8 +34,28 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
- NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+ Status TestAnotherUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ typedef std::array<uint32_t, 3> ArgType;
+ // The values array needs to be kept in memory until after this method call
+ // returns since the response is not encoded until after returning from this
+ // method.
+ static const ArgType values = {7, 8, 9};
+ response.repeated_field.funcs.encode = +[](pb_ostream_t* stream,
+ const pb_field_t* field,
+ void* const* arg) -> bool {
+ // Note: nanopb passes the pointer to the repeated_filed.arg member as
+ // arg, not its contents.
+ for (auto elem : *static_cast<const ArgType*>(*arg)) {
+ if (!pb_encode_tag_for_field(stream, field) ||
+ !pb_encode_varint(stream, elem))
+ return false;
+ }
+ return true;
+ };
+ response.repeated_field.arg = const_cast<ArgType*>(&values);
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(
const pw_rpc_test_TestRequest&,
@@ -48,34 +71,44 @@ class TestService final : public GeneratedService::Service<TestService> {
class RpcCaller {
public:
- void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ template <auto kMethod = GeneratedService::TestUnaryRpc>
+ Status BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ response_status_ = OkStatus();
pw_rpc_test_TestRequest request{.integer = i,
.status_code = OkStatus().code()};
- auto call = GeneratedService::TestUnaryRpc(
+ auto call = kMethod(
client,
channel_id,
request,
- [this](const pw_rpc_test_TestResponse&, Status) {
+ [this](const pw_rpc_test_TestResponse&, Status status) {
+ response_status_ = status;
semaphore_.release();
},
- [](Status) {});
+ [this](Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ });
semaphore_.acquire();
+ return response_status_;
}
private:
pw::sync::BinarySemaphore semaphore_;
+ Status response_status_ = OkStatus();
};
-TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcResponseThreaded) {
NanopbClientServerTestContextThreaded<> ctx(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
thread::test::TestOptionsThread0());
TestService service;
ctx.server().RegisterService(service);
RpcCaller caller;
constexpr auto value = 1;
- caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request =
ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
@@ -86,7 +119,7 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
EXPECT_EQ(value + 1, response.value);
}
-TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleResponsesThreaded) {
NanopbClientServerTestContextThreaded<> ctx(
thread::test::TestOptionsThread0());
TestService service;
@@ -95,8 +128,10 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
RpcCaller caller;
constexpr auto value1 = 1;
constexpr auto value2 = 2;
- caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
- caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request1 =
ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
@@ -113,5 +148,109 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
EXPECT_EQ(value2 + 1, response2.value);
}
+TEST(NanopbClientServerTestContextThreaded,
+ ReceivesMultipleResponsesThreadedWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0(), server_processor, client_processor);
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(NanopbClientServerTestContextThreaded, ResponseWithCallbacks) {
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0());
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ EXPECT_EQ(caller.BlockOnResponse<GeneratedService::TestAnotherUnaryRpc>(
+ 0, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ // To decode a response object that requires to set pb_callback_t members,
+ // pass it to the response() method as a parameter.
+ constexpr size_t kMaxNumValues = 4;
+ struct DecoderContext {
+ uint32_t num_calls = 0;
+ uint32_t values[kMaxNumValues];
+ bool failed = false;
+ } decoder_context;
+
+ pw_rpc_test_TestResponse response = pw_rpc_test_TestResponse_init_default;
+ response.repeated_field.funcs.decode = +[](pb_istream_t* stream,
+ const pb_field_t* /* field */,
+ void** arg) -> bool {
+ DecoderContext* dec_ctx = static_cast<DecoderContext*>(*arg);
+ uint64_t value;
+ if (!pb_decode_varint(stream, &value)) {
+ dec_ctx->failed = true;
+ return false;
+ }
+ if (dec_ctx->num_calls < kMaxNumValues) {
+ dec_ctx->values[dec_ctx->num_calls] = value;
+ }
+ dec_ctx->num_calls++;
+ return true;
+ };
+ response.repeated_field.arg = &decoder_context;
+ ctx.response<GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_FALSE(decoder_context.failed);
+ EXPECT_EQ(3u, decoder_context.num_calls);
+ EXPECT_EQ(7u, decoder_context.values[0]);
+ EXPECT_EQ(8u, decoder_context.values[1]);
+ EXPECT_EQ(9u, decoder_context.values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/docs.rst b/pw_rpc/nanopb/docs.rst
index 993f7c5fb..4ae7dedba 100644
--- a/pw_rpc/nanopb/docs.rst
+++ b/pw_rpc/nanopb/docs.rst
@@ -16,25 +16,25 @@ 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.
-.. code::
+.. code-block::
- # chat/BUILD.gn
+ # chat/BUILD.gn
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ 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" ]
- }
+ 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" ]
- }
+ # 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
@@ -45,7 +45,7 @@ Generated code API
==================
All examples in this document use the following RPC service definition.
-.. code:: protobuf
+.. code-block:: protobuf
// chat/chat_protos/chat_service.proto
@@ -74,7 +74,7 @@ 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.
-.. code:: c++
+.. code-block:: c++
#include "chat_protos/chat_service.rpc.pb.h"
@@ -89,7 +89,7 @@ A unary RPC is implemented as a function which takes in the RPC's request struct
and populates a response struct to send back, with a status indicating whether
the request succeeded.
-.. code:: c++
+.. code-block:: c++
pw::Status GetRoomInformation(pw::rpc::
const RoomInfoRequest& request,
@@ -100,7 +100,7 @@ Server streaming RPC
A server streaming RPC receives the client's request message alongside a
``ServerWriter``, used to stream back responses.
-.. code:: c++
+.. code-block:: c++
void ListUsersInRoom(pw::rpc::
const ListUsersRequest& request,
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
index 3aac5a576..bc3fbe814 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
@@ -55,6 +55,8 @@ class NanopbUnaryResponseClientCall : public UnaryResponseClientCall {
return call;
}
+ ~NanopbUnaryResponseClientCall() { DestroyClientCall(); }
+
protected:
constexpr NanopbUnaryResponseClientCall() = default;
@@ -146,6 +148,8 @@ class NanopbStreamResponseClientCall : public StreamResponseClientCall {
return call;
}
+ ~NanopbStreamResponseClientCall() { DestroyClientCall(); }
+
protected:
constexpr NanopbStreamResponseClientCall() = default;
@@ -220,6 +224,8 @@ class NanopbClientReaderWriter
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Writes a response struct. Returns the following Status codes:
//
// OK - the response was successfully sent
@@ -233,16 +239,18 @@ class NanopbClientReaderWriter
&request);
}
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -284,12 +292,15 @@ class NanopbClientReader
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbStreamResponseClientCall<Response>::set_on_next;
using internal::Call::set_on_error;
using internal::StreamResponseClientCall::set_on_completed;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -325,6 +336,8 @@ class NanopbClientWriter
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbUnaryResponseClientCall<Response>::set_on_completed;
using internal::Call::set_on_error;
@@ -335,7 +348,7 @@ class NanopbClientWriter
}
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -371,6 +384,8 @@ class NanopbUnaryReceiver
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbUnaryResponseClientCall<Response>::set_on_completed;
using internal::Call::set_on_error;
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
index 429cb719c..c1dec95b9 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
@@ -15,6 +15,8 @@
#include <cinttypes>
+#include "pb_decode.h"
+#include "pb_encode.h"
#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/nanopb/fake_channel_output.h"
@@ -45,7 +47,11 @@ class NanopbForwardingChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr NanopbForwardingChannelOutput() = default;
+ constexpr NanopbForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index) {
@@ -54,6 +60,17 @@ class NanopbForwardingChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index) {
PW_ASSERT(Base::PacketCount() >= index);
return Base::output_.template requests<kMethod>(channel_id)[index];
@@ -90,7 +107,11 @@ class NanopbClientServerTestContext final
kPayloadsBufferSizeBytes>;
public:
- NanopbClientServerTestContext() = default;
+ NanopbClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -99,12 +120,21 @@ class NanopbClientServerTestContext final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using nanopb. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
index 0e08f0b38..187d267f5 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
@@ -15,6 +15,8 @@
#include <cinttypes>
+#include "pb_decode.h"
+#include "pb_encode.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/nanopb/fake_channel_output.h"
@@ -45,7 +47,11 @@ class NanopbWatchableChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr NanopbWatchableChannelOutput() = default;
+ constexpr NanopbWatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index)
@@ -56,6 +62,18 @@ class NanopbWatchableChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index)
PW_LOCKS_EXCLUDED(Base::mutex_) {
std::lock_guard lock(Base::mutex_);
@@ -94,8 +112,13 @@ class NanopbClientServerTestContextThreaded final
kPayloadsBufferSizeBytes>;
public:
- NanopbClientServerTestContextThreaded(const thread::Options& options)
- : Base(options) {}
+ NanopbClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(options,
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -104,12 +127,21 @@ class NanopbClientServerTestContextThreaded final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using nanopb. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
index a06d53798..05c80af1b 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
@@ -15,6 +15,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include "pw_bytes/span.h"
#include "pw_rpc/client.h"
@@ -24,7 +25,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
// payloads as Nanopb structs.
@@ -39,28 +40,32 @@ class NanopbFakeServer : public FakeServer {
// Sends a response packet for a server or bidirectional streaming RPC to the
// client.
template <auto kMethod>
- void SendResponse(Status status) const {
- FakeServer::SendResponse<kMethod>(status);
+ void SendResponse(Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
+ FakeServer::SendResponse<kMethod>(status, call_id);
}
// Sends a response packet for a unary or client streaming streaming RPC to
// the client.
template <auto kMethod,
size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
- void SendResponse(const Response<kMethod>& payload, Status status) const {
+ void SendResponse(const Response<kMethod>& payload,
+ Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
std::byte buffer[kEncodeBufferSizeBytes] = {};
- FakeServer::SendResponse<kMethod>(EncodeResponse<kMethod>(&payload, buffer),
- status);
+ FakeServer::SendResponse<kMethod>(
+ EncodeResponse<kMethod>(&payload, buffer), status, call_id);
}
// Sends a stream packet for a server or bidirectional streaming RPC to the
// client.
template <auto kMethod,
size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
- void SendServerStream(const Response<kMethod>& payload) const {
+ void SendServerStream(const Response<kMethod>& payload,
+ std::optional<uint32_t> call_id = std::nullopt) const {
std::byte buffer[kEncodeBufferSizeBytes] = {};
FakeServer::SendServerStream<kMethod>(
- EncodeResponse<kMethod>(&payload, buffer));
+ EncodeResponse<kMethod>(&payload, buffer), call_id);
}
private:
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
index f71c87795..150a42811 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
@@ -86,6 +86,9 @@ class NanopbPayloadsView {
iterator begin() const { return iterator(view_.begin(), serde_); }
iterator end() const { return iterator(view_.end(), serde_); }
+ PayloadsView& payloads() { return view_; }
+ internal::NanopbSerde& serde() { return serde_; }
+
private:
using Base =
containers::WrappedIterator<iterator, PayloadsView::iterator, Payload>;
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
index 06bb538a4..ebb80c289 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
@@ -46,6 +46,8 @@ class NanopbServerCall : public ServerCall {
NanopbServerCall(const LockedCallContext& context, MethodType type)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ ~NanopbServerCall() { DestroyServerCall(); }
+
Status SendUnaryResponse(const void* payload, Status status)
PW_LOCKS_EXCLUDED(rpc_lock()) {
return SendFinalResponse(*this, payload, status);
@@ -93,6 +95,8 @@ class BaseNanopbServerReader : public NanopbServerCall {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: NanopbServerCall(context, type) {}
+ ~BaseNanopbServerReader() { DestroyServerCall(); }
+
protected:
constexpr BaseNanopbServerReader() = default;
@@ -192,7 +196,8 @@ class NanopbServerReaderWriter
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
using internal::BaseNanopbServerReader<Request>::set_on_next;
private:
@@ -249,7 +254,8 @@ class NanopbServerReader : private internal::BaseNanopbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
using internal::BaseNanopbServerReader<Request>::set_on_next;
Status Finish(const Response& response, Status status = OkStatus()) {
@@ -322,7 +328,8 @@ class NanopbServerWriter : private internal::NanopbServerCall {
}
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
private:
friend class internal::NanopbMethod;
@@ -382,7 +389,6 @@ class NanopbUnaryResponder : private internal::NanopbServerCall {
}
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
private:
friend class internal::NanopbMethod;
diff --git a/pw_rpc/packet.cc b/pw_rpc/packet.cc
index 3e9803c23..0bd452190 100644
--- a/pw_rpc/packet.cc
+++ b/pw_rpc/packet.cc
@@ -14,6 +14,7 @@
#include "pw_rpc/internal/packet.h"
+#include "pw_log/log.h"
#include "pw_protobuf/decoder.h"
namespace pw::rpc::internal {
@@ -131,4 +132,25 @@ size_t Packet::MinEncodedSizeBytes() const {
return reserved_size;
}
+void Packet::DebugLog() const {
+ PW_LOG_INFO(
+ "Packet {\n"
+ " Type : %s (%d)\n"
+ " Channel: %u\n"
+ " Service: %08x\n"
+ " Method : %08x\n"
+ " ID : %08x\n"
+ " Payload: %u B\n"
+ " Status : %s\n"
+ "}",
+ PacketTypeToString(type_),
+ static_cast<int>(type_),
+ static_cast<unsigned>(channel_id_),
+ static_cast<unsigned>(service_id_),
+ static_cast<unsigned>(method_id_),
+ static_cast<unsigned>(call_id_),
+ static_cast<unsigned>(payload_.size()),
+ status_.str());
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/packet_meta.cc b/pw_rpc/packet_meta.cc
index 5de0ba4b9..d0192d36f 100644
--- a/pw_rpc/packet_meta.cc
+++ b/pw_rpc/packet_meta.cc
@@ -36,4 +36,4 @@ Result<PacketMeta> PacketMeta::FromBuffer(ConstByteSpan data) {
return PacketMeta(packet);
}
-} // namespace pw::rpc \ No newline at end of file
+} // namespace pw::rpc
diff --git a/pw_rpc/packet_meta_test.cc b/pw_rpc/packet_meta_test.cc
index d21f3a48d..0f8a9683a 100644
--- a/pw_rpc/packet_meta_test.cc
+++ b/pw_rpc/packet_meta_test.cc
@@ -15,21 +15,22 @@
#include "pw_rpc/packet_meta.h"
#include "gtest/gtest.h"
+#include "pw_fuzzer/fuzztest.h"
#include "pw_rpc/internal/packet.h"
namespace pw::rpc {
namespace {
-TEST(PacketMeta, FromBufferDecodesValidMinimalPacket) {
- const uint32_t kChannelId = 12;
- const ServiceId kServiceId = internal::WrapServiceId(0xdeadbeef);
- const uint32_t kMethodId = 44;
+using namespace fuzzer;
+void FromBufferDecodesValidMinimalPacket(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id) {
internal::Packet packet;
- packet.set_channel_id(kChannelId);
- packet.set_service_id(internal::UnwrapServiceId(kServiceId));
+ packet.set_channel_id(channel_id);
+ packet.set_service_id(service_id);
packet.set_type(internal::pwpb::PacketType::RESPONSE);
- packet.set_method_id(kMethodId);
+ packet.set_method_id(method_id);
std::byte buffer[128];
Result<ConstByteSpan> encode_result = packet.Encode(buffer);
@@ -37,11 +38,21 @@ TEST(PacketMeta, FromBufferDecodesValidMinimalPacket) {
Result<PacketMeta> decode_result = PacketMeta::FromBuffer(*encode_result);
ASSERT_EQ(decode_result.status(), OkStatus());
- EXPECT_EQ(decode_result->channel_id(), kChannelId);
- EXPECT_EQ(decode_result->service_id(), kServiceId);
+ EXPECT_EQ(decode_result->channel_id(), channel_id);
+ EXPECT_EQ(decode_result->service_id(), internal::WrapServiceId(service_id));
EXPECT_TRUE(decode_result->destination_is_client());
}
+TEST(PacketMeta, FromBufferDecodesValidMinimalPacketConst) {
+ const uint32_t kChannelId = 12;
+ const uint32_t kServiceId = 0xdeadbeef;
+ const uint32_t kMethodId = 44;
+ FromBufferDecodesValidMinimalPacket(kChannelId, kServiceId, kMethodId);
+}
+
+FUZZ_TEST(PacketMeta, FromBufferDecodesValidMinimalPacket)
+ .WithDomains(NonZero<uint32_t>(), NonZero<uint32_t>(), NonZero<uint32_t>());
+
TEST(PacketMeta, FromBufferFailsOnIncompletePacket) {
internal::Packet packet;
diff --git a/pw_rpc/packet_test.cc b/pw_rpc/packet_test.cc
index a0d7da3b6..b1051950f 100644
--- a/pw_rpc/packet_test.cc
+++ b/pw_rpc/packet_test.cc
@@ -16,6 +16,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_fuzzer/fuzztest.h"
#include "pw_protobuf/wire_format.h"
namespace pw::rpc::internal {
@@ -24,6 +25,7 @@ namespace {
using protobuf::FieldKey;
using ::pw::rpc::internal::pwpb::PacketType;
using std::byte;
+using namespace fuzzer;
constexpr auto kPayload = bytes::Array<0x82, 0x02, 0xff, 0xff>();
@@ -116,16 +118,19 @@ TEST(Packet, Decode_InvalidPacket) {
EXPECT_EQ(Status::DataLoss(), Packet::FromBuffer(bad_data).status());
}
-TEST(Packet, EncodeDecode) {
- constexpr byte payload[]{byte(0x00), byte(0x01), byte(0x02), byte(0x03)};
-
+void EncodeDecode(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id,
+ ConstByteSpan payload,
+ Status status) {
Packet packet;
- packet.set_channel_id(12);
- packet.set_service_id(0xdeadbeef);
- packet.set_method_id(0x03a82921);
- packet.set_call_id(33);
+ packet.set_channel_id(channel_id);
+ packet.set_service_id(service_id);
+ packet.set_method_id(method_id);
+ packet.set_call_id(call_id);
packet.set_payload(payload);
- packet.set_status(Status::Unavailable());
+ packet.set_status(status);
byte buffer[128];
Result result = packet.Encode(buffer);
@@ -146,9 +151,22 @@ TEST(Packet, EncodeDecode) {
packet.payload().data(),
packet.payload().size()),
0);
- EXPECT_EQ(decoded.status(), Status::Unavailable());
+ EXPECT_EQ(decoded.status(), status);
+}
+
+TEST(Packet, EncodeDecodeFixed) {
+ constexpr byte payload[]{byte(0x00), byte(0x01), byte(0x02), byte(0x03)};
+ EncodeDecode(12, 0xdeadbeef, 0x03a82921, 33, payload, Status::Unavailable());
}
+FUZZ_TEST(Packet, EncodeDecode)
+ .WithDomains(NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ VectorOf<100>(Arbitrary<byte>()),
+ Arbitrary<Status>());
+
constexpr size_t kReservedSize = 2 /* type */ + 2 /* channel */ +
5 /* service */ + 5 /* method */ +
2 /* payload key */ + 2 /* status */;
diff --git a/pw_rpc/public/pw_rpc/benchmark.h b/pw_rpc/public/pw_rpc/benchmark.h
index 7ac45133c..ea40b7708 100644
--- a/pw_rpc/public/pw_rpc/benchmark.h
+++ b/pw_rpc/public/pw_rpc/benchmark.h
@@ -13,12 +13,17 @@
// the License.
#pragma once
+#include <cstdint>
+#include <unordered_map>
+
#include "pw_rpc/benchmark.raw_rpc.pb.h"
namespace pw::rpc {
// RPC service with low-level RPCs for transmitting data. Used for benchmarking
// and testing.
+//
+// NOTE: the implementation of `BidirectionalEcho` is *not* thread-safe.
class BenchmarkService
: public pw_rpc::raw::Benchmark::Service<BenchmarkService> {
public:
@@ -27,7 +32,11 @@ class BenchmarkService
void BidirectionalEcho(RawServerReaderWriter& reader_writer);
private:
- RawServerReaderWriter reader_writer_;
+ using ReaderWriterId = uint64_t;
+ ReaderWriterId AllocateReaderWriterId();
+
+ ReaderWriterId next_reader_writer_id_ = 0;
+ std::unordered_map<ReaderWriterId, RawServerReaderWriter> reader_writers_;
};
} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/channel.h b/pw_rpc/public/pw_rpc/channel.h
index 19c65b428..956548a93 100644
--- a/pw_rpc/public/pw_rpc/channel.h
+++ b/pw_rpc/public/pw_rpc/channel.h
@@ -117,7 +117,7 @@ class Channel {
// Creates a dynamically assignable channel without a set ID or output.
constexpr Channel() : id_(kUnassignedChannelId), output_(nullptr) {}
- // TODO(b/234876441): Remove the Configure and set_channel_output functions.
+ // TODO: b/234876441 - Remove the Configure and set_channel_output functions.
// Users should call CloseChannel() / OpenChannel() to change a channel.
// This ensures calls are properly update and works consistently between
// static and dynamic channel allocation.
diff --git a/pw_rpc/public/pw_rpc/internal/call.h b/pw_rpc/public/pw_rpc/internal/call.h
index ca9597a04..bb6cdc7e1 100644
--- a/pw_rpc/public/pw_rpc/internal/call.h
+++ b/pw_rpc/public/pw_rpc/internal/call.h
@@ -64,9 +64,10 @@ class CallProperties {
constexpr CallProperties(MethodType method_type,
CallType call_type,
CallbackProtoType callback_proto_type)
- : bits_((static_cast<uint8_t>(method_type) << 0) |
- (static_cast<uint8_t>(call_type) << 2) |
- (static_cast<uint8_t>(callback_proto_type) << 3)) {}
+ : bits_(static_cast<uint8_t>(
+ (static_cast<uint8_t>(method_type) << 0) |
+ (static_cast<uint8_t>(call_type) << 2) |
+ (static_cast<uint8_t>(callback_proto_type) << 3))) {}
constexpr CallProperties(const CallProperties&) = default;
@@ -100,8 +101,20 @@ inline constexpr uint32_t kOpenCallId = std::numeric_limits<uint32_t>::max();
//
// Private inheritance is used in place of composition or more complex
// inheritance hierarchy so that these objects all inherit from a common
-// IntrusiveList::Item object. Private inheritance also gives the derived classs
+// IntrusiveList::Item object. Private inheritance also gives the derived class
// full control over their interfaces.
+//
+// IMPLEMENTATION NOTE:
+//
+// Subclasses of `Call` must include a destructor which calls
+// `DestroyServerCall` or `DestroyClientCall` (as appropriate) if the subclass
+// contains any fields which might be referenced by the call's callbacks. This
+// ensures that the callbacks do not reference fields which may have already
+// been destroyed.
+//
+// At the top level, `ServerCall` and `ClientCall` invoke `DestroyServerCall`
+// `DestroyClientCall` respectively to perform cleanup in the case where no
+// subclass carries additional state.
class Call : public IntrusiveList<Call>::Item {
public:
Call(const Call&) = delete;
@@ -112,7 +125,12 @@ class Call : public IntrusiveList<Call>::Item {
Call& operator=(const Call&) = delete;
Call& operator=(Call&&) = delete;
- ~Call() PW_LOCKS_EXCLUDED(rpc_lock());
+ ~Call() {
+ // Ensure that calls have already been closed and unregistered.
+ // See class IMPLEMENTATION NOTE for further details.
+ PW_DASSERT((state_ & kHasBeenDestroyed) != 0);
+ PW_DASSERT(!active_locked() && !CallbacksAreRunning());
+ }
// True if the Call is active and ready to send responses.
[[nodiscard]] bool active() const PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -183,16 +201,24 @@ class Call : public IntrusiveList<Call>::Item {
pwpb::PacketType::SERVER_ERROR, {}, error);
}
- // Public function that ends the client stream for a client call.
- Status CloseClientStream() PW_LOCKS_EXCLUDED(rpc_lock()) {
+ // Public function that indicates that the client requests completion of the
+ // RPC, but is still active and listening to responses. For client streaming
+ // and bi-directional streaming RPCs, this also closes the client stream. If
+ // PW_RPC_COMPLETION_REQUEST_CALLBACK is enabled and
+ // on_client_requested_completion callback is set using the
+ // set_on_completion_requested_if_enabled, then the callback will be invoked
+ // on the server side. The server may then take an appropriate action to
+ // cleanup and stop server streaming.
+ Status RequestCompletion() PW_LOCKS_EXCLUDED(rpc_lock()) {
RpcLockGuard lock;
- return CloseClientStreamLocked();
+ return RequestCompletionLocked();
}
- // Internal function that closes the client stream.
- Status CloseClientStreamLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- MarkClientStreamCompleted();
- return SendPacket(pwpb::PacketType::CLIENT_STREAM_END, {}, {});
+ // Internal function that closes the client stream (if applicable) and sends
+ // CLIENT_REQUEST_COMPLETION packet to request call completion.
+ Status RequestCompletionLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MarkStreamCompleted();
+ return SendPacket(pwpb::PacketType::CLIENT_REQUEST_COMPLETION, {}, {});
}
// Sends a payload in either a server or client stream packet.
@@ -254,8 +280,10 @@ class Call : public IntrusiveList<Call>::Item {
return HasServerStream(properties_.method_type());
}
- bool client_stream_open() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return (state_ & kClientStreamActive) != 0;
+ // Returns true if the client has already requested completion.
+ bool client_requested_completion() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return (state_ & kClientRequestedCompletion) != 0;
}
// Closes a call without doing anything else. Called from the Endpoint
@@ -266,6 +294,9 @@ class Call : public IntrusiveList<Call>::Item {
endpoint_ = nullptr;
}
+ // Logs detailed info about this call at INFO level. NOT for production use!
+ void DebugLog() const;
+
protected:
// Creates an inactive Call.
constexpr Call()
@@ -290,6 +321,11 @@ class Call : public IntrusiveList<Call>::Item {
uint32_t method_id,
CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ // Closes the call and waits for their callbacks to complete so destructors
+ // can run safely.
+ void DestroyServerCall() PW_LOCKS_EXCLUDED(rpc_lock());
+ void DestroyClientCall() PW_LOCKS_EXCLUDED(rpc_lock());
+
void CallbackStarted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
callbacks_executing_ += 1;
}
@@ -331,10 +367,15 @@ class Call : public IntrusiveList<Call>::Item {
on_error_ = std::move(on_error);
}
- void MarkClientStreamCompleted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- state_ &= ~kClientStreamActive;
+ void MarkStreamCompleted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ state_ |= kClientRequestedCompletion;
}
+ // Closes a client call. Sends a CLIENT_REQUEST_COMPLETION for client /
+ // bidirectional streaming RPCs if not sent yet.
+ void CloseClientCall() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ // Closes a server call.
Status CloseAndSendResponseLocked(Status status)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return CloseAndSendFinalPacketLocked(
@@ -438,8 +479,9 @@ class Call : public IntrusiveList<Call>::Item {
private:
enum State : uint8_t {
- kActive = 0b01,
- kClientStreamActive = 0b10,
+ kActive = 0b001,
+ kClientRequestedCompletion = 0b010,
+ kHasBeenDestroyed = 0b100,
};
// Common constructor for server & client calls.
@@ -468,7 +510,7 @@ class Call : public IntrusiveList<Call>::Item {
void MarkClosed() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
channel_id_ = Channel::kUnassignedChannelId;
id_ = 0;
- state_ = 0;
+ state_ = kClientRequestedCompletion;
}
// Calls the on_error callback without closing the RPC. This is used when the
@@ -498,6 +540,9 @@ class Call : public IntrusiveList<Call>::Item {
return callbacks_executing_ != 0u;
}
+ // Waits for callbacks to complete so that a call object can be destroyed.
+ void WaitForCallbacksToComplete() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
Endpoint* endpoint_ PW_GUARDED_BY(rpc_lock());
uint32_t channel_id_ PW_GUARDED_BY(rpc_lock());
uint32_t id_ PW_GUARDED_BY(rpc_lock());
@@ -508,7 +553,7 @@ class Call : public IntrusiveList<Call>::Item {
//
// bit 0: call is active
// bit 1: client stream is active
- //
+ // bit 2: call has been destroyed
uint8_t state_ PW_GUARDED_BY(rpc_lock());
// If non-OK, indicates that the call was closed and needs to have its
diff --git a/pw_rpc/public/pw_rpc/internal/client_call.h b/pw_rpc/public/pw_rpc/internal/client_call.h
index 793d581e4..e30dbc3b1 100644
--- a/pw_rpc/public/pw_rpc/internal/client_call.h
+++ b/pw_rpc/public/pw_rpc/internal/client_call.h
@@ -26,8 +26,6 @@ namespace pw::rpc::internal {
// A Call object, as used by an RPC client.
class ClientCall : public Call {
public:
- ~ClientCall() PW_LOCKS_EXCLUDED(rpc_lock()) { Abandon(); }
-
uint32_t id() const PW_LOCKS_EXCLUDED(rpc_lock()) {
RpcLockGuard lock;
return Call::id();
@@ -53,6 +51,8 @@ class ClientCall : public Call {
CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: Call(client, channel_id, service_id, method_id, properties) {}
+ ~ClientCall() { DestroyClientCall(); }
+
// Public function that closes a call client-side without cancelling it on the
// server.
void Abandon() PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -60,9 +60,6 @@ class ClientCall : public Call {
CloseClientCall();
}
- // Sends CLIENT_STREAM_END if applicable and marks the call as closed.
- void CloseClientCall() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
-
void MoveClientCallFrom(ClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
};
@@ -71,6 +68,8 @@ class ClientCall : public Call {
// on_completed callback. The on_next callback is not used.
class UnaryResponseClientCall : public ClientCall {
public:
+ ~UnaryResponseClientCall() { DestroyClientCall(); }
+
// Start call for raw unary response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
@@ -143,6 +142,8 @@ class UnaryResponseClientCall : public ClientCall {
// callback. Payloads are sent through the on_next callback.
class StreamResponseClientCall : public ClientCall {
public:
+ ~StreamResponseClientCall() { DestroyClientCall(); }
+
// Start call for raw stream response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
@@ -152,7 +153,7 @@ class StreamResponseClientCall : public ClientCall {
Function<void(ConstByteSpan)>&& on_next,
Function<void(Status)>&& on_completed,
Function<void(Status)>&& on_error,
- ConstByteSpan request) {
+ ConstByteSpan request) PW_LOCKS_EXCLUDED(rpc_lock()) {
rpc_lock().lock();
CallType call(client.ClaimLocked(), channel_id, service_id, method_id);
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing.h b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
index 01cfd16a3..f9523e61f 100644
--- a/pw_rpc/public/pw_rpc/internal/client_server_testing.h
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
@@ -16,14 +16,19 @@
#include <cinttypes>
#include <mutex>
+#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/client_server.h"
#include "pw_rpc/internal/fake_channel_output.h"
#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/packet_meta.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::rpc {
+using TestPacketProcessor =
+ pw::Function<pw::Status(ClientServer&, pw::ConstByteSpan)>;
+
namespace internal {
// Expands on a Fake Channel Output implementation to allow for forwarding of
@@ -50,14 +55,29 @@ class ForwardingChannelOutput : public ChannelOutput {
return false;
}
++sent_packets_;
- const auto process_result = client_server.ProcessPacket(*result);
+
+ Result<PacketMeta> meta = pw::rpc::PacketMeta::FromBuffer(*result);
+ PW_ASSERT(meta.ok());
+
+ pw::Status process_result = pw::Status::Internal();
+ if (meta->destination_is_server() && server_packet_processor_) {
+ process_result = server_packet_processor_(client_server, *result);
+ } else if (meta->destination_is_client() && client_packet_processor_) {
+ process_result = client_packet_processor_(client_server, *result);
+ } else {
+ process_result = client_server.ProcessPacket(*result);
+ }
PW_ASSERT(process_result.ok());
return true;
}
protected:
- constexpr ForwardingChannelOutput()
- : ChannelOutput("testing::FakeChannelOutput") {}
+ explicit ForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : ChannelOutput("testing::FakeChannelOutput"),
+ server_packet_processor_(std::move(server_packet_processor)),
+ client_packet_processor_(std::move(client_packet_processor)) {}
FakeChannelOutputImpl output_;
@@ -76,6 +96,9 @@ class ForwardingChannelOutput : public ChannelOutput {
}
uint16_t sent_packets_ = 0;
+
+ const TestPacketProcessor server_packet_processor_;
+ const TestPacketProcessor client_packet_processor_;
};
// Provides a testing context with a real client and server
@@ -97,8 +120,12 @@ class ClientServerTestContext {
}
protected:
- explicit ClientServerTestContext()
- : channel_(Channel::Create<1>(&channel_output_)),
+ explicit ClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : channel_output_(std::move(server_packet_processor),
+ std::move(client_packet_processor)),
+ channel_(Channel::Create<1>(&channel_output_)),
client_server_({&channel_, 1}) {}
~ClientServerTestContext() = default;
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
index ff838a28a..d10f19256 100644
--- a/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/client_server.h"
#include "pw_rpc/internal/client_server_testing.h"
@@ -73,7 +74,11 @@ class WatchableChannelOutput
}
protected:
- constexpr WatchableChannelOutput() = default;
+ explicit WatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
size_t PacketCount() const PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) override {
return Base::PacketCount();
@@ -119,8 +124,13 @@ class ClientServerTestContextThreaded
}
protected:
- explicit ClientServerTestContextThreaded(const thread::Options& options)
- : thread_(options, Instance::Run, this) {}
+ explicit ClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)),
+ thread_(options, Instance::Run, this) {}
private:
using Base::ForwardNewPackets;
diff --git a/pw_rpc/public/pw_rpc/internal/config.h b/pw_rpc/public/pw_rpc/internal/config.h
index 24a227159..dec868b05 100644
--- a/pw_rpc/public/pw_rpc/internal/config.h
+++ b/pw_rpc/public/pw_rpc/internal/config.h
@@ -18,18 +18,30 @@
#include <cstddef>
#include <type_traits>
-/// In client and bidirectional RPCs, pw_rpc clients may signal that they have
-/// finished sending requests with a `CLIENT_STREAM_END` packet. While this can
-/// be useful in some circumstances, it is often not necessary.
+#if defined(PW_RPC_CLIENT_STREAM_END_CALLBACK) && \
+ PW_RPC_CLIENT_STREAM_END_CALLBACK
+#pragma message( \
+ "Warning PW_RPC_CLIENT_STREAM_END_CALLBACK is deprecated! " \
+ "Use PW_RPC_COMPLETION_REQUEST_CALLBACK instead.")
+#define PW_RPC_COMPLETION_REQUEST_CALLBACK 1
+#endif
+
+#undef PW_RPC_CLIENT_STREAM_END_CALLBACK
+
+/// pw_rpc clients may request call completion by sending
+/// `CLIENT_REQUEST_COMPLETION` packet. For client streaming or bi-direction
+/// RPCs, this also indicates that the client is done sending requests. While
+/// this can be useful in some circumstances, it is often not necessary.
///
/// This option controls whether or not include a callback that is called when
-/// the client stream ends. The callback is included in all ServerReader/Writer
-/// objects as a @cpp_type{pw::Function}, so may have a significant cost.
+/// the client stream requests for completion. The callback is included in all
+/// ServerReader/Writer objects as a @cpp_type{pw::Function}, so may have a
+/// significant cost.
///
/// This is disabled by default.
-#ifndef PW_RPC_CLIENT_STREAM_END_CALLBACK
-#define PW_RPC_CLIENT_STREAM_END_CALLBACK 0
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#ifndef PW_RPC_COMPLETION_REQUEST_CALLBACK
+#define PW_RPC_COMPLETION_REQUEST_CALLBACK 0
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
/// The Nanopb-based pw_rpc implementation allocates memory to use for Nanopb
/// structs for the request and response protobufs. The template function that
@@ -96,11 +108,13 @@
// When building for a desktop operating system, use a 1ms sleep by default.
// 1-tick duration sleeps can result in spurious timeouts.
-#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+#if defined(_WIN32) || defined(__APPLE__) || \
+ defined(__linux__) && !defined(__ZEPHYR__)
#define PW_RPC_YIELD_SLEEP_DURATION std::chrono::milliseconds(1)
#else
#define PW_RPC_YIELD_SLEEP_DURATION pw::chrono::SystemClock::duration(1)
#endif // defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+ // && !defined(__ZEPHYR__)
#endif // PW_RPC_YIELD_SLEEP_DURATION
@@ -185,6 +199,22 @@ static_assert(
#define PW_RPC_DYNAMIC_CONTAINER_INCLUDE <vector>
#endif // PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this macro must expand to
+/// a statement that creates a `std::unique_ptr`-like smart pointer.
+/// @param type The type of the object to construct (e.g. with `new`)
+/// @param ... Arguments to pass to the constructor, if any
+#ifndef PW_RPC_MAKE_UNIQUE_PTR
+#define PW_RPC_MAKE_UNIQUE_PTR(type, ...) \
+ std::unique_ptr<type>(new type(__VA_ARGS__))
+#endif // PW_RPC_DYNAMIC_CONTAINER
+
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this header file is
+/// included in files that use @c_macro{PW_RPC_MAKE_UNIQUE_PTR}. Defaults to
+/// `<memory>` for `std::make_unique`.
+#ifndef PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#define PW_RPC_MAKE_UNIQUE_PTR_INCLUDE <memory>
+#endif // PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+
/// Size of the global RPC packet encoding buffer in bytes. If dynamic
/// allocation is enabled, this value is only used for test helpers that
/// allocate RPC encoding buffers.
@@ -205,7 +235,7 @@ static_assert(
namespace pw::rpc::cfg {
template <typename...>
-constexpr std::bool_constant<PW_RPC_CLIENT_STREAM_END_CALLBACK>
+constexpr std::bool_constant<PW_RPC_COMPLETION_REQUEST_CALLBACK>
kClientStreamEndCallbackEnabled;
template <typename...>
diff --git a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
index b60f5c598..b0e1e6795 100644
--- a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
+++ b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
@@ -158,8 +158,8 @@ class FakeChannelOutput : public ChannelOutput {
return internal::test::PacketsView(
packets_,
internal::test::PacketFilter(
- internal::pwpb::PacketType::CLIENT_STREAM_END,
- internal::pwpb::PacketType::CLIENT_STREAM_END,
+ internal::pwpb::PacketType::CLIENT_REQUEST_COMPLETION,
+ internal::pwpb::PacketType::CLIENT_REQUEST_COMPLETION,
channel_id,
MethodInfo<kMethod>::kServiceId,
MethodInfo<kMethod>::kMethodId))
diff --git a/pw_rpc/public/pw_rpc/internal/hash.h b/pw_rpc/public/pw_rpc/internal/hash.h
index 57fee5287..ae4efe903 100644
--- a/pw_rpc/public/pw_rpc/internal/hash.h
+++ b/pw_rpc/public/pw_rpc/internal/hash.h
@@ -30,14 +30,14 @@ constexpr uint32_t Hash(std::string_view string)
constexpr uint32_t kHashConstant = 65599;
// The length is hashed as if it were the first character.
- uint32_t hash = string.size();
+ uint32_t hash = static_cast<uint32_t>(string.size());
uint32_t coefficient = kHashConstant;
// Hash all of the characters in the string as unsigned ints.
// The coefficient calculation is done modulo 0x100000000, so the unsigned
// integer overflows are intentional.
- for (uint8_t ch : string) {
- hash += coefficient * ch;
+ for (char ch : string) {
+ hash += coefficient * static_cast<uint8_t>(ch);
coefficient *= kHashConstant;
}
diff --git a/pw_rpc/public/pw_rpc/internal/method_lookup.h b/pw_rpc/public/pw_rpc/internal/method_lookup.h
index 232e0c4db..13fc226ff 100644
--- a/pw_rpc/public/pw_rpc/internal/method_lookup.h
+++ b/pw_rpc/public/pw_rpc/internal/method_lookup.h
@@ -52,8 +52,14 @@ class MethodLookup {
template <typename Service, uint32_t kMethodId>
static constexpr const auto& GetMethodUnion() {
constexpr auto method = GetMethodUnionPointer<Service>(kMethodId);
+// TODO: b/285367496 - Remove this #ifndef guard when the static assert
+// compiles correctly when using the Andestech RISC-V GCC 10.3.0 toolchain.
+#if !(defined(__riscv) && defined(__nds_v5) && (__GNUC__ == 10) && \
+ (__GNUC_MINOR__ == 3) && (__GNUC_PATCHLEVEL__ == 0))
static_assert(method != nullptr,
"The selected function is not an RPC service method");
+#endif // !(defined(__riscv) && defined(__nds_v5) && (__GNUC__ == 10)
+ // && (__GNUC_MINOR__ == 3) && (__GNUC_PATCHLEVEL__ == 0))
return *method;
}
diff --git a/pw_rpc/public/pw_rpc/internal/packet.h b/pw_rpc/public/pw_rpc/internal/packet.h
index 64fe6b2e4..237356164 100644
--- a/pw_rpc/public/pw_rpc/internal/packet.h
+++ b/pw_rpc/public/pw_rpc/internal/packet.h
@@ -28,7 +28,7 @@ class Packet {
public:
static constexpr uint32_t kUnassignedId = 0;
- // TODO(b/236156534): This can use the pwpb generated
+ // TODO: b/236156534 - This can use the pwpb generated
// pw::rpc::internal::pwpb::RpcPacket::kMaxEncodedSizeBytes once the max value
// of enums is properly accounted for and when `status` is changed from a
// uint32 to a StatusCode.
@@ -141,6 +141,9 @@ class Packet {
constexpr void set_payload(ConstByteSpan payload) { payload_ = payload; }
constexpr void set_status(Status status) { status_ = status; }
+ // Logs detailed info about this packet at INFO level. NOT for production use!
+ void DebugLog() const;
+
private:
pwpb::PacketType type_;
uint32_t channel_id_;
diff --git a/pw_rpc/public/pw_rpc/internal/server_call.h b/pw_rpc/public/pw_rpc/internal/server_call.h
index 65cb7375d..f6cccb0f9 100644
--- a/pw_rpc/public/pw_rpc/internal/server_call.h
+++ b/pw_rpc/public/pw_rpc/internal/server_call.h
@@ -23,21 +23,22 @@ namespace pw::rpc::internal {
// A Call object, as used by an RPC server.
class ServerCall : public Call {
public:
- void HandleClientStreamEnd() PW_UNLOCK_FUNCTION(rpc_lock()) {
- MarkClientStreamCompleted();
+ void HandleClientRequestedCompletion() PW_UNLOCK_FUNCTION(rpc_lock()) {
+ MarkStreamCompleted();
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- auto on_client_stream_end_local = std::move(on_client_stream_end_);
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ auto on_client_requested_completion_local =
+ std::move(on_client_requested_completion_);
CallbackStarted();
rpc_lock().unlock();
- if (on_client_stream_end_local) {
- on_client_stream_end_local();
+ if (on_client_requested_completion_local) {
+ on_client_requested_completion_local();
}
rpc_lock().lock();
CallbackFinished();
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
rpc_lock().unlock();
}
@@ -46,10 +47,7 @@ class ServerCall : public Call {
ServerCall(ServerCall&& other) { *this = std::move(other); }
- ~ServerCall() PW_LOCKS_EXCLUDED(rpc_lock()) {
- // Any errors are logged in Channel::Send.
- CloseAndSendResponse(OkStatus()).IgnoreError();
- }
+ ~ServerCall() { DestroyServerCall(); }
// Version of operator= used by the raw call classes.
ServerCall& operator=(ServerCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -65,28 +63,43 @@ class ServerCall : public Call {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: Call(context, properties) {}
- // set_on_client_stream_end is templated so that it can be conditionally
- // disabled with a helpful static_assert message.
+ // set_on_completion_requested is templated so that it can be
+ // conditionally disabled with a helpful static_assert message.
template <typename UnusedType = void>
- void set_on_client_stream_end(
- [[maybe_unused]] Function<void()>&& on_client_stream_end)
+ void set_on_completion_requested(
+ [[maybe_unused]] Function<void()>&& on_client_requested_completion)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ static_assert(cfg::kClientStreamEndCallbackEnabled<UnusedType>,
+ "The client stream end callback is disabled, so "
+ "set_on_completion_requested cannot be called. To "
+ "enable the client end "
+ "callback, set PW_RPC_REQUEST_COMPLETION_CALLBACK to 1.");
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ RpcLockGuard lock;
+ on_client_requested_completion_ = std::move(on_client_requested_completion);
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
+ }
+
+ // Sets the provided on_client_requested_completion callback if
+ // PW_RPC_COMPLETION_REQUEST_CALLBACK is defined. Unlike
+ // set_on_completion_requested this API will not raise a static_assert
+ // message at compile time even when the macro is not defined.
+ void set_on_completion_requested_if_enabled(
+ Function<void()>&& on_client_requested_completion)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- static_assert(
- cfg::kClientStreamEndCallbackEnabled<UnusedType>,
- "The client stream end callback is disabled, so "
- "set_on_client_stream_end cannot be called. To enable the client end "
- "callback, set PW_RPC_CLIENT_STREAM_END_CALLBACK to 1.");
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
RpcLockGuard lock;
- on_client_stream_end_ = std::move(on_client_stream_end);
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ on_client_requested_completion_ = std::move(on_client_requested_completion);
+#else
+ on_client_requested_completion = nullptr;
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
}
private:
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
// Called when a client stream completes.
- Function<void()> on_client_stream_end_ PW_GUARDED_BY(rpc_lock());
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ Function<void()> on_client_requested_completion_ PW_GUARDED_BY(rpc_lock());
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
};
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h b/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h
new file mode 100644
index 000000000..7ca7b991e
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h
@@ -0,0 +1,200 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <utility>
+
+#include "pw_rpc/internal/config.h"
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/synchronous_call_result.h"
+#include "pw_sync/timed_thread_notification.h"
+
+#if PW_RPC_DYNAMIC_ALLOCATION
+#include PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+namespace pw::rpc::internal {
+
+template <typename Response>
+struct SynchronousCallState {
+ auto OnCompletedCallback() {
+ return [this](const Response& response, Status status) {
+ result = SynchronousCallResult<Response>(status, response);
+ notify.release();
+ };
+ }
+
+ auto OnRpcErrorCallback() {
+ return [this](Status status) {
+ result = SynchronousCallResult<Response>::RpcError(status);
+ notify.release();
+ };
+ }
+
+ SynchronousCallResult<Response> result;
+ sync::TimedThreadNotification notify;
+};
+
+class RawSynchronousCallState {
+ public:
+ RawSynchronousCallState(Function<void(ConstByteSpan, Status)> on_completed)
+ : on_completed_(std::move(on_completed)) {}
+
+ auto OnCompletedCallback() {
+ return [this](ConstByteSpan response, Status status) {
+ if (on_completed_) {
+ on_completed_(response, status);
+ }
+ notify.release();
+ };
+ }
+
+ auto OnRpcErrorCallback() {
+ return [this](Status status) {
+ error = status;
+ notify.release();
+ };
+ }
+
+ Status error;
+ sync::TimedThreadNotification notify;
+
+ private:
+ Function<void(ConstByteSpan, Status)> on_completed_;
+};
+
+// Overloaded function to choose detween timeout and deadline APIs.
+inline bool AcquireNotification(sync::TimedThreadNotification& notification,
+ chrono::SystemClock::duration timeout) {
+ return notification.try_acquire_for(timeout);
+}
+
+inline bool AcquireNotification(sync::TimedThreadNotification& notification,
+ chrono::SystemClock::time_point timeout) {
+ return notification.try_acquire_until(timeout);
+}
+
+template <auto kRpcMethod,
+ typename Response = typename MethodInfo<kRpcMethod>::Response,
+ typename DoCall,
+ typename... TimeoutArg>
+SynchronousCallResult<Response> StructSynchronousCall(
+ DoCall&& do_call, TimeoutArg... timeout_arg) {
+ static_assert(MethodInfo<kRpcMethod>::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ // If dynamic allocation is enabled, heap-allocate the call_state.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ auto call_state_ptr = PW_RPC_MAKE_UNIQUE_PTR(SynchronousCallState<Response>);
+ SynchronousCallState<Response>& call_state(*call_state_ptr);
+#else
+ SynchronousCallState<Response> call_state;
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+ auto call = std::forward<DoCall>(do_call)(call_state);
+
+ // Wait for the notification based on the type of the timeout argument.
+ if constexpr (sizeof...(TimeoutArg) == 0) {
+ call_state.notify.acquire(); // Wait forever, since no timeout was given.
+ } else if (!AcquireNotification(call_state.notify, timeout_arg...)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+
+// Template for a raw synchronous call. Used for SynchronousCall,
+// SynchronousCallFor, and SynchronousCallUntil. The type of the timeout
+// argument is used to determine the behavior.
+template <auto kRpcMethod, typename DoCall, typename... TimeoutArg>
+Status RawSynchronousCall(Function<void(ConstByteSpan, Status)>&& on_completed,
+ DoCall&& do_call,
+ TimeoutArg... timeout_arg) {
+ static_assert(MethodInfo<kRpcMethod>::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ RawSynchronousCallState call_state{std::move(on_completed)};
+
+ auto call = std::forward<DoCall>(do_call)(call_state);
+
+ // Wait for the notification based on the type of the timeout argument.
+ if constexpr (sizeof...(TimeoutArg) == 0) {
+ call_state.notify.acquire(); // Wait forever, since no timeout was given.
+ } else if (!AcquireNotification(call_state.notify, timeout_arg...)) {
+ return Status::DeadlineExceeded();
+ }
+
+ return call_state.error;
+}
+
+// Choose which call state object to use (raw or struct).
+template <auto kRpcMethod,
+ typename Response =
+ typename internal::MethodInfo<kRpcMethod>::Response>
+using CallState = std::conditional_t<
+ std::is_same_v<typename MethodInfo<kRpcMethod>::Request, void>,
+ RawSynchronousCallState,
+ SynchronousCallState<Response>>;
+
+// Invokes the RPC method free function using a call_state.
+template <auto kRpcMethod, typename Request>
+constexpr auto CallFreeFunction(Client& client,
+ uint32_t channel_id,
+ const Request& request) {
+ return [&client, channel_id, &request](CallState<kRpcMethod>& call_state) {
+ return kRpcMethod(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+// Invokes the RPC method free function using a call_state and a custom
+// response.
+template <
+ auto kRpcMethod,
+ typename Response = typename internal::MethodInfo<kRpcMethod>::Response,
+ typename Request>
+constexpr auto CallFreeFunctionWithCustomResponse(Client& client,
+ uint32_t channel_id,
+ const Request& request) {
+ return [&client, channel_id, &request](
+ CallState<kRpcMethod, Response>& call_state) {
+ constexpr auto kMemberFunction =
+ MethodInfo<kRpcMethod>::template FunctionTemplate<
+ typename MethodInfo<kRpcMethod>::ServiceClass,
+ Response>();
+ return (*kMemberFunction)(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+// Invokes the RPC function on the generated service client using a call_state.
+template <auto kRpcMethod, typename GeneratedClient, typename Request>
+constexpr auto CallGeneratedClient(const GeneratedClient& client,
+ const Request& request) {
+ return [&client, &request](CallState<kRpcMethod>& call_state) {
+ constexpr auto kMemberFunction =
+ MethodInfo<kRpcMethod>::template Function<GeneratedClient>();
+ return (client.*kMemberFunction)(request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/test_method_context.h b/pw_rpc/public/pw_rpc/internal/test_method_context.h
index 2cc49953c..aee85e6af 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method_context.h
+++ b/pw_rpc/public/pw_rpc/internal/test_method_context.h
@@ -135,7 +135,7 @@ class InvocationContext {
std::byte packet[kNoPayloadPacketSizeBytes];
PW_ASSERT(server_
- .ProcessPacket(Packet(PacketType::CLIENT_STREAM_END,
+ .ProcessPacket(Packet(PacketType::CLIENT_REQUEST_COMPLETION,
channel_.id(),
UnwrapServiceId(service_.service_id()),
kMethodId)
diff --git a/pw_rpc/public/pw_rpc/packet_meta.h b/pw_rpc/public/pw_rpc/packet_meta.h
index efa5e5dba..e6dce0269 100644
--- a/pw_rpc/public/pw_rpc/packet_meta.h
+++ b/pw_rpc/public/pw_rpc/packet_meta.h
@@ -16,6 +16,7 @@
#include <cstdint>
#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/internal/packet.pwpb.h"
#include "pw_rpc/method_id.h"
#include "pw_rpc/service_id.h"
#include "pw_span/span.h"
@@ -40,6 +41,12 @@ class PacketMeta {
constexpr bool destination_is_server() const {
return destination_ == internal::Packet::kServer;
}
+ constexpr bool type_is_client_error() const {
+ return type_ == internal::pwpb::PacketType::CLIENT_ERROR;
+ }
+ constexpr bool type_is_server_error() const {
+ return type_ == internal::pwpb::PacketType::SERVER_ERROR;
+ }
// Note: this `payload` is only valid so long as the original `data` buffer
// passed to `PacketMeta::FromBuffer` remains valid.
constexpr ConstByteSpan payload() const { return payload_; }
@@ -50,11 +57,13 @@ class PacketMeta {
service_id_(internal::WrapServiceId(packet.service_id())),
method_id_(internal::WrapMethodId(packet.method_id())),
destination_(packet.destination()),
+ type_(packet.type()),
payload_(packet.payload()) {}
uint32_t channel_id_;
ServiceId service_id_;
MethodId method_id_;
internal::Packet::Destination destination_;
+ internal::pwpb::PacketType type_;
ConstByteSpan payload_;
};
diff --git a/pw_rpc/public/pw_rpc/server.h b/pw_rpc/public/pw_rpc/server.h
index 19c9333c5..2369c46a1 100644
--- a/pw_rpc/public/pw_rpc/server.h
+++ b/pw_rpc/public/pw_rpc/server.h
@@ -171,6 +171,11 @@ class Server : public internal::Endpoint {
const internal::Packet& packet)
PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock());
+ void HandleCompletionRequest(const internal::Packet& packet,
+ internal::Channel& channel,
+ IntrusiveList<internal::Call>::iterator call)
+ const PW_UNLOCK_FUNCTION(internal::rpc_lock());
+
void HandleClientStreamPacket(const internal::Packet& packet,
internal::Channel& channel,
IntrusiveList<internal::Call>::iterator call)
diff --git a/pw_rpc/public/pw_rpc/synchronous_call.h b/pw_rpc/public/pw_rpc/synchronous_call.h
index 5dc58b1c8..ec02cac95 100644
--- a/pw_rpc/public/pw_rpc/synchronous_call.h
+++ b/pw_rpc/public/pw_rpc/synchronous_call.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -18,145 +18,171 @@
#include "pw_chrono/system_clock.h"
#include "pw_rpc/client.h"
#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/internal/synchronous_call_impl.h"
#include "pw_rpc/synchronous_call_result.h"
-#include "pw_sync/timed_thread_notification.h"
-
-// Synchronous Call wrappers
-//
-// Wraps an asynchronous RPC client call, converting it to a synchronous
-// interface.
-//
-// WARNING! This should not be called from any context that cannot be blocked!
-// This method will block the calling thread until the RPC completes, and
-// translate the response into a pw::rpc::SynchronousCallResult that contains
-// the error type and status or the proto response.
-//
-// Example:
-//
-// pw_rpc_EchoMessage request{.msg = "hello" };
-// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
-// pw::rpc::SynchronousCall<EchoService::Echo>(rpc_client,
-// channel_id,
-// request);
-// if (result.ok()) {
-// printf("%s", result.response().msg);
-// }
-//
-// Note: The above example will block indefinitely. If you'd like to include a
-// timeout for how long the call should block for, use the
-// `SynchronousCallFor()` or `SynchronousCallUntil()` variants.
-//
-// Additionally, the use of a generated Client object is supported:
-//
-// pw_rpc::nanopb::EchoService::client client;
-// pw_rpc_EchoMessage request{.msg = "hello" };
-// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
-// pw::rpc::SynchronousCall<EchoService::Echo>(client, request);
-//
-// if (result.ok()) {
-// printf("%s", result.response().msg);
-// }
+/// @file pw_rpc/synchronous_call.h
+///
+/// `pw_rpc` provides wrappers that convert the asynchronous client API to a
+/// synchronous API. The `SynchronousCall<RpcMethod>` functions wrap the
+/// asynchronous client RPC call with a timed thread notification and returns
+/// once a result is known or a timeout has occurred. Only unary methods are
+/// supported.
+///
+/// The Nanopb and pwpb APIs return a `SynchronousCallResult<Response>` object,
+/// which can be queried to determine whether any error scenarios occurred and,
+/// if not, access the response. The raw API executes a function when the call
+/// completes or returns a `pw::Status` if it does not.
+///
+/// `SynchronousCall<RpcMethod>` blocks indefinitely, whereas
+/// `SynchronousCallFor<RpcMethod>` and `SynchronousCallUntil<RpcMethod>` block
+/// for a given timeout or until a deadline, respectively. All wrappers work
+/// with either the standalone static RPC functions or the generated service
+/// client member methods.
+///
+/// @note Use of the SynchronousCall wrappers requires a
+/// @cpp_class{pw::sync::TimedThreadNotification} backend.
+///
+/// The following examples use the Nanopb API to make a call that blocks
+/// indefinitely. If you'd like to include a timeout for how long the call
+/// should block for, use the `SynchronousCallFor()` or `SynchronousCallUntil()`
+/// variants.
+///
+/// @code{.cpp}
+/// pw_rpc_EchoMessage request{.msg = "hello" };
+/// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo>(rpc_client,
+/// channel_id,
+/// request);
+/// if (result.ok()) {
+/// PW_LOG_INFO("%s", result.response().msg);
+/// }
+/// @endcode
+///
+/// Additionally, the use of a generated `Client` object is supported:
+///
+/// @code{.cpp}
+/// pw_rpc::nanopb::EchoService::Client client(rpc_client, channel_id);
+/// pw_rpc_EchoMessage request{.msg = "hello" };
+/// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo>(client, request);
+///
+/// if (result.ok()) {
+/// PW_LOG_INFO("%s", result.response().msg);
+/// }
+/// @endcode
+///
+/// `SynchronousCall<RpcMethod>` also supports using an optional custom response
+/// message class, `SynchronousCall<RpcMethod, Response>`. This enables the use
+/// of response messages with variable-length fields.
+///
+/// @code{.cpp}
+/// pw_rpc_MyMethodRequestMessage request{};
+/// class CustomResponse : public pw_rpc_MyMethodResponseMessage {
+/// public:
+/// CustomResponse() {
+/// repeated_field.SetDecoder([this](
+/// MyMethodResponse::StreamDecoder& decoder) {
+/// return decoder.ReadRepeatedField(values);
+/// }
+/// }
+/// pw::Vector<uint32_t, 4> values();
+/// };
+/// pw::rpc::SynchronousCallResult<CustomResponse> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo, CustomResponse>(rpc_client,
+/// channel_id,
+/// request);
+/// if (result.ok()) {
+/// PW_LOG_INFO("%d", result.response().values[0]);
+/// }
+/// };
+/// @endcode
+///
+/// The raw API works similarly to the Nanopb API, but takes a
+/// @cpp_type{pw::Function} and returns a @cpp_class{pw::Status}. If the RPC
+/// completes, the @cpp_type{pw::Function} is called with the response and
+/// returned status, and the `SynchronousCall` invocation returns
+/// @pw_status{OK}. If the RPC fails, `SynchronousCall` returns an error.
+///
+/// @code{.cpp}
+/// pw::Status rpc_status = pw::rpc::SynchronousCall<EchoService::Echo>(
+/// rpc_client, channel_id, encoded_request,
+/// [](pw::ConstByteSpan reply, pw::Status status) {
+/// PW_LOG_INFO("Received %zu bytes with status %s",
+/// reply.size(),
+/// status.str());
+/// });
+/// @endcode
+///
+/// @warning These wrappers should not be used from any context that cannot be
+/// blocked! This method will block the calling thread until the RPC completes,
+/// and translate the response into a `pw::rpc::SynchronousCallResult` that
+/// contains the error type and status or the proto response.
namespace pw::rpc {
-namespace internal {
-
-template <typename Response>
-struct SynchronousCallState {
- auto OnCompletedCallback() {
- return [this](const Response& response, Status status) {
- result = SynchronousCallResult<Response>(status, response);
- notify.release();
- };
- }
- auto OnRpcErrorCallback() {
- return [this](Status status) {
- result = SynchronousCallResult<Response>::RpcError(status);
- notify.release();
- };
- }
-
- SynchronousCallResult<Response> result;
- sync::TimedThreadNotification notify;
-};
-
-} // namespace internal
-
-// SynchronousCall
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-template <auto kRpcMethod>
-SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
-SynchronousCall(
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks indefinitely
+/// until a response is received.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+template <
+ auto kRpcMethod,
+ typename Response = typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallResult<Response> SynchronousCall(
Client& client,
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- call_state.notify.acquire();
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod, Response>(
+ internal::CallFreeFunctionWithCustomResponse<kRpcMethod, Response>(
+ client, channel_id, request));
}
-// SynchronousCall
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-template <auto kRpcMethod>
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks indefinitely
+/// until a response is received.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+template <auto kRpcMethod, typename GeneratedClient>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCall(
- const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request));
+}
- call_state.notify.acquire();
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received.
+template <auto kRpcMethod>
+Status SynchronousCall(Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request));
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received.
+template <auto kRpcMethod>
+Status SynchronousCall(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request));
}
-// SynchronousCallFor
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-// timeout: Duration to block for before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided timeout passes.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+/// @param timeout Duration to block for before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallFor(
@@ -164,72 +190,63 @@ SynchronousCallFor(
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::duration timeout) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- if (!call_state.notify.try_acquire_for(timeout)) {
- return SynchronousCallResult<Response>::Timeout();
- }
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ timeout);
}
-// SynchronousCallFor
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-// timeout: Duration to block for before returning with Timeout
-template <auto kRpcMethod>
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided timeout passes.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+/// @param timeout Duration to block for before returning with Timeout
+template <auto kRpcMethod, typename GeneratedClient>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallFor(
- const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::duration timeout) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request), timeout);
+}
- if (!call_state.notify.try_acquire_for(timeout)) {
- return SynchronousCallResult<Response>::Timeout();
- }
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided timeout passes.
+template <auto kRpcMethod>
+Status SynchronousCallFor(
+ Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ chrono::SystemClock::duration timeout,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ timeout);
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided timeout passes.
+template <auto kRpcMethod>
+Status SynchronousCallFor(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ chrono::SystemClock::duration timeout,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request),
+ timeout);
}
-// SynchronousCallUntil
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-// deadline: Timepoint to block until before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided deadline arrives.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+/// @param deadline Timepoint to block until before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallUntil(
@@ -237,59 +254,54 @@ SynchronousCallUntil(
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::time_point deadline) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- if (!call_state.notify.try_acquire_until(deadline)) {
- return SynchronousCallResult<Response>::Timeout();
- }
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ deadline);
}
-// SynchronousCallUntil
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-// deadline: Timepoint to block until before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided deadline arrives.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+/// @param deadline Timepoint to block until before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallUntil(
const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::time_point deadline) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request), deadline);
+}
- if (!call_state.notify.try_acquire_until(deadline)) {
- return SynchronousCallResult<Response>::Timeout();
- }
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided deadline arrives.
+template <auto kRpcMethod>
+Status SynchronousCallUntil(
+ Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ chrono::SystemClock::time_point deadline,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ deadline);
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided deadline arrives.
+template <auto kRpcMethod>
+Status SynchronousCallUntil(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ chrono::SystemClock::time_point deadline,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request),
+ deadline);
}
+
} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/writer.h b/pw_rpc/public/pw_rpc/writer.h
index e7e7c4155..57b1e63d2 100644
--- a/pw_rpc/public/pw_rpc/writer.h
+++ b/pw_rpc/public/pw_rpc/writer.h
@@ -20,7 +20,7 @@ namespace pw::rpc {
// The Writer class allows writing requests or responses to a streaming RPC.
// ClientWriter, ClientReaderWriter, ServerWriter, and ServerReaderWriter
// classes can be used as a generic Writer.
-class Writer : private internal::Call {
+class Writer final : private internal::Call {
public:
// Writers cannot be created directly. They may only be used as a reference to
// an existing call object.
diff --git a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
index 89c5d91c3..ef24c126e 100644
--- a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
+++ b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
@@ -57,7 +57,8 @@ class FakeServerReaderWriter : private ServerCall {
using Call::active;
using Call::set_on_error;
using Call::set_on_next;
- using ServerCall::set_on_client_stream_end;
+ using ServerCall::set_on_completion_requested;
+ using ServerCall::set_on_completion_requested_if_enabled;
Status Finish(Status status = OkStatus()) {
return CloseAndSendResponse(status);
@@ -68,6 +69,7 @@ class FakeServerReaderWriter : private ServerCall {
// Expose a few additional methods for test use.
ServerCall& as_server_call() { return *this; }
using Call::channel_id_locked;
+ using Call::DebugLog;
using Call::id;
using Call::set_id;
};
@@ -85,6 +87,8 @@ class FakeServerWriter : private FakeServerReaderWriter {
// Common reader/writer functions.
using FakeServerReaderWriter::active;
using FakeServerReaderWriter::Finish;
+ using FakeServerReaderWriter::set_on_completion_requested;
+ using FakeServerReaderWriter::set_on_completion_requested_if_enabled;
using FakeServerReaderWriter::set_on_error;
using FakeServerReaderWriter::Write;
diff --git a/pw_rpc/public/pw_rpc/internal/test_method.h b/pw_rpc/pw_rpc_private/test_method.h
index cf06bb338..1c4ea5b0a 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method.h
+++ b/pw_rpc/pw_rpc_private/test_method.h
@@ -16,6 +16,7 @@
#include <cstdint>
#include <cstring>
+#include "fake_server_reader_writer.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/method_union.h"
@@ -32,19 +33,6 @@ namespace pw::rpc::internal {
// channel ID, request, and payload buffer, and optionally provides a response.
class TestMethod : public Method {
public:
- class FakeServerCall : public ServerCall {
- public:
- constexpr FakeServerCall() = default;
- FakeServerCall(const LockedCallContext& context, MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
- : ServerCall(context, CallProperties(type, kServerCall, kRawProto)) {}
-
- FakeServerCall(FakeServerCall&&) = default;
- FakeServerCall& operator=(FakeServerCall&&) = default;
-
- using internal::Call::set_on_error;
- };
-
constexpr TestMethod(uint32_t id, MethodType type = MethodType::kUnary)
: Method(id, GetInvoker(type)),
last_channel_id_(0),
@@ -58,7 +46,7 @@ class TestMethod : public Method {
// Sets a call object into which to move the call object when the RPC is
// invoked. This keeps the RPC active until the provided call object is
// finished or goes out of scope.
- void keep_call_active(FakeServerCall& move_to_call) const {
+ void keep_call_active(test::FakeServerReaderWriter& move_to_call) const {
move_to_call_ = &move_to_call;
}
@@ -72,7 +60,7 @@ class TestMethod : public Method {
test_method.invocations_ += 1;
// Create a call object so it registers / unregisters with the server.
- FakeServerCall fake_call(context.ClaimLocked(), kType);
+ test::FakeServerReaderWriter fake_call(context.ClaimLocked(), kType);
context.server().CleanUpCalls();
@@ -100,7 +88,7 @@ class TestMethod : public Method {
mutable uint32_t last_channel_id_;
mutable Packet last_request_;
mutable size_t invocations_;
- mutable FakeServerCall* move_to_call_;
+ mutable test::FakeServerReaderWriter* move_to_call_;
span<const std::byte> response_;
Status response_status_;
diff --git a/pw_rpc/pwpb/Android.bp b/pw_rpc/pwpb/Android.bp
new file mode 100644
index 000000000..ad4c8bb65
--- /dev/null
+++ b/pw_rpc/pwpb/Android.bp
@@ -0,0 +1,48 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+filegroup {
+ name: "pw_rpc_pwpb_src_files",
+ srcs: [
+ "server_reader_writer.cc",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_rpc_pwpb_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_rpc_pwpb_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_rpc_pwpb_include_dirs",
+ ],
+ export_header_lib_headers: [
+ "pw_rpc_pwpb_include_dirs",
+ ],
+ srcs: [
+ ":pw_rpc_pwpb_src_files",
+ ],
+}
diff --git a/pw_rpc/pwpb/BUILD.bazel b/pw_rpc/pwpb/BUILD.bazel
index 786b03cd0..0fd8ff726 100644
--- a/pw_rpc/pwpb/BUILD.bazel
+++ b/pw_rpc/pwpb/BUILD.bazel
@@ -105,6 +105,7 @@ pw_cc_library(
includes = ["public"],
deps = [
":test_method_context",
+ "//pw_assert",
"//pw_rpc:client_server_testing",
],
)
@@ -135,7 +136,7 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Enable this library when logging_event_handler can be used.
+# TODO: b/242059613 - Enable this library when logging_event_handler can be used.
filegroup(
name = "client_integration_test",
srcs = [
@@ -182,6 +183,7 @@ pw_cc_test(
":client_api",
":client_server_testing",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ "//pw_sync:mutex",
],
)
@@ -196,8 +198,9 @@ pw_cc_test(
":client_server_testing_threaded",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
"//pw_sync:binary_semaphore",
- "//pw_thread:test_threads_header",
- "//pw_thread_stl:test_threads",
+ "//pw_sync:mutex",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -271,14 +274,13 @@ pw_cc_test(
],
)
-# TODO(b/234874064): Requires pwpb options file support to compile.
-filegroup(
+pw_cc_test(
name = "echo_service_test",
srcs = ["echo_service_test.cc"],
- # deps = [
- # ":echo_service",
- # ":test_method_context",
- # ],
+ deps = [
+ ":echo_service",
+ ":test_method_context",
+ ],
)
pw_cc_test(
@@ -326,6 +328,7 @@ pw_cc_test(
":test_method_context",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
"//pw_rpc:synchronous_client_api",
+ "//pw_rpc_transport:test_loopback_service_registry",
"//pw_work_queue",
"//pw_work_queue:stl_test_thread",
"//pw_work_queue:test_thread_header",
diff --git a/pw_rpc/pwpb/BUILD.gn b/pw_rpc/pwpb/BUILD.gn
index 0c611bb99..e036a8019 100644
--- a/pw_rpc/pwpb/BUILD.gn
+++ b/pw_rpc/pwpb/BUILD.gn
@@ -105,6 +105,7 @@ pw_source_set("client_server_testing") {
public = [ "public/pw_rpc/pwpb/client_server_testing.h" ]
public_deps = [
":test_method_context",
+ "$dir_pw_assert",
"..:client_server_testing",
]
}
@@ -132,6 +133,7 @@ pw_source_set("echo_service") {
}
pw_source_set("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
public_configs = [ ":public" ]
public_deps = [
"$dir_pw_sync:binary_semaphore",
@@ -182,8 +184,10 @@ pw_test("client_reader_writer_test") {
deps = [
":client_api",
":client_testing",
+ "$dir_pw_sync:mutex",
"..:test_protos.pwpb_rpc",
]
+ enable_if = pw_sync_MUTEX_BACKEND != ""
sources = [ "client_reader_writer_test.cc" ]
}
@@ -205,8 +209,9 @@ pw_test("client_server_context_threaded_test") {
":client_api",
":client_server_testing_threaded",
"$dir_pw_sync:binary_semaphore",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"..:test_protos.pwpb_rpc",
]
sources = [ "client_server_context_threaded_test.cc" ]
@@ -318,6 +323,8 @@ pw_test("stub_generation_test") {
pw_test("synchronous_call_test") {
deps = [
":test_method_context",
+ "$dir_pw_rpc_transport:test_loopback_service_registry",
+ "$dir_pw_thread:thread",
"$dir_pw_work_queue:pw_work_queue",
"$dir_pw_work_queue:stl_test_thread",
"$dir_pw_work_queue:test_thread",
diff --git a/pw_rpc/pwpb/CMakeLists.txt b/pw_rpc/pwpb/CMakeLists.txt
index ce75e479c..ba4a3a3f7 100644
--- a/pw_rpc/pwpb/CMakeLists.txt
+++ b/pw_rpc/pwpb/CMakeLists.txt
@@ -162,17 +162,20 @@ pw_add_test(pw_rpc.pwpb.client_reader_writer_test
pw_rpc.pwpb
)
-pw_add_test(pw_rpc.pwpb.client_server_context_test
- SOURCES
- client_server_context_test.cc
- PRIVATE_DEPS
- pw_rpc.pwpb.client_api
- pw_rpc.pwpb.client_server_testing
- pw_rpc.test_protos.pwpb_rpc
- GROUPS
- modules
- pw_rpc.pwpb
-)
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+ pw_add_test(pw_rpc.pwpb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.client_server_testing
+ pw_rpc.test_protos.pwpb_rpc
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_rpc.pwpb
+ )
+endif()
if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
@@ -185,7 +188,8 @@ if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
pw_rpc.pwpb.client_server_testing_threaded
pw_rpc.test_protos.pwpb_rpc
pw_sync.binary_semaphore
- pw_thread.test_threads
+ pw_sync.mutex
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_thread_stl.test_threads
GROUPS
@@ -331,7 +335,7 @@ pw_add_test(pw_rpc.pwpb.stub_generation_test
pw_rpc.pwpb
)
-# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+# TODO: b/231950909 - Test disabled as pw_work_queue lacks CMakeLists.txt
if((TARGET pw_work_queue.pw_work_queue) AND
("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
@@ -343,6 +347,7 @@ if((TARGET pw_work_queue.pw_work_queue) AND
pw_rpc.pwpb.test_method_context
pw_rpc.synchronous_client_api
pw_rpc.test_protos.pwpb_rpc
+ pw_rpc_transport.test_loopback_service_registry
pw_thread.thread
pw_work_queue.pw_work_queue
pw_work_queue.stl_test_thread
diff --git a/pw_rpc/pwpb/client_reader_writer_test.cc b/pw_rpc/pwpb/client_reader_writer_test.cc
index 540f0106d..f22313d82 100644
--- a/pw_rpc/pwpb/client_reader_writer_test.cc
+++ b/pw_rpc/pwpb/client_reader_writer_test.cc
@@ -62,7 +62,7 @@ TEST(PwpbClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
call.set_on_error([](Status) {});
@@ -75,6 +75,7 @@ TEST(PwpbClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -90,7 +91,74 @@ TEST(PwpbClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientWriter, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientWriter<TestRequest::Message, TestStreamResponse::Message> call =
+ TestService::TestClientStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnCompletedCalled<TestStreamResponse::Message>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReader, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientReader<TestStreamResponse::Message> call =
+ TestService::TestServerStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReaderWriter, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -131,7 +199,7 @@ TEST(PwpbClientWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
call.set_on_error([](Status) {});
@@ -153,6 +221,7 @@ TEST(PwpbClientReader, Closed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -175,7 +244,7 @@ TEST(PwpbClientReaderWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
diff --git a/pw_rpc/pwpb/client_server_context_test.cc b/pw_rpc/pwpb/client_server_context_test.cc
index bd45154ef..184bc3948 100644
--- a/pw_rpc/pwpb/client_server_context_test.cc
+++ b/pw_rpc/pwpb/client_server_context_test.cc
@@ -12,9 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <mutex>
+#include <utility>
+
#include "gtest/gtest.h"
#include "pw_rpc/pwpb/client_server_testing.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_sync/mutex.h"
namespace pw::rpc {
namespace {
@@ -37,8 +41,16 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const TestRequest::Message&,
- PwpbUnaryResponder<TestResponse::Message>&) {}
+ Status TestAnotherUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(const TestRequest::Message&,
ServerWriter<TestStreamResponse::Message>&) {}
@@ -54,7 +66,7 @@ class TestService final : public GeneratedService::Service<TestService> {
namespace {
-TEST(PwpbClientServerTestContext, ReceivesUnaryRpcReponse) {
+TEST(PwpbClientServerTestContext, ReceivesUnaryRpcResponse) {
PwpbClientServerTestContext<> ctx;
test::TestService service;
ctx.server().RegisterService(service);
@@ -79,7 +91,7 @@ TEST(PwpbClientServerTestContext, ReceivesUnaryRpcReponse) {
EXPECT_EQ(request.integer, sent_request.integer);
}
-TEST(PwpbClientServerTestContext, ReceivesMultipleReponses) {
+TEST(PwpbClientServerTestContext, ReceivesMultipleResponses) {
PwpbClientServerTestContext<> ctx;
test::TestService service;
ctx.server().RegisterService(service);
@@ -119,5 +131,104 @@ TEST(PwpbClientServerTestContext, ReceivesMultipleReponses) {
EXPECT_EQ(request2.integer, sent_request2.integer);
}
+TEST(PwpbClientServerTestContext,
+ ReceivesMultipleResponsesWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ PwpbClientServerTestContext<> ctx(server_processor, client_processor);
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestResponse::Message response1 = {};
+ TestResponse::Message response2 = {};
+ auto handler1 = [&response1](const TestResponse::Message& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const TestResponse::Message& server_response,
+ pw::Status) { response2 = server_response; };
+
+ TestRequest::Message request1{.integer = 1, .status_code = OkStatus().code()};
+ TestRequest::Message request2{.integer = 2, .status_code = OkStatus().code()};
+ const auto call1 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(PwpbClientServerTestContext, ResponseWithCallbacks) {
+ PwpbClientServerTestContext<> ctx;
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestRequest::Message request{};
+ const auto call = test::GeneratedService::TestAnotherUnaryRpc(
+ ctx.client(), ctx.channel().id(), request);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ // To decode a response object that requires to set callbacks, pass it to the
+ // response() method as a parameter.
+ pw::Vector<uint32_t, 4> values{};
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_EQ(42, response.value);
+
+ EXPECT_EQ(3u, values.size());
+ EXPECT_EQ(7u, values[0]);
+ EXPECT_EQ(8u, values[1]);
+ EXPECT_EQ(9u, values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/client_server_context_threaded_test.cc b/pw_rpc/pwpb/client_server_context_threaded_test.cc
index ae1306df3..d9d6f7f65 100644
--- a/pw_rpc/pwpb/client_server_context_threaded_test.cc
+++ b/pw_rpc/pwpb/client_server_context_threaded_test.cc
@@ -12,11 +12,15 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <atomic>
+#include <iostream>
+
#include "gtest/gtest.h"
+#include "pw_function/function.h"
#include "pw_rpc/pwpb/client_server_testing_threaded.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
#include "pw_sync/binary_semaphore.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc {
namespace {
@@ -39,8 +43,16 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const TestRequest::Message&,
- PwpbUnaryResponder<TestResponse::Message>&) {}
+ Status TestAnotherUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(const TestRequest::Message&,
ServerWriter<TestStreamResponse::Message>&) {}
@@ -58,31 +70,43 @@ namespace {
class RpcCaller {
public:
- void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ template <auto kMethod = test::GeneratedService::TestUnaryRpc>
+ Status BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
TestRequest::Message request{.integer = i,
.status_code = OkStatus().code()};
- auto call = test::GeneratedService::TestUnaryRpc(
+ response_status_ = OkStatus();
+ auto call = kMethod(
client,
channel_id,
request,
- [this](const TestResponse::Message&, Status) { semaphore_.release(); },
- [](Status) {});
+ [this](const TestResponse::Message&, Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ },
+ [this](Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ });
semaphore_.acquire();
+ return response_status_;
}
private:
+ Status response_status_ = OkStatus();
pw::sync::BinarySemaphore semaphore_;
};
-TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcResponseThreaded) {
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
test::TestService service;
ctx.server().RegisterService(service);
RpcCaller caller;
constexpr auto value = 1;
- caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request =
ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
@@ -93,7 +117,7 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
EXPECT_EQ(value + 1, response.value);
}
-TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleResponsesThreaded) {
PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
test::TestService service;
ctx.server().RegisterService(service);
@@ -101,8 +125,10 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
RpcCaller caller;
constexpr auto value1 = 1;
constexpr auto value2 = 2;
- caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
- caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request1 =
ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
@@ -119,5 +145,94 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
EXPECT_EQ(value2 + 1, response2.value);
}
+TEST(PwpbClientServerTestContextThreaded,
+ ReceivesMultipleResponsesThreadedWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ PwpbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0(), server_processor, client_processor);
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(PwpbClientServerTestContextThreaded, ResponseWithCallbacks) {
+ PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ // DataLoss expected on initial response, since pwpb provides no way to
+ // populate response callback. We setup callbacks on response packet below.
+ EXPECT_EQ(caller.BlockOnResponse<test::GeneratedService::TestAnotherUnaryRpc>(
+ 0, ctx.client(), ctx.channel().id()),
+ Status::DataLoss());
+
+ // To decode a response object that requires to set callbacks, pass it to the
+ // response() method as a parameter.
+ pw::Vector<uint32_t, 4> values{};
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_EQ(42, response.value);
+
+ EXPECT_EQ(3u, values.size());
+ EXPECT_EQ(7u, values[0]);
+ EXPECT_EQ(8u, values[1]);
+ EXPECT_EQ(9u, values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/codegen_test.cc b/pw_rpc/pwpb/codegen_test.cc
index d85760350..cd5b29b19 100644
--- a/pw_rpc/pwpb/codegen_test.cc
+++ b/pw_rpc/pwpb/codegen_test.cc
@@ -258,6 +258,52 @@ TEST(PwpbCodegen, Client_InvokesUnaryRpcWithCallback) {
EXPECT_FALSE(call.active());
}
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+TEST(PwpbCodegen, DynamicClient_InvokesUnaryRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestUnaryRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ test::pw_rpc::pwpb::TestService::DynamicClient test_client(
+ context.client(), context.channel().id());
+
+ struct {
+ Status last_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test_client.TestUnaryRpc(
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestResponse::Message& response,
+ Status status) {
+ result.last_status = status;
+ result.response_value = response.value;
+ });
+
+ EXPECT_TRUE(call->active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(test::pwpb::TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+ EXPECT_EQ(result.last_status, OkStatus());
+ EXPECT_EQ(result.response_value, 42);
+
+ EXPECT_FALSE(call->active());
+}
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
TEST(PwpbCodegen, Client_InvokesServerStreamingRpcWithCallback) {
constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
constexpr uint32_t kMethodId = internal::Hash("TestServerStreamRpc");
diff --git a/pw_rpc/pwpb/docs.rst b/pw_rpc/pwpb/docs.rst
index a1332a5a7..28dd4e991 100644
--- a/pw_rpc/pwpb/docs.rst
+++ b/pw_rpc/pwpb/docs.rst
@@ -12,25 +12,25 @@ Define a ``pw_proto_library`` containing the .proto file defining your service
(and optionally other related protos), then depend on the ``pwpb_rpc``
version of that library in the code implementing the service.
-.. code::
+.. code-block::
- # chat/BUILD.gn
+ # chat/BUILD.gn
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ 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" ]
- }
+ 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.pwpb_rpc" ]
- }
+ # Library that implements the Chat service.
+ pw_source_set("chat_service") {
+ sources = [
+ "chat_service.cc",
+ "chat_service.h",
+ ]
+ public_deps = [ ":chat_protos.pwpb_rpc" ]
+ }
A C++ header file is generated for each input .proto file, with the ``.proto``
extension replaced by ``.rpc.pwpb.h``. For example, given the input file
@@ -41,7 +41,7 @@ Generated code API
==================
All examples in this document use the following RPC service definition.
-.. code:: protobuf
+.. code-block:: protobuf
// chat/chat_protos/chat_service.proto
@@ -70,7 +70,7 @@ located within a special ``pw_rpc::pwpb`` 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.
-.. code:: c++
+.. code-block:: c++
#include "chat_protos/chat_service.rpc.pwpb.h"
@@ -85,7 +85,7 @@ A unary RPC is implemented as a function which takes in the RPC's request struct
and populates a response struct to send back, with a status indicating whether
the request succeeded.
-.. code:: c++
+.. code-block:: c++
pw::Status GetRoomInformation(const RoomInfoRequest::Message& request,
RoomInfoResponse::Message& response);
@@ -95,7 +95,7 @@ Server streaming RPC
A server streaming RPC receives the client's request message alongside a
``ServerWriter``, used to stream back responses.
-.. code:: c++
+.. code-block:: c++
void ListUsersInRoom(const ListUsersRequest::Message& request,
pw::rpc::ServerWriter<ListUsersResponse::Message>& writer);
@@ -127,6 +127,9 @@ Bidirectional streaming RPC
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. attention:: Supported, but the documentation is still under construction.
+
+.. _module-pw_rpc_pw_protobuf-client:
+
Client-side
-----------
A corresponding client class is generated for every service defined in the proto
@@ -166,6 +169,19 @@ the type of RPC. Each method returns a client call object which stores the
context of the ongoing RPC call. For more information on call objects, refer to
the :ref:`core RPC docs <module-pw_rpc-making-calls>`.
+If dynamic allocation is enabled (:c:macro:`PW_RPC_DYNAMIC_ALLOCATION` is 1), a
+``DynamicClient`` is generated, which dynamically allocates the call object with
+:c:macro:`PW_RPC_MAKE_UNIQUE_PTR`. For example:
+
+.. code-block:: c++
+
+ my_namespace::pw_rpc::pwpb::ServiceName::DynamicClient dynamic_client(
+ client, channel_id);
+ auto call = dynamic_client.TestUnaryRpc(request, response_callback);
+
+ if (call->active()) { // Access the call as a std::unique_ptr
+ // ...
+
.. admonition:: Callback invocation
RPC callbacks are invoked synchronously from ``Client::ProcessPacket``.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
index e66c42092..d47582165 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
@@ -21,8 +21,13 @@
#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/internal/client_call.h"
+#include "pw_rpc/internal/config.h"
#include "pw_rpc/pwpb/internal/common.h"
+#if PW_RPC_DYNAMIC_ALLOCATION
+#include PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
namespace pw::rpc {
namespace internal {
@@ -47,17 +52,38 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
rpc_lock().lock();
CallType call(
client.ClaimLocked(), channel_id, service_id, method_id, serde);
+ SetCallbacksAndSendRequest(call,
+ client,
+ serde,
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
+ return call;
+ }
- call.set_pwpb_on_completed_locked(std::move(on_completed));
- call.set_on_error_locked(std::move(on_error));
-
- if constexpr (sizeof...(Request) == 0u) {
- call.SendInitialClientRequest({});
- } else {
- PwpbSendInitialRequest(call, serde.request(), request...);
- }
-
- client.CleanUpCalls();
+ template <typename CallType, typename... Request>
+ static auto StartDynamic(
+ Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&, Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ auto call = PW_RPC_MAKE_UNIQUE_PTR(CallType,
+ client.ClaimLocked(),
+ channel_id,
+ service_id,
+ method_id,
+ serde);
+ SetCallbacksAndSendRequest(*call,
+ client,
+ serde,
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
return call;
}
@@ -91,6 +117,8 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
return *this;
}
+ ~PwpbUnaryResponseClientCall() { DestroyClientCall(); }
+
// Implement moving by copying the serde pointer and on_completed function.
void MovePwpbUnaryResponseClientCallFrom(PwpbUnaryResponseClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -123,6 +151,26 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
}
private:
+ template <typename CallType, typename... Request>
+ static void SetCallbacksAndSendRequest(
+ CallType& call,
+ Endpoint& client,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&, Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ call.set_pwpb_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+
+ client.CleanUpCalls();
+ }
+
void set_pwpb_on_completed_locked(
Function<void(const Response& response, Status)>&& on_completed)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -163,17 +211,41 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
rpc_lock().lock();
CallType call(
client.ClaimLocked(), channel_id, service_id, method_id, serde);
+ SetCallbacksAndSendRequest(call,
+ client,
+ serde,
+ std::move(on_next),
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
+ return call;
+ }
- call.set_pwpb_on_next_locked(std::move(on_next));
- call.set_on_completed_locked(std::move(on_completed));
- call.set_on_error_locked(std::move(on_error));
-
- if constexpr (sizeof...(Request) == 0u) {
- call.SendInitialClientRequest({});
- } else {
- PwpbSendInitialRequest(call, serde.request(), request...);
- }
- client.CleanUpCalls();
+ template <typename CallType, typename... Request>
+ static auto StartDynamic(Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&)>&& on_next,
+ Function<void(Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ auto call = PW_RPC_MAKE_UNIQUE_PTR(CallType,
+ client.ClaimLocked(),
+ channel_id,
+ service_id,
+ method_id,
+ serde);
+ SetCallbacksAndSendRequest(*call,
+ client,
+ serde,
+ std::move(on_next),
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
return call;
}
@@ -207,6 +279,8 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
return *this;
}
+ ~PwpbStreamResponseClientCall() { DestroyClientCall(); }
+
// Implement moving by copying the serde pointer and on_next function.
void MovePwpbStreamResponseClientCallFrom(PwpbStreamResponseClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -238,6 +312,27 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
}
private:
+ template <typename CallType, typename... Request>
+ static void SetCallbacksAndSendRequest(
+ CallType& call,
+ Endpoint& client,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&)>&& on_next,
+ Function<void(Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ call.set_pwpb_on_next_locked(std::move(on_next));
+ call.set_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+ client.CleanUpCalls();
+ }
+
void set_pwpb_on_next_locked(
Function<void(const Response& response)>&& on_next)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -288,16 +383,18 @@ class PwpbClientReaderWriter
request);
}
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -343,6 +440,7 @@ class PwpbClientReader
using internal::StreamResponseClientCall::channel_id;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -401,7 +499,7 @@ class PwpbClientWriter
}
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
index 394c49f0d..425f0b960 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_assert/assert.h"
#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
@@ -45,7 +46,11 @@ class PwpbForwardingChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr PwpbForwardingChannelOutput() = default;
+ explicit PwpbForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index) {
@@ -54,6 +59,17 @@ class PwpbForwardingChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index) {
PW_ASSERT(Base::PacketCount() >= index);
return Base::output_.template requests<kMethod>(channel_id)[index];
@@ -90,7 +106,11 @@ class PwpbClientServerTestContext final
kPayloadsBufferSizeBytes>;
public:
- PwpbClientServerTestContext() = default;
+ PwpbClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -99,12 +119,21 @@ class PwpbClientServerTestContext final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using pw_protobuf. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
index f37d3ecf0..64808f09f 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
@@ -45,7 +46,11 @@ class PwpbWatchableChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr PwpbWatchableChannelOutput() = default;
+ explicit PwpbWatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index)
@@ -56,6 +61,18 @@ class PwpbWatchableChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index)
PW_LOCKS_EXCLUDED(Base::mutex_) {
std::lock_guard lock(Base::mutex_);
@@ -94,8 +111,13 @@ class PwpbClientServerTestContextThreaded final
kPayloadsBufferSizeBytes>;
public:
- PwpbClientServerTestContextThreaded(const thread::Options& options)
- : Base(options) {}
+ PwpbClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(options,
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -110,6 +132,15 @@ class PwpbClientServerTestContextThreaded final
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using pw_protobuf. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
index d91ebc6d3..272d3dd97 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
@@ -24,7 +24,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
// payloads as pw_protobuf message structs.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
index c164f689a..1dde53455 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
@@ -89,6 +89,9 @@ class PwpbPayloadsView {
iterator begin() const { return iterator(view_.begin(), serde_); }
iterator end() const { return iterator(view_.end(), serde_); }
+ PayloadsView& payloads() { return view_; }
+ PwpbSerde& serde() { return serde_; }
+
private:
template <size_t, size_t>
friend class PwpbFakeChannelOutput;
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
index a0b484d59..a8a7b9903 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
@@ -70,7 +70,7 @@ class PwpbMethod : public Method {
}
// Creates a PwpbMethod for a synchronous unary RPC.
- // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // TODO: b/234874001 - Find a way to reduce the number of monomorphized copies
// of this method.
template <auto kMethod>
static constexpr PwpbMethod SynchronousUnary(uint32_t id,
@@ -97,7 +97,7 @@ class PwpbMethod : public Method {
}
// Creates a PwpbMethod for an asynchronous unary RPC.
- // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // TODO: b/234874001 - Find a way to reduce the number of monomorphized copies
// of this method.
template <auto kMethod>
static constexpr PwpbMethod AsynchronousUnary(uint32_t id,
@@ -365,7 +365,7 @@ class PwpbMethod : public Method {
};
// MethodTraits specialization for a static synchronous unary method.
-// TODO(b/234874320): Further qualify this (and nanopb) definition so that they
+// TODO: b/234874320 - Further qualify this (and nanopb) definition so that they
// can co-exist in the same project.
template <typename Req, typename Res>
struct MethodTraits<PwpbSynchronousUnary<Req, Res>*> {
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
index 92572de84..6b1306592 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
@@ -44,16 +44,16 @@ class PwpbSerde {
// actually encoding it.
template <typename Message>
StatusWithSize EncodedSizeBytes(const Message& message) const {
- // TODO(b/269515470): Use kScratchBufferSizeBytes instead of a fixed size.
+ // TODO: b/269515470 - Use kScratchBufferSizeBytes instead of a fixed size.
std::array<std::byte, 64> scratch_buffer;
stream::CountingNullStream output;
StreamEncoder encoder(output, scratch_buffer);
const Status result = encoder.Write(as_bytes(span(&message, 1)), *table_);
- // TODO(b/269633514): Add 1 to the encoded size because pw_protobuf
+ // TODO: b/269633514 - Add 16 to the encoded size because pw_protobuf
// sometimes fails to encode to buffers that exactly fit the output.
- return StatusWithSize(result, output.bytes_written() + 1);
+ return StatusWithSize(result, output.bytes_written() + 16);
}
// Decodes a serialized protobuf into a pw_protobuf message struct.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
index 4d80c7176..b8d42d075 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
@@ -48,6 +48,8 @@ class PwpbServerCall : public ServerCall {
PwpbServerCall(const LockedCallContext& context, MethodType type)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ ~PwpbServerCall() { DestroyServerCall(); }
+
// Sends a unary response.
// Returns the following Status codes:
//
@@ -134,6 +136,8 @@ class BasePwpbServerReader : public PwpbServerCall {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: PwpbServerCall(context, type) {}
+ ~BasePwpbServerReader() { DestroyServerCall(); }
+
protected:
// Allow default construction so that users can declare a variable into
// which to move server reader/writers from RPC calls.
@@ -228,7 +232,8 @@ class PwpbServerReaderWriter : private internal::BasePwpbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
using internal::BasePwpbServerReader<Request>::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Writes a response. Returns the following Status codes:
//
@@ -303,7 +308,8 @@ class PwpbServerReader : private internal::BasePwpbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
using internal::BasePwpbServerReader<Request>::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Sends the response. Returns the following Status codes:
//
@@ -370,7 +376,8 @@ class PwpbServerWriter : private internal::PwpbServerCall {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Writes a response. Returns the following Status codes:
//
@@ -440,7 +447,6 @@ class PwpbUnaryResponder : private internal::PwpbServerCall {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
// Sends the response. Returns the following Status codes:
//
diff --git a/pw_rpc/pwpb/synchronous_call_test.cc b/pw_rpc/pwpb/synchronous_call_test.cc
index 33a639658..b5f18e658 100644
--- a/pw_rpc/pwpb/synchronous_call_test.cc
+++ b/pw_rpc/pwpb/synchronous_call_test.cc
@@ -22,6 +22,7 @@
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_rpc_transport/test_loopback_service_registry.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_thread/thread.h"
@@ -36,6 +37,32 @@ using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+class TestServiceImpl final
+ : public pw::rpc::test::pw_rpc::pwpb::TestService::Service<
+ TestServiceImpl> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message& /*request*/,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return OkStatus();
+ }
+ Status TestAnotherUnaryRpc(const TestRequest::Message& /*request*/,
+ TestResponse::Message& /*response*/) {
+ return OkStatus();
+ }
+ void TestServerStreamRpc(const TestRequest::Message&,
+ ServerWriter<TestStreamResponse::Message>&) {}
+ void TestClientStreamRpc(RawServerReader&) {}
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>&) {}
+};
class SynchronousCallTest : public ::testing::Test {
public:
@@ -49,7 +76,11 @@ class SynchronousCallTest : public ::testing::Test {
void TearDown() override {
work_queue_.RequestStop();
+#if PW_THREAD_JOINING_ENABLED
work_thread_.join();
+#else
+ work_thread_.detach();
+#endif // PW_THREAD_JOINING_ENABLED
}
protected:
@@ -172,6 +203,33 @@ TEST_F(SynchronousCallTest, SynchronousCallUntilTimeoutError) {
EXPECT_EQ(result.status(), Status::DeadlineExceeded());
}
+TEST_F(SynchronousCallTest, SynchronousCallCustomResponse) {
+ TestServiceImpl test_service;
+ TestLoopbackServiceRegistry service_registry;
+ service_registry.RegisterService(test_service);
+
+ class CustomResponse : public TestResponse::Message {
+ public:
+ CustomResponse() {
+ repeated_field.SetDecoder([this](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ }
+ pw::Vector<uint32_t, 4> values{};
+ };
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc, CustomResponse>(
+ service_registry.client_server().client(),
+ service_registry.channel_id(),
+ {.integer = 5, .status_code = 0});
+ EXPECT_EQ(result.status(), OkStatus());
+
+ EXPECT_EQ(3u, result.response().values.size());
+ EXPECT_EQ(7u, result.response().values[0]);
+ EXPECT_EQ(8u, result.response().values[1]);
+ EXPECT_EQ(9u, result.response().values[2]);
+}
+
TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
TestRequest::Message request{.integer = 5, .status_code = 0};
TestResponse::Message response{.value = 42, .repeated_field{}};
@@ -184,6 +242,23 @@ TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
EXPECT_EQ(result.response().value, 42);
}
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+TEST_F(SynchronousCallTest, GeneratedDynamicClientSynchronousCallSuccess) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ TestService::DynamicClient dynamic_client(client(), channel().id());
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(dynamic_client, request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallServerError) {
TestRequest::Message request{.integer = 5, .status_code = 0};
TestResponse::Message response{.value = 42, .repeated_field{}};
diff --git a/pw_rpc/py/Android.bp b/pw_rpc/py/Android.bp
index 10b3aedbe..ada8d3796 100644
--- a/pw_rpc/py/Android.bp
+++ b/pw_rpc/py/Android.bp
@@ -44,4 +44,19 @@ python_binary_host {
"pw_rpc_internal_packet_py_lib",
"pw_status_py_lib",
],
+}
+
+python_binary_host {
+ name: "pw_rpc_plugin_pwpb_py",
+ main: "pw_rpc/plugin_pwpb.py",
+ srcs: [
+ "pw_rpc/**/*.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_compiler_py_lib",
+ "pw_protobuf_plugin_py_lib",
+ "pw_rpc_internal_packet_py_lib",
+ "pw_status_py_lib",
+ ],
} \ No newline at end of file
diff --git a/pw_rpc/py/BUILD.bazel b/pw_rpc/py/BUILD.bazel
index 64c9c8b29..ed78e3065 100644
--- a/pw_rpc/py/BUILD.bazel
+++ b/pw_rpc/py/BUILD.bazel
@@ -107,6 +107,12 @@ py_test(
srcs = [
"tests/callback_client_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_protobuf_compiler:pw_protobuf_compiler_protos",
@@ -121,6 +127,12 @@ py_test(
srcs = [
"tests/client_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_rpc:internal_packet_proto_pb2",
@@ -134,6 +146,12 @@ py_test(
srcs = [
"tests/descriptors_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_protobuf_compiler:pw_protobuf_compiler_protos",
diff --git a/pw_rpc/py/BUILD.gn b/pw_rpc/py/BUILD.gn
index 2c091a70f..16ddc6f4a 100644
--- a/pw_rpc/py/BUILD.gn
+++ b/pw_rpc/py/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_rpc/internal/integration_test_ports.gni")
@@ -25,7 +26,10 @@ pw_python_package("py") {
version = "0.0.1"
}
options = {
- install_requires = [ "protobuf" ]
+ install_requires = [
+ "protobuf",
+ "pyserial",
+ ]
}
}
@@ -84,7 +88,7 @@ pw_doc_group("docs") {
other_deps = [ ":py" ]
}
-pw_python_script("python_client_cpp_server_test") {
+pw_python_action_test("python_client_cpp_server_test") {
sources = [ "tests/python_client_cpp_server_test.py" ]
python_deps = [
":py",
@@ -94,17 +98,11 @@ pw_python_script("python_client_cpp_server_test") {
"$dir_pw_status/py",
"$dir_pw_tokenizer/py:test_proto.python",
]
-
- pylintrc = "$dir_pigweed/.pylintrc"
- mypy_ini = "$dir_pigweed/.mypy.ini"
-
- action = {
- args = [
- "--port=$pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT",
- "--test-server-command",
- "<TARGET_FILE(..:test_rpc_server)>",
- ]
- deps = [ "..:test_rpc_server" ]
- stamp = true
- }
+ args = [
+ "--port=$pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT",
+ "--test-server-command",
+ "<TARGET_FILE(..:test_rpc_server)>",
+ ]
+ deps = [ "..:test_rpc_server" ]
+ tags = [ "integration" ]
}
diff --git a/pw_rpc/py/pw_rpc/callback_client/call.py b/pw_rpc/py/pw_rpc/callback_client/call.py
index 6cb09aaef..581e4baf0 100644
--- a/pw_rpc/py/pw_rpc/callback_client/call.py
+++ b/pw_rpc/py/pw_rpc/callback_client/call.py
@@ -141,6 +141,10 @@ class Call:
_LOG.warning('%s terminated due to an error: %s', self._rpc, error)
@property
+ def call_id(self) -> int:
+ return self._rpc.call_id
+
+ @property
def method(self) -> Method:
return self._rpc.method
@@ -318,6 +322,11 @@ class ServerStreamingCall(Call):
) -> Iterator:
return self._get_responses(count=count, timeout_s=timeout_s)
+ def request_completion(self) -> None:
+ """Sends client completion packet to server."""
+ if not self.completed():
+ self._rpcs.send_client_stream_end(self._rpc)
+
def __iter__(self) -> Iterator:
return self.get_responses()
diff --git a/pw_rpc/py/pw_rpc/callback_client/impl.py b/pw_rpc/py/pw_rpc/callback_client/impl.py
index 474757373..df9129861 100644
--- a/pw_rpc/py/pw_rpc/callback_client/impl.py
+++ b/pw_rpc/py/pw_rpc/callback_client/impl.py
@@ -18,6 +18,7 @@ import logging
import textwrap
from typing import Any, Callable, Dict, Iterable, Optional, Type
+from dataclasses import dataclass
from pw_status import Status
from google.protobuf.message import Message
@@ -44,6 +45,15 @@ from pw_rpc.callback_client.call import (
_LOG = logging.getLogger(__package__)
+@dataclass(eq=True, frozen=True)
+class CallInfo:
+ method: Method
+
+ @property
+ def service(self) -> Service:
+ return self.method.service
+
+
class _MethodClient:
"""A method that can be invoked for a particular channel."""
@@ -57,20 +67,21 @@ class _MethodClient:
) -> None:
self._impl = client_impl
self._rpcs = rpcs
- self._rpc = PendingRpc(channel, method.service, method)
+ self._channel = channel
+ self._method = method
self.default_timeout_s: Optional[float] = default_timeout_s
@property
def channel(self) -> Channel:
- return self._rpc.channel
+ return self._channel
@property
def method(self) -> Method:
- return self._rpc.method
+ return self._method
@property
def service(self) -> Service:
- return self._rpc.service
+ return self._method.service
@property
def request(self) -> type:
@@ -118,8 +129,17 @@ class _MethodClient:
if timeout_s is UseDefault.VALUE:
timeout_s = self.default_timeout_s
+ if self._impl.on_call_hook:
+ self._impl.on_call_hook(CallInfo(self._method))
+
+ rpc = PendingRpc(
+ self._channel,
+ self.service,
+ self.method,
+ self._rpcs.allocate_call_id(),
+ )
call = call_type(
- self._rpcs, self._rpc, timeout_s, on_next, on_completed, on_error
+ self._rpcs, rpc, timeout_s, on_next, on_completed, on_error
)
call._invoke(request, ignore_errors) # pylint: disable=protected-access
return call
@@ -378,18 +398,24 @@ asynchronously using the invoke method.
class Impl(client.ClientImpl):
- """Callback-based ClientImpl, for use with pw_rpc.Client."""
+ """Callback-based ClientImpl, for use with pw_rpc.Client.
+
+ Args:
+ on_call_hook: A callable object to handle RPC method calls.
+ If hook is set, it will be called before RPC execution.
+ """
def __init__(
self,
default_unary_timeout_s: Optional[float] = None,
default_stream_timeout_s: Optional[float] = None,
+ on_call_hook: Optional[Callable[[CallInfo], Any]] = None,
cancel_duplicate_calls: Optional[bool] = True,
) -> None:
super().__init__()
self._default_unary_timeout_s = default_unary_timeout_s
self._default_stream_timeout_s = default_stream_timeout_s
-
+ self.on_call_hook = on_call_hook
# Temporary workaround for clients that rely on mulitple in-flight
# instances of an RPC on the same channel, which is not supported.
# TODO(hepler): Remove this option when clients have updated.
diff --git a/pw_rpc/py/pw_rpc/client.py b/pw_rpc/py/pw_rpc/client.py
index d8c3390d7..df7413751 100644
--- a/pw_rpc/py/pw_rpc/client.py
+++ b/pw_rpc/py/pw_rpc/client.py
@@ -23,7 +23,6 @@ from typing import (
Dict,
Iterable,
Iterator,
- NamedTuple,
Optional,
)
@@ -36,20 +35,41 @@ from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
_LOG = logging.getLogger(__package__)
+# Calls with ID of `kOpenCallId` were unrequested, and are updated to have the
+# call ID of the first matching request.
+OPEN_CALL_ID: int = (2**32) - 1
+
+_MAX_CALL_ID: int = 1 << 14
+
class Error(Exception):
"""Error from incorrectly using the RPC client classes."""
-class PendingRpc(NamedTuple):
- """Uniquely identifies an RPC call."""
+class PendingRpc(packets.RpcIds):
+ """Uniquely identifies an RPC call.
- channel: Channel
- service: Service
- method: Method
+ Attributes:
+ channel: Channel
+ service: Service
+ method: Method
+ channel_id: int
+ service_id: int
+ method_id: int
+ call_id: int
+ """
- def __str__(self) -> str:
- return f'PendingRpc(channel={self.channel.id}, method={self.method})'
+ def __init__(
+ self,
+ channel: Channel,
+ service: Service,
+ method: Method,
+ call_id: int,
+ ) -> None:
+ super().__init__(channel.id, service.id, method.id, call_id)
+ self.channel = channel
+ self.service = service
+ self.method = method
class _PendingRpcMetadata:
@@ -60,8 +80,14 @@ class _PendingRpcMetadata:
class PendingRpcs:
"""Tracks pending RPCs and encodes outgoing RPC packets."""
- def __init__(self):
+ def __init__(self) -> None:
self._pending: Dict[PendingRpc, _PendingRpcMetadata] = {}
+ self._next_call_id: int = 0
+
+ def allocate_call_id(self) -> int:
+ call_id = self._next_call_id
+ self._next_call_id = (self._next_call_id + 1) % _MAX_CALL_ID
+ return call_id
def request(
self,
@@ -179,6 +205,17 @@ class PendingRpcs:
def get_pending(self, rpc: PendingRpc, status: Optional[Status]):
"""Gets the pending RPC's context. If status is set, clears the RPC."""
+ if rpc.call_id == OPEN_CALL_ID:
+ # Calls with ID `OPEN_CALL_ID` were unrequested, and are updated to
+ # have the call ID of the first matching request.
+ for pending in self._pending:
+ if (
+ pending.channel == rpc.channel
+ and pending.service == rpc.service
+ and pending.method == rpc.method
+ ):
+ rpc = pending
+
if status is None:
return self._pending[rpc].context
@@ -193,7 +230,7 @@ class ClientImpl(abc.ABC):
client.
"""
- def __init__(self):
+ def __init__(self) -> None:
self.client: Optional['Client'] = None
self.rpcs: Optional[PendingRpcs] = None
@@ -611,7 +648,9 @@ class Client:
f'No method ID {method_id} in service {service.name}'
)
- return PendingRpc(channel_client.channel, service, method)
+ return PendingRpc(
+ channel_client.channel, service, method, packet.call_id
+ )
def __repr__(self) -> str:
return (
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index dba811bca..03c784ce2 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -113,7 +113,9 @@ class CodeGenerator(abc.ABC):
"""Generates code for a service method."""
@abc.abstractmethod
- def client_member_function(self, method: ProtoServiceMethod) -> None:
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
"""Generates the client code for the Client member functions."""
@abc.abstractmethod
@@ -149,6 +151,7 @@ def generate_package(
gen.line('#include <type_traits>\n')
include_lines = [
+ '#include "pw_rpc/internal/config.h"',
'#include "pw_rpc/internal/method_info.h"',
'#include "pw_rpc/internal/method_lookup.h"',
'#include "pw_rpc/internal/service_client.h"',
@@ -225,6 +228,14 @@ def _generate_service_and_client(
_generate_client(gen, service)
+ # DynamicClient is only generated for pwpb for now.
+ if gen.name() == 'pwpb':
+ gen.line('#if PW_RPC_DYNAMIC_ALLOCATION')
+ _generate_client(gen, service, dynamic=True)
+ gen.line('#endif // PW_RPC_DYNAMIC_ALLOCATION')
+
+ _generate_client_free_functions(gen, service)
+
gen.line(' private:')
with gen.indent():
@@ -253,17 +264,21 @@ def _check_method_name(method: ProtoServiceMethod) -> None:
)
-def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
+def _generate_client(
+ gen: CodeGenerator, service: ProtoService, *, dynamic: bool = False
+) -> None:
+ class_name = 'DynamicClient' if dynamic else 'Client'
+
gen.line('// The Client is used to invoke RPCs for this service.')
gen.line(
- f'class Client final : public {RPC_NAMESPACE}::internal::'
+ f'class {class_name} final : public {RPC_NAMESPACE}::internal::'
'ServiceClient {'
)
gen.line(' public:')
with gen.indent():
gen.line(
- f'constexpr Client({RPC_NAMESPACE}::Client& client,'
+ f'constexpr {class_name}({RPC_NAMESPACE}::Client& client,'
' uint32_t channel_id)'
)
gen.line(' : ServiceClient(client, channel_id) {}')
@@ -272,11 +287,15 @@ def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
for method in service.methods():
gen.line()
- gen.client_member_function(method)
+ gen.client_member_function(method, dynamic=dynamic)
gen.line('};')
gen.line()
+
+def _generate_client_free_functions(
+ gen: CodeGenerator, service: ProtoService
+) -> None:
gen.line(
'// Static functions for invoking RPCs on a pw_rpc server. '
'These functions are '
@@ -324,12 +343,28 @@ def _generate_info(
gen.line('}')
+ if gen.name() in ['pwpb', 'nanopb']:
+ gen.line('template <typename ServiceImpl, typename Response>')
+ gen.line('static constexpr auto FunctionTemplate() {')
+
+ with gen.indent():
+ template_name = method.name() + 'Template<Response>'
+ gen.line(f'return &ServiceImpl::template {template_name};')
+
+ gen.line('}')
+
gen.line(
'using GeneratedClient = '
f'{"::" + namespace if namespace else ""}'
f'::pw_rpc::{gen.name()}::{service.name()}::Client;'
)
+ gen.line(
+ 'using ServiceClass = '
+ f'{"::" + namespace if namespace else ""}'
+ f'::pw_rpc::{gen.name()}::{service.name()};'
+ )
+
gen.method_info_specialization(method)
gen.line('};')
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 35a119418..a1c171dc4 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -52,26 +52,41 @@ def _proto_filename_to_generated_header(proto_file: str) -> str:
return f'{filename}.rpc{PROTO_H_EXTENSION}'
-def _client_call(method: ProtoServiceMethod) -> str:
+def _client_call(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> str:
template_args = []
if method.client_streaming():
template_args.append(method.request_type().nanopb_struct())
- template_args.append(method.response_type().nanopb_struct())
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ template_args.append(response)
return f'{client_call_type(method, "Nanopb")}<{", ".join(template_args)}>'
-def _function(method: ProtoServiceMethod) -> str:
- return f'{_client_call(method)} {method.name()}'
+def _function(
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+) -> str:
+ if name is None:
+ name = method.name()
+
+ return f'{_client_call(method, response)} {name}'
-def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
+def _user_args(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> Iterable[str]:
if not method.client_streaming():
yield f'const {method.request_type().nanopb_struct()}& request'
- response = method.response_type().nanopb_struct()
+ if response is None:
+ response = method.response_type().nanopb_struct()
if method.server_streaming():
yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
@@ -134,20 +149,28 @@ class NanopbCodeGenerator(CodeGenerator):
self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
self.line(f'{_serde(method)}),')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
- """Outputs client code for a single RPC method."""
+ def _client_member_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ if name is None:
+ name = method.name()
- self.line(f'{_function(method)}(')
- self.indented_list(*_user_args(method), end=') const {')
+ self.line(f'{_function(method, response, name)}(')
+ self.indented_list(*_user_args(method, response), end=') const {')
with self.indent():
- client_call = _client_call(method)
+ client_call = _client_call(method, response)
base = 'Stream' if method.server_streaming() else 'Unary'
self.line(
f'return {RPC_NAMESPACE}::internal::'
- f'Nanopb{base}ResponseClientCall<'
- f'{method.response_type().nanopb_struct()}>::'
- f'Start<{client_call}>('
+ f'Nanopb{base}ResponseClientCall<{response}>::'
+ f'template Start<{client_call}>('
)
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
@@ -172,17 +195,46 @@ class NanopbCodeGenerator(CodeGenerator):
self.line('}')
- def client_static_function(self, method: ProtoServiceMethod) -> None:
- self.line(f'static {_function(method)}(')
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ """Outputs client code for a single RPC method."""
+ if dynamic:
+ self.line('// DynamicClient is not implemented for Nanopb')
+ return
+
+ self._client_member_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().nanopb_struct()}>'
+ )
+ self._client_member_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
+ def _client_static_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ if name is None:
+ name = method.name()
+
+ self.line(f'static {_function(method, response, name)}(')
self.indented_list(
f'{RPC_NAMESPACE}::Client& client',
'uint32_t channel_id',
- *_user_args(method),
+ *_user_args(method, response),
end=') {',
)
with self.indent():
- self.line(f'return Client(client, channel_id).{method.name()}(')
+ self.line(f'return Client(client, channel_id).{name}(')
args = []
@@ -201,6 +253,17 @@ class NanopbCodeGenerator(CodeGenerator):
self.line('}')
+ def client_static_function(self, method: ProtoServiceMethod) -> None:
+ self._client_static_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().nanopb_struct()}>'
+ )
+ self._client_static_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
def method_info_specialization(self, method: ProtoServiceMethod) -> None:
self.line()
self.line(f'using Request = {method.request_type().nanopb_struct()};')
diff --git a/pw_rpc/py/pw_rpc/codegen_pwpb.py b/pw_rpc/py/pw_rpc/codegen_pwpb.py
index a9bf2383b..abf8bd80f 100644
--- a/pw_rpc/py/pw_rpc/codegen_pwpb.py
+++ b/pw_rpc/py/pw_rpc/codegen_pwpb.py
@@ -14,7 +14,7 @@
"""This module generates the code for pw_protobuf pw_rpc services."""
import os
-from typing import Iterable
+from typing import Iterable, Optional
from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoServiceMethod
@@ -52,26 +52,37 @@ def _serde(method: ProtoServiceMethod) -> str:
)
-def _client_call(method: ProtoServiceMethod) -> str:
+def _client_call(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> str:
template_args = []
if method.client_streaming():
template_args.append(method.request_type().pwpb_struct())
- template_args.append(method.response_type().pwpb_struct())
+ if response is None:
+ response = method.response_type().pwpb_struct()
+
+ template_args.append(response)
return f'{client_call_type(method, "Pwpb")}<{", ".join(template_args)}>'
-def _function(method: ProtoServiceMethod) -> str:
- return f'{_client_call(method)} {method.name()}'
+def _function(
+ method: ProtoServiceMethod,
+ name: Optional[str] = None,
+) -> str:
+ return f'auto {name or method.name()}'
-def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
+def _user_args(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> Iterable[str]:
if not method.client_streaming():
yield f'const {method.request_type().pwpb_struct()}& request'
- response = method.response_type().pwpb_struct()
+ if response is None:
+ response = method.response_type().pwpb_struct()
if method.server_streaming():
yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
@@ -135,20 +146,31 @@ class PwpbCodeGenerator(CodeGenerator):
self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
self.line(f'{_serde(method)}),')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
- """Outputs client code for a single RPC method."""
+ def _client_member_function(
+ self,
+ method: ProtoServiceMethod,
+ *,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ dynamic: bool,
+ ) -> None:
+ if response is None:
+ response = method.response_type().pwpb_struct()
- self.line(f'{_function(method)}(')
- self.indented_list(*_user_args(method), end=') const {')
+ if name is None:
+ name = method.name()
+
+ self.line(f'{_function(method, name)}(')
+ self.indented_list(*_user_args(method, response), end=') const {')
with self.indent():
- client_call = _client_call(method)
+ client_call = _client_call(method, response)
base = 'Stream' if method.server_streaming() else 'Unary'
self.line(
f'return {RPC_NAMESPACE}::internal::'
- f'Pwpb{base}ResponseClientCall<'
- f'{method.response_type().pwpb_struct()}>::'
- f'Start<{client_call}>('
+ f'Pwpb{base}ResponseClientCall<{response}>::'
+ f'template Start{"Dynamic" if dynamic else ""}'
+ f'<{client_call}>('
)
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
@@ -173,17 +195,49 @@ class PwpbCodeGenerator(CodeGenerator):
self.line('}')
- def client_static_function(self, method: ProtoServiceMethod) -> None:
- self.line(f'static {_function(method)}(')
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ """Outputs client code for a single RPC method."""
+ self._client_member_function(method, dynamic=dynamic)
+
+ if dynamic: # Skip custom response overload
+ return
+
+ # Generate functions that allow specifying a custom response struct.
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().pwpb_struct()}>'
+ )
+ self._client_member_function(
+ method,
+ response='Response',
+ name=method.name() + 'Template',
+ dynamic=dynamic,
+ )
+
+ def _client_static_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().pwpb_struct()
+
+ if name is None:
+ name = method.name()
+
+ self.line(f'static {_function(method, name)}(')
self.indented_list(
f'{RPC_NAMESPACE}::Client& client',
'uint32_t channel_id',
- *_user_args(method),
+ *_user_args(method, response),
end=') {',
)
with self.indent():
- self.line(f'return Client(client, channel_id).{method.name()}(')
+ self.line(f'return Client(client, channel_id).{name}(')
args = []
@@ -202,6 +256,17 @@ class PwpbCodeGenerator(CodeGenerator):
self.line('}')
+ def client_static_function(self, method: ProtoServiceMethod) -> None:
+ self._client_static_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().pwpb_struct()}>'
+ )
+ self._client_static_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
def method_info_specialization(self, method: ProtoServiceMethod) -> None:
self.line()
self.line(f'using Request = {method.request_type().pwpb_struct()};')
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 01e03466a..6f5aeb77c 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -93,7 +93,13 @@ class RawCodeGenerator(CodeGenerator):
)
self.line(f' {get_id(method)}), // Hash of "{method.name()}"')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ if dynamic:
+ self.line('// DynamicClient is not implemented for raw RPC')
+ return
+
self.line(f'{_function(method)}(')
self.indented_list(*_user_args(method), end=') const {')
diff --git a/pw_rpc/py/pw_rpc/console_tools/console.py b/pw_rpc/py/pw_rpc/console_tools/console.py
index 51544f6f9..361ed7a4d 100644
--- a/pw_rpc/py/pw_rpc/console_tools/console.py
+++ b/pw_rpc/py/pw_rpc/console_tools/console.py
@@ -176,10 +176,9 @@ class Context:
.. code-block:: python
- context = console_tools.Context(
- clients, default_client, protos, help_header=WELCOME_MESSAGE)
- IPython.terminal.embed.InteractiveShellEmbed().mainloop(
- module=types.SimpleNamespace(**context.variables()))
+ context = console_tools.Context(
+ clients, default_client, protos, help_header=WELCOME_MESSAGE)
+ IPython.start_ipython(argv=[], user_ns=dict(**context.variables()))
"""
def __init__(
diff --git a/pw_rpc/py/pw_rpc/descriptors.py b/pw_rpc/py/pw_rpc/descriptors.py
index f3daf8787..728061e88 100644
--- a/pw_rpc/py/pw_rpc/descriptors.py
+++ b/pw_rpc/py/pw_rpc/descriptors.py
@@ -46,7 +46,7 @@ from pw_rpc import ids
@dataclass(frozen=True)
class Channel:
id: int
- output: Callable[[bytes], Any]
+ output: Optional[Callable[[bytes], Any]]
def __repr__(self) -> str:
return f'Channel({self.id})'
@@ -85,10 +85,14 @@ class ChannelManipulator(abc.ABC):
channels = tuple(Channel(_DEFAULT_CHANNEL, packet_logger))
# Create a RPC client.
- client = HdlcRpcClient(socket.read, protos, channels, stdout)
+ reader = SocketReader(socket)
+ with reader:
+ client = HdlcRpcClient(reader, protos, channels, stdout)
+ with client:
+ # Do something with client
"""
- def __init__(self):
+ def __init__(self) -> None:
self.send_packet: Callable[[bytes], Any] = lambda _: None
@abc.abstractmethod
@@ -388,21 +392,29 @@ class ServiceAccessor(Collection[T]):
if not isinstance(members, dict):
members = {m: m for m in members}
- by_name = {_name(k): v for k, v in members.items()}
+ by_name: Dict[str, Any] = {_name(k): v for k, v in members.items()}
self._by_id = {k.id: v for k, v in members.items()}
+ # Note: a dictionary is used rather than `setattr` in order to
+ # (1) Hint to the type checker that there will be extra fields
+ # (2) Ensure that built-in attributes such as `_by_id`` are not
+ # overwritten.
+ self._attrs: Dict[str, Any] = {}
if as_attrs == 'members':
for name, member in by_name.items():
- setattr(self, name, member)
+ self._attrs[name] = member
elif as_attrs == 'packages':
for package in python_protos.as_packages(
(m.package, _AccessByName(m.name, members[m])) for m in members
).packages:
- setattr(self, str(package), package)
+ self._attrs[str(package)] = package
elif as_attrs:
raise ValueError(f'Unexpected value {as_attrs!r} for as_attrs')
- def __getitem__(self, name_or_id: Union[str, int]):
+ def __getattr__(self, name: str) -> Any:
+ return self._attrs[name]
+
+ def __getitem__(self, name_or_id: Union[str, int]) -> Any:
"""Accesses a service/method by the string name or ID."""
try:
return self._by_id[_id(name_or_id)]
diff --git a/pw_rpc/py/pw_rpc/packets.py b/pw_rpc/py/pw_rpc/packets.py
index ddcc03e74..54d4b5085 100644
--- a/pw_rpc/py/pw_rpc/packets.py
+++ b/pw_rpc/py/pw_rpc/packets.py
@@ -13,6 +13,7 @@
# the License.
"""Functions for working with pw_rpc packets."""
+import dataclasses
from typing import Optional
from google.protobuf import message
@@ -33,43 +34,47 @@ def decode_payload(packet, payload_type):
return payload
-def _ids(rpc: tuple) -> tuple:
- return tuple(item if isinstance(item, int) else item.id for item in rpc)
+@dataclasses.dataclass(eq=True, frozen=True)
+class RpcIds:
+ """Integer IDs that uniquely identify a remote procedure call."""
+ channel_id: int
+ service_id: int
+ method_id: int
+ call_id: int
-def encode_request(rpc: tuple, request: Optional[message.Message]) -> bytes:
- channel, service, method = _ids(rpc)
+
+def encode_request(rpc: RpcIds, request: Optional[message.Message]) -> bytes:
payload = request.SerializeToString() if request is not None else bytes()
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.REQUEST,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=payload,
).SerializeToString()
-def encode_response(rpc: tuple, response: message.Message) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_response(rpc: RpcIds, response: message.Message) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.RESPONSE,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=response.SerializeToString(),
).SerializeToString()
-def encode_client_stream(rpc: tuple, request: message.Message) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_client_stream(rpc: RpcIds, request: message.Message) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.CLIENT_STREAM,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=request.SerializeToString(),
).SerializeToString()
@@ -80,29 +85,29 @@ def encode_client_error(packet: packet_pb2.RpcPacket, status: Status) -> bytes:
channel_id=packet.channel_id,
service_id=packet.service_id,
method_id=packet.method_id,
+ call_id=packet.call_id,
status=status.value,
).SerializeToString()
-def encode_cancel(rpc: tuple) -> bytes:
- channel, service, method = _ids(rpc)
+def encode_cancel(rpc: RpcIds) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.CLIENT_ERROR,
status=Status.CANCELLED.value,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
).SerializeToString()
-def encode_client_stream_end(rpc: tuple) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_client_stream_end(rpc: RpcIds) -> bytes:
return packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.CLIENT_STREAM_END,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ type=packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
).SerializeToString()
diff --git a/pw_rpc/py/pw_rpc/testing.py b/pw_rpc/py/pw_rpc/testing.py
index de8ab6223..f8b129df4 100644
--- a/pw_rpc/py/pw_rpc/testing.py
+++ b/pw_rpc/py/pw_rpc/testing.py
@@ -128,7 +128,7 @@ def execute_integration_test(
client_cmdline += [*common_args]
server_process = subprocess.Popen(server_cmdline)
- # TODO(b/234879791): Replace this delay with some sort of IPC.
+ # TODO: b/234879791 - Replace this delay with some sort of IPC.
time.sleep(setup_time_s)
result = subprocess.run(client_cmdline).returncode
diff --git a/pw_rpc/py/tests/callback_client_test.py b/pw_rpc/py/tests/callback_client_test.py
index 406182983..c014e3eee 100755
--- a/pw_rpc/py/tests/callback_client_test.py
+++ b/pw_rpc/py/tests/callback_client_test.py
@@ -21,7 +21,7 @@ from typing import Any, List, Optional, Tuple
from pw_protobuf_compiler import python_protos
from pw_status import Status
-from pw_rpc import callback_client, client, packets
+from pw_rpc import callback_client, client, descriptors, packets
from pw_rpc.internal import packet_pb2
TEST_PROTO_1 = """\
@@ -52,6 +52,8 @@ service PublicService {
}
"""
+CLIENT_CHANNEL_ID: int = 489
+
def _message_bytes(msg) -> bytes:
return msg if isinstance(msg, bytes) else msg.SerializeToString()
@@ -66,10 +68,12 @@ class _CallbackClientImplTestBase(unittest.TestCase):
self._client = client.Client.from_modules(
callback_client.Impl(),
- [client.Channel(1, self._handle_packet)],
+ [client.Channel(CLIENT_CHANNEL_ID, self._handle_packet)],
self._protos.modules(),
)
- self._service = self._client.channel(1).rpcs.pw.test1.PublicService
+ self._service = self._client.channel(
+ CLIENT_CHANNEL_ID
+ ).rpcs.pw.test1.PublicService
self.requests: List[packet_pb2.RpcPacket] = []
self._next_packets: List[Tuple[bytes, Status]] = []
@@ -83,13 +87,14 @@ class _CallbackClientImplTestBase(unittest.TestCase):
def _enqueue_response(
self,
- channel_id: int,
- method=None,
+ channel_id: int = CLIENT_CHANNEL_ID,
+ method: Optional[descriptors.Method] = None,
status: Status = Status.OK,
- payload=b'',
+ payload: bytes = b'',
*,
ids: Optional[Tuple[int, int]] = None,
- process_status=Status.OK,
+ process_status: Status = Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
if method:
assert ids is None
@@ -105,6 +110,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
channel_id=channel_id,
service_id=service_id,
method_id=method_id,
+ call_id=call_id,
status=status.value,
payload=_message_bytes(payload),
).SerializeToString(),
@@ -113,7 +119,12 @@ class _CallbackClientImplTestBase(unittest.TestCase):
)
def _enqueue_server_stream(
- self, channel_id: int, method, response, process_status=Status.OK
+ self,
+ channel_id: int,
+ method,
+ response,
+ process_status=Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
self._next_packets.append(
(
@@ -122,6 +133,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
channel_id=channel_id,
service_id=method.service.id,
method_id=method.id,
+ call_id=call_id,
payload=_message_bytes(response),
).SerializeToString(),
process_status,
@@ -135,6 +147,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
method,
status: Status,
process_status=Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
self._next_packets.append(
(
@@ -145,6 +158,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
if isinstance(service, int)
else service.id,
method_id=method if isinstance(method, int) else method.id,
+ call_id=call_id,
status=status.value,
).SerializeToString(),
process_status,
@@ -181,13 +195,17 @@ class _CallbackClientImplTestBase(unittest.TestCase):
return message
+# Disable docstring requirements for test functions.
+# pylint: disable=missing-function-docstring
+
+
class CallbackClientImplTest(_CallbackClientImplTestBase):
"""Tests the callback_client.Impl client implementation."""
def test_callback_exceptions_suppressed(self) -> None:
stub = self._service.SomeUnary
- self._enqueue_response(1, stub.method)
+ self._enqueue_response(CLIENT_CHANNEL_ID, stub.method)
exception_msg = 'YOU BROKE IT O-]-<'
with self.assertLogs(callback_client.__package__, 'ERROR') as logs:
@@ -198,7 +216,7 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
self.assertIn(exception_msg, ''.join(logs.output))
# Make sure we can still invoke the RPC.
- self._enqueue_response(1, stub.method, Status.UNKNOWN)
+ self._enqueue_response(CLIENT_CHANNEL_ID, stub.method, Status.UNKNOWN)
status, _ = stub()
self.assertIs(status, Status.UNKNOWN)
@@ -210,20 +228,22 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
self._enqueue_response(999, method, process_status=Status.NOT_FOUND)
# Bad service
self._enqueue_response(
- 1, ids=(999, method.id), process_status=Status.OK
+ CLIENT_CHANNEL_ID, ids=(999, method.id), process_status=Status.OK
)
# Bad method
self._enqueue_response(
- 1, ids=(service_id, 999), process_status=Status.OK
+ CLIENT_CHANNEL_ID, ids=(service_id, 999), process_status=Status.OK
)
# For RPC not pending (is Status.OK because the packet is processed)
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
ids=(service_id, self._service.SomeBidiStreaming.method.id),
process_status=Status.OK,
)
- self._enqueue_response(1, method, process_status=Status.OK)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, method, process_status=Status.OK
+ )
status, response = self._service.SomeUnary(magic_number=6)
self.assertIs(Status.OK, status)
@@ -242,12 +262,16 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
process_status=Status.NOT_FOUND,
)
# Bad service
- self._enqueue_error(1, 999, method.id, Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ CLIENT_CHANNEL_ID, 999, method.id, Status.INVALID_ARGUMENT
+ )
# Bad method
- self._enqueue_error(1, service_id, 999, Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ CLIENT_CHANNEL_ID, service_id, 999, Status.INVALID_ARGUMENT
+ )
# For RPC not pending
self._enqueue_error(
- 1,
+ CLIENT_CHANNEL_ID,
service_id,
self._service.SomeBidiStreaming.method.id,
Status.NOT_FOUND,
@@ -261,7 +285,11 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
method = self._service.SomeUnary.method
self._enqueue_response(
- 1, method, Status.OK, b'INVALID DATA!!!', process_status=Status.OK
+ CLIENT_CHANNEL_ID,
+ method,
+ Status.OK,
+ b'INVALID DATA!!!',
+ process_status=Status.OK,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -282,10 +310,10 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
def test_default_timeouts_set_for_all_rpcs(self) -> None:
rpc_client = client.Client.from_modules(
callback_client.Impl(99, 100),
- [client.Channel(1, lambda *a, **b: None)],
+ [client.Channel(CLIENT_CHANNEL_ID, lambda *a, **b: None)],
self._protos.modules(),
)
- rpcs = rpc_client.channel(1).rpcs
+ rpcs = rpc_client.channel(CLIENT_CHANNEL_ID).rpcs
self.assertEqual(
rpcs.pw.test1.PublicService.SomeUnary.default_timeout_s, 99
@@ -326,7 +354,7 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_blocking_call(self) -> None:
for _ in range(3):
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
@@ -345,17 +373,19 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_nonblocking_call(self) -> None:
for _ in range(3):
+ callback = mock.Mock()
+ call = self.rpc.invoke(
+ self._request(magic_number=5), callback, callback
+ )
+
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
+ call_id=call.call_id,
)
-
- callback = mock.Mock()
- call = self.rpc.invoke(
- self._request(magic_number=5), callback, callback
- )
+ self._process_enqueued_packets()
callback.assert_has_calls(
[
@@ -368,12 +398,50 @@ class UnaryTest(_CallbackClientImplTestBase):
5, self._sent_payload(self.method.request_type).magic_number
)
+ def test_concurrent_nonblocking_calls(self) -> None:
+ # Start several calls to the same method
+ callbacks_and_calls: List[
+ Tuple[mock.Mock, callback_client.call.Call]
+ ] = []
+ for _ in range(3):
+ callback = mock.Mock()
+ call = self.rpc.invoke(self._request(magic_number=5), callback)
+ callbacks_and_calls.append((callback, call))
+
+ # Respond only to the last call
+ last_callback, last_call = callbacks_and_calls.pop()
+ last_payload = self.method.response_type(payload='last payload')
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID,
+ self.method,
+ payload=last_payload,
+ call_id=last_call.call_id,
+ )
+ self._process_enqueued_packets()
+
+ # Assert that only the last caller received a response
+ last_callback.assert_called_once_with(last_call, last_payload)
+ for remaining_callback, _ in callbacks_and_calls:
+ remaining_callback.assert_not_called()
+
+ # Respond to the other callers and check for receipt
+ other_payload = self.method.response_type(payload='other payload')
+ for callback, call in callbacks_and_calls:
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID,
+ self.method,
+ payload=other_payload,
+ call_id=call.call_id,
+ )
+ self._process_enqueued_packets()
+ callback.assert_called_once_with(call, other_payload)
+
def test_open(self) -> None:
self.output_exception = IOError('something went wrong sending!')
for _ in range(3):
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
@@ -397,7 +465,10 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_blocking_server_error(self) -> None:
for _ in range(3):
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -444,19 +515,19 @@ class UnaryTest(_CallbackClientImplTestBase):
with self.assertRaises(callback_client.RpcTimeout):
self._service.SomeUnary()
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_nonblocking_exception_in_callback(self) -> None:
- exception = ValueError('something went wrong!')
+ exception = ValueError('something went wrong! (intentionally)')
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke(on_completed=mock.Mock(side_effect=exception))
@@ -476,6 +547,28 @@ class UnaryTest(_CallbackClientImplTestBase):
'(Status.OK, None)',
)
+ def test_on_call_hook(self) -> None:
+ hook_function = mock.Mock()
+
+ self._client = client.Client.from_modules(
+ callback_client.Impl(on_call_hook=hook_function),
+ [client.Channel(CLIENT_CHANNEL_ID, self._handle_packet)],
+ self._protos.modules(),
+ )
+
+ self._service = self._client.channel(
+ CLIENT_CHANNEL_ID
+ ).rpcs.pw.test1.PublicService
+
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
+ self._service.SomeUnary(self.method.request_type(magic_number=6))
+
+ hook_function.assert_called_once()
+ self.assertEqual(
+ hook_function.call_args[0][0].method.full_name,
+ self.method.full_name,
+ )
+
class ServerStreamingTest(_CallbackClientImplTestBase):
"""Tests for server streaming RPCs."""
@@ -490,9 +583,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
self.assertEqual(
[rep1, rep2],
@@ -508,9 +603,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
callback = mock.Mock()
call = self.rpc.invoke(
@@ -535,9 +632,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
callback = mock.Mock()
call = self.rpc.open(
@@ -557,7 +656,7 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_cancel(self) -> None:
resp = self.rpc.method.response_type(payload='!!!')
- self._enqueue_server_stream(1, self.rpc.method, resp)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.rpc.method, resp)
callback = mock.Mock()
call = self.rpc.invoke(self._request(magic_number=3), callback)
@@ -575,8 +674,42 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
self.assertEqual(self.last_request().status, Status.CANCELLED.value)
# Ensure the RPC can be called after being cancelled.
- self._enqueue_server_stream(1, self.method, resp)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, resp)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
+
+ call = self.rpc.invoke(
+ self._request(magic_number=3), callback, callback
+ )
+
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, Status.OK),
+ ]
+ )
+
+ def test_request_completion(self) -> None:
+ resp = self.rpc.method.response_type(payload='!!!')
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.rpc.method, resp)
+
+ callback = mock.Mock()
+ call = self.rpc.invoke(self._request(magic_number=3), callback)
+ callback.assert_called_once_with(
+ call, self.rpc.method.response_type(payload='!!!')
+ )
+
+ callback.reset_mock()
+
+ call.request_completion()
+
+ self.assertEqual(
+ self.last_request().type,
+ packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
+ )
+
+ # Ensure the RPC can be called after being completed.
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, resp)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke(
self._request(magic_number=3), callback, callback
@@ -605,20 +738,20 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
for _ in call:
pass
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_nonblocking_iterate_over_count(self) -> None:
reply = self.method.response_type(payload='!?')
for _ in range(4):
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
call = self.rpc.invoke()
@@ -628,8 +761,8 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_iterate_after_completed_doesnt_block(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke()
@@ -655,7 +788,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self.send_responses_after_packets = 3
response = self.method.response_type(payload='yo')
- self._enqueue_response(1, self.method, Status.OK, response)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, response
+ )
results = self.rpc(requests)
self.assertIs(results.status, Status.OK)
@@ -666,7 +801,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -692,7 +830,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
- self._enqueue_response(1, self.method, Status.OK, payload_1)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload_1
+ )
stream.send(magic_number=32)
self.assertIs(
@@ -712,7 +852,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
payload = self.method.response_type(payload='-_-')
for _ in range(3):
- self._enqueue_response(1, self.method, Status.OK, payload)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload
+ )
callback = mock.Mock()
call = self.rpc.open(callback, callback, callback)
@@ -745,11 +887,13 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
- self._enqueue_response(1, self.method, Status.OK, payload_1)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload_1
+ )
stream.finish_and_wait()
self.assertIs(
- packet_pb2.PacketType.CLIENT_STREAM_END,
+ packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
self.last_request().type,
)
@@ -778,7 +922,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
stream = self._service.SomeClientStreaming.invoke()
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
stream.send(magic_number=2**32 - 1)
@@ -791,9 +938,13 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
stream = self._service.SomeClientStreaming.invoke()
- # Error will be sent in response to the CLIENT_STREAM_END packet.
+ # Error will be sent in response to the CLIENT_REQUEST_COMPLETION
+ # packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -812,7 +963,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_completed(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_response(1, self.method, Status.UNAVAILABLE, reply)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.UNAVAILABLE, reply
+ )
call = self.rpc.invoke()
result = call.finish_and_wait()
@@ -823,7 +976,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_error(self) -> None:
self._enqueue_error(
- 1, self.method.service, self.method, Status.UNAVAILABLE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.UNAVAILABLE,
)
call = self.rpc.invoke()
@@ -836,14 +992,14 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertIs(call.error, Status.UNAVAILABLE)
self.assertIsNone(call.response)
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
class BidirectionalStreamingTest(_CallbackClientImplTestBase):
@@ -862,7 +1018,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self.send_responses_after_packets = 3
- self._enqueue_response(1, self.method, Status.NOT_FOUND)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.NOT_FOUND)
results = self.rpc(requests)
self.assertIs(results.status, Status.NOT_FOUND)
@@ -873,7 +1029,10 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -903,8 +1062,8 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
self.assertEqual([], responses)
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
stream.send(magic_number=66)
self.assertIs(
@@ -916,7 +1075,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
self.assertEqual([rep1, rep2], responses)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
stream.send(magic_number=77)
self.assertTrue(stream.completed())
@@ -931,9 +1090,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
callback = mock.Mock()
call = self.rpc.open(callback, callback, callback)
@@ -953,7 +1112,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking(self, callback) -> None:
"""Tests a bidirectional streaming RPC ended by the server."""
reply = self.method.response_type(payload='This is the payload!')
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
self._service.SomeBidiStreaming.invoke()
@@ -969,14 +1128,17 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
)
self.assertFalse(stream.completed())
- self._enqueue_server_stream(1, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
stream.send(magic_number=55)
self.assertFalse(stream.completed())
self.assertEqual([rep1], responses)
self._enqueue_error(
- 1, self.method.service, self.method, Status.OUT_OF_RANGE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.OUT_OF_RANGE,
)
stream.send(magic_number=99999)
@@ -994,9 +1156,13 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
stream = self._service.SomeBidiStreaming.invoke()
- # Error will be sent in response to the CLIENT_STREAM_END packet.
+ # Error will be sent in response to the CLIENT_REQUEST_COMPLETION
+ # packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -1015,8 +1181,10 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_completed(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
- self._enqueue_response(1, self.method, Status.UNAVAILABLE)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.UNAVAILABLE
+ )
call = self.rpc.invoke()
result = call.finish_and_wait()
@@ -1027,9 +1195,12 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_error(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
self._enqueue_error(
- 1, self.method.service, self.method, Status.UNAVAILABLE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.UNAVAILABLE,
)
call = self.rpc.invoke()
@@ -1042,14 +1213,14 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertIs(call.error, Status.UNAVAILABLE)
self.assertEqual(call.responses, [reply])
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_stream_response(self) -> None:
proto = self._protos.packages.pw.test1.SomeMessage(magic_number=123)
diff --git a/pw_rpc/py/tests/client_test.py b/pw_rpc/py/tests/client_test.py
index 92a1f8236..28bea9069 100755
--- a/pw_rpc/py/tests/client_test.py
+++ b/pw_rpc/py/tests/client_test.py
@@ -15,7 +15,7 @@
"""Tests creating pw_rpc client."""
import unittest
-from typing import Optional
+from typing import Any, Callable, Optional
from pw_protobuf_compiler import python_protos
from pw_status import Status
@@ -24,6 +24,8 @@ from pw_rpc import callback_client, client, packets
import pw_rpc.ids
from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
+RpcIds = packets.RpcIds
+
TEST_PROTO_1 = """\
syntax = "proto3";
@@ -73,13 +75,30 @@ service Bravo {
}
"""
+SOME_CHANNEL_ID: int = 237
+SOME_SERVICE_ID: int = 193
+SOME_METHOD_ID: int = 769
+SOME_CALL_ID: int = 452
+
+CLIENT_FIRST_CHANNEL_ID: int = 557
+CLIENT_SECOND_CHANNEL_ID: int = 474
+
+
+def create_protos() -> Any:
+ return python_protos.Library.from_strings([TEST_PROTO_1, TEST_PROTO_2])
-def _test_setup(output=None):
- protos = python_protos.Library.from_strings([TEST_PROTO_1, TEST_PROTO_2])
- return protos, client.Client.from_modules(
+
+def create_client(
+ proto_modules: Any,
+ first_channel_output_fn: Optional[Callable[[bytes], Any]] = None,
+) -> client.Client:
+ return client.Client.from_modules(
callback_client.Impl(),
- [client.Channel(1, output), client.Channel(2, lambda _: None)],
- protos.modules(),
+ [
+ client.Channel(CLIENT_FIRST_CHANNEL_ID, first_channel_output_fn),
+ client.Channel(CLIENT_SECOND_CHANNEL_ID, lambda _: None),
+ ],
+ proto_modules,
)
@@ -87,7 +106,10 @@ class ChannelClientTest(unittest.TestCase):
"""Tests the ChannelClient."""
def setUp(self) -> None:
- self._channel_client = _test_setup()[1].channel(1)
+ client_instance = create_client(create_protos().modules())
+ self._channel_client: client.ChannelClient = client_instance.channel(
+ CLIENT_FIRST_CHANNEL_ID
+ )
def test_access_service_client_as_attribute_or_index(self) -> None:
self.assertIs(
@@ -182,7 +204,8 @@ class ClientTest(unittest.TestCase):
def setUp(self) -> None:
self._last_packet_sent_bytes: Optional[bytes] = None
- self._protos, self._client = _test_setup(self._save_packet)
+ self._protos = create_protos()
+ self._client = create_client(self._protos.modules(), self._save_packet)
def _save_packet(self, packet) -> None:
self._last_packet_sent_bytes = packet
@@ -194,11 +217,19 @@ class ClientTest(unittest.TestCase):
return packet
def test_channel(self) -> None:
- self.assertEqual(self._client.channel(1).channel.id, 1)
- self.assertEqual(self._client.channel(2).channel.id, 2)
+ self.assertEqual(
+ self._client.channel(CLIENT_FIRST_CHANNEL_ID).channel.id,
+ CLIENT_FIRST_CHANNEL_ID,
+ )
+ self.assertEqual(
+ self._client.channel(CLIENT_SECOND_CHANNEL_ID).channel.id,
+ CLIENT_SECOND_CHANNEL_ID,
+ )
def test_channel_default_is_first_listed(self) -> None:
- self.assertEqual(self._client.channel().channel.id, 1)
+ self.assertEqual(
+ self._client.channel().channel.id, CLIENT_FIRST_CHANNEL_ID
+ )
def test_channel_invalid(self) -> None:
with self.assertRaises(KeyError):
@@ -259,7 +290,13 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (123, 456, 789), self._protos.packages.pw.test2.Request()
+ RpcIds(
+ SOME_CHANNEL_ID,
+ SOME_SERVICE_ID,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
+ self._protos.packages.pw.test2.Request(),
)
),
Status.NOT_FOUND,
@@ -269,7 +306,13 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, 456, 789), self._protos.packages.pw.test2.Request()
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ SOME_SERVICE_ID,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
+ self._protos.packages.pw.test2.Request(),
)
),
Status.OK,
@@ -279,9 +322,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=456,
- method_id=789,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
+ service_id=SOME_SERVICE_ID,
+ method_id=SOME_METHOD_ID,
+ call_id=SOME_CALL_ID,
status=Status.NOT_FOUND.value,
),
)
@@ -292,7 +336,12 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, service.id, 789),
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ service.id,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
self._protos.packages.pw.test2.Request(),
)
),
@@ -303,9 +352,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
service_id=service.id,
- method_id=789,
+ method_id=SOME_METHOD_ID,
+ call_id=SOME_CALL_ID,
status=Status.NOT_FOUND.value,
),
)
@@ -317,7 +367,12 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, service.id, method.id),
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ service.id,
+ method.id,
+ SOME_CALL_ID,
+ ),
self._protos.packages.pw.test2.Request(),
)
),
@@ -328,9 +383,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
service_id=service.id,
method_id=method.id,
+ call_id=SOME_CALL_ID,
status=Status.FAILED_PRECONDITION.value,
),
)
@@ -340,12 +396,17 @@ class ClientTest(unittest.TestCase):
reply = method.response_type(payload='hello')
def response_callback(
- rpc: client.PendingRpc, message, status: Optional[Status]
+ rpc: client.PendingRpc,
+ message,
+ status: Optional[Status],
) -> None:
self.assertEqual(
rpc,
client.PendingRpc(
- self._client.channel(1).channel, method.service, method
+ self._client.channel(CLIENT_FIRST_CHANNEL_ID).channel,
+ method.service,
+ method,
+ call_id=SOME_CALL_ID,
),
)
self.assertEqual(message, reply)
@@ -355,7 +416,15 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
- packets.encode_response((1, method.service, method), reply)
+ packets.encode_response(
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ method.service.id,
+ method.id,
+ SOME_CALL_ID,
+ ),
+ reply,
+ )
),
Status.OK,
)
diff --git a/pw_rpc/py/tests/packets_test.py b/pw_rpc/py/tests/packets_test.py
index d6fa87935..3edded35e 100755
--- a/pw_rpc/py/tests/packets_test.py
+++ b/pw_rpc/py/tests/packets_test.py
@@ -21,12 +21,23 @@ from pw_status import Status
from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
from pw_rpc import packets
+_TEST_IDS = packets.RpcIds(1, 2, 3, 4)
+_TEST_STATUS = 321
_TEST_REQUEST = RpcPacket(
type=PacketType.REQUEST,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
+ payload=RpcPacket(status=_TEST_STATUS).SerializeToString(),
+)
+_TEST_RESPONSE = RpcPacket(
+ type=PacketType.RESPONSE,
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
+ payload=RpcPacket(status=_TEST_STATUS).SerializeToString(),
)
@@ -34,29 +45,23 @@ class PacketsTest(unittest.TestCase):
"""Tests for packet encoding and decoding."""
def test_encode_request(self):
- data = packets.encode_request((1, 2, 3), RpcPacket(status=321))
+ data = packets.encode_request(_TEST_IDS, RpcPacket(status=_TEST_STATUS))
packet = RpcPacket()
packet.ParseFromString(data)
self.assertEqual(_TEST_REQUEST, packet)
def test_encode_response(self):
- response = RpcPacket(
- type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
+ data = packets.encode_response(
+ _TEST_IDS, RpcPacket(status=_TEST_STATUS)
)
-
- data = packets.encode_response((1, 2, 3), RpcPacket(status=321))
packet = RpcPacket()
packet.ParseFromString(data)
- self.assertEqual(response, packet)
+ self.assertEqual(_TEST_RESPONSE, packet)
def test_encode_cancel(self):
- data = packets.encode_cancel((9, 8, 7))
+ data = packets.encode_cancel(packets.RpcIds(9, 8, 7, 6))
packet = RpcPacket()
packet.ParseFromString(data)
@@ -68,6 +73,7 @@ class PacketsTest(unittest.TestCase):
channel_id=9,
service_id=8,
method_id=7,
+ call_id=6,
status=Status.CANCELLED.value,
),
)
@@ -82,9 +88,10 @@ class PacketsTest(unittest.TestCase):
packet,
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=2,
- method_id=3,
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
status=Status.NOT_FOUND.value,
),
)
@@ -96,18 +103,7 @@ class PacketsTest(unittest.TestCase):
def test_for_server(self):
self.assertTrue(packets.for_server(_TEST_REQUEST))
-
- self.assertFalse(
- packets.for_server(
- RpcPacket(
- type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
- )
- )
- )
+ self.assertFalse(packets.for_server(_TEST_RESPONSE))
if __name__ == '__main__':
diff --git a/pw_rpc/py/tests/python_client_cpp_server_test.py b/pw_rpc/py/tests/python_client_cpp_server_test.py
index ea180b2eb..fad61d5a3 100755
--- a/pw_rpc/py/tests/python_client_cpp_server_test.py
+++ b/pw_rpc/py/tests/python_client_cpp_server_test.py
@@ -14,6 +14,7 @@
# the License.
"""Tests using the callback client for pw_rpc."""
+import contextlib
from typing import List, Tuple
import unittest
@@ -62,28 +63,32 @@ class RpcIntegrationTest(unittest.TestCase):
rpc = self.rpcs.pw.rpc.Benchmark.BidirectionalEcho
for _ in range(ITERATIONS):
- first_call = rpc.invoke()
- first_call.send(payload=b'abc')
- self.assertEqual(
- next(iter(first_call)), rpc.response(payload=b'abc')
- )
- self.assertFalse(first_call.completed())
-
- second_call = rpc.invoke()
- second_call.send(payload=b'123')
- self.assertEqual(
- next(iter(second_call)), rpc.response(payload=b'123')
- )
-
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertEqual(
- first_call.responses, [rpc.response(payload=b'abc')]
- )
-
- self.assertFalse(second_call.completed())
- self.assertEqual(
- second_call.responses, [rpc.response(payload=b'123')]
- )
+ with contextlib.ExitStack() as stack:
+ first_call = stack.enter_context(rpc.invoke())
+ first_call_responses = first_call.get_responses()
+ first_call.send(payload=b'abc')
+ self.assertEqual(
+ next(first_call_responses), rpc.response(payload=b'abc')
+ )
+ self.assertFalse(first_call.completed())
+
+ second_call = stack.enter_context(rpc.invoke())
+ second_call_responses = second_call.get_responses()
+ second_call.send(payload=b'123')
+ self.assertEqual(
+ next(second_call_responses), rpc.response(payload=b'123')
+ )
+ self.assertFalse(second_call.completed())
+
+ # Check that issuing `second_call` did not cancel `first call`.
+ self.assertFalse(first_call.completed())
+ self.assertIs(first_call.error, None)
+
+ # Send to `first_call` again and check for a response.
+ first_call.send(payload=b'def')
+ self.assertEqual(
+ next(first_call_responses), rpc.response(payload=b'def')
+ )
def _main(
diff --git a/pw_rpc/raw/Android.bp b/pw_rpc/raw/Android.bp
index bcd39d4d1..5ed876706 100644
--- a/pw_rpc/raw/Android.bp
+++ b/pw_rpc/raw/Android.bp
@@ -52,7 +52,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -70,6 +70,4 @@ cc_defaults {
srcs: [
":pw_rpc_raw_src_files",
],
- host_supported: true,
- vendor_available: true,
}
diff --git a/pw_rpc/raw/BUILD.bazel b/pw_rpc/raw/BUILD.bazel
index ac9c10e8c..7d9033296 100644
--- a/pw_rpc/raw/BUILD.bazel
+++ b/pw_rpc/raw/BUILD.bazel
@@ -195,3 +195,16 @@ pw_cc_test(
"//pw_rpc:pw_rpc_test_cc.raw_rpc",
],
)
+
+pw_cc_test(
+ name = "synchronous_call_test",
+ srcs = ["synchronous_call_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:pw_rpc_test_cc.raw_rpc",
+ "//pw_rpc:synchronous_client_api",
+ "//pw_work_queue",
+ "//pw_work_queue:stl_test_thread",
+ "//pw_work_queue:test_thread_header",
+ ],
+)
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index 48b35bf50..ca43d6f1c 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -17,6 +17,8 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
import("$dir_pw_unit_test/test.gni")
config("public") {
@@ -85,6 +87,7 @@ pw_test_group("tests") {
":method_union_test",
":server_reader_writer_test",
":stub_generation_test",
+ ":synchronous_call_test",
]
}
@@ -163,6 +166,20 @@ pw_test("stub_generation_test") {
sources = [ "stub_generation_test.cc" ]
}
+pw_test("synchronous_call_test") {
+ deps = [
+ ":test_method_context",
+ "$dir_pw_thread:thread",
+ "$dir_pw_work_queue:pw_work_queue",
+ "$dir_pw_work_queue:stl_test_thread",
+ "$dir_pw_work_queue:test_thread",
+ "..:synchronous_client_api",
+ "..:test_protos.raw_rpc",
+ ]
+ sources = [ "synchronous_call_test.cc" ]
+ enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != ""
+}
+
pw_cc_negative_compilation_test("service_nc_test") {
sources = [ "service_nc_test.cc" ]
deps = [ "..:test_protos.raw_rpc" ]
diff --git a/pw_rpc/raw/client_reader_writer_test.cc b/pw_rpc/raw/client_reader_writer_test.cc
index 8c9553c09..c8dd07aa2 100644
--- a/pw_rpc/raw/client_reader_writer_test.cc
+++ b/pw_rpc/raw/client_reader_writer_test.cc
@@ -50,7 +50,7 @@ TEST(RawClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
@@ -63,6 +63,7 @@ TEST(RawClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -77,7 +78,67 @@ TEST(RawClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](ConstByteSpan) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientWriter, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientWriter call = TestService::TestClientStreamRpc(
+ ctx.client(), ctx.channel().id(), FailIfOnCompletedCalled, FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](ConstByteSpan, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientReader, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientReader call = TestService::TestServerStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](ConstByteSpan) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientReaderWriter, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientReaderWriter call =
+ TestService::TestBidirectionalStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -119,7 +180,7 @@ TEST(RawClientWriter, Cancel) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
@@ -141,6 +202,7 @@ TEST(RawClientReader, Cancel) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -164,7 +226,7 @@ TEST(RawClientReaderWriter, Cancel) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -201,7 +263,7 @@ TEST(RawClientWriter, Abandon) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
}
@@ -220,6 +282,7 @@ TEST(RawClientReader, Abandon) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 1u); // request only
}
@@ -239,7 +302,7 @@ TEST(RawClientReaderWriter, Abandon) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
}
@@ -260,7 +323,8 @@ TEST(RawClientReaderWriter, Move_InactiveToActive_EndsClientStream) {
active_call = std::move(inactive_call);
- EXPECT_EQ(ctx.output().total_packets(), 2u); // Sent CLIENT_STREAM_END
+ EXPECT_EQ(ctx.output().total_packets(),
+ 2u); // Sent CLIENT_REQUEST_COMPLETION
EXPECT_EQ(
ctx.output()
.client_stream_end_packets<TestService::TestBidirectionalStreamRpc>(),
@@ -356,7 +420,8 @@ TEST(RawClientWriter, WithClientStream_OutOfScope_SendsClientStreamEnd) {
ASSERT_EQ(ctx.output().total_packets(), 1u); // Sent the request
}
- EXPECT_EQ(ctx.output().total_packets(), 2u); // Sent CLIENT_STREAM_END
+ EXPECT_EQ(ctx.output().total_packets(),
+ 2u); // Sent CLIENT_REQUEST_COMPLETION
EXPECT_EQ(ctx.output()
.client_stream_end_packets<TestService::TestClientStreamRpc>(),
1u);
diff --git a/pw_rpc/raw/client_test.cc b/pw_rpc/raw/client_test.cc
index a7009d649..b78fa2307 100644
--- a/pw_rpc/raw/client_test.cc
+++ b/pw_rpc/raw/client_test.cc
@@ -19,6 +19,7 @@
#include "gtest/gtest.h"
#include "pw_rpc/internal/client_call.h"
#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/raw/client_reader_writer.h"
#include "pw_rpc/raw/client_testing.h"
namespace pw::rpc {
@@ -42,113 +43,103 @@ struct internal::MethodInfo<BidirectionalStreamMethod> {
namespace {
-template <auto kMethod, typename Call, typename Context>
-Call StartCall(Context& context,
- std::optional<uint32_t> channel_id = std::nullopt)
- PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
- internal::rpc_lock().lock();
- Call call(static_cast<internal::Endpoint&>(context.client()).ClaimLocked(),
- channel_id.value_or(context.channel().id()),
- internal::MethodInfo<kMethod>::kServiceId,
- internal::MethodInfo<kMethod>::kMethodId,
- internal::MethodInfo<kMethod>::kType);
- call.SendInitialClientRequest({});
- // As in the real implementations, immediately clean up aborted calls.
- static_cast<internal::Endpoint&>(context.client()).CleanUpCalls();
- return call;
-}
-
-class TestStreamCall : public internal::StreamResponseClientCall {
- public:
- TestStreamCall(internal::LockedEndpoint& client,
- uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id,
- MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
- : StreamResponseClientCall(
- client,
- channel_id,
- service_id,
- method_id,
- internal::CallProperties(
- type, internal::kClientCall, internal::kRawProto)),
- payload(nullptr) {
- set_on_next_locked([this](ConstByteSpan string) {
+// Captures payload from on_next and statuses from on_error and on_completed.
+// Payloads are assumed to be null-terminated strings.
+template <typename CallType>
+struct CallContext {
+ auto OnNext() {
+ return [this](ConstByteSpan string) {
payload = reinterpret_cast<const char*>(string.data());
- });
- set_on_completed_locked([this](Status status) { completed = status; });
- set_on_error_locked([this](Status status) { error = status; });
+ };
}
- const char* payload;
- std::optional<Status> completed;
- std::optional<Status> error;
-};
-
-class TestUnaryCall : public internal::UnaryResponseClientCall {
- public:
- TestUnaryCall() = default;
-
- TestUnaryCall(internal::LockedEndpoint& client,
- uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id,
- MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
- : UnaryResponseClientCall(
- client,
- channel_id,
- service_id,
- method_id,
- internal::CallProperties(
- type, internal::kClientCall, internal::kRawProto)),
- payload(nullptr) {
- set_on_completed_locked([this](ConstByteSpan string, Status status) {
+ auto UnaryOnCompleted() {
+ return [this](ConstByteSpan string, Status status) {
payload = reinterpret_cast<const char*>(string.data());
completed = status;
- });
- set_on_error_locked([this](Status status) { error = status; });
+ };
+ }
+
+ auto StreamOnCompleted() {
+ return [this](Status status) { completed = status; };
}
- using Call::set_on_error;
- using UnaryResponseClientCall::set_on_completed;
+ auto OnError() {
+ return [this](Status status) { error = status; };
+ }
+
+ CallType call;
const char* payload;
std::optional<Status> completed;
std::optional<Status> error;
};
+template <auto kMethod, typename Context>
+CallContext<RawUnaryReceiver> StartUnaryCall(
+ Context& context, std::optional<uint32_t> channel_id = std::nullopt)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ CallContext<RawUnaryReceiver> call_context;
+ call_context.call =
+ internal::UnaryResponseClientCall::Start<RawUnaryReceiver>(
+ context.client(),
+ channel_id.value_or(context.channel().id()),
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId,
+ call_context.UnaryOnCompleted(),
+ call_context.OnError(),
+ {});
+ return call_context;
+}
+
+template <auto kMethod, typename Context>
+CallContext<RawClientReaderWriter> StartStreamCall(
+ Context& context, std::optional<uint32_t> channel_id = std::nullopt)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ CallContext<RawClientReaderWriter> call_context;
+ call_context.call =
+ internal::StreamResponseClientCall::Start<RawClientReaderWriter>(
+ context.client(),
+ channel_id.value_or(context.channel().id()),
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId,
+ call_context.OnNext(),
+ call_context.StreamOnCompleted(),
+ call_context.OnError(),
+ {});
+ return call_context;
+}
+
TEST(Client, ProcessPacket_InvokesUnaryCallbacks) {
RawClientTestContext context;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
+ CallContext call_context = StartUnaryCall<UnaryMethod>(context);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_context.completed, OkStatus());
context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
OkStatus());
- ASSERT_NE(call.payload, nullptr);
- EXPECT_STREQ(call.payload, "you nary?!?");
- EXPECT_EQ(call.completed, OkStatus());
- EXPECT_FALSE(call.active());
+ ASSERT_NE(call_context.payload, nullptr);
+ EXPECT_STREQ(call_context.payload, "you nary?!?");
+ EXPECT_EQ(call_context.completed, OkStatus());
+ EXPECT_FALSE(call_context.call.active());
}
TEST(Client, ProcessPacket_NoCallbackSet) {
RawClientTestContext context;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
- call.set_on_completed(nullptr);
+ CallContext call_context = StartUnaryCall<UnaryMethod>(context);
+ call_context.call.set_on_completed(nullptr);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_context.completed, OkStatus());
context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
OkStatus());
- EXPECT_FALSE(call.active());
+ EXPECT_FALSE(call_context.call.active());
}
TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call = StartStreamCall<BidirectionalStreamMethod>(context);
context.server().SendServerStream<BidirectionalStreamMethod>(
as_bytes(span("<=>")));
@@ -163,7 +154,7 @@ TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
TEST(Client, ProcessPacket_UnassignedChannelId_ReturnsDataLoss) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call_cts = StartStreamCall<BidirectionalStreamMethod>(context);
std::byte encoded[64];
Result<span<const std::byte>> result =
@@ -180,7 +171,7 @@ TEST(Client, ProcessPacket_UnassignedChannelId_ReturnsDataLoss) {
TEST(Client, ProcessPacket_InvokesErrorCallback) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call = StartStreamCall<BidirectionalStreamMethod>(context);
context.server().SendServerError<BidirectionalStreamMethod>(
Status::Aborted());
@@ -247,9 +238,9 @@ TEST(Client, CloseChannel_UnknownChannel) {
TEST(Client, CloseChannel_CallsErrorCallback) {
RawClientTestContext ctx;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(ctx);
+ CallContext call_ctx = StartUnaryCall<UnaryMethod>(ctx);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_ctx.completed, OkStatus());
ASSERT_EQ(1u,
static_cast<internal::Endpoint&>(ctx.client()).active_call_count());
@@ -257,25 +248,25 @@ TEST(Client, CloseChannel_CallsErrorCallback) {
EXPECT_EQ(0u,
static_cast<internal::Endpoint&>(ctx.client()).active_call_count());
- ASSERT_EQ(call.error, Status::Aborted()); // set by the on_error callback
+ ASSERT_EQ(call_ctx.error, Status::Aborted()); // set by the on_error callback
}
TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForCallOnClosedChannel) {
struct {
RawClientTestContext<> ctx;
- TestUnaryCall call;
+ CallContext<RawUnaryReceiver> call_ctx;
} context;
- context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx);
- context.call.set_on_error([&context](Status error) {
- context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx, 1);
- context.call.error = error;
+ context.call_ctx = StartUnaryCall<UnaryMethod>(context.ctx);
+ context.call_ctx.call.set_on_error([&context](Status error) {
+ context.call_ctx = StartUnaryCall<UnaryMethod>(context.ctx, 1);
+ context.call_ctx.error = error;
});
EXPECT_EQ(OkStatus(), context.ctx.client().CloseChannel(1));
- EXPECT_EQ(context.call.error, Status::Aborted());
+ EXPECT_EQ(context.call_ctx.error, Status::Aborted());
- EXPECT_FALSE(context.call.active());
+ EXPECT_FALSE(context.call_ctx.call.active());
EXPECT_EQ(0u,
static_cast<internal::Endpoint&>(context.ctx.client())
.active_call_count());
@@ -293,7 +284,12 @@ TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForActiveCall) {
Channel& channel() { return channels_[0]; }
Client& client() { return client_; }
- TestUnaryCall& call() { return call_; }
+ CallContext<RawUnaryReceiver>& call_ctx() { return call_context_; }
+ RawUnaryReceiver& call() { return call_context_.call; }
+
+ void StartCall(uint32_t channel_id) {
+ call_context_ = StartUnaryCall<UnaryMethod>(*this, channel_id);
+ }
private:
RawFakeChannelOutput<10, 256> channel_output_;
@@ -302,17 +298,17 @@ TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForActiveCall) {
std::byte packet_buffer[64];
FakeServer fake_server_;
- TestUnaryCall call_;
+ CallContext<RawUnaryReceiver> call_context_;
} context;
- context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 1);
+ context.StartCall(1);
context.call().set_on_error([&context](Status error) {
- context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 2);
- context.call().error = error;
+ context.StartCall(2);
+ context.call_ctx().error = error;
});
EXPECT_EQ(OkStatus(), context.client().CloseChannel(1));
- EXPECT_EQ(context.call().error, Status::Aborted());
+ EXPECT_EQ(context.call_ctx().error, Status::Aborted());
EXPECT_TRUE(context.call().active());
EXPECT_EQ(
diff --git a/pw_rpc/raw/codegen_test.cc b/pw_rpc/raw/codegen_test.cc
index 765dfdcda..072f87139 100644
--- a/pw_rpc/raw/codegen_test.cc
+++ b/pw_rpc/raw/codegen_test.cc
@@ -579,7 +579,7 @@ TEST_F(RawCodegenClientTest, ClientStream_Move) {
UnaryOnCompleted(),
OnError());
- EXPECT_EQ(OkStatus(), call.CloseClientStream());
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
RawClientWriter call_2;
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
index 2d780279b..e4ae19f3f 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
@@ -48,16 +48,18 @@ class RawClientReaderWriter : private internal::StreamResponseClientCall {
// Sends a stream request packet with the given raw payload.
using internal::Call::Write;
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Allow use as a generic RPC Writer.
@@ -96,6 +98,7 @@ class RawClientReader : private internal::StreamResponseClientCall {
using internal::StreamResponseClientCall::set_on_next;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -121,6 +124,10 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
RawClientWriter(RawClientWriter&&) = default;
RawClientWriter& operator=(RawClientWriter&&) = default;
+ ~RawClientWriter() PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ DestroyClientCall();
+ }
+
using internal::UnaryResponseClientCall::active;
using internal::UnaryResponseClientCall::channel_id;
@@ -128,7 +135,7 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
using internal::UnaryResponseClientCall::set_on_error;
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::Call::Write;
using internal::ClientCall::Abandon;
@@ -159,6 +166,10 @@ class RawUnaryReceiver : private internal::UnaryResponseClientCall {
RawUnaryReceiver(RawUnaryReceiver&&) = default;
RawUnaryReceiver& operator=(RawUnaryReceiver&&) = default;
+ ~RawUnaryReceiver() PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ DestroyClientCall();
+ }
+
using internal::UnaryResponseClientCall::active;
using internal::UnaryResponseClientCall::channel_id;
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
index dbda24d58..9402e5275 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
@@ -15,6 +15,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include <type_traits>
#include "pw_bytes/span.h"
@@ -26,7 +27,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server.
class FakeServer {
diff --git a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
index 4de87fc03..91a85e552 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
@@ -75,7 +75,8 @@ class RawServerReaderWriter : private internal::ServerCall {
// Functions for setting the callbacks.
using internal::Call::set_on_error;
using internal::Call::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Sends a response packet with the given raw payload.
using internal::Call::Write;
@@ -140,7 +141,8 @@ class RawServerReader : private RawServerReaderWriter {
using RawServerReaderWriter::active;
using RawServerReaderWriter::channel_id;
- using RawServerReaderWriter::set_on_client_stream_end;
+ using RawServerReaderWriter::set_on_completion_requested;
+ using RawServerReaderWriter::set_on_completion_requested_if_enabled;
using RawServerReaderWriter::set_on_error;
using RawServerReaderWriter::set_on_next;
@@ -188,6 +190,8 @@ class RawServerWriter : private RawServerReaderWriter {
using RawServerReaderWriter::active;
using RawServerReaderWriter::channel_id;
+ using RawServerReaderWriter::set_on_completion_requested;
+ using RawServerReaderWriter::set_on_completion_requested_if_enabled;
using RawServerReaderWriter::set_on_error;
using RawServerReaderWriter::Finish;
diff --git a/pw_rpc/raw/synchronous_call_test.cc b/pw_rpc/raw/synchronous_call_test.cc
new file mode 100644
index 000000000..8e00631a2
--- /dev/null
+++ b/pw_rpc/raw/synchronous_call_test.cc
@@ -0,0 +1,241 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc/synchronous_call.h"
+
+#include <chrono>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/raw/fake_channel_output.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_thread/thread.h"
+#include "pw_work_queue/test_thread.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::rpc::test {
+namespace {
+
+using ::pw::rpc::test::pw_rpc::raw::TestService;
+using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
+
+class RawSynchronousCallTest : public ::testing::Test {
+ public:
+ RawSynchronousCallTest()
+ : channels_({{Channel::Create<42>(&fake_output_)}}), client_(channels_) {}
+
+ void SetUp() override {
+ work_thread_ =
+ thread::Thread(work_queue::test::WorkQueueThreadOptions(), work_queue_);
+ }
+
+ void TearDown() override {
+ work_queue_.RequestStop();
+#if PW_THREAD_JOINING_ENABLED
+ work_thread_.join();
+#else
+ work_thread_.detach();
+#endif // PW_THREAD_JOINING_ENABLED
+ }
+
+ protected:
+ void OnSend(span<const std::byte> buffer, Status status) {
+ if (!status.ok()) {
+ return;
+ }
+ auto result = internal::Packet::FromBuffer(buffer);
+ EXPECT_TRUE(result.ok());
+ request_packet_ = *result;
+
+ EXPECT_TRUE(work_queue_.PushWork([this]() { SendResponse(); }).ok());
+ }
+
+ void SendResponse() {
+ std::array<std::byte, 256> buffer;
+ std::array<char, 32> payload_buffer;
+
+ PW_CHECK_UINT_LE(response_.size(), payload_buffer.size());
+ size_t size = response_.copy(payload_buffer.data(), payload_buffer.size());
+
+ auto response =
+ internal::Packet::Response(request_packet_, response_status_);
+ response.set_payload(as_bytes(span(payload_buffer.data(), size)));
+ EXPECT_TRUE(client_.ProcessPacket(response.Encode(buffer).value()).ok());
+ }
+
+ void set_response(std::string_view response,
+ Status response_status = OkStatus()) {
+ response_ = response;
+ response_status_ = response_status;
+ output().set_on_send([this](span<const std::byte> buffer, Status status) {
+ OnSend(buffer, status);
+ });
+ }
+
+ MethodInfo::GeneratedClient generated_client() {
+ return MethodInfo::GeneratedClient(client(), channel().id());
+ }
+
+ RawFakeChannelOutput<2>& output() { return fake_output_; }
+ const Channel& channel() const { return channels_.front(); }
+ Client& client() { return client_; }
+
+ private:
+ RawFakeChannelOutput<2> fake_output_;
+ std::array<Channel, 1> channels_;
+ Client client_;
+ thread::Thread work_thread_;
+ work_queue::WorkQueueWithBuffer<1> work_queue_;
+ std::string_view response_;
+ Status response_status_ = OkStatus();
+ internal::Packet request_packet_;
+};
+
+template <Status::Code kExpectedStatus = OkStatus().code()>
+auto CopyReply(InlineString<32>& reply) {
+ return [&reply](ConstByteSpan response, Status status) {
+ EXPECT_EQ(Status(kExpectedStatus), status);
+ reply.assign(reinterpret_cast<const char*>(response.data()),
+ response.size());
+ };
+}
+
+void ExpectNoReply(ConstByteSpan, Status) { FAIL(); }
+
+TEST_F(RawSynchronousCallTest, SynchronousCallSuccess) {
+ set_response("jicama", OkStatus());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), {}, CopyReply(reply)));
+ EXPECT_EQ("jicama", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallServerError) {
+ set_response("raddish", Status::Internal());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ CopyReply<Status::Internal().code()>(reply)));
+ // We should still receive the response
+ EXPECT_EQ("raddish", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallRpcError) {
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ EXPECT_EQ(Status::Unknown(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), {}, ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallFor) {
+ set_response("broccoli", Status::NotFound());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::seconds(1)),
+ [&reply](ConstByteSpan response, Status status) {
+ EXPECT_EQ(Status::NotFound(), status);
+ reply.assign(reinterpret_cast<const char*>(response.data()),
+ response.size());
+ }));
+ EXPECT_EQ("broccoli", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallForTimeoutError) {
+ ASSERT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallUntilTimeoutError) {
+ EXPECT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallUntil<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::now(),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
+ set_response("lettuce", OkStatus());
+
+ InlineString<32> reply;
+ EXPECT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, CopyReply(reply)));
+ EXPECT_EQ("lettuce", reply);
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallServerError) {
+ set_response("cabbage", Status::Internal());
+
+ InlineString<32> reply;
+ EXPECT_EQ(
+ OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, CopyReply<Status::Internal().code()>(reply)));
+ EXPECT_EQ("cabbage", reply);
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallRpcError) {
+ output().set_send_status(Status::Unknown());
+
+ EXPECT_EQ(Status::Unknown(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallForTimeoutError) {
+ EXPECT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ generated_client(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest,
+ GeneratedClientSynchronousCallUntilTimeoutError) {
+ EXPECT_EQ(
+ Status::DeadlineExceeded(),
+ SynchronousCallUntil<TestService::TestUnaryRpc>(
+ generated_client(), {}, chrono::SystemClock::now(), ExpectNoReply));
+}
+
+} // namespace
+} // namespace pw::rpc::test
diff --git a/pw_rpc/request_packets.svg b/pw_rpc/request_packets.svg
deleted file mode 100644
index 7a54d8d09..000000000
--- a/pw_rpc/request_packets.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="280" viewBox="0 0 640 280" width="640" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>pw_rpc Requests</title>
-<desc></desc>
-<rect fill="rgb(243,152,0)" height="60" style="filter:url(#filter_blur)" width="528" x="56" y="110"></rect>
-<ellipse cx="131" cy="66" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="126"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="126"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="126"></rect>
-<ellipse cx="131" cy="226" fill="rgb(0,0,0)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<path d="M 267 206 L 379 206 A8,8 0 0 1 387 214 L 387 238 A8,8 0 0 1 379 246 L 267 246 A8,8 0 0 1 259 238 L 259 214 A8,8 0 0 1 267 206" fill="rgb(0,0,0)" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></path>
-<ellipse cx="128" cy="60" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="157.0" y="56">packets</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="128.0" y="146">Server</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="320.0" y="146">Service</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="96" x="512.0" y="146">internal::Method</text>
-<ellipse cx="128" cy="220" fill="rgb(255,255,255)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="128.0" y="226">generated services</text>
-<path d="M 264 200 L 376 200 A8,8 0 0 1 384 208 L 384 232 A8,8 0 0 1 376 240 L 264 240 A8,8 0 0 1 256 232 L 256 208 A8,8 0 0 1 264 200" fill="rgb(255,255,255)" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="102" x="320.0" y="226">user-defined RPCs</text>
-<path d="M 128 68 L 128 112" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,119 124,112 132,112 128,119" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 220 L 248 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,220 248,216 248,224 255,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 140 L 248 140" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,140 248,136 248,144 255,140" stroke="rgb(0,0,0)"></polygon>
-<path d="M 384 140 L 440 140" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="447,140 440,136 440,144 447,140" stroke="rgb(0,0,0)"></polygon>
-<path d="M 512 160 L 512 180" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 180 L 512 180" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 180 L 128 192" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,199 124,192 132,192 128,199" stroke="rgb(0,0,0)"></polygon>
-<rect class="highlighted" width="68.9688" x="285.766" y="106" height="12"></rect><text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="84" x="320.0" y="116"><tspan>pw_rpc</tspan> library</text>
-</svg>
diff --git a/pw_rpc/response_packets.svg b/pw_rpc/response_packets.svg
deleted file mode 100644
index 065132e94..000000000
--- a/pw_rpc/response_packets.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="360" viewBox="0 0 640 360" width="640" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>pw_rpc Responses</title>
-<desc></desc>
-<rect fill="rgb(243,152,0)" height="60" style="filter:url(#filter_blur)" width="528" x="56" y="190"></rect>
-<path d="M 75 46 L 187 46 A8,8 0 0 1 195 54 L 195 78 A8,8 0 0 1 187 86 L 75 86 A8,8 0 0 1 67 78 L 67 54 A8,8 0 0 1 75 46" fill="rgb(0,0,0)" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></path>
-<ellipse cx="131" cy="146" fill="rgb(0,0,0)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="206"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="206"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="206"></rect>
-<ellipse cx="131" cy="306" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<path d="M 72 40 L 184 40 A8,8 0 0 1 192 48 L 192 72 A8,8 0 0 1 184 80 L 72 80 A8,8 0 0 1 64 72 L 64 48 A8,8 0 0 1 72 40" fill="rgb(255,255,255)" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="102" x="128.0" y="66">user-defined RPCs</text>
-<ellipse cx="128" cy="140" fill="rgb(255,255,255)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="128.0" y="146">generated services</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="320.0" y="226">Server</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="96" x="128.0" y="226">internal::Method</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="512.0" y="226">Channel</text>
-<ellipse cx="128" cy="300" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="157.0" y="296">packets</text>
-<path d="M 128 80 L 128 112" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,119 124,112 132,112 128,119" stroke="rgb(0,0,0)"></polygon>
-<path d="M 128 160 L 128 192" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,199 124,192 132,192 128,199" stroke="rgb(0,0,0)"></polygon>
-<path d="M 384 220 L 440 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="447,220 440,216 440,224 447,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 220 L 248 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,220 248,216 248,224 255,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 512 240 L 512 260" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 260 L 512 260" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 260 L 128 284" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,291 124,284 132,284 128,291" stroke="rgb(0,0,0)"></polygon>
-<rect class="highlighted" width="68.9688" x="285.766" y="186" height="12"></rect><text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="84" x="320.0" y="196"><tspan>pw_rpc</tspan> library</text>
-</svg>
diff --git a/pw_rpc/server.cc b/pw_rpc/server.cc
index 4af9ff4e6..8822a0fd0 100644
--- a/pw_rpc/server.cc
+++ b/pw_rpc/server.cc
@@ -92,8 +92,8 @@ Status Server::ProcessPacket(ConstByteSpan packet_data) {
internal::rpc_lock().unlock();
}
break;
- case PacketType::CLIENT_STREAM_END:
- HandleClientStreamPacket(packet, *channel, call);
+ case PacketType::CLIENT_REQUEST_COMPLETION:
+ HandleCompletionRequest(packet, *channel, call);
break;
case PacketType::REQUEST: // Handled above
case PacketType::RESPONSE:
@@ -122,6 +122,35 @@ std::tuple<Service*, const internal::Method*> Server::FindMethod(
return {&(*service), service->FindMethod(packet.method_id())};
}
+void Server::HandleCompletionRequest(
+ const internal::Packet& packet,
+ internal::Channel& channel,
+ IntrusiveList<internal::Call>::iterator call) const {
+ if (call == calls_end()) {
+ channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
+ .IgnoreError(); // Errors are logged in Channel::Send.
+ internal::rpc_lock().unlock();
+ PW_LOG_DEBUG(
+ "Received a request completion packet for %u:%08x/%08x, which is not a"
+ "pending call",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
+ return;
+ }
+
+ if (call->client_requested_completion()) {
+ internal::rpc_lock().unlock();
+ PW_LOG_DEBUG("Received multiple completion requests for %u:%08x/%08x",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
+ return;
+ }
+
+ static_cast<internal::ServerCall&>(*call).HandleClientRequestedCompletion();
+}
+
void Server::HandleClientStreamPacket(
const internal::Packet& packet,
internal::Channel& channel,
@@ -151,7 +180,7 @@ void Server::HandleClientStreamPacket(
return;
}
- if (!call->client_stream_open()) {
+ if (call->client_requested_completion()) {
channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
.IgnoreError(); // Errors are logged in Channel::Send.
internal::rpc_lock().unlock();
@@ -164,11 +193,7 @@ void Server::HandleClientStreamPacket(
return;
}
- if (packet.type() == PacketType::CLIENT_STREAM) {
- call->HandlePayload(packet.payload());
- } else { // Handle PacketType::CLIENT_STREAM_END.
- static_cast<internal::ServerCall&>(*call).HandleClientStreamEnd();
- }
+ call->HandlePayload(packet.payload());
}
} // namespace pw::rpc
diff --git a/pw_rpc/server_call.cc b/pw_rpc/server_call.cc
index c665595ba..ced7958a1 100644
--- a/pw_rpc/server_call.cc
+++ b/pw_rpc/server_call.cc
@@ -26,9 +26,10 @@ void ServerCall::MoveServerCallFrom(ServerCall& other) {
MoveFrom(other);
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- on_client_stream_end_ = std::move(other.on_client_stream_end_);
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ on_client_requested_completion_ =
+ std::move(other.on_client_requested_completion_);
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/server_streaming_rpc.svg b/pw_rpc/server_streaming_rpc.svg
deleted file mode 100644
index e153a0a82..000000000
--- a/pw_rpc/server_streaming_rpc.svg
+++ /dev/null
@@ -1,58 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="511.50000000000006" viewBox="0 0 564 465" width="620.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Server Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="171" y="159"></rect>
-<rect fill="rgb(0,0,0)" height="214" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="363" y="159"></rect>
-<polygon fill="rgb(0,0,0)" points="27,126 151,126 159,134 159,193 27,193 27,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,233 543,233 551,241 551,300 383,300 383,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,340 513,340 521,348 521,407 383,407 383,340" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="111" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="303" y="46"></rect>
-<path d="M 172 80 L 172 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="168" y="153"></rect>
-<path d="M 364 80 L 364 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="214" stroke="rgb(0,0,0)" width="8" x="360" y="153"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="108" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="172.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="300" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="364.0" y="66">server</text>
-<path d="M 180 153 L 356 153" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="348,149 356,153 348,157" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,120 148,120 156,128 156,187 24,187 24,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 148 120 L 148 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 148 128 L 156 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="86.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="172">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="53.0" y="185">payload</text>
-<path d="M 180 260 L 356 260" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="188,256 180,260 188,264" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,227 540,227 548,235 548,294 380,294 380,227" stroke="rgb(0,0,0)"></polygon>
-<path d="M 540 227 L 540 235" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 540 235 L 548 235" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="460.0" y="240">PacketType.SERVER_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="253">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="266">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="279">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="409.0" y="292">payload</text>
-<path d="M 180 367 L 356 367" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="188,363 180,367 188,371" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,334 510,334 518,342 518,401 380,401 380,334" stroke="rgb(0,0,0)"></polygon>
-<path d="M 510 334 L 510 342" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 510 342 L 518 342" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="445.0" y="347">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="360">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="373">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="386">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="406.0" y="399">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="205.0" y="151">request</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="287.0" y="258">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="344.0" y="365">done</text>
-</svg>
diff --git a/pw_rpc/server_streaming_rpc_cancelled.svg b/pw_rpc/server_streaming_rpc_cancelled.svg
deleted file mode 100644
index d7432da7f..000000000
--- a/pw_rpc/server_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,77 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg viewBox="0 0 598 465" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" width="598px" height="465px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
- </filter>
- </defs>
- <title>Cancelled Server Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "request",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client &lt;-- server [
- noactivate,
- label = "messages (zero or more)",
- rightnote = "PacketType.SERVER_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="203" y="159" />
- <rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="395" y="159" />
- <polygon fill="rgb(0,0,0)" points="58,126 183,126 191,134 191,193 58,193 58,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="415,233 577,233 585,241 585,300 415,300 415,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="27,340 183,340 191,348 191,407 27,407 27,340" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="143" y="46" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="335" y="46" />
- <path d="M 204 80 L 204 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="200" y="153" />
- <path d="M 396 80 L 396 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="392" y="153" />
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="140" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="204.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="332" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="396.5" y="66">server</text>
- <path d="M 212 153 L 388 153" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,149 388,153 380,157" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="55,120 180,120 188,128 188,187 55,187 55,120" stroke="rgb(0,0,0)" />
- <path d="M 180 120 L 180 128" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 128 L 188 128" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="117.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="90.5" y="172">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="84.5" y="185">payload</text>
- <path d="M 212 260 L 388 260" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4" />
- <polygon fill="rgb(0,0,0)" points="220,256 212,260 220,264" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="412,227 574,227 582,235 582,294 412,294 412,227" stroke="rgb(0,0,0)" />
- <path d="M 574 227 L 574 235" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 574 235 L 582 235" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="493.0" y="240">PacketType.SERVER_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="450.5" y="253">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="450.5" y="266">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="447.5" y="279">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="441.5" y="292">payload</text>
- <path d="M 212 367 L 388 367" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,363 388,367 380,371" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="24,334 180,334 188,342 188,401 24,401 24,334" stroke="rgb(0,0,0)" />
- <path d="M 180 334 L 180 342" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 342 L 188 342" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="102.0" y="347">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="360">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="373">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="386">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="80.5" y="399">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="237.5" y="151">request</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="318.0" y="258">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="234.5" y="365">cancel</text>
-</svg>
diff --git a/pw_rpc/server_test.cc b/pw_rpc/server_test.cc
index 20e91e954..bbdb62421 100644
--- a/pw_rpc/server_test.cc
+++ b/pw_rpc/server_test.cc
@@ -22,10 +22,10 @@
#include "pw_rpc/internal/call.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/packet.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/internal/test_utils.h"
#include "pw_rpc/service.h"
#include "pw_rpc_private/fake_server_reader_writer.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc {
namespace {
@@ -344,7 +344,7 @@ TEST_F(BasicServer, CloseChannel_PendingCall) {
EXPECT_NE(nullptr, GetChannel(server_, 1));
EXPECT_EQ(static_cast<internal::Endpoint&>(server_).active_call_count(), 0u);
- internal::TestMethod::FakeServerCall call;
+ internal::test::FakeServerReaderWriter call;
service_42_.method(100).keep_call_active(call);
EXPECT_EQ(
@@ -617,26 +617,45 @@ TEST_F(BidiMethod, UnregsiterService_AbortsActiveCalls) {
EXPECT_EQ(Status::Aborted(), on_error_status);
}
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+TEST_F(BidiMethod, ClientRequestedCompletion_CallsCallback) {
+ bool called = false;
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ responder_.set_on_completion_requested([&called]() { called = true; });
+#endif
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
-TEST_F(BidiMethod, ClientStreamEnd_CallsCallback) {
+TEST_F(BidiMethod, ClientRequestedCompletion_CallsCallbackIfEnabled) {
bool called = false;
- responder_.set_on_client_stream_end([&called]() { called = true; });
+ responder_.set_on_completion_requested_if_enabled(
+ [&called]() { called = true; });
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
EXPECT_EQ(output_.total_packets(), 0u);
- EXPECT_TRUE(called);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
}
-TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
- const auto end = PacketForRpc(PacketType::CLIENT_STREAM_END);
+TEST_F(BidiMethod, ClientRequestedCompletion_ErrorWhenClosed) {
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
- bool called = false;
- responder_.set_on_client_stream_end([&called]() { called = true; });
+ ASSERT_EQ(output_.total_packets(), 0u);
+}
+TEST_F(BidiMethod, ClientRequestedCompletion_ErrorWhenAlreadyClosed) {
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
+ EXPECT_FALSE(responder_.active());
+
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(output_.total_packets(), 1u);
@@ -646,8 +665,6 @@ TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
EXPECT_EQ(packet.status(), Status::FailedPrecondition());
}
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
-
class ServerStreamingMethod : public BasicServer {
protected:
ServerStreamingMethod() {
@@ -677,15 +694,55 @@ TEST_F(ServerStreamingMethod, ClientStream_InvalidArgumentError) {
EXPECT_EQ(packet.status(), Status::InvalidArgument());
}
-TEST_F(ServerStreamingMethod, ClientStreamEnd_InvalidArgumentError) {
+TEST_F(ServerStreamingMethod, ClientRequestedCompletion_CallsCallback) {
+ bool called = false;
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ responder_.set_on_completion_requested([&called]() { called = true; });
+#endif
+
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
+
+TEST_F(ServerStreamingMethod,
+ ClientRequestedCompletion_CallsCallbackIfEnabled) {
+ bool called = false;
+ responder_.set_on_completion_requested_if_enabled(
+ [&called]() { called = true; });
+
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
+
+TEST_F(ServerStreamingMethod, ClientRequestedCompletion_ErrorWhenClosed) {
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
+
+ ASSERT_EQ(output_.total_packets(), 0u);
+}
+
+TEST_F(ServerStreamingMethod,
+ ClientRequestedCompletion_ErrorWhenAlreadyClosed) {
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
+ EXPECT_FALSE(responder_.active());
+
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(output_.total_packets(), 1u);
const Packet& packet =
static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
EXPECT_EQ(packet.type(), PacketType::SERVER_ERROR);
- EXPECT_EQ(packet.status(), Status::InvalidArgument());
+ EXPECT_EQ(packet.status(), Status::FailedPrecondition());
}
} // namespace
diff --git a/pw_rpc/size_report/base.cc b/pw_rpc/size_report/base.cc
index 5248c8034..fa85564d8 100644
--- a/pw_rpc/size_report/base.cc
+++ b/pw_rpc/size_report/base.cc
@@ -28,9 +28,9 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/size_report/server_only.cc b/pw_rpc/size_report/server_only.cc
index a8644f778..7f7b1cbcd 100644
--- a/pw_rpc/size_report/server_only.cc
+++ b/pw_rpc/size_report/server_only.cc
@@ -46,12 +46,12 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
my_product::server.ProcessPacket(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/system_server/BUILD.bazel b/pw_rpc/system_server/BUILD.bazel
index 407c6da73..eca9534d0 100644
--- a/pw_rpc/system_server/BUILD.bazel
+++ b/pw_rpc/system_server/BUILD.bazel
@@ -44,13 +44,13 @@ pw_cc_library(
"//pw_span",
"//pw_status",
"//pw_stream",
- "@pigweed_config//:pw_rpc_system_server_backend",
+ "@pigweed//targets:pw_rpc_system_server_backend",
],
)
pw_cc_library(
name = "system_server_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/board:stm32f429i-disc1": ["//pw_hdlc:hdlc_sys_io_system_server"],
"//pw_build/constraints/board:mimxrt595_evk": ["//pw_hdlc:hdlc_sys_io_system_server"],
diff --git a/pw_rpc/ts/call.ts b/pw_rpc/ts/call.ts
index 11894dc8c..e418760c3 100644
--- a/pw_rpc/ts/call.ts
+++ b/pw_rpc/ts/call.ts
@@ -12,12 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
-import WaitQueue from "./queue";
+import WaitQueue from './queue';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { PendingCalls, Rpc } from './rpc_classes';
export type Callback = (a: any) => any;
@@ -70,7 +70,7 @@ export class Call {
rpc: Rpc,
onNext: Callback,
onCompleted: Callback,
- onError: Callback
+ onError: Callback,
) {
this.rpcs = rpcs;
this.rpc = rpc;
@@ -86,7 +86,7 @@ export class Call {
this.rpc,
this,
ignoreErrors,
- request
+ request,
);
if (previous !== undefined && !previous.completed) {
@@ -98,13 +98,14 @@ export class Call {
return this.status !== undefined || this.error !== undefined;
}
+ // eslint-disable-next-line @typescript-eslint/ban-types
private invokeCallback(func: () => {}) {
try {
func();
} catch (err: unknown) {
if (err instanceof Error) {
console.error(
- `An exception was raised while invoking a callback: ${err}`
+ `An exception was raised while invoking a callback: ${err}`,
);
this.callbackException = err;
}
@@ -131,8 +132,9 @@ export class Call {
}
private async queuePopWithTimeout(
- timeoutMs: number
+ timeoutMs: number,
): Promise<Message | undefined> {
+ // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
let timeoutExpired = false;
const timeoutWatcher = setTimeout(() => {
@@ -170,7 +172,7 @@ export class Call {
*/
async *getResponses(
count?: number,
- timeoutMs?: number
+ timeoutMs?: number,
): AsyncGenerator<Message> {
this.checkErrors();
@@ -213,6 +215,7 @@ export class Call {
protected async unaryWait(timeoutMs?: number): Promise<[Status, Message]> {
for await (const response of this.getResponses(1, timeoutMs)) {
+ // Do nothing.
}
if (this.status === undefined) {
throw Error('Unexpected undefined status at end of stream');
@@ -225,6 +228,7 @@ export class Call {
protected async streamWait(timeoutMs?: number): Promise<[Status, Message[]]> {
for await (const response of this.getResponses(undefined, timeoutMs)) {
+ // Do nothing.
}
if (this.status === undefined) {
throw Error('Unexpected undefined status at end of stream');
@@ -276,7 +280,7 @@ export class ClientStreamingCall extends Call {
/** Ends the client stream and waits for the RPC to complete. */
async finishAndWait(
requests: Message[] = [],
- timeoutMs?: number
+ timeoutMs?: number,
): Promise<[Status, Message[]]> {
this.finishClientStream(requests);
return await this.streamWait(timeoutMs);
@@ -300,7 +304,7 @@ export class BidirectionalStreamingCall extends Call {
/** Ends the client stream and waits for the RPC to complete. */
async finishAndWait(
requests: Array<Message> = [],
- timeoutMs?: number
+ timeoutMs?: number,
): Promise<[Status, Array<Message>]> {
this.finishClientStream(requests);
return await this.streamWait(timeoutMs);
diff --git a/pw_rpc/ts/call_test.ts b/pw_rpc/ts/call_test.ts
index 47af6dab8..98590c88a 100644
--- a/pw_rpc/ts/call_test.ts
+++ b/pw_rpc/ts/call_test.ts
@@ -14,11 +14,11 @@
/* eslint-env browser */
-import {SomeMessage} from 'pigweedjs/protos/pw_rpc/ts/test2_pb';
+import { SomeMessage } from 'pigweedjs/protos/pw_rpc/ts/test2_pb';
-import {Call} from './call';
-import {Channel, Method, Service} from './descriptors';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { Call } from './call';
+import { Channel, Method, Service } from './descriptors';
+import { PendingCalls, Rpc } from './rpc_classes';
class FakeRpc {
readonly channel: any = undefined;
@@ -33,7 +33,9 @@ describe('Call', () => {
let call: Call;
beforeEach(() => {
- const noop = () => { };
+ const noop = () => {
+ // Do nothing.
+ };
const pendingCalls = new PendingCalls();
const rpc = new FakeRpc();
call = new Call(pendingCalls, rpc, noop, noop, noop);
diff --git a/pw_rpc/ts/client.ts b/pw_rpc/ts/client.ts
index e7a97176d..e99554877 100644
--- a/pw_rpc/ts/client.ts
+++ b/pw_rpc/ts/client.ts
@@ -14,18 +14,18 @@
/** Provides a pw_rpc client for TypeScript. */
-import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Channel, Service} from './descriptors';
-import {MethodStub, methodStubFactory} from './method';
+import { Channel, Service } from './descriptors';
+import { MethodStub, methodStubFactory } from './method';
import * as packets from './packets';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { PendingCalls, Rpc } from './rpc_classes';
/**
* Object for managing RPC service and contained methods.
@@ -38,7 +38,7 @@ export class ServiceClient {
constructor(client: Client, channel: Channel, service: Service) {
this.service = service;
const methods = service.methods;
- methods.forEach(method => {
+ methods.forEach((method) => {
const stub = methodStubFactory(client.rpcs, channel, method);
this.methods.push(stub);
this.methodsByName.set(method.name, stub);
@@ -67,7 +67,7 @@ export class ChannelClient {
constructor(client: Client, channel: Channel, services: Service[]) {
this.channel = channel;
- services.forEach(service => {
+ services.forEach((service) => {
const serviceClient = new ServiceClient(client, this.channel, service);
this.services.set(service.name, serviceClient);
});
@@ -122,14 +122,14 @@ export class Client {
constructor(channels: Channel[], services: Service[]) {
this.rpcs = new PendingCalls();
- services.forEach(service => {
+ services.forEach((service) => {
this.services.set(service.id, service);
});
- channels.forEach(channel => {
+ channels.forEach((channel) => {
this.channelsById.set(
channel.id,
- new ChannelClient(this, channel, services)
+ new ChannelClient(this, channel, services),
);
});
}
@@ -145,11 +145,11 @@ export class Client {
static fromProtoSet(channels: Channel[], protoSet: ProtoCollection): Client {
let services: Service[] = [];
const descriptors = protoSet.fileDescriptorSet.getFileList();
- descriptors.forEach(fileDescriptor => {
+ descriptors.forEach((fileDescriptor) => {
const packageName = fileDescriptor.getPackage()!;
- fileDescriptor.getServiceList().forEach(serviceDescriptor => {
+ fileDescriptor.getServiceList().forEach((serviceDescriptor) => {
services = services.concat(
- new Service(serviceDescriptor, protoSet, packageName)
+ new Service(serviceDescriptor, protoSet, packageName),
);
});
});
@@ -176,7 +176,7 @@ export class Client {
*/
private rpc(
packet: RpcPacket,
- channelClient: ChannelClient
+ channelClient: ChannelClient,
): Rpc | undefined {
const service = this.services.get(packet.getServiceId());
if (service == undefined) {
@@ -215,7 +215,7 @@ export class Client {
private sendClientError(
client: ChannelClient,
packet: RpcPacket,
- error: Status
+ error: Status,
) {
client.channel.send(packets.encodeClientError(packet, error));
}
@@ -290,10 +290,10 @@ export class Client {
if (packet.getType() === PacketType.SERVER_ERROR) {
if (status === Status.OK) {
- throw 'Unexpected OK status on SERVER_ERROR';
+ throw new Error('Unexpected OK status on SERVER_ERROR');
}
if (status === undefined) {
- throw 'Missing status on SERVER_ERROR';
+ throw new Error('Missing status on SERVER_ERROR');
}
console.warn(`${rpc}: invocation failed with status: ${Status[status]}`);
call.handleError(status);
diff --git a/pw_rpc/ts/client_test.ts b/pw_rpc/ts/client_test.ts
index 0dfed21b1..0535fddb9 100644
--- a/pw_rpc/ts/client_test.ts
+++ b/pw_rpc/ts/client_test.ts
@@ -14,21 +14,18 @@
/* eslint-env browser */
-import {Status} from 'pigweedjs/pw_status';
-import {MessageCreator} from 'pigweedjs/pw_protobuf_compiler';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { MessageCreator } from 'pigweedjs/pw_protobuf_compiler';
+import { Message } from 'google-protobuf';
import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {
- Request,
- Response,
-} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { Request, Response } from 'pigweedjs/protos/pw_rpc/ts/test_pb';
-import {Client} from './client';
-import {Channel, Method} from './descriptors';
+import { Client } from './client';
+import { Channel, Method } from './descriptors';
import {
BidirectionalStreamingMethodStub,
ClientStreamingMethodStub,
@@ -77,10 +74,10 @@ describe('Client', () => {
const channel = client.channel()!;
expect(channel.methodStub('')).toBeUndefined();
expect(
- channel.methodStub('pw.rpc.test1.Garbage.SomeUnary')
+ channel.methodStub('pw.rpc.test1.Garbage.SomeUnary'),
).toBeUndefined();
expect(
- channel.methodStub('pw.rpc.test1.TheTestService.Garbage')
+ channel.methodStub('pw.rpc.test1.TheTestService.Garbage'),
).toBeUndefined();
});
@@ -134,7 +131,7 @@ describe('Client', () => {
const packet = packets.encodeResponse(
[1, service.id, method.id],
- new Request()
+ new Request(),
);
const status = client.processPacket(packet);
expect(client.processPacket(packet)).toEqual(Status.OK);
@@ -159,7 +156,12 @@ describe('RPC', () => {
beforeEach(async () => {
protoCollection = new ProtoCollection();
- const channels = [new Channel(1, handlePacket), new Channel(2, () => { })];
+ const channels = [
+ new Channel(1, handlePacket),
+ new Channel(2, () => {
+ // Do nothing.
+ }),
+ ];
client = Client.fromProtoSet(channels, protoCollection);
lastPacketSent = undefined;
requests = [];
@@ -185,7 +187,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
status: Status,
- response?: Message
+ response?: Message,
) {
const packet = new RpcPacket();
packet.setType(PacketType.RESPONSE);
@@ -194,7 +196,7 @@ describe('RPC', () => {
packet.setMethodId(method.id);
packet.setStatus(status);
if (response === undefined) {
- packet.setPayload(new Uint8Array());
+ packet.setPayload(new Uint8Array(0));
} else {
packet.setPayload(response.serializeBinary());
}
@@ -205,7 +207,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
response: Message,
- status: Status = Status.OK
+ status: Status = Status.OK,
) {
const packet = new RpcPacket();
packet.setType(PacketType.SERVER_STREAM);
@@ -221,7 +223,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
status: Status,
- processStatus: Status
+ processStatus: Status,
) {
const packet = new RpcPacket();
packet.setType(PacketType.SERVER_ERROR);
@@ -276,8 +278,8 @@ describe('RPC', () => {
unaryStub = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeUnary'
- )! as UnaryMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeUnary',
+ ) as UnaryMethodStub;
});
it('blocking call', async () => {
@@ -286,7 +288,7 @@ describe('RPC', () => {
1,
unaryStub.method,
Status.ABORTED,
- newResponse('0_o')
+ newResponse('0_o'),
);
const [status, response] = await unaryStub.call(newRequest(6));
@@ -308,7 +310,7 @@ describe('RPC', () => {
newRequest(5),
onNext,
onCompleted,
- onError
+ onError,
);
expect(sentPayload(Request).getMagicNumber()).toEqual(5);
@@ -406,8 +408,8 @@ describe('RPC', () => {
serverStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeServerStreaming'
- )! as ServerStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeServerStreaming',
+ ) as ServerStreamingMethodStub;
});
it('non-blocking call', () => {
@@ -430,7 +432,7 @@ describe('RPC', () => {
expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
expect(
- sentPayload(serverStreaming.method.requestType).getMagicNumber()
+ sentPayload(serverStreaming.method.requestType).getMagicNumber(),
).toEqual(4);
}
});
@@ -452,7 +454,7 @@ describe('RPC', () => {
newRequest(3),
onNext,
onCompleted,
- onError
+ onError,
);
expect(requests).toHaveLength(0);
@@ -507,8 +509,8 @@ describe('RPC', () => {
clientStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeClientStreaming'
- )! as ClientStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeClientStreaming',
+ ) as ClientStreamingMethodStub;
});
it('non-blocking call', () => {
@@ -584,7 +586,9 @@ describe('RPC', () => {
enqueueResponse(1, clientStreaming.method, Status.OK, testResponse);
stream.finishAndWait();
- expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM_END);
+ expect(lastRequest().getType()).toEqual(
+ PacketType.CLIENT_REQUEST_COMPLETION,
+ );
expect(onNext).toHaveBeenCalledWith(testResponse);
expect(stream.completed).toBe(true);
@@ -614,7 +618,7 @@ describe('RPC', () => {
1,
clientStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
stream.send(newRequest());
@@ -624,7 +628,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -633,12 +637,12 @@ describe('RPC', () => {
it('non-blocking call server error after stream end', async () => {
for (let i = 0; i < 3; i++) {
const stream = clientStreaming.invoke();
- // Error will be sent in response to the CLIENT_STREAM_END packet.
+ // Error will be sent in response to the CLIENT_REQUEST_COMPLETION packet.
enqueueError(
1,
clientStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
await stream
@@ -646,7 +650,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -659,8 +663,7 @@ describe('RPC', () => {
try {
stream.send(newRequest());
- }
- catch (e) {
+ } catch (e) {
console.log(e);
expect(e.status).toEqual(Status.CANCELLED);
}
@@ -675,7 +678,7 @@ describe('RPC', () => {
1,
clientStreaming.method,
Status.UNAVAILABLE,
- enqueuedResponse
+ enqueuedResponse,
);
const stream = clientStreaming.invoke();
@@ -696,7 +699,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.UNAVAILABLE);
expect(stream.error).toEqual(Status.UNAVAILABLE);
expect(stream.response).toBeUndefined();
@@ -721,8 +724,8 @@ describe('RPC', () => {
bidiStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeBidiStreaming'
- )! as BidirectionalStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeBidiStreaming',
+ ) as BidirectionalStreamingMethodStub;
});
it('blocking call', async () => {
@@ -745,7 +748,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.NOT_FOUND);
});
});
@@ -756,7 +759,7 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const testResponses: Array<Message> = [];
- const stream = bidiStreaming.invoke(response => {
+ const stream = bidiStreaming.invoke((response) => {
testResponses.push(response);
});
expect(stream.completed).toBe(false);
@@ -825,7 +828,7 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const testResponses: Array<Message> = [];
- const stream = bidiStreaming.invoke(response => {
+ const stream = bidiStreaming.invoke((response) => {
testResponses.push(response);
});
expect(stream.completed).toBe(false);
@@ -849,7 +852,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.OUT_OF_RANGE);
});
}
@@ -858,12 +861,12 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const stream = bidiStreaming.invoke();
- // Error is sent in response to CLIENT_STREAM_END packet.
+ // Error is sent in response to CLIENT_REQUEST_COMPLETION packet.
enqueueError(
1,
bidiStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
await stream
@@ -871,7 +874,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -915,7 +918,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.UNAVAILABLE);
expect(stream.error).toEqual(Status.UNAVAILABLE);
});
diff --git a/pw_rpc/ts/descriptors.ts b/pw_rpc/ts/descriptors.ts
index eb4581f3e..bd0d470de 100644
--- a/pw_rpc/ts/descriptors.ts
+++ b/pw_rpc/ts/descriptors.ts
@@ -12,13 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
+import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler';
import {
MethodDescriptorProto,
ServiceDescriptorProto,
} from 'google-protobuf/google/protobuf/descriptor_pb';
-import {hash} from './hash';
+import { hash } from './hash';
interface ChannelOutput {
(data: Uint8Array): void;
@@ -28,7 +28,12 @@ export class Channel {
readonly id: number;
private output: ChannelOutput;
- constructor(id: number, output: ChannelOutput = () => {}) {
+ constructor(
+ id: number,
+ output: ChannelOutput = () => {
+ /* do nothing. */
+ },
+ ) {
this.id = id;
this.output = output;
}
@@ -48,7 +53,7 @@ export class Service {
constructor(
descriptor: ServiceDescriptorProto,
protoCollection: ProtoCollection,
- packageName: string
+ packageName: string,
) {
this.name = packageName + '.' + descriptor.getName()!;
this.id = hash(this.name);
@@ -83,7 +88,7 @@ export class Method {
constructor(
descriptor: MethodDescriptorProto,
protoCollection: ProtoCollection,
- service: Service
+ service: Service,
) {
this.name = descriptor.getName()!;
this.id = hash(this.name);
@@ -97,10 +102,10 @@ export class Method {
// Remove leading period if it exists.
this.requestType = protoCollection.getMessageCreator(
- requestTypePath.replace(/^\./, '')
+ requestTypePath.replace(/^\./, ''),
)!;
this.responseType = protoCollection.getMessageCreator(
- responseTypePath.replace(/^\./, '')
+ responseTypePath.replace(/^\./, ''),
)!;
}
diff --git a/pw_rpc/ts/descriptors_test.ts b/pw_rpc/ts/descriptors_test.ts
index a6b11e26a..c09d3d3fa 100644
--- a/pw_rpc/ts/descriptors_test.ts
+++ b/pw_rpc/ts/descriptors_test.ts
@@ -14,11 +14,8 @@
/* eslint-env browser */
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {
- Request,
- Response,
-} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { Request, Response } from 'pigweedjs/protos/pw_rpc/ts/test_pb';
import * as descriptors from './descriptors';
@@ -27,13 +24,14 @@ const TEST_PROTO_PATH = 'pw_rpc/ts/test_protos-descriptor-set.proto.bin';
describe('Descriptors', () => {
it('parses from ServiceDescriptor binary', async () => {
const protoCollection = new ProtoCollection();
- const fd = protoCollection.fileDescriptorSet.getFileList()
- .find((file: any) => file.array[1].indexOf("pw.rpc.test1") !== -1);
+ const fd = protoCollection.fileDescriptorSet
+ .getFileList()
+ .find((file: any) => file.array[1].indexOf('pw.rpc.test1') !== -1);
const sd = fd.getServiceList()[0];
const service = new descriptors.Service(
sd,
protoCollection,
- fd.getPackage()!
+ fd.getPackage()!,
);
expect(service.name).toEqual('pw.rpc.test1.TheTestService');
diff --git a/pw_rpc/ts/method.ts b/pw_rpc/ts/method.ts
index 18cf6db07..f67d9cfb0 100644
--- a/pw_rpc/ts/method.ts
+++ b/pw_rpc/ts/method.ts
@@ -12,8 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
import {
BidirectionalStreamingCall,
@@ -23,13 +23,13 @@ import {
ServerStreamingCall,
UnaryCall,
} from './call';
-import {Channel, Method, MethodType, Service} from './descriptors';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { Channel, Method, MethodType, Service } from './descriptors';
+import { PendingCalls, Rpc } from './rpc_classes';
export function methodStubFactory(
rpcs: PendingCalls,
channel: Channel,
- method: Method
+ method: Method,
): MethodStub {
switch (method.type) {
case MethodType.BIDIRECTIONAL_STREAMING:
@@ -64,16 +64,22 @@ export abstract class MethodStub {
export class UnaryMethodStub extends MethodStub {
invoke(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request);
return call;
@@ -81,16 +87,22 @@ export class UnaryMethodStub extends MethodStub {
open(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request, true);
return call;
@@ -104,16 +116,22 @@ export class UnaryMethodStub extends MethodStub {
export class ServerStreamingMethodStub extends MethodStub {
invoke(
request?: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ServerStreamingCall {
const call = new ServerStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request);
return call;
@@ -121,16 +139,22 @@ export class ServerStreamingMethodStub extends MethodStub {
open(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request, true);
return call;
@@ -143,32 +167,44 @@ export class ServerStreamingMethodStub extends MethodStub {
export class ClientStreamingMethodStub extends MethodStub {
invoke(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ClientStreamingCall {
const call = new ClientStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke();
return call;
}
open(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ClientStreamingCall {
const call = new ClientStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(undefined, true);
return call;
@@ -181,32 +217,44 @@ export class ClientStreamingMethodStub extends MethodStub {
export class BidirectionalStreamingMethodStub extends MethodStub {
invoke(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): BidirectionalStreamingCall {
const call = new BidirectionalStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke();
return call;
}
open(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): BidirectionalStreamingCall {
const call = new BidirectionalStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(undefined, true);
return call;
diff --git a/pw_rpc/ts/packets.ts b/pw_rpc/ts/packets.ts
index 68cf80393..b6cb71395 100644
--- a/pw_rpc/ts/packets.ts
+++ b/pw_rpc/ts/packets.ts
@@ -14,33 +14,34 @@
/** Functions for working with pw_rpc packets. */
-import {Message} from 'google-protobuf';
-import {MethodDescriptorProto} from 'google-protobuf/google/protobuf/descriptor_pb';
-import * as packetPb from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Status} from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
+import {
+ RpcPacket,
+ PacketType,
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import { Status } from 'pigweedjs/pw_status';
// Channel, Service, Method
type idSet = [number, number, number];
-export function decode(data: Uint8Array): packetPb.RpcPacket {
- return packetPb.RpcPacket.deserializeBinary(data);
+export function decode(data: Uint8Array): RpcPacket {
+ return RpcPacket.deserializeBinary(data);
}
-export function decodePayload(payload: Uint8Array, payloadType: any): Message {
- const message = payloadType.deserializeBinary(payload);
- return message;
+export function decodePayload(payload: Uint8Array, payloadType: any): any {
+ return payloadType['deserializeBinary'](payload);
}
-export function forServer(packet: packetPb.RpcPacket): boolean {
+export function forServer(packet: RpcPacket): boolean {
return packet.getType() % 2 == 0;
}
export function encodeClientError(
- packet: packetPb.RpcPacket,
- status: Status
+ packet: RpcPacket,
+ status: Status,
): Uint8Array {
- const errorPacket = new packetPb.RpcPacket();
- errorPacket.setType(packetPb.PacketType.CLIENT_ERROR);
+ const errorPacket = new RpcPacket();
+ errorPacket.setType(PacketType.CLIENT_ERROR);
errorPacket.setChannelId(packet.getChannelId());
errorPacket.setMethodId(packet.getMethodId());
errorPacket.setServiceId(packet.getServiceId());
@@ -49,18 +50,19 @@ export function encodeClientError(
}
export function encodeClientStream(ids: idSet, message: Message): Uint8Array {
- const streamPacket = new packetPb.RpcPacket();
- streamPacket.setType(packetPb.PacketType.CLIENT_STREAM);
+ const streamPacket = new RpcPacket();
+ streamPacket.setType(PacketType.CLIENT_STREAM);
streamPacket.setChannelId(ids[0]);
streamPacket.setServiceId(ids[1]);
streamPacket.setMethodId(ids[2]);
- streamPacket.setPayload(message.serializeBinary());
+ const msgSerialized = (message as any)['serializeBinary']();
+ streamPacket.setPayload(msgSerialized);
return streamPacket.serializeBinary();
}
export function encodeClientStreamEnd(ids: idSet): Uint8Array {
- const streamEnd = new packetPb.RpcPacket();
- streamEnd.setType(packetPb.PacketType.CLIENT_STREAM_END);
+ const streamEnd = new RpcPacket();
+ streamEnd.setType(PacketType.CLIENT_REQUEST_COMPLETION);
streamEnd.setChannelId(ids[0]);
streamEnd.setServiceId(ids[1]);
streamEnd.setMethodId(ids[2]);
@@ -70,11 +72,11 @@ export function encodeClientStreamEnd(ids: idSet): Uint8Array {
export function encodeRequest(ids: idSet, request?: Message): Uint8Array {
const payload: Uint8Array =
typeof request !== 'undefined'
- ? request.serializeBinary()
- : new Uint8Array();
+ ? (request as any)['serializeBinary']()
+ : new Uint8Array(0);
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.REQUEST);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.REQUEST);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
packet.setMethodId(ids[2]);
@@ -83,18 +85,19 @@ export function encodeRequest(ids: idSet, request?: Message): Uint8Array {
}
export function encodeResponse(ids: idSet, response: Message): Uint8Array {
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.RESPONSE);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.RESPONSE);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
packet.setMethodId(ids[2]);
- packet.setPayload(response.serializeBinary());
+ const msgSerialized = (response as any)['serializeBinary']();
+ packet.setPayload(msgSerialized);
return packet.serializeBinary();
}
export function encodeCancel(ids: idSet): Uint8Array {
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.CLIENT_ERROR);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.CLIENT_ERROR);
packet.setStatus(Status.CANCELLED);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
diff --git a/pw_rpc/ts/packets_test.ts b/pw_rpc/ts/packets_test.ts
index b399e2578..50cdb3f1a 100644
--- a/pw_rpc/ts/packets_test.ts
+++ b/pw_rpc/ts/packets_test.ts
@@ -17,7 +17,7 @@ import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Status} from 'pigweedjs/pw_status';
+import { Status } from 'pigweedjs/pw_status';
import * as packets from './packets';
@@ -31,7 +31,9 @@ function addTestData(packet: RpcPacket) {
}
describe('Packets', () => {
- beforeEach(() => { });
+ beforeEach(() => {
+ // Do nothing.
+ });
it('encodeRequest sets packet fields', () => {
const goldenRequest = new RpcPacket();
@@ -95,7 +97,7 @@ describe('Packets', () => {
addTestData(request);
expect(request.toObject()).toEqual(
- packets.decode(request.serializeBinary()).toObject()
+ packets.decode(request.serializeBinary()).toObject(),
);
});
diff --git a/pw_rpc/ts/queue.ts b/pw_rpc/ts/queue.ts
index 0bd2b8c6c..44dd48cbd 100644
--- a/pw_rpc/ts/queue.ts
+++ b/pw_rpc/ts/queue.ts
@@ -32,7 +32,7 @@ export default class Queue<T> {
}
shift(): Promise<T> {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
if (this.length > 0) {
return resolve(this.queue.shift()!);
} else {
diff --git a/pw_rpc/ts/rpc_classes.ts b/pw_rpc/ts/rpc_classes.ts
index 703612e19..83001e268 100644
--- a/pw_rpc/ts/rpc_classes.ts
+++ b/pw_rpc/ts/rpc_classes.ts
@@ -12,11 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Message} from 'google-protobuf';
-import {Status} from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
-import {Call} from './call';
-import {Channel, Method, Service} from './descriptors';
+import { Call } from './call';
+import { Channel, Method, Service } from './descriptors';
import * as packets from './packets';
/** Data class for a pending RPC call. */
@@ -70,7 +70,7 @@ export class PendingCalls {
rpc: Rpc,
call: Call,
ignoreError: boolean,
- request?: Message
+ request?: Message,
): Call | undefined {
const previous = this.open(rpc, call);
const packet = packets.encodeRequest(rpc.idSet, request);
diff --git a/pw_rpc/unary_rpc.svg b/pw_rpc/unary_rpc.svg
deleted file mode 100644
index 1a0a28d24..000000000
--- a/pw_rpc/unary_rpc.svg
+++ /dev/null
@@ -1,47 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="408.1" viewBox="0 0 534 371" width="587.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Unary RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="190" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="171" y="159"></rect>
-<rect fill="rgb(0,0,0)" height="114" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="363" y="159"></rect>
-<polygon fill="rgb(0,0,0)" points="27,126 151,126 159,134 159,193 27,193 27,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,233 513,233 521,241 521,313 383,313 383,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="111" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="303" y="46"></rect>
-<path d="M 172 80 L 172 359" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="190" stroke="rgb(0,0,0)" width="8" x="168" y="153"></rect>
-<path d="M 364 80 L 364 359" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="114" stroke="rgb(0,0,0)" width="8" x="360" y="153"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="108" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="172.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="300" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="364.0" y="66">server</text>
-<path d="M 180 153 L 356 153" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="348,149 356,153 348,157" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,120 148,120 156,128 156,187 24,187 24,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 148 120 L 148 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 148 128 L 156 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="86.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="172">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="53.0" y="185">payload</text>
-<path d="M 180 267 L 356 267" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="188,263 180,267 188,271" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,227 510,227 518,235 518,307 380,307 380,227" stroke="rgb(0,0,0)"></polygon>
-<path d="M 510 227 L 510 235" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 510 235 L 518 235" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="445.0" y="240">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="253">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="266">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="279">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="409.0" y="292">payload</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="406.0" y="305">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="205.0" y="151">request</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="48" x="332.0" y="265">response</text>
-</svg>
diff --git a/pw_rpc/unary_rpc_cancelled.svg b/pw_rpc/unary_rpc_cancelled.svg
deleted file mode 100644
index c253780f8..000000000
--- a/pw_rpc/unary_rpc_cancelled.svg
+++ /dev/null
@@ -1,59 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg viewBox="0 0 524 358" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" width="524px" height="358px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
- </filter>
- </defs>
- <title>Cancelled Unary RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "request",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="177" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="203" y="159" />
- <rect fill="rgb(0,0,0)" height="177" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="395" y="159" />
- <polygon fill="rgb(0,0,0)" points="58,126 183,126 191,134 191,193 58,193 58,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="27,233 183,233 191,241 191,300 27,300 27,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="143" y="46" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="335" y="46" />
- <path d="M 204 80 L 204 346" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="177" stroke="rgb(0,0,0)" width="8" x="200" y="153" />
- <path d="M 396 80 L 396 346" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="177" stroke="rgb(0,0,0)" width="8" x="392" y="153" />
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="140" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="204.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="332" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="396.5" y="66">server</text>
- <path d="M 212 153 L 388 153" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,149 388,153 380,157" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="55,120 180,120 188,128 188,187 55,187 55,120" stroke="rgb(0,0,0)" />
- <path d="M 180 120 L 180 128" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 128 L 188 128" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="117.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="90.5" y="172">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="84.5" y="185">payload</text>
- <path d="M 212 260 L 388 260" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,256 388,260 380,264" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="24,227 180,227 188,235 188,294 24,294 24,227" stroke="rgb(0,0,0)" />
- <path d="M 180 227 L 180 235" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 235 L 188 235" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="102.0" y="240">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="266">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="279">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="80.5" y="292">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="237.5" y="151">request</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="234.5" y="258">cancel</text>
-</svg>