[i2c_sample] Add test i2c controller

Add a test controller that interacts with the i2c
temperature driver via the i2c protocol. The test
controller receives commands in the form of read/write
transactions through the protocol.

It returns a fake temperature for read transactions.
Each time the controller receives a measure command, it
increase the temperature by 5. Once it receives a reset
command, it reset back to the starting temperature

Bug: 98633
Change-Id: I518bef9977a0d9ca96bd3419a32b70f77abc082a
Reviewed-on: https://fuchsia-review.googlesource.com/c/sdk-samples/drivers/+/679311
Reviewed-by: David Gilhooley <dgilhooley@google.com>
Commit-Queue: Sarah Chan <spqchan@google.com>
Reviewed-by: Renato Mangini Dias <mangini@google.com>
diff --git a/src/BUILD.bazel b/src/BUILD.bazel
index cff01c4..cccb7b1 100644
--- a/src/BUILD.bazel
+++ b/src/BUILD.bazel
@@ -13,6 +13,8 @@
     repo_name = "fuchsiasamples.com",
     deps = [
         "//src/example_driver:pkg",
+        "//src/i2c_temperature:pkg",
+        "//src/i2c_temperature:test_controller_pkg",
         "//src/qemu_edu:eductl_pkg",
         "//src/qemu_edu:pkg",
         "//src/qemu_edu:test_pkg",
diff --git a/src/i2c_temperature/BUILD.bazel b/src/i2c_temperature/BUILD.bazel
index 75ae20f..77ef42b 100644
--- a/src/i2c_temperature/BUILD.bazel
+++ b/src/i2c_temperature/BUILD.bazel
@@ -86,3 +86,55 @@
         ":component",
     ],
 )
+
+fuchsia_driver_bytecode_bind_rules(
+    name = "test_controller_bind_bytecode",
+    output = "test_controller.bindbc",
+    rules = "test_i2c_controller.bind",
+    deps = [
+        "@fuchsia_sdk//bind/fuchsia.acpi",
+    ],
+)
+
+cc_binary(
+    name = "test_controller",
+    srcs = [
+        "constants.h",
+        "test_i2c_controller.cc",
+        "test_i2c_controller.h",
+    ],
+    linkshared = True,
+    deps = [
+        "@fuchsia_sdk//fidl/fuchsia.device.fs:fuchsia.device.fs_llcpp_cc",
+        "@fuchsia_sdk//fidl/fuchsia.driver.compat:fuchsia.driver.compat_llcpp_cc",
+        "@fuchsia_sdk//fidl/fuchsia.hardware.i2c:fuchsia.hardware.i2c_llcpp_cc",
+        "@fuchsia_sdk//fidl/zx:zx_cc",
+        "@fuchsia_sdk//pkg/async-cpp",
+        "@fuchsia_sdk//pkg/driver2-llcpp",
+        "@fuchsia_sdk//pkg/fidl-llcpp-experimental-driver-only",
+        "@fuchsia_sdk//pkg/sys_component_llcpp",
+        "@fuchsia_sdk//pkg/zx-experimental-driver-only",
+    ],
+)
+
+fuchsia_component_manifest(
+    name = "test_controller_manifest",
+    src = "meta/test_i2c_controller.cml",
+)
+
+fuchsia_driver_component(
+    name = "test_controller_component",
+    bind_bytecode = ":test_controller_bind_bytecode",
+    driver_lib = ":test_controller",
+    manifest = ":test_controller_manifest",
+)
+
+fuchsia_package(
+    name = "test_controller_pkg",
+    package_name = "test_i2c_controller",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":test_controller_component",
+    ],
+)
+
diff --git a/src/i2c_temperature/meta/test_i2c_controller.cml b/src/i2c_temperature/meta/test_i2c_controller.cml
new file mode 100644
index 0000000..3ee96c4
--- /dev/null
+++ b/src/i2c_temperature/meta/test_i2c_controller.cml
@@ -0,0 +1,28 @@
+{
+    program: {
+        runner: 'driver',
+        binary: 'lib/libtest_controller.so',
+        bind: 'meta/bind/test_controller.bindbc'
+    },
+    use: [
+        {
+            protocol: [
+              'fuchsia.logger.LogSink',
+            ],
+        },
+        {
+            directory: 'fuchsia.driver.compat.Service-default',
+            rights: ['rw*'],
+            path: '/fuchsia.driver.compat.Service/default',
+        }
+    ],
+    capabilities: [
+        { protocol: 'fuchsia.hardware.i2c.Device' },
+    ],
+    expose: [
+        {
+            protocol: 'fuchsia.hardware.i2c.Device',
+            from: 'self',
+        },
+    ],
+}
diff --git a/src/i2c_temperature/test_i2c_controller.bind b/src/i2c_temperature/test_i2c_controller.bind
new file mode 100644
index 0000000..4179540
--- /dev/null
+++ b/src/i2c_temperature/test_i2c_controller.bind
@@ -0,0 +1,13 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+using fuchsia.acpi;
+
+// Bind to an unused ACPI node in Qemu or Atlas.
+// TODO(fxb/100716): Bind to a test node once it's available.
+if fuchsia.acpi.hid == "QEMU0002" {
+    true;
+} else {
+    fuchsia.acpi.hid == "GOOG000A";
+}
diff --git a/src/i2c_temperature/test_i2c_controller.cc b/src/i2c_temperature/test_i2c_controller.cc
new file mode 100644
index 0000000..3396fc9
--- /dev/null
+++ b/src/i2c_temperature/test_i2c_controller.cc
@@ -0,0 +1,185 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/i2c_temperature/test_i2c_controller.h"
+
+#include <endian.h>
+#include <fidl/fuchsia.component.decl/cpp/wire.h>
+#include <lib/driver2/record_cpp.h>
+
+#include "src/i2c_temperature/constants.h"
+
+namespace fdf {
+using namespace fuchsia_driver_framework;
+}  // namespace fdf
+
+namespace test_i2c_controller {
+
+namespace fcd = fuchsia_component_decl;
+
+namespace {
+
+constexpr uint32_t kStartingTemp = 20;
+constexpr uint32_t kTempIncrement = 5;
+
+}  // namespace
+
+zx::status<std::unique_ptr<TestI2cController>> TestI2cController::Start(
+    fdf::wire::DriverStartArgs& start_args, fdf::UnownedDispatcher dispatcher,
+    fidl::WireSharedClient<fdf::Node> node, driver::Namespace ns, driver::Logger logger) {
+  auto driver = std::make_unique<TestI2cController>(dispatcher->async_dispatcher(), std::move(node),
+                                                    std::move(ns), std::move(logger));
+  auto result = driver->Run(std::move(start_args.outgoing_dir()));
+  if (result.is_error()) {
+    return result.take_error();
+  }
+
+  return zx::ok(std::move(driver));
+}
+
+zx::status<> TestI2cController::Run(fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir) {
+  temperature_ = kStartingTemp;
+
+  auto service = [this](fidl::ServerEnd<fuchsia_hardware_i2c::Device> server_end) {
+    fidl::BindServer(dispatcher_, std::move(server_end), this);
+  };
+
+  auto status = outgoing_.AddProtocol<fuchsia_hardware_i2c::Device>(std::move(service));
+  if (status.status_value() != ZX_OK) {
+    FDF_SLOG(ERROR, "Failed to add protocol", KV("status", status.status_string()));
+    return status;
+  }
+
+  status = AddChild();
+  if (status.status_value() != ZX_OK) {
+    return status;
+  }
+
+  return outgoing_.Serve(std::move(outgoing_dir));
+}
+
+zx::status<> TestI2cController::AddChild() {
+  fidl::Arena arena;
+
+  // Offer `fuchsia.hardware.i2c.Device` to the driver that binds to the node.
+  auto protocol_offer =
+      fcd::wire::OfferProtocol::Builder(arena)
+          .source_name(arena, fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>)
+          .target_name(arena, fidl::DiscoverableProtocolName<fuchsia_hardware_i2c::Device>)
+          .dependency_type(fcd::wire::DependencyType::kStrong)
+          .Build();
+  fcd::wire::Offer offer = fcd::wire::Offer::WithProtocol(arena, protocol_offer);
+
+  // Set the properties of the node that a driver will bind to.
+  // TODO(fxb/98831): Replace this once bind library protocol definitions are available.
+  auto properties = fidl::VectorView<fdf::wire::NodeProperty>(arena, 2);
+  properties[0] = fdf::wire::NodeProperty::Builder(arena)
+                      .key(fdf::wire::NodePropertyKey::WithIntValue(1 /* BIND_PROTOCOL */))
+                      .value(fdf::wire::NodePropertyValue::WithIntValue(24))
+                      .Build();
+  properties[1] = fdf::wire::NodeProperty::Builder(arena)
+                      .key(fdf::wire::NodePropertyKey::WithIntValue(0x0A02 /* BIND_I2C_ADDRESS */))
+                      .value(fdf::wire::NodePropertyValue::WithIntValue(0xff))
+                      .Build();
+
+  auto args = fdf::wire::NodeAddArgs::Builder(arena)
+                  .name(arena, "i2c-child")
+                  .offers(fidl::VectorView<fcd::wire::Offer>::FromExternal(&offer, 1))
+                  .properties(properties)
+                  .Build();
+
+  // Create endpoints of the `NodeController` for the node.
+  auto endpoints = fidl::CreateEndpoints<fdf::NodeController>();
+  if (endpoints.is_error()) {
+    FDF_SLOG(ERROR, "Failed to create endpoint", KV("status", endpoints.status_string()));
+    return zx::error(endpoints.status_value());
+  }
+
+  auto result = node_.sync()->AddChild(args, std::move(endpoints->server), {});
+  if (!result.ok()) {
+    FDF_SLOG(ERROR, "Failed to add child", KV("status", result.status_string()));
+    return zx::error(result.status());
+  }
+
+  return zx::ok();
+}
+
+void TestI2cController::HandleWrite(const fidl::VectorView<uint8_t> write_data) {
+  // For simplicity, assume that we'll only receive integer commands from
+  // I2cTemperatureDriver.
+  if (write_data.count() > 4) {
+    return;
+  }
+
+  uint8_t bytes[4];
+  for (uint32_t i = 0; i < write_data.count(); i++) {
+    bytes[i] = write_data.at(i);
+  }
+
+  int value;
+  std::memcpy(&value, bytes, sizeof(int));
+  value = be16toh(value);
+
+  if (value == i2c_temperature::kSoftResetCommand) {
+    FDF_SLOG(INFO, "Reset command received");
+    temperature_ = kStartingTemp;
+  } else if (value == i2c_temperature::kStartMeasurementCommand) {
+    FDF_SLOG(INFO, "Measurement command received");
+    temperature_ += kTempIncrement;
+  } else {
+    FDF_SLOG(WARNING, "Received unknown command ", KV("command", value));
+  }
+}
+
+void TestI2cController::Transfer(TransferRequestView request, TransferCompleter::Sync& completer) {
+  FDF_SLOG(INFO, "Received transfer request");
+
+  if (request->transactions.count() < 1) {
+    completer.ReplyError(ZX_ERR_INVALID_ARGS);
+    return;
+  }
+
+  // Create a vector of read transfers
+  fidl::Arena allocator;
+  std::vector<fidl::VectorView<uint8_t>> read_data;
+
+  for (size_t i = 0; i < request->transactions.count(); ++i) {
+    if (!request->transactions[i].has_data_transfer()) {
+      completer.ReplyError(ZX_ERR_INVALID_ARGS);
+      return;
+    }
+
+    const auto& transfer = request->transactions[i].data_transfer();
+
+    // Handle write transaction.
+    if (transfer.is_write_data()) {
+      if (transfer.write_data().count() <= 0) {
+        completer.ReplyError(ZX_ERR_INVALID_ARGS);
+        return;
+      }
+
+      HandleWrite(transfer.write_data());
+      continue;
+    }
+
+    // Handle read transaction.
+    auto read_value = htobe16(temperature_);
+    auto read_bytes = fidl::VectorView<uint8_t>(allocator, 4);
+    for (size_t i = 0; i < 4; i++) {
+      read_bytes[i] = read_value >> (i * 8) & 0xff;
+    }
+    read_data.push_back(read_bytes);
+  }
+
+  auto read_results = fidl::VectorView<fidl::VectorView<uint8_t>>(allocator, read_data.size());
+  for (size_t i = 0; i < read_data.size(); i++) {
+    read_results[i] = read_data[i];
+  }
+
+  completer.ReplySuccess(read_results);
+}
+
+}  // namespace test_i2c_controller
+
+FUCHSIA_DRIVER_RECORD_CPP_V1(test_i2c_controller::TestI2cController);
diff --git a/src/i2c_temperature/test_i2c_controller.h b/src/i2c_temperature/test_i2c_controller.h
new file mode 100644
index 0000000..403fe81
--- /dev/null
+++ b/src/i2c_temperature/test_i2c_controller.h
@@ -0,0 +1,66 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FUCHSIA_SDK_EXAMPLES_SRC_I2C_TEMPERATURE_TESTING_TEST_I2C_CONTROLLER_H_
+#define FUCHSIA_SDK_EXAMPLES_SRC_I2C_TEMPERATURE_TESTING_TEST_I2C_CONTROLLER_H_
+
+#include <fidl/fuchsia.driver.framework/cpp/wire.h>
+#include <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+#include <lib/async/cpp/executor.h>
+#include <lib/driver2/namespace.h>
+#include <lib/driver2/structured_logger.h>
+#include <lib/fdf/cpp/dispatcher.h>
+#include <lib/sys/component/llcpp/outgoing_directory.h>
+
+namespace test_i2c_controller {
+
+// Test driver that interacts with the i2c temperature driver. It implements the I2c protocol
+// and emulates a behavior where it receives commands for a temperature and returns a fake
+// temperature. The temperature it returns increase by 5 each time it receives a measurement
+// command. When it receives a reset command, the temperature is reset back to the starting
+// value.
+class TestI2cController : public fidl::WireServer<fuchsia_hardware_i2c::Device> {
+ public:
+  TestI2cController(async_dispatcher_t* dispatcher,
+                    fidl::WireSharedClient<fuchsia_driver_framework::Node> node,
+                    driver::Namespace ns, driver::Logger logger)
+      : dispatcher_(dispatcher),
+        outgoing_(component::OutgoingDirectory::Create(dispatcher)),
+        ns_(std::move(ns)),
+        logger_(std::move(logger)),
+        node_(std::move(node)) {}
+
+  virtual ~TestI2cController() = default;
+
+  static constexpr const char* Name() { return "test-i2c-controller"; }
+
+  static zx::status<std::unique_ptr<TestI2cController>> Start(
+      fuchsia_driver_framework::wire::DriverStartArgs& start_args,
+      fdf::UnownedDispatcher dispatcher,
+      fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
+      driver::Logger logger);
+
+  void Transfer(TransferRequestView request, TransferCompleter::Sync& completer);
+
+ private:
+  zx::status<> Run(fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
+  zx::status<> AddChild();
+
+  void HandleWrite(const fidl::VectorView<uint8_t> write_data);
+
+  uint32_t temperature_;
+
+  async_dispatcher_t* const dispatcher_;
+
+  component::OutgoingDirectory outgoing_;
+  driver::Namespace ns_;
+  driver::Logger logger_;
+
+  fidl::WireSharedClient<fuchsia_driver_framework::Node> node_;
+  fidl::WireSharedClient<fuchsia_driver_framework::NodeController> controller_;
+};
+
+}  // namespace test_i2c_controller
+
+#endif  // FUCHSIA_SDK_EXAMPLES_SRC_I2C_TEMPERATURE_TESTING_TEST_I2C_CONTROLLER_H_