pw_rpc: Add a sample project

Add a sample application that uses the current PW RPC module (and many
more). The application can also be run as a test using Zephyr's
twister command.

Bug: b/236263182
Change-Id: I0a5e35e8c5427886a12fb0640d6d1b586ec8b763
Signed-off-by: Yuval Peress <peress@google.com>
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/zephyr-integration/+/108833
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..000a9af
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Default build directories
+build/
+twister-out*/
+
+# West modules and install directories
+bootloader/
+modules/
+tools/
+.west
+
+# IDE specific directories
+.idea/
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..382053e
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,23 @@
+Pigweed samples and tests
+##################
+
+Setting up:
+
+.. code-block:: console
+
+  $ pip install pigweed
+  $ west init -l zephyr
+  $ west update
+  $ pip install -r zephyr/scripts/requirements.txt
+
+Running the RPC sample:
+
+.. code-block:: console
+
+  $ west build -b native_posix -p -t run samples/pw_rpc/
+
+Test everything under samples/:
+
+.. code-block:: console
+
+  $ ./zephyr/scripts/twister -v -c -p native_posix --coverage -T samples/
diff --git a/samples/pw_rpc/CMakeLists.txt b/samples/pw_rpc/CMakeLists.txt
new file mode 100644
index 0000000..184248f
--- /dev/null
+++ b/samples/pw_rpc/CMakeLists.txt
@@ -0,0 +1,60 @@
+# 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.
+
+cmake_minimum_required(VERSION 3.20)
+
+set(BOARD native_posix)
+set(NO_BUILD_TYPE_WARNING ON)
+set(CMAKE_VERBOSE_MAKEFILE ON)
+
+# Use this copy of Pigweed
+get_filename_component(PW_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../pigweed" ABSOLUTE)
+set(ENV{PW_ROOT} ${PW_ROOT})
+
+set(pw_third_party_nanopb_ADD_SUBDIRECTORY ON CACHE BOOL "" FORCE)
+
+list(APPEND ZEPHYR_EXTRA_MODULES
+    ${PW_ROOT}
+)
+
+find_package(Zephyr REQUIRED PATHS $ENV{ZEPHYR_BASE})
+project(rpc_demo)
+
+add_definitions(-DVERSION=5)
+add_definitions(-DCONFIG_RPC_BUFFER_SIZE=1024)
+add_definitions(-DPW_LOG_LEVEL=PW_LOG_LEVEL_INFO)
+add_definitions(-DPW_LOG_SHOW_MODULE=1)
+
+# Define the proto library
+include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+pw_proto_library(
+    rpc_demo.protos
+    SOURCES proto/demo.proto
+)
+
+file(GLOB app_sources src/*.cc)
+target_sources(app
+  PRIVATE
+    ${app_sources}
+)
+zephyr_include_directories(include)
+zephyr_link_libraries(
+    rpc_demo.protos.nanopb
+    rpc_demo.protos.nanopb_rpc
+)
+target_link_libraries(app
+  PRIVATE
+    rpc_demo.protos.nanopb
+    rpc_demo.protos.nanopb_rpc
+)
diff --git a/samples/pw_rpc/include/rpc_demo/client/client_reader.h b/samples/pw_rpc/include/rpc_demo/client/client_reader.h
new file mode 100644
index 0000000..b9b9b64
--- /dev/null
+++ b/samples/pw_rpc/include/rpc_demo/client/client_reader.h
@@ -0,0 +1,38 @@
+// 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.
+#pragma once
+
+#include <pw_hdlc/rpc_channel.h>
+#include <pw_hdlc/rpc_packets.h>
+#include <pw_rpc/client.h>
+#include <pw_stream/stream.h>
+
+#include <array>
+#include <atomic>
+
+namespace rpc_demo {
+
+class ClientReader {
+ public:
+  ClientReader(pw::stream::Reader& rx, pw::rpc::Client* rpc_client);
+  bool ParsePacket();
+
+ private:
+  pw::stream::Reader& rx_;
+  pw::rpc::Client* rpc_client_;
+  std::array<std::byte, CONFIG_RPC_BUFFER_SIZE> decode_buffer_;
+  pw::hdlc::Decoder decoder_;
+};
+
+}  // namespace rpc_demo
diff --git a/samples/pw_rpc/include/rpc_demo/deque_stream.h b/samples/pw_rpc/include/rpc_demo/deque_stream.h
new file mode 100644
index 0000000..f34d07c
--- /dev/null
+++ b/samples/pw_rpc/include/rpc_demo/deque_stream.h
@@ -0,0 +1,32 @@
+// 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.
+#pragma once
+
+#include <pw_stream/stream.h>
+#include <pw_sync/mutex.h>
+
+#include <deque>
+
+class DequeReadWriter : public pw::stream::NonSeekableReaderWriter {
+ public:
+  DequeReadWriter() = default;
+
+ protected:
+  pw::StatusWithSize DoRead(pw::ByteSpan destination) override;
+  pw::Status DoWrite(pw::ConstByteSpan data) override;
+
+ private:
+  std::deque<std::byte> buff_;
+  pw::sync::Mutex mutex_;
+};
diff --git a/samples/pw_rpc/include/rpc_demo/rxtx.h b/samples/pw_rpc/include/rpc_demo/rxtx.h
new file mode 100644
index 0000000..f20db35
--- /dev/null
+++ b/samples/pw_rpc/include/rpc_demo/rxtx.h
@@ -0,0 +1,26 @@
+// 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.
+#pragma once
+
+#include <pw_stream/stream.h>
+
+/**
+ * @return The ReaderWriter stream used for client-to-service communication.
+ */
+pw::stream::ReaderWriter& ClientToServiceStream();
+
+/**
+ * @return The ReaderWriter stream used for service-to-client communication.
+ */
+pw::stream::ReaderWriter& ServiceToClientStream();
diff --git a/samples/pw_rpc/include/rpc_demo/service/demo_service.h b/samples/pw_rpc/include/rpc_demo/service/demo_service.h
new file mode 100644
index 0000000..377981d
--- /dev/null
+++ b/samples/pw_rpc/include/rpc_demo/service/demo_service.h
@@ -0,0 +1,37 @@
+// 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.
+#pragma once
+
+#include "proto/demo.rpc.pb.h"
+
+namespace rpc_demo {
+
+class DemoService : public pw_rpc::nanopb::DemoService::Service<DemoService> {
+ public:
+  ::pw::Status GetVersion(const ::rpc_demo_Empty& request,
+                          ::rpc_demo_GetVersionResponse& response);
+
+  ::pw::Status GetSensorList(const ::rpc_demo_Empty& request,
+                             ::rpc_demo_GetSensorListResponse& response);
+
+  ::pw::Status UpdateSensorFrequency(
+      const ::rpc_demo_UpdateSensorFrequencyRequest& request,
+      ::rpc_demo_UpdateSensorFrequencyResponse& response);
+
+  ::pw::Status GetSensorSamples(
+      const ::rpc_demo_GetSensorSamplesRequest& request,
+      ::rpc_demo_GetSensorSamplesResponse& response);
+};
+
+}  // namespace rpc_demo
\ No newline at end of file
diff --git a/samples/pw_rpc/include/rpc_demo/service/service_reader.h b/samples/pw_rpc/include/rpc_demo/service/service_reader.h
new file mode 100644
index 0000000..2f9835b
--- /dev/null
+++ b/samples/pw_rpc/include/rpc_demo/service/service_reader.h
@@ -0,0 +1,43 @@
+// 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.
+#pragma once
+
+#include <pw_hdlc/rpc_channel.h>
+#include <pw_span/span.h>
+#include <pw_stream/stream.h>
+
+#include <atomic>
+#include <thread>
+
+#include "rpc_demo/service/demo_service.h"
+
+namespace rpc_demo {
+
+class ServiceReader {
+ public:
+  ServiceReader(pw::stream::Reader* reader,
+                pw::stream::Writer* writer,
+                pw::hdlc::RpcChannelOutput* output,
+                pw::span<pw::rpc::Channel> channels);
+
+  bool ParsePacket();
+
+ private:
+  pw::stream::Reader* reader_;
+  pw::stream::Writer* writer_;
+  pw::hdlc::RpcChannelOutput* output_;
+  pw::span<pw::rpc::Channel> channels_;
+};
+
+}  // namespace rpc_demo
diff --git a/samples/pw_rpc/prj.conf b/samples/pw_rpc/prj.conf
new file mode 100644
index 0000000..2beca2c
--- /dev/null
+++ b/samples/pw_rpc/prj.conf
@@ -0,0 +1,46 @@
+# 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.
+
+CONFIG_ASSERT=y
+CONFIG_NANOPB=y
+CONFIG_BOARD_NATIVE_POSIX_64BIT=y
+CONFIG_LOG=y
+CONFIG_LOG_DEFAULT_LEVEL=4
+
+CONFIG_PIGWEED_ASSERT=y
+CONFIG_PIGWEED_BYTES=y
+CONFIG_PIGWEED_CHECKSUM=y
+CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK=y
+CONFIG_PIGWEED_CONTAINERS=y
+CONFIG_PIGWEED_FUNCTION=y
+CONFIG_PIGWEED_HDLC=y
+CONFIG_PIGWEED_INTERRUPT_CONTEXT=y
+CONFIG_PIGWEED_LOG=y
+CONFIG_PIGWEED_POLYFILL=y
+CONFIG_PIGWEED_POLYFILL_OVERRIDES=y
+CONFIG_PIGWEED_PREPROCESSOR=y
+CONFIG_PIGWEED_RESULT=y
+CONFIG_PIGWEED_ROUTER_PACKET_PARSER=y
+CONFIG_PIGWEED_RPC_COMMON=y
+CONFIG_PIGWEED_SPAN=y
+CONFIG_PIGWEED_STATUS=y
+CONFIG_PIGWEED_STREAM=y
+CONFIG_PIGWEED_STRING=y
+CONFIG_PIGWEED_SYNC_MUTEX=y
+CONFIG_PIGWEED_SYS_IO=y
+CONFIG_PIGWEED_VARINT=y
+
+CONFIG_CPLUSPLUS=y
+CONFIG_STD_CPP17=y
+CONFIG_LIB_CPLUSPLUS=y
diff --git a/samples/pw_rpc/proto/demo.proto b/samples/pw_rpc/proto/demo.proto
new file mode 100644
index 0000000..122427f
--- /dev/null
+++ b/samples/pw_rpc/proto/demo.proto
@@ -0,0 +1,74 @@
+// 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.
+syntax = "proto3";
+
+package rpc_demo;
+
+message GetVersionResponse {
+  uint32 version_number = 1;
+}
+
+message ThreeAxisData {
+  float x = 1;
+  float y = 2;
+  float z = 3;
+}
+
+message SensorInfo {
+  string name = 1;
+  enum Type {
+    ACCEL = 0;
+    GYRO = 1;
+    MAG = 2;
+  }
+  Type type = 2;
+  uint32 sample_frequency_hz = 3;
+}
+
+message GetSensorListResponse {
+  repeated SensorInfo sensors = 1;
+}
+
+message UpdateSensorFrequencyRequest {
+  uint32 sensor_index = 1;
+  uint32 sample_frequency_hz = 2;
+}
+
+message UpdateSensorFrequencyResponse {
+  uint32 sample_frequency_hz = 1;
+}
+
+message GetSensorSamplesRequest {
+  uint32 sensor_index = 1;
+}
+
+message GetSensorSamplesResponse {
+  repeated ThreeAxisData data = 1;
+}
+
+message Empty {}
+
+enum RpcClientChannelId {
+  UNASSIGNED = 0;
+  DEFAULT = 1;
+}
+
+service DemoService {
+  rpc GetVersion(Empty) returns (GetVersionResponse) {}
+  rpc GetSensorList(Empty) returns (GetSensorListResponse) {}
+  rpc UpdateSensorFrequency(UpdateSensorFrequencyRequest)
+      returns (UpdateSensorFrequencyResponse) {}
+  rpc GetSensorSamples(GetSensorSamplesRequest)
+      returns (GetSensorSamplesResponse) {}
+}
diff --git a/samples/pw_rpc/sample.yaml b/samples/pw_rpc/sample.yaml
new file mode 100644
index 0000000..acf3ad4
--- /dev/null
+++ b/samples/pw_rpc/sample.yaml
@@ -0,0 +1,20 @@
+sample:
+  name: pw_rpc sample
+tests:
+  sample.pw_rpc:
+    tags: pw_rpc
+    platform_allow: native_posix
+    timeout: 10
+    harness: console
+    harness_config:
+      type: multi_line
+      ordered: true
+      regex:
+        - "(.*)Starting RPC demo(.*)"
+        - "(.*)Service waiting for data(.*)"
+        - "(.*)Starting pw_rpc service(.*)"
+        - "(.*)Service got packet!(.*)"
+        - "(.*)Client waiting for response(.*)"
+        - "(.*)Reading bytes(.*)"
+        - "(.*)Client got packet!(.*)"
+        - "(.*)Received version #5(.*)"
diff --git a/samples/pw_rpc/src/client_reader.cc b/samples/pw_rpc/src/client_reader.cc
new file mode 100644
index 0000000..0f4cbff
--- /dev/null
+++ b/samples/pw_rpc/src/client_reader.cc
@@ -0,0 +1,57 @@
+// 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.
+#define PW_LOG_MODULE_NAME "ClientReader"
+
+#include "rpc_demo/client/client_reader.h"
+
+#include <pw_hdlc/decoder.h>
+#include <pw_hdlc/rpc_packets.h>
+#include <pw_log/log.h>
+#include <pw_status/status.h>
+
+namespace rpc_demo {
+
+ClientReader::ClientReader(pw::stream::Reader& rx, pw::rpc::Client* rpc_client)
+    : rx_(rx), rpc_client_(rpc_client), decoder_(decode_buffer_) {}
+
+bool ClientReader::ParsePacket() {
+  PW_LOG_INFO("Reading bytes...");
+  while (true) {
+    std::byte data;
+    if (auto result = rx_.Read(&data, 1); !result.ok()) {
+      PW_LOG_ERROR("Failed to read data");
+      continue;
+    }
+    PW_LOG_DEBUG("0x%02x", static_cast<uint8_t>(data));
+
+    pw::Result<pw::hdlc::Frame> result = decoder_.Process(data);
+    if (!result.ok()) {
+      continue;
+    }
+
+    pw::hdlc::Frame& frame = result.value();
+    if (frame.address() != pw::hdlc::kDefaultRpcAddress) {
+      return false;
+    }
+
+    PW_LOG_INFO("Client got packet!");
+    auto client_result = rpc_client_->ProcessPacket(frame.data());
+    PW_LOG_DEBUG("Client ProcessPacket status (%s)",
+                 pw_StatusString(client_result));
+    decoder_.Clear();
+    return true;
+  }
+}
+
+}  // namespace rpc_demo
diff --git a/samples/pw_rpc/src/demo_service.cc b/samples/pw_rpc/src/demo_service.cc
new file mode 100644
index 0000000..1707965
--- /dev/null
+++ b/samples/pw_rpc/src/demo_service.cc
@@ -0,0 +1,55 @@
+// 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.
+#include "rpc_demo/service/demo_service.h"
+
+namespace rpc_demo {
+
+// Method definitions for pw_demo.DemoHostService.
+::pw::Status DemoService::GetVersion(const rpc_demo_Empty& request,
+                                     rpc_demo_GetVersionResponse& response) {
+  static_cast<void>(request);
+  response.version_number = VERSION;
+  return ::pw::OkStatus();
+}
+
+::pw::Status DemoService::GetSensorList(
+    const rpc_demo_Empty& request, rpc_demo_GetSensorListResponse& response) {
+  // TODO: Read the request as appropriate for your application
+  static_cast<void>(request);
+  // TODO: Fill in the response as appropriate for your application
+  static_cast<void>(response);
+  return ::pw::Status::Unimplemented();
+}
+
+::pw::Status DemoService::UpdateSensorFrequency(
+    const rpc_demo_UpdateSensorFrequencyRequest& request,
+    rpc_demo_UpdateSensorFrequencyResponse& response) {
+  // TODO: Read the request as appropriate for your application
+  static_cast<void>(request);
+  // TODO: Fill in the response as appropriate for your application
+  static_cast<void>(response);
+  return ::pw::Status::Unimplemented();
+}
+
+::pw::Status DemoService::GetSensorSamples(
+    const rpc_demo_GetSensorSamplesRequest& request,
+    rpc_demo_GetSensorSamplesResponse& response) {
+  // TODO: Read the request as appropriate for your application
+  static_cast<void>(request);
+  // TODO: Fill in the response as appropriate for your application
+  static_cast<void>(response);
+  return ::pw::Status::Unimplemented();
+}
+
+}  // namespace rpc_demo
diff --git a/samples/pw_rpc/src/deque_stream.cc b/samples/pw_rpc/src/deque_stream.cc
new file mode 100644
index 0000000..e05df96
--- /dev/null
+++ b/samples/pw_rpc/src/deque_stream.cc
@@ -0,0 +1,37 @@
+// 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.
+#include <mutex>
+#include "rpc_demo/deque_stream.h"
+
+pw::StatusWithSize DequeReadWriter::DoRead(pw::ByteSpan destination) {
+  size_t count = 0;
+  std::lock_guard lock(mutex_);
+
+  while (!buff_.empty() && count < destination.size_bytes()) {
+    destination[count++] = buff_.front();
+    buff_.pop_front();
+  }
+
+  auto status = (count == 0) ? pw::Status::OutOfRange() : pw::OkStatus();
+  return pw::StatusWithSize(status, count);
+}
+
+pw::Status DequeReadWriter::DoWrite(pw::ConstByteSpan data) {
+  std::lock_guard lock(mutex_);
+  for (size_t i = 0; i < data.size_bytes(); ++i) {
+    buff_.push_back(data[i]);
+  }
+
+  return pw::OkStatus();
+}
diff --git a/samples/pw_rpc/src/main.cc b/samples/pw_rpc/src/main.cc
new file mode 100644
index 0000000..25789a1
--- /dev/null
+++ b/samples/pw_rpc/src/main.cc
@@ -0,0 +1,83 @@
+// 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.
+#define PW_LOG_MODULE_NAME "MAIN"
+
+#include <pw_hdlc/rpc_channel.h>
+#include <pw_hdlc/rpc_packets.h>
+#include <pw_log/log.h>
+#include <pw_status/status.h>
+
+#include "proto/demo.pb.h"
+#include "rpc_demo/client/client_reader.h"
+#include "rpc_demo/service/service_reader.h"
+#include "rpc_demo/rxtx.h"
+
+
+namespace {
+// Create the client channel output with a built-in buffer.
+pw::hdlc::RpcChannelOutput
+    client_to_service_channel_output(ClientToServiceStream().as_writer(),
+                                     pw::hdlc::kDefaultRpcAddress,
+                                     "HDLC client output");
+pw::hdlc::RpcChannelOutput
+    service_to_client_channel_output(ServiceToClientStream().as_writer(),
+                                     pw::hdlc::kDefaultRpcAddress,
+                                     "HDLC service output");
+
+// Configure the channel. Is this an output channel only?
+pw::rpc::Channel client_channels[] = {
+    pw::rpc::Channel::Create<rpc_demo_RpcClientChannelId_DEFAULT,
+                             rpc_demo_RpcClientChannelId>(
+        &client_to_service_channel_output),
+};
+pw::rpc::Channel service_channels[] = {
+    pw::rpc::Channel::Create<rpc_demo_RpcClientChannelId_DEFAULT,
+                             rpc_demo_RpcClientChannelId>(
+        &service_to_client_channel_output),
+};
+
+pw::rpc::Client rpc_client(client_channels);
+rpc_demo::pw_rpc::nanopb::DemoService::Client
+    service_client(rpc_client, rpc_demo_RpcClientChannelId_DEFAULT);
+
+void GetVersionResponse(const rpc_demo_GetVersionResponse &response,
+                               pw::Status status) {
+  if (status.ok()) {
+    PW_LOG_INFO("Received version #%u", response.version_number);
+  } else {
+    PW_LOG_ERROR("Failed to get version number, status: %s",
+                 pw_StatusString(status));
+  }
+}
+
+} // namespace
+
+void main(void) {
+  PW_LOG_INFO("Starting RPC demo...");
+  rpc_demo::ServiceReader ec_service(
+      &ClientToServiceStream().as_reader(),
+      &ServiceToClientStream().as_writer(),
+      reinterpret_cast<pw::hdlc::RpcChannelOutput*>(
+          &service_to_client_channel_output),
+      service_channels);
+  rpc_demo::ClientReader ap_client(ServiceToClientStream().as_reader(), &rpc_client);
+
+  auto response = service_client.GetVersion(rpc_demo_Empty_init_default,
+                                            GetVersionResponse);
+  PW_ASSERT(response.active());
+  PW_LOG_INFO("Service waiting for data...");
+  PW_ASSERT(ec_service.ParsePacket());
+  PW_LOG_INFO("Client waiting for response...");
+  PW_ASSERT(ap_client.ParsePacket());
+}
diff --git a/samples/pw_rpc/src/rxtx.cc b/samples/pw_rpc/src/rxtx.cc
new file mode 100644
index 0000000..52c1982
--- /dev/null
+++ b/samples/pw_rpc/src/rxtx.cc
@@ -0,0 +1,28 @@
+// 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.
+
+#include "rpc_demo/rxtx.h"
+#include "rpc_demo/deque_stream.h"
+
+namespace {
+DequeReadWriter client_to_service_stream;
+DequeReadWriter service_to_client_stream;
+} // namespace
+
+pw::stream::ReaderWriter &ClientToServiceStream() {
+  return client_to_service_stream;
+}
+pw::stream::ReaderWriter &ServiceToClientStream() {
+  return service_to_client_stream;
+}
diff --git a/samples/pw_rpc/src/service_reader.cc b/samples/pw_rpc/src/service_reader.cc
new file mode 100644
index 0000000..aaf888d
--- /dev/null
+++ b/samples/pw_rpc/src/service_reader.cc
@@ -0,0 +1,70 @@
+// 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.
+#define PW_LOG_MODULE_NAME "ServiceReader"
+
+#include "rpc_demo/service/service_reader.h"
+
+#include <pw_hdlc/rpc_channel.h>
+#include <pw_hdlc/rpc_packets.h>
+#include <pw_log/log.h>
+#include <pw_span/span.h>
+
+namespace rpc_demo {
+
+ServiceReader::ServiceReader(pw::stream::Reader* reader,
+                             pw::stream::Writer* writer,
+                             pw::hdlc::RpcChannelOutput* output,
+                             pw::span<pw::rpc::Channel> channels)
+    : reader_(reader), writer_(writer), output_(output), channels_(channels) {}
+
+bool ServiceReader::ParsePacket() {
+  std::array<std::byte, CONFIG_RPC_BUFFER_SIZE> decode_buffer{};
+  pw::rpc::Server server(channels_);
+  pw::hdlc::Decoder decoder(decode_buffer);
+  DemoService service;
+
+  server.RegisterService(service);
+
+  // Input buffer
+  PW_LOG_INFO("Starting pw_rpc service...");
+  while (true) {
+    std::byte data;
+
+    if (auto result = reader_->Read(&data, 1); !result.ok()) {
+      std::this_thread::sleep_for(std::chrono::milliseconds(50));
+      continue;
+    }
+
+    PW_LOG_DEBUG("0x%02x", static_cast<uint8_t>(data));
+    auto result = decoder.Process(data);
+    if (!result.ok()) {
+      continue;
+    }
+
+    auto& frame = result.value();
+    if (frame.address() != pw::hdlc::kDefaultRpcAddress) {
+      decoder.Clear();
+      return false;
+    }
+
+    PW_LOG_INFO("Service got packet!");
+    auto server_result = server.ProcessPacket(frame.data());
+    PW_LOG_DEBUG("Client ProcessPacket status (%s)",
+                 pw_StatusString(server_result));
+    decoder.Clear();
+    return true;
+  }
+}
+
+}  // namespace rpc_demo