pw_i2c: Add i2c rpc service

Service to read/write from arbitrary i2c devices. Useful for bringup and
debugging.

Change-Id: Ife50e6126a0e718aed1cb68822ac074e3b5f5874
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155250
Reviewed-by: Jonathon Reinhart <jrreinhart@google.com>
Commit-Queue: Austin Foxley <afoxley@google.com>
Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
diff --git a/pw_i2c/BUILD.bazel b/pw_i2c/BUILD.bazel
index 98afc22..d17d9ea 100644
--- a/pw_i2c/BUILD.bazel
+++ b/pw_i2c/BUILD.bazel
@@ -17,6 +17,12 @@
     "pw_cc_library",
     "pw_cc_test",
 )
+load(
+    "//pw_protobuf_compiler:pw_proto_library.bzl",
+    "pw_proto_filegroup",
+    "pw_proto_library",
+)
+load("@rules_proto//proto:defs.bzl", "proto_library")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -157,3 +163,46 @@
         "//pw_unit_test",
     ],
 )
+
+pw_proto_filegroup(
+    name = "i2c_proto_and_options",
+    srcs = ["i2c.proto"],
+    options_files = ["i2c.options"],
+)
+
+proto_library(
+    name = "i2c_proto",
+    srcs = [":i2c_proto_and_options"],
+)
+
+pw_proto_library(
+    name = "i2c_cc",
+    deps = [":i2c_proto"],
+)
+
+pw_cc_library(
+    name = "i2c_service",
+    srcs = ["i2c_service.cc"],
+    hdrs = ["public/pw_i2c/i2c_service.h"],
+    includes = ["public"],
+    deps = [
+        ":address",
+        ":i2c_cc.pwpb_rpc",
+        ":initiator",
+        "//pw_chrono:system_clock",
+        "//pw_containers:vector",
+        "//pw_status",
+    ],
+)
+
+pw_cc_test(
+    name = "i2c_service_test",
+    srcs = ["i2c_service_test.cc"],
+    deps = [
+        ":i2c_service",
+        ":initiator_mock",
+        "//pw_containers:vector",
+        "//pw_rpc/pwpb:test_method_context",
+        "//pw_status",
+    ],
+)
diff --git a/pw_i2c/BUILD.gn b/pw_i2c/BUILD.gn
index 700f8ed..4eed45a 100644
--- a/pw_i2c/BUILD.gn
+++ b/pw_i2c/BUILD.gn
@@ -17,6 +17,7 @@
 import("$dir_pw_build/target_types.gni")
 import("$dir_pw_chrono/backend.gni")
 import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
 import("$dir_pw_unit_test/test.gni")
 
 config("public_include_path") {
@@ -70,6 +71,27 @@
   deps = [ "$dir_pw_assert" ]
 }
 
+pw_proto_library("protos") {
+  sources = [ "i2c.proto" ]
+  inputs = [ "i2c.options" ]
+  prefix = "pw_i2c"
+}
+
+pw_source_set("i2c_service") {
+  public = [ "public/pw_i2c/i2c_service.h" ]
+  sources = [ "i2c_service.cc" ]
+  public_deps = [
+    ":protos.pwpb_rpc",
+    "$dir_pw_chrono:system_clock",
+    "$dir_pw_i2c:initiator",
+  ]
+  deps = [
+    "$dir_pw_containers:vector",
+    "$dir_pw_i2c:address",
+    "$dir_pw_status",
+  ]
+}
+
 pw_source_set("mock") {
   public_configs = [ ":public_include_path" ]
   public = [ "public/pw_i2c/initiator_mock.h" ]
@@ -102,6 +124,7 @@
     ":device_test",
     ":initiator_mock_test",
     ":register_device_test",
+    ":i2c_service_test",
   ]
 }
 
@@ -138,6 +161,18 @@
   ]
 }
 
+pw_test("i2c_service_test") {
+  enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+  sources = [ "i2c_service_test.cc" ]
+  deps = [
+    ":i2c_service",
+    "$dir_pw_containers:vector",
+    "$dir_pw_i2c:mock",
+    "$dir_pw_rpc/pwpb:test_method_context",
+    "$dir_pw_status",
+  ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
 }
diff --git a/pw_i2c/docs.rst b/pw_i2c/docs.rst
index 4a96584..3b0b160 100644
--- a/pw_i2c/docs.rst
+++ b/pw_i2c/docs.rst
@@ -92,3 +92,42 @@
 pw::i2c::GmockInitiator
 -----------------------
 gMock of Initiator used for testing and mocking out the Initiator.
+
+I2c Debug Service
+=================
+This module implements an I2C register access service for debugging and bringup.
+To use, provide it with a callback function that returns an ``Initiator`` for
+the specified ``bus_index``.
+
+Example invocations
+-------------------
+Using the pigweed console, you can invoke the service to perform an I2C read:
+
+.. code:: python
+
+  device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=0x22, register_address=b'\x0e', read_size=1)
+
+The above shows reading register 0x0e on a device located at
+I2C address 0x22.
+
+For peripherals that support 4 byte register width, you can specify as:
+
+.. code:: python
+
+  device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=<address>, register_address=b'\x00\x00\x00\x00', read_size=4)
+
+
+And similarly, for performing I2C write:
+
+.. code:: python
+
+  device.rpcs.pw.i2c.I2c.I2cWrite(bus_index=0, target_address=0x22,register_address=b'\x0e', value=b'\xbc')
+
+
+Similarly, multi-byte writes can also be specified with the bytes fields for
+`register_address` and `value`.
+
+I2C peripherals that require multi-byte access may expect a specific endianness.
+The order of bytes specified in the bytes field will match the order of bytes
+sent/received on the bus. Maximum supported value for multi-byte access is
+4 bytes.
diff --git a/pw_i2c/i2c.options b/pw_i2c/i2c.options
new file mode 100644
index 0000000..09e71ff
--- /dev/null
+++ b/pw_i2c/i2c.options
@@ -0,0 +1,19 @@
+// 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.
+
+pw.i2c.I2cWriteRequest.register_address max_size:4
+pw.i2c.I2cWriteRequest.value max_size:32
+pw.i2c.I2cReadRequest.register_address max_size:4
+pw.i2c.I2cReadResponse.value max_size:32
+
diff --git a/pw_i2c/i2c.proto b/pw_i2c/i2c.proto
new file mode 100644
index 0000000..4080410
--- /dev/null
+++ b/pw_i2c/i2c.proto
@@ -0,0 +1,52 @@
+// 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.
+syntax = "proto3";
+
+package pw.i2c;
+
+message I2cWriteRequest {
+  // Which I2C initiator bus to communicate on.
+  uint32 bus_index = 1;
+  // 7-bit I2C target address to write to.
+  uint32 target_address = 2;
+  // Register address to write. Follow the endianness required by the
+  // peripheral for multi-byte address.
+  bytes register_address = 3;
+  // Value to write. Follow the endianness required by the peripheral.
+  bytes value = 4;
+}
+
+message I2cWriteResponse {}
+
+message I2cReadRequest {
+  // Which I2C initiator bus to communicate on.
+  uint32 bus_index = 1;
+  // 7-bit I2C target address to read from.
+  uint32 target_address = 2;
+  // Register address to write. Follow the endianness required by the
+  // peripheral for multi-byte address.
+  bytes register_address = 3;
+  // Expected number of bytes from the peripheral.
+  uint32 read_size = 4;
+}
+
+message I2cReadResponse {
+  bytes value = 1;
+}
+
+service I2c {
+  // Enable access to I2C devices implementing register read/writes.
+  rpc I2cWrite(I2cWriteRequest) returns (I2cWriteResponse) {}
+  rpc I2cRead(I2cReadRequest) returns (I2cReadResponse) {}
+}
diff --git a/pw_i2c/i2c_service.cc b/pw_i2c/i2c_service.cc
new file mode 100644
index 0000000..5d6da05
--- /dev/null
+++ b/pw_i2c/i2c_service.cc
@@ -0,0 +1,90 @@
+// 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_i2c/i2c_service.h"
+
+#include <algorithm>
+#include <chrono>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/vector.h"
+#include "pw_i2c/address.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_status/status.h"
+
+namespace pw::i2c {
+namespace {
+
+constexpr auto kI2cTimeout =
+    chrono::SystemClock::for_at_least(std::chrono::milliseconds(100));
+
+}  // namespace
+
+void I2cService::I2cWrite(
+    const pwpb::I2cWriteRequest::Message& request,
+    rpc::PwpbUnaryResponder<pwpb::I2cWriteResponse::Message>& responder) {
+  Initiator* initiator = initiator_selector_(request.bus_index);
+  if (initiator == nullptr) {
+    responder.Finish({}, Status::InvalidArgument()).IgnoreError();
+    return;
+  }
+
+  // Get the underlying buffer size of the register_address and value fields.
+  pwpb::I2cWriteRequest::Message size_message;
+  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
+  constexpr auto kMaxWriteSize =
+      size_message.register_address.max_size() + size_message.value.max_size();
+
+  Vector<std::byte, kMaxWriteSize> write_buffer{};
+  write_buffer.assign(std::begin(request.register_address),
+                      std::end(request.register_address));
+  std::copy(std::begin(request.value),
+            std::end(request.value),
+            std::back_inserter(write_buffer));
+  auto result = initiator->WriteFor(
+      Address{static_cast<uint16_t>(request.target_address)},
+      write_buffer,
+      kI2cTimeout);
+  responder.Finish({}, result).IgnoreError();
+}
+
+void I2cService::I2cRead(
+    const pwpb::I2cReadRequest::Message& request,
+    rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& responder) {
+  // Get the underlying buffer size of the ReadResponse message.
+  pwpb::I2cReadResponse::Message size_message;
+  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
+  constexpr auto kMaxReadSize = size_message.value.max_size();
+
+  Initiator* initiator = initiator_selector_(request.bus_index);
+  if (initiator == nullptr || request.read_size > kMaxReadSize) {
+    responder.Finish({}, Status::InvalidArgument()).IgnoreError();
+    return;
+  }
+  Vector<std::byte, kMaxReadSize> value{};
+  value.resize(request.read_size);
+  auto result = initiator->WriteReadFor(
+      Address{static_cast<uint16_t>(request.target_address)},
+      request.register_address,
+      {value.data(), value.size()},
+      kI2cTimeout);
+
+  if (result.ok()) {
+    responder.Finish({value}, OkStatus()).IgnoreError();
+  } else {
+    responder.Finish({}, result).IgnoreError();
+  }
+}
+
+}  // namespace pw::i2c
diff --git a/pw_i2c/i2c_service_test.cc b/pw_i2c/i2c_service_test.cc
new file mode 100644
index 0000000..7f70dda
--- /dev/null
+++ b/pw_i2c/i2c_service_test.cc
@@ -0,0 +1,245 @@
+// 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_i2c/i2c_service.h"
+
+#include <algorithm>
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_i2c/initiator_mock.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_status/status.h"
+
+namespace pw::i2c {
+namespace {
+
+auto MakeSingletonSelector(Initiator* initiator) {
+  return [initiator](size_t pos) { return pos == 0 ? initiator : nullptr; };
+}
+
+TEST(I2cServiceTest, I2cWriteSingleByteOk) {
+  Vector<std::byte, 4> register_addr{};
+  Vector<std::byte, 4> register_value{};
+  constexpr auto kExpectWrite = bytes::Array<0x02, 0x03>();
+  register_addr.push_back(kExpectWrite[0]);
+  register_value.push_back(kExpectWrite[1]);
+  auto transactions = MakeExpectedTransactionArray(
+      {Transaction(OkStatus(),
+                   Address{0x01},
+                   kExpectWrite,
+                   {},
+                   std::chrono::milliseconds(100))});
+  MockInitiator i2c_initiator(transactions);
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 0,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .value = register_value});
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), OkStatus());
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cWriteMultiByteOk) {
+  constexpr int kWriteSize = 4;
+  Vector<std::byte, kWriteSize> register_addr{};
+  Vector<std::byte, kWriteSize> register_value{};
+  constexpr auto kExpectWrite =
+      bytes::Array<0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09>();
+  std::copy(kExpectWrite.begin(),
+            kExpectWrite.begin() + kWriteSize,
+            std::back_inserter(register_addr));
+  std::copy(kExpectWrite.begin() + kWriteSize,
+            kExpectWrite.end(),
+            std::back_inserter(register_value));
+  auto transactions = MakeExpectedTransactionArray(
+      {Transaction(OkStatus(),
+                   Address{0x01},
+                   kExpectWrite,
+                   {},
+                   std::chrono::milliseconds(100))});
+  MockInitiator i2c_initiator(transactions);
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 0,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .value = register_value});
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), OkStatus());
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cWriteInvalidBusIndex) {
+  Vector<std::byte, 4> register_addr{};
+  Vector<std::byte, 4> register_value{};
+
+  MockInitiator i2c_initiator({});
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 1,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .value = register_value});
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), Status::InvalidArgument());
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadSingleByteOk) {
+  constexpr auto kExpectWrite = bytes::Array<0x02>();
+  constexpr auto kExpectRead = bytes::Array<0x03>();
+  Vector<std::byte, 4> register_addr{};
+  register_addr.push_back(kExpectWrite[0]);
+
+  auto transactions = MakeExpectedTransactionArray(
+      {Transaction(OkStatus(),
+                   Address{0x01},
+                   kExpectWrite,
+                   kExpectRead,
+                   std::chrono::milliseconds(100))});
+  MockInitiator i2c_initiator(transactions);
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 0,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .read_size = static_cast<uint32_t>(kExpectRead.size())});
+
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), OkStatus());
+  for (size_t i = 0; i < kExpectRead.size(); ++i) {
+    EXPECT_EQ(kExpectRead[i], context.response().value[i]);
+  }
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMultiByteOk) {
+  constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+  constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>();
+  Vector<std::byte, 4> register_addr{};
+  std::copy(kExpectWrite.begin(),
+            kExpectWrite.end(),
+            std::back_inserter(register_addr));
+  auto transactions = MakeExpectedTransactionArray(
+      {Transaction(OkStatus(),
+                   Address{0x01},
+                   kExpectWrite,
+                   kExpectRead,
+                   std::chrono::milliseconds(100))});
+  MockInitiator i2c_initiator(transactions);
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 0,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .read_size = static_cast<uint32_t>(kExpectRead.size())});
+
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), OkStatus());
+  for (size_t i = 0; i < kExpectRead.size(); ++i) {
+    EXPECT_EQ(kExpectRead[i], context.response().value[i]);
+  }
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMaxByteOk) {
+  constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+  constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>();
+  pwpb::I2cReadResponse::Message size_message;
+  static_assert(sizeof(kExpectRead) <= size_message.value.max_size());
+
+  Vector<std::byte, 4> register_addr{};
+  std::copy(kExpectWrite.begin(),
+            kExpectWrite.end(),
+            std::back_inserter(register_addr));
+  auto transactions = MakeExpectedTransactionArray(
+      {Transaction(OkStatus(),
+                   Address{0x01},
+                   kExpectWrite,
+                   kExpectRead,
+                   std::chrono::milliseconds(100))});
+  MockInitiator i2c_initiator(transactions);
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({
+      .bus_index = 0,
+      .target_address = 0x01,
+      .register_address = register_addr,
+      .read_size = sizeof(kExpectRead),
+  });
+
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), OkStatus());
+  // EXPECT_EQ(kExpectRead, context.response().value);
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMultiByteOutOfBounds) {
+  pwpb::I2cReadResponse::Message response_message;
+  constexpr auto kMaxReadSize = response_message.value.max_size();
+  constexpr auto kRegisterAddr = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+  Vector<std::byte, 4> register_addr{};
+  std::copy(kRegisterAddr.begin(),
+            kRegisterAddr.end(),
+            std::back_inserter(register_addr));
+  MockInitiator i2c_initiator({});
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 0,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .read_size = kMaxReadSize + 1});
+
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), Status::InvalidArgument());
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadInvalidBusIndex) {
+  Vector<std::byte, 4> register_addr{};
+  MockInitiator i2c_initiator({});
+
+  PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+  context{MakeSingletonSelector(&i2c_initiator)};
+
+  context.call({.bus_index = 1,
+                .target_address = 0x01,
+                .register_address = register_addr,
+                .read_size = 1});
+
+  EXPECT_TRUE(context.done());
+  EXPECT_EQ(context.status(), Status::InvalidArgument());
+  EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+}  // namespace
+}  // namespace pw::i2c
diff --git a/pw_i2c/public/pw_i2c/i2c_service.h b/pw_i2c/public/pw_i2c/i2c_service.h
new file mode 100644
index 0000000..7b029b1
--- /dev/null
+++ b/pw_i2c/public/pw_i2c/i2c_service.h
@@ -0,0 +1,50 @@
+// 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 <array>
+#include <cstddef>
+#include <memory>
+#include <utility>
+
+#include "pw_function/function.h"
+#include "pw_i2c/i2c.pwpb.h"
+#include "pw_i2c/i2c.rpc.pwpb.h"
+#include "pw_i2c/initiator.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+
+namespace pw::i2c {
+
+// RPC service to perform I2C transactions.
+class I2cService final : public pw_rpc::pwpb::I2c::Service<I2cService> {
+ public:
+  // Callback which returns an initiator for the given position or nullptr if
+  // the position not valid on this device.
+  using InitiatorSelector = pw::Function<Initiator*(size_t pos)>;
+
+  explicit I2cService(InitiatorSelector&& initiator_selector)
+      : initiator_selector_(std::move(initiator_selector)) {}
+
+  void I2cWrite(
+      const pwpb::I2cWriteRequest::Message& request,
+      pw::rpc::PwpbUnaryResponder<pwpb::I2cWriteResponse::Message>& responder);
+  void I2cRead(
+      const pwpb::I2cReadRequest::Message& request,
+      pw::rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& responder);
+
+ private:
+  InitiatorSelector initiator_selector_;
+};
+
+}  // namespace pw::i2c