pw_rpc: Some initial server code

This change starts adding basic RPC server code and request processing.

Change-Id: Ia59a3e65aef4117fc593db46450a6b428bf136f5
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 2d1c1a5..97ac763 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -64,6 +64,7 @@
   source_set(target_name) {
     all_dependent_configs = [ ":$_include_config_target" ]
     deps = [ ":$_gen_target" ] + invoker.deps
+    public_deps = [ dir_pw_protobuf ]
     sources = get_target_outputs(":$_gen_target")
     public = filter_include(sources, [ "*.pwpb.h" ])
   }
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index d56573e..4d5936e 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -31,7 +31,19 @@
     includes = ["public"],
     deps = [
         "//pw_assert",
+        "//pw_log",
         "//pw_span",
+        "//pw_status",
+    ],
+    srcs = [
+      "channel.cc",
+      "packet.cc",
+      "public/pw_rpc/internal/packet.h",
+      "public/pw_rpc/internal/service.h",
+      "public/pw_rpc/internal/service_registry.h",
+      "server.cc",
+      "service.cc",
+      "service_registry.cc",
     ],
 )
 
@@ -44,3 +56,13 @@
         ":pw_rpc",
     ],
 )
+
+pw_cc_test(
+    name = "packet_test",
+    srcs = [
+        "packet_test.cc",
+    ],
+    deps = [
+        ":pw_rpc",
+    ],
+)
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index de98060..192a9bc 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -13,6 +13,7 @@
 # the License.
 
 import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
 import("$dir_pw_unit_test/test.gni")
 
 config("default_config") {
@@ -20,15 +21,33 @@
 }
 
 source_set("pw_rpc") {
+  deps = [ ":protos_pwpb" ]
   public_configs = [ ":default_config" ]
   public_deps = [
     dir_pw_assert,
+    dir_pw_log,
     dir_pw_span,
+    dir_pw_status,
   ]
   public = [
     "public/pw_rpc/channel.h",
     "public/pw_rpc/server.h",
   ]
+  sources = [
+    "channel.cc",
+    "packet.cc",
+    "public/pw_rpc/internal/packet.h",
+    "public/pw_rpc/internal/service.h",
+    "public/pw_rpc/internal/service_registry.h",
+    "server.cc",
+    "service.cc",
+    "service_registry.cc",
+  ]
+  friend = [ ":*" ]
+}
+
+pw_proto_library("protos") {
+  sources = [ "pw_rpc_protos/packet.proto" ]
 }
 
 pw_doc_group("docs") {
@@ -36,10 +55,21 @@
 }
 
 pw_test_group("tests") {
-  tests = [ ":server_test" ]
+  tests = [
+    ":packet_test",
+    ":server_test",
+  ]
 }
 
 pw_test("server_test") {
   deps = [ ":pw_rpc" ]
   sources = [ "server_test.cc" ]
 }
+
+pw_test("packet_test") {
+  deps = [
+    ":pw_rpc",
+    dir_pw_protobuf,
+  ]
+  sources = [ "packet_test.cc" ]
+}
diff --git a/pw_rpc/channel.cc b/pw_rpc/channel.cc
new file mode 100644
index 0000000..6095479
--- /dev/null
+++ b/pw_rpc/channel.cc
@@ -0,0 +1,17 @@
+// Copyright 2020 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/channel.h"
+
+namespace pw::rpc {}  // namespace pw::rpc
diff --git a/pw_rpc/packet.cc b/pw_rpc/packet.cc
new file mode 100644
index 0000000..e332c5e
--- /dev/null
+++ b/pw_rpc/packet.cc
@@ -0,0 +1,80 @@
+// Copyright 2020 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/internal/packet.h"
+
+#include "pw_protobuf/decoder.h"
+
+namespace pw::rpc::internal {
+
+Packet Packet::FromBuffer(span<const std::byte> data) {
+  PacketType type = PacketType::kRpc;
+  uint32_t channel_id = 0;
+  uint32_t service_id = 0;
+  uint32_t method_id = 0;
+  span<const std::byte> payload;
+
+  protobuf::Decoder decoder(data);
+
+  while (decoder.Next().ok()) {
+    RpcPacket::Fields field =
+        static_cast<RpcPacket::Fields>(decoder.FieldNumber());
+    uint32_t proto_value = 0;
+
+    switch (field) {
+      case RpcPacket::Fields::kType:
+        decoder.ReadUint32(&proto_value);
+        type = static_cast<PacketType>(proto_value);
+        break;
+
+      case RpcPacket::Fields::kChannelId:
+        decoder.ReadUint32(&channel_id);
+        break;
+
+      case RpcPacket::Fields::kServiceId:
+        decoder.ReadUint32(&service_id);
+        break;
+
+      case RpcPacket::Fields::kMethodId:
+        decoder.ReadUint32(&method_id);
+        break;
+
+      case RpcPacket::Fields::kPayload:
+        decoder.ReadBytes(&payload);
+        break;
+    }
+  }
+
+  return Packet(type, channel_id, service_id, method_id, payload);
+}
+
+StatusWithSize Packet::Encode(span<std::byte> buffer) const {
+  pw::protobuf::NestedEncoder encoder(buffer);
+  RpcPacket::Encoder rpc_packet(&encoder);
+
+  rpc_packet.WriteType(type_);
+  rpc_packet.WriteChannelId(channel_id_);
+  rpc_packet.WriteServiceId(service_id_);
+  rpc_packet.WriteMethodId(method_id_);
+  rpc_packet.WritePayload(payload_);
+
+  span<const std::byte> proto;
+  if (Status status = encoder.Encode(&proto); !status.ok()) {
+    return StatusWithSize(status, 0);
+  }
+
+  return StatusWithSize(proto.size());
+}
+
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/packet_test.cc b/pw_rpc/packet_test.cc
new file mode 100644
index 0000000..744b7e1
--- /dev/null
+++ b/pw_rpc/packet_test.cc
@@ -0,0 +1,53 @@
+// Copyright 2020 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/internal/packet.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+using std::byte;
+
+TEST(Packet, EncodeDecode) {
+  constexpr byte payload[]{byte(0x00), byte(0x01), byte(0x02), byte(0x03)};
+
+  Packet packet = Packet::Empty();
+  packet.set_type(PacketType::kRpc);
+  packet.set_channel_id(12);
+  packet.set_service_id(0xdeadbeef);
+  packet.set_method_id(0x03a82921);
+  packet.set_payload(payload);
+
+  byte buffer[128];
+  StatusWithSize sws = packet.Encode(buffer);
+  ASSERT_EQ(sws.status(), Status::OK);
+
+  span<byte> packet_data(buffer, sws.size());
+  Packet decoded = Packet::FromBuffer(packet_data);
+
+  EXPECT_EQ(decoded.type(), packet.type());
+  EXPECT_EQ(decoded.channel_id(), packet.channel_id());
+  EXPECT_EQ(decoded.service_id(), packet.service_id());
+  EXPECT_EQ(decoded.method_id(), packet.method_id());
+  ASSERT_EQ(decoded.payload().size(), packet.payload().size());
+  EXPECT_EQ(std::memcmp(decoded.payload().data(),
+                        packet.payload().data(),
+                        packet.payload().size()),
+            0);
+}
+
+}  // namespace
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/channel.h b/pw_rpc/public/pw_rpc/channel.h
index 339a827..1ef6880 100644
--- a/pw_rpc/public/pw_rpc/channel.h
+++ b/pw_rpc/public/pw_rpc/channel.h
@@ -16,11 +16,14 @@
 #include <cstdint>
 
 #include "pw_span/span.h"
+#include "pw_status/status.h"
 
 namespace pw::rpc {
 
 class ChannelOutput {
  public:
+  constexpr ChannelOutput(uint32_t id) : id_(id) {}
+
   virtual ~ChannelOutput() = default;
 
   // Acquire a buffer into which to write an outgoing RPC packet.
@@ -28,6 +31,11 @@
 
   // Sends the contents of the buffer from AcquireBuffer().
   virtual void SendAndReleaseBuffer(size_t size) = 0;
+
+  uint32_t id() const { return id_; }
+
+ private:
+  uint32_t id_;
 };
 
 class Channel {
@@ -43,7 +51,10 @@
 
   constexpr uint32_t id() const { return id_; }
 
-  void Write(span<std::byte> packet);
+  span<std::byte> AcquireBuffer() const { return output_->AcquireBuffer(); }
+  void SendAndReleaseBuffer(size_t size) const {
+    output_->SendAndReleaseBuffer(size);
+  }
 
  private:
   static constexpr uint32_t kUnassignedChannelId = 0;
diff --git a/pw_rpc/public/pw_rpc/internal/packet.h b/pw_rpc/public/pw_rpc/internal/packet.h
new file mode 100644
index 0000000..2733f2e
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/packet.h
@@ -0,0 +1,73 @@
+// Copyright 2020 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 <cstddef>
+#include <cstdint>
+
+#include "pw_rpc_protos/packet.pwpb.h"
+#include "pw_span/span.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc::internal {
+
+class Packet {
+ public:
+  // Parses a packet from a protobuf message. Missing or malformed fields take
+  // their default values.
+  static Packet FromBuffer(span<const std::byte> data);
+
+  // Returns an empty packet with default values set.
+  static constexpr Packet Empty() {
+    return Packet(PacketType::kRpc, 0, 0, 0, {});
+  }
+
+  // Encodes the packet into its wire format. Returns the encoded size.
+  StatusWithSize Encode(span<std::byte> buffer) const;
+
+  bool is_control() const { return !is_rpc(); }
+  bool is_rpc() const { return type_ == PacketType::kRpc; }
+
+  PacketType type() const { return type_; }
+  uint32_t channel_id() const { return channel_id_; }
+  uint32_t service_id() const { return service_id_; }
+  uint32_t method_id() const { return method_id_; }
+  span<const std::byte> payload() const { return payload_; }
+
+  void set_type(PacketType type) { type_ = type; }
+  void set_channel_id(uint32_t channel_id) { channel_id_ = channel_id; }
+  void set_service_id(uint32_t service_id) { service_id_ = service_id; }
+  void set_method_id(uint32_t method_id) { method_id_ = method_id; }
+  void set_payload(span<const std::byte> payload) { payload_ = payload; }
+
+ private:
+  constexpr Packet(PacketType type,
+                   uint32_t channel_id,
+                   uint32_t service_id,
+                   uint32_t method_id,
+                   span<const std::byte> payload)
+      : type_(type),
+        channel_id_(channel_id),
+        service_id_(service_id),
+        method_id_(method_id),
+        payload_(payload) {}
+
+  PacketType type_;
+  uint32_t channel_id_;
+  uint32_t service_id_;
+  uint32_t method_id_;
+  span<const std::byte> payload_;
+};
+
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/service.h b/pw_rpc/public/pw_rpc/internal/service.h
new file mode 100644
index 0000000..e457285
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/service.h
@@ -0,0 +1,55 @@
+// Copyright 2020 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 <cstdint>
+
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::internal {
+
+// Forward-declare Packet instead of including it to avoid exposing the internal
+// dependency on pw_protobuf.
+class Packet;
+
+class ServiceRegistry;
+
+// Base class for all RPC services. This is not meant to be instantiated
+// directly; use a generated subclass instead.
+class Service {
+ public:
+  struct Method {
+    uint32_t id;
+  };
+
+  Service(uint32_t id, span<const Method> methods)
+      : id_(id), methods_(methods), next_(nullptr) {}
+
+  uint32_t id() const { return id_; }
+
+  // Handles an incoming packet and populates a response. Errors that occur
+  // should be set within the response packet.
+  void ProcessPacket(const internal::Packet& request,
+                     internal::Packet& response);
+
+ private:
+  friend class internal::ServiceRegistry;
+
+  uint32_t id_;
+  span<const Method> methods_;
+  Service* next_;
+};
+
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/service_registry.h b/pw_rpc/public/pw_rpc/internal/service_registry.h
new file mode 100644
index 0000000..283a36c
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/service_registry.h
@@ -0,0 +1,40 @@
+// Copyright 2020 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_assert/assert.h"
+#include "pw_rpc/internal/service.h"
+
+namespace pw::rpc::internal {
+
+// Manages a list of registered RPC services.
+class ServiceRegistry {
+ public:
+  constexpr ServiceRegistry() : first_service_(nullptr) {}
+
+  ServiceRegistry(const ServiceRegistry& other) = delete;
+  ServiceRegistry& operator=(const ServiceRegistry& other) = delete;
+
+  void Register(Service& service) {
+    service.next_ = first_service_;
+    first_service_ = &service;
+  }
+
+  Service* Find(uint32_t id) const;
+
+ private:
+  Service* first_service_;
+};
+
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/server.h b/pw_rpc/public/pw_rpc/server.h
index c1ffd22..2faf1b8 100644
--- a/pw_rpc/public/pw_rpc/server.h
+++ b/pw_rpc/public/pw_rpc/server.h
@@ -16,17 +16,36 @@
 #include <cstddef>
 
 #include "pw_rpc/channel.h"
+#include "pw_rpc/internal/service_registry.h"
 
 namespace pw::rpc {
 
 class Server {
  public:
-  constexpr Server(span<const Channel> channels) : channels_(channels) {}
+  constexpr Server(span<Channel> channels) : channels_(channels) {}
+
+  Server(const Server& other) = delete;
+  Server& operator=(const Server& other) = delete;
+
+  // Registers a service with the server. This should not be called directly
+  // with an internal::Service; instead, use a generated class which inherits
+  // from it.
+  void RegisterService(internal::Service& service) {
+    services_.Register(service);
+  }
+
+  void ProcessPacket(span<const std::byte> packet, ChannelOutput& interface);
 
   constexpr size_t channel_count() const { return channels_.size(); }
 
  private:
-  span<const Channel> channels_;
+  using Service = internal::Service;
+  using ServiceRegistry = internal::ServiceRegistry;
+
+  Channel* FindChannel(uint32_t id);
+
+  span<Channel> channels_;
+  ServiceRegistry services_;
 };
 
 }  // namespace pw::rpc
diff --git a/pw_rpc/pw_rpc_protos/packet.proto b/pw_rpc/pw_rpc_protos/packet.proto
new file mode 100644
index 0000000..c8936e4
--- /dev/null
+++ b/pw_rpc/pw_rpc_protos/packet.proto
@@ -0,0 +1,37 @@
+// Copyright 2020 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 pw.rpc.internal;
+
+enum PacketType { RPC = 0; };
+
+message RpcPacket {
+  // The type of packet. Either a general RPC packet or a specific control
+  // packet. Required.
+  PacketType type = 1;
+
+  // Channel through which the packet is sent. Required.
+  uint32 channel_id = 2;
+
+  // Tokenized fully-qualified name of the service with which this packet is
+  // associated. For RPC packets, this is the service that processes the packet.
+  uint32 service_id = 3;
+
+  // Tokenized name of the method which should process this packet.
+  uint32 method_id = 4;
+
+  // The packet's payload.
+  bytes payload = 5;
+};
diff --git a/pw_rpc/server.cc b/pw_rpc/server.cc
new file mode 100644
index 0000000..fb44de7
--- /dev/null
+++ b/pw_rpc/server.cc
@@ -0,0 +1,78 @@
+// Copyright 2020 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/server.h"
+
+#include "pw_log/log.h"
+#include "pw_rpc/internal/packet.h"
+
+namespace pw::rpc {
+
+using internal::Packet;
+
+void Server::ProcessPacket(span<const std::byte> data,
+                           ChannelOutput& interface) {
+  Packet packet = Packet::FromBuffer(data);
+  if (packet.is_control()) {
+    // TODO(frolv): Handle control packets.
+    return;
+  }
+
+  if (packet.service_id() == 0 || packet.method_id() == 0) {
+    // Malformed packet; don't even try to process it.
+    PW_LOG_ERROR("Received incomplete RPC packet on interface %u",
+                 unsigned(interface.id()));
+    return;
+  }
+
+  Channel* channel = FindChannel(packet.channel_id());
+  if (channel == nullptr) {
+    // TODO(frolv): Dynamically assign channel.
+    return;
+  }
+
+  span<std::byte> response_buffer = channel->AcquireBuffer();
+
+  Service* service = services_.Find(packet.service_id());
+  if (service == nullptr) {
+    // TODO(frolv): Send back a NOT_FOUND response.
+    channel->SendAndReleaseBuffer(0);
+    return;
+  }
+
+  Packet response = Packet::Empty();
+  response.set_channel_id(channel->id());
+
+  service->ProcessPacket(packet, response);
+
+  StatusWithSize sws = response.Encode(response_buffer);
+  if (!sws.ok()) {
+    // TODO(frolv): What should be done here?
+    channel->SendAndReleaseBuffer(0);
+    return;
+  }
+
+  channel->SendAndReleaseBuffer(sws.size());
+}
+
+Channel* Server::FindChannel(uint32_t id) {
+  for (Channel& c : channels_) {
+    if (c.id() == id) {
+      return &c;
+    }
+  }
+  return nullptr;
+}
+
+}  // namespace pw::rpc
diff --git a/pw_rpc/server_test.cc b/pw_rpc/server_test.cc
index 3079f00..aae410e 100644
--- a/pw_rpc/server_test.cc
+++ b/pw_rpc/server_test.cc
@@ -19,13 +19,55 @@
 namespace pw::rpc {
 namespace {
 
+using std::byte;
+
+template <size_t buffer_size>
+class TestOutput : public ChannelOutput {
+ public:
+  constexpr TestOutput(uint32_t id) : ChannelOutput(id), sent_packet_({}) {}
+
+  span<byte> AcquireBuffer() override { return buffer_; }
+
+  void SendAndReleaseBuffer(size_t size) override {
+    sent_packet_ = {buffer_, size};
+  }
+
+  span<const byte> sent_packet() const { return sent_packet_; }
+
+ private:
+  byte buffer_[buffer_size];
+  span<const byte> sent_packet_;
+};
+
+TestOutput<512> output(1);
+
+// clang-format off
+constexpr uint8_t encoded_packet[] = {
+  // type = PacketType::kRpc
+  0x08, 0x00,
+  // channel_id = 1
+  0x10, 0x01,
+  // service_id = 42
+  0x18, 0x2a,
+  // method_id = 27
+  0x20, 0x1b,
+  // payload
+  0x82, 0x02, 0xff, 0xff,
+};
+// clang-format on
+
 TEST(Server, DoesStuff) {
-  constexpr Channel channels[] = {
-      Channel(1, nullptr),
-      Channel(2, nullptr),
+  Channel channels[] = {
+      Channel(1, &output),
+      Channel(2, &output),
   };
   Server server(channels);
-  ASSERT_EQ(server.channel_count(), 2u);
+  internal::Service service(42, {});
+  server.RegisterService(service);
+
+  server.ProcessPacket(as_bytes(span(encoded_packet)), output);
+  auto packet = output.sent_packet();
+  EXPECT_GT(packet.size(), 0u);
 }
 
 }  // namespace
diff --git a/pw_rpc/service.cc b/pw_rpc/service.cc
new file mode 100644
index 0000000..af82d19
--- /dev/null
+++ b/pw_rpc/service.cc
@@ -0,0 +1,33 @@
+// Copyright 2020 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/internal/service.h"
+
+#include "pw_rpc/internal/packet.h"
+
+namespace pw::rpc::internal {
+
+void Service::ProcessPacket(const Packet& request, Packet& response) {
+  response.set_type(PacketType::kRpc);
+  response.set_service_id(id_);
+
+  for (const Method& method : methods_) {
+    if (method.id == request.method_id()) {
+      // TODO(frolv): call the method i guess
+      response.set_method_id(method.id);
+    }
+  }
+}
+
+}  // namespace pw::rpc::internal
diff --git a/pw_rpc/service_registry.cc b/pw_rpc/service_registry.cc
new file mode 100644
index 0000000..d9182d7
--- /dev/null
+++ b/pw_rpc/service_registry.cc
@@ -0,0 +1,28 @@
+// Copyright 2020 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/internal/service_registry.h"
+
+namespace pw::rpc::internal {
+
+Service* ServiceRegistry::Find(uint32_t id) const {
+  for (Service* s = first_service_; s != nullptr; s = s->next_) {
+    if (s->id() == id) {
+      return s;
+    }
+  }
+  return nullptr;
+}
+
+}  // namespace pw::rpc::internal