[i2c_sample] Add I2c temperature sample driver

The sample driver emulates a temperature sensor
that reads the temperature through an I2C protocol.

The driver can be interacted through a
sample.i2ctemperature FIDL protocol.

Bug: 98633
Change-Id: I6d430ee8e11a343e00953a3d553e3dc56f66dd52
Reviewed-on: https://fuchsia-review.googlesource.com/c/sdk-samples/drivers/+/679384
Reviewed-by: David Gilhooley <dgilhooley@google.com>
Commit-Queue: Sarah Chan <spqchan@google.com>
Reviewed-by: Suraj Malhotra <surajmalhotra@google.com>
diff --git a/src/i2c_temperature/BUILD.bazel b/src/i2c_temperature/BUILD.bazel
new file mode 100644
index 0000000..75ae20f
--- /dev/null
+++ b/src/i2c_temperature/BUILD.bazel
@@ -0,0 +1,88 @@
+# 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.
+
+load(
+    "@rules_fuchsia//fuchsia:defs.bzl",
+    "fuchsia_cc_binary",
+    "fuchsia_component_manifest",
+    "fuchsia_driver_bytecode_bind_rules",
+    "fuchsia_driver_component",
+    "fuchsia_fidl_library",
+    "fuchsia_fidl_llcpp_library",
+    "fuchsia_package",
+)
+
+fuchsia_fidl_library(
+    name = "sample.i2ctemperature",
+    srcs = [
+        "i2c_temperature.fidl",
+    ],
+    library = "sample.i2ctemperature",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@fuchsia_sdk//fidl/zx:zx",
+    ],
+)
+
+fuchsia_fidl_llcpp_library(
+    name = "sample.i2ctemperature_cc",
+    library = ":sample.i2ctemperature",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@fuchsia_sdk//fidl/zx:zx_llcpp_cc",
+        "@fuchsia_sdk//pkg/fidl-llcpp-experimental-driver-only",
+    ],
+)
+
+fuchsia_driver_bytecode_bind_rules(
+    name = "bind_bytecode",
+    output = "i2c_temperature.bindbc",
+    rules = "i2c_temperature.bind",
+    deps = [
+        "@fuchsia_sdk//bind/fuchsia.i2c",
+    ],
+)
+
+cc_binary(
+    name = "i2c_temperature",
+    srcs = [
+        "constants.h",
+        "i2c_channel.cc",
+        "i2c_channel.h",
+        "i2c_temperature.cc",
+        "i2c_temperature.h",
+    ],
+    linkshared = True,
+    deps = [
+        ":sample.i2ctemperature_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 = "manifest",
+    src = "meta/i2c_temperature.cml",
+)
+
+fuchsia_driver_component(
+    name = "component",
+    bind_bytecode = ":bind_bytecode",
+    driver_lib = ":i2c_temperature",
+    manifest = ":manifest",
+)
+
+fuchsia_package(
+    name = "pkg",
+    package_name = "i2c_temperature",
+    visibility = ["//visibility:public"],
+    deps = [
+        ":component",
+    ],
+)
diff --git a/src/i2c_temperature/constants.h b/src/i2c_temperature/constants.h
new file mode 100644
index 0000000..501945b
--- /dev/null
+++ b/src/i2c_temperature/constants.h
@@ -0,0 +1,11 @@
+// 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.
+
+namespace i2c_temperature {
+
+// Command values sent to the I2C Device.
+constexpr uint16_t kSoftResetCommand = 0x805d;
+constexpr uint16_t kStartMeasurementCommand = 0x7866;
+
+}  // namespace i2c_temperature
diff --git a/src/i2c_temperature/i2c_channel.cc b/src/i2c_temperature/i2c_channel.cc
new file mode 100644
index 0000000..8d25781
--- /dev/null
+++ b/src/i2c_temperature/i2c_channel.cc
@@ -0,0 +1,80 @@
+// 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/i2c_channel.h"
+
+#include <endian.h>
+
+zx::status<uint16_t> I2cChannel::Read16() {
+  uint16_t value;
+  auto status = WriteReadSync(nullptr, 0, reinterpret_cast<uint8_t*>(&value), sizeof(value));
+
+  if (status.is_error()) {
+    return status.take_error();
+  }
+
+  return zx::ok(be16toh(value));
+}
+
+zx::status<> I2cChannel::Write16(uint16_t value) {
+  value = htobe16(value);
+  return WriteReadSync(reinterpret_cast<uint8_t*>(&value), sizeof(value), nullptr, 0);
+}
+
+zx::status<> I2cChannel::WriteReadSync(const uint8_t* tx_buf, size_t tx_len, uint8_t* rx_buf,
+                                       size_t rx_len) {
+  if (tx_len > fuchsia_hardware_i2c::wire::kMaxTransferSize ||
+      rx_len > fuchsia_hardware_i2c::wire::kMaxTransferSize) {
+    return zx::error(ZX_ERR_OUT_OF_RANGE);
+  }
+
+  fidl::Arena arena;
+  fidl::VectorView<uint8_t> write_data(arena, tx_len);
+  if (tx_len) {
+    memcpy(write_data.mutable_data(), tx_buf, tx_len);
+  }
+
+  auto write_transfer = fuchsia_hardware_i2c::wire::DataTransfer::WithWriteData(arena, write_data);
+  auto read_transfer =
+      fuchsia_hardware_i2c::wire::DataTransfer::WithReadSize(static_cast<uint32_t>(rx_len));
+
+  fuchsia_hardware_i2c::wire::Transaction transactions[2];
+  size_t index = 0;
+  if (tx_len > 0) {
+    transactions[index++] = fuchsia_hardware_i2c::wire::Transaction::Builder(arena)
+                                .data_transfer(write_transfer)
+                                .Build();
+  }
+
+  if (rx_len > 0) {
+    transactions[index++] = fuchsia_hardware_i2c::wire::Transaction::Builder(arena)
+                                .data_transfer(read_transfer)
+                                .Build();
+  }
+
+  if (index == 0) {
+    return zx::error(ZX_ERR_INVALID_ARGS);
+  }
+
+  const auto reply = fidl_client_.sync()->Transfer(
+      fidl::VectorView<fuchsia_hardware_i2c::wire::Transaction>::FromExternal(transactions, index));
+  if (!reply.ok()) {
+    return zx::error(reply.status());
+  }
+  if (reply->result.is_err()) {
+    return zx::error(reply->result.err());
+  }
+
+  if (rx_len > 0) {
+    const auto& read_data = reply->result.response().read_data;
+    // Truncate the returned buffer to match the behavior of the Banjo version.
+    if (read_data.count() != 1) {
+      return zx::error(ZX_ERR_IO);
+    }
+
+    memcpy(rx_buf, read_data[0].data(), std::min(rx_len, read_data[0].count()));
+  }
+
+  return zx::ok();
+}
diff --git a/src/i2c_temperature/i2c_channel.h b/src/i2c_temperature/i2c_channel.h
new file mode 100644
index 0000000..b737369
--- /dev/null
+++ b/src/i2c_temperature/i2c_channel.h
@@ -0,0 +1,28 @@
+// 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 <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+
+// Sends read and write transactions through a fuchsia.hardware.i2c client.
+class I2cChannel {
+ public:
+  explicit I2cChannel(fidl::WireClient<fuchsia_hardware_i2c::Device> client_end)
+      : fidl_client_(std::move(client_end)) {}
+
+  I2cChannel(I2cChannel&& other) noexcept = default;
+  I2cChannel& operator=(I2cChannel&& other) noexcept = default;
+
+  ~I2cChannel() = default;
+
+  // Functions that perform read/write transactions through the client.
+  // The values are converted from big-endian order to host byte order for write
+  // and vice versa for read.
+  zx::status<uint16_t> Read16();
+  zx::status<> Write16(uint16_t value);
+
+ private:
+  zx::status<> WriteReadSync(const uint8_t* tx_buf, size_t tx_len, uint8_t* rx_buf, size_t rx_len);
+
+  fidl::WireClient<fuchsia_hardware_i2c::Device> fidl_client_;
+};
diff --git a/src/i2c_temperature/i2c_temperature.bind b/src/i2c_temperature/i2c_temperature.bind
new file mode 100644
index 0000000..bbf5f17
--- /dev/null
+++ b/src/i2c_temperature/i2c_temperature.bind
@@ -0,0 +1,8 @@
+// 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.i2c;
+
+fuchsia.BIND_PROTOCOL == fuchsia.i2c.BIND_PROTOCOL.DEVICE;
+fuchsia.BIND_I2C_ADDRESS == 0xFF;
\ No newline at end of file
diff --git a/src/i2c_temperature/i2c_temperature.cc b/src/i2c_temperature/i2c_temperature.cc
new file mode 100644
index 0000000..1e77aa7
--- /dev/null
+++ b/src/i2c_temperature/i2c_temperature.cc
@@ -0,0 +1,114 @@
+// 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/i2c_temperature.h"
+
+#include <lib/driver2/record_cpp.h>
+
+#include "src/i2c_temperature/constants.h"
+
+namespace fio = fuchsia_io;
+
+namespace i2c_temperature {
+
+// static
+zx::status<std::unique_ptr<I2cTemperatureDriver>> I2cTemperatureDriver::Start(
+    fuchsia_driver_framework::wire::DriverStartArgs &start_args, fdf::UnownedDispatcher dispatcher,
+    fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
+    driver::Logger logger) {
+  auto driver = std::make_unique<I2cTemperatureDriver>(
+      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<> I2cTemperatureDriver::Run(fidl::ServerEnd<fio::Directory> outgoing_dir) {
+  auto status = outgoing_.AddProtocol<sample_i2ctemperature::Device>(this, Name());
+  if (status.status_value() != ZX_OK) {
+    FDF_SLOG(ERROR, "Failed to add protocol", KV("status", status.status_string()));
+    return status;
+  }
+
+  status = SetupI2cChannel();
+  if (status.is_error()) {
+    return status.take_error();
+  }
+
+  // Reset the sensor.
+  auto result = i2c_channel_->Write16(kSoftResetCommand);
+  if (result.is_error()) {
+    FDF_SLOG(ERROR, "Failed to send reset command: ", KV("status", result.status_string()));
+  }
+
+  return zx::ok();
+}
+
+zx::status<> I2cTemperatureDriver::SetupI2cChannel() {
+  if (i2c_channel_) {
+    return zx::ok();
+  }
+
+  auto client_endpoint = ns_.Connect<fuchsia_hardware_i2c::Device>();
+  if (client_endpoint.status_value() != ZX_OK) {
+    FDF_SLOG(ERROR, "Failed to setup I2cChannel", KV("status", client_endpoint.status_string()));
+    return client_endpoint.take_error();
+  }
+
+  auto i2c_client = fidl::WireClient(std::move(*client_endpoint), dispatcher_);
+  i2c_channel_ = std::make_unique<I2cChannel>(std::move(i2c_client));
+  return zx::ok();
+}
+
+void I2cTemperatureDriver::ReadTemperature(ReadTemperatureRequestView request,
+                                           ReadTemperatureCompleter::Sync &completer) {
+  auto channel_status = SetupI2cChannel();
+  if (channel_status.is_error()) {
+    completer.ReplyError(channel_status.status_value());
+    return;
+  }
+
+  // Send a command to start the measurement.
+  auto status = i2c_channel_->Write16(kStartMeasurementCommand);
+  if (status.is_error()) {
+    FDF_SLOG(ERROR, "Failed to send measurement command: ", KV("status", status.status_string()));
+    completer.ReplyError(status.status_value());
+    return;
+  }
+
+  // Read the temperature measurement.
+  auto temp_result = i2c_channel_->Read16();
+  if (temp_result.is_error()) {
+    FDF_SLOG(ERROR, "Failed to read temperature: ", KV("status", temp_result.status_string()));
+    completer.ReplyError(temp_result.status_value());
+    return;
+  }
+
+  auto temp_value = static_cast<uint32_t>(temp_result.value());
+  FDF_SLOG(INFO, "Received temperature: ", KV("value", temp_value));
+  return completer.ReplySuccess(temp_value);
+}
+
+void I2cTemperatureDriver::ResetSensor(ResetSensorRequestView request,
+                                       ResetSensorCompleter::Sync &completer) {
+  auto channel_status = SetupI2cChannel();
+  if (channel_status.is_error()) {
+    completer.ReplyError(channel_status.status_value());
+    return;
+  }
+
+  auto result = i2c_channel_->Write16(kSoftResetCommand);
+  if (result.is_error()) {
+    FDF_SLOG(ERROR, "Failed to send reset command: ", KV("status", result.status_string()));
+    completer.ReplyError(result.status_value());
+    return;
+  }
+}
+
+}  // namespace i2c_temperature
+
+FUCHSIA_DRIVER_RECORD_CPP_V1(i2c_temperature::I2cTemperatureDriver);
diff --git a/src/i2c_temperature/i2c_temperature.fidl b/src/i2c_temperature/i2c_temperature.fidl
new file mode 100644
index 0000000..dc93cdc
--- /dev/null
+++ b/src/i2c_temperature/i2c_temperature.fidl
@@ -0,0 +1,19 @@
+// 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.
+
+library sample.i2ctemperature;
+
+using zx;
+
+// Protocol for interacting with I2cTemperature.
+protocol Device {
+    /// Sends a command to measure the temperature, then read
+    /// the value.
+    ReadTemperature() -> (struct {
+        temperature float32;
+    }) error zx.status;
+
+    /// Sends a command to reset the sensor.
+    ResetSensor() -> (struct {}) error zx.status;
+};
diff --git a/src/i2c_temperature/i2c_temperature.h b/src/i2c_temperature/i2c_temperature.h
new file mode 100644
index 0000000..c6b9f73
--- /dev/null
+++ b/src/i2c_temperature/i2c_temperature.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_CC_I2C_TEMPERATURE_H_
+#define FUCHSIA_SDK_EXAMPLES_CC_I2C_TEMPERATURE_H_
+
+#include <fidl/fuchsia.driver.framework/cpp/wire.h>
+#include <fidl/fuchsia.hardware.i2c/cpp/wire.h>
+#include <fidl/sample.i2ctemperature/cpp/wire.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>
+#include <lib/zx/status.h>
+
+#include "i2c_channel.h"
+
+namespace i2c_temperature {
+
+// Sample driver that demonstrates how to communicate through an I2C protocol.
+class I2cTemperatureDriver : public fidl::WireServer<sample_i2ctemperature::Device> {
+ public:
+  I2cTemperatureDriver(async_dispatcher_t* dispatcher,
+                       fidl::WireSharedClient<fuchsia_driver_framework::Node> node,
+                       driver::Namespace ns, driver::Logger logger)
+      : dispatcher_(dispatcher),
+        node_(std::move(node)),
+        outgoing_(component::OutgoingDirectory::Create(dispatcher)),
+        ns_(std::move(ns)),
+        logger_(std::move(logger)) {}
+
+  virtual ~I2cTemperatureDriver() = default;
+
+  static constexpr const char* Name() { return "i2c-temperature"; }
+
+  static zx::status<std::unique_ptr<I2cTemperatureDriver>> Start(
+      fuchsia_driver_framework::wire::DriverStartArgs& start_args,
+      fdf::UnownedDispatcher dispatcher,
+      fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
+      driver::Logger logger);
+
+  // fuchsia.hardware.i2ctemperature/Device:
+  void ReadTemperature(ReadTemperatureRequestView request,
+                       ReadTemperatureCompleter::Sync& completer);
+  void ResetSensor(ResetSensorRequestView request, ResetSensorCompleter::Sync& completer);
+
+ private:
+  zx::status<> Run(fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
+
+  zx::status<> SetupI2cChannel();
+
+  async_dispatcher_t* const dispatcher_;
+
+  fidl::WireSharedClient<fuchsia_driver_framework::Node> node_;
+
+  component::OutgoingDirectory outgoing_;
+  driver::Namespace ns_;
+  driver::Logger logger_;
+
+  std::unique_ptr<I2cChannel> i2c_channel_;
+};
+
+}  // namespace i2c_temperature
+
+#endif  // FUCHSIA_SDK_EXAMPLES_CC_I2C_TEMPERATURE_H_
diff --git a/src/i2c_temperature/meta/i2c_temperature.cml b/src/i2c_temperature/meta/i2c_temperature.cml
new file mode 100644
index 0000000..8d5fcf3
--- /dev/null
+++ b/src/i2c_temperature/meta/i2c_temperature.cml
@@ -0,0 +1,20 @@
+{
+    program: {
+        runner: 'driver',
+        binary: 'lib/libi2c_temperature.so',
+        bind: 'meta/bind/i2c_temperature.bindbc'
+    },
+    use: [
+        {
+            protocol: [
+              'fuchsia.logger.LogSink',
+              'fuchsia.hardware.i2c.Device',
+            ],
+        },
+        {
+            directory: 'fuchsia.driver.compat.Service-default',
+            rights: ['rw*'],
+            path: '/fuchsia.driver.compat.Service/default',
+        }
+    ],
+}