pw_imu: Driver for icm42670p IMU

Change-Id: I05c3ccc5a49ff71501d322a47bd264fed8f77c3f
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/kudzu/+/175692
Reviewed-by: Erik Gilling <konkers@google.com>
Commit-Queue: Asad Memon <asadmemon@google.com>
diff --git a/applications/app_common/BUILD.gn b/applications/app_common/BUILD.gn
index 161ee7c..64bfca0 100644
--- a/applications/app_common/BUILD.gn
+++ b/applications/app_common/BUILD.gn
@@ -30,6 +30,7 @@
     "$dir_pw_display",
     "$dir_pw_status",
     "$dir_pw_thread:thread",
+    "//lib/kudzu_imu",
     "//lib/pw_touchscreen",
   ]
   public = [ "public/app_common/common.h" ]
diff --git a/applications/app_common/public/app_common/common.h b/applications/app_common/public/app_common/common.h
index dfe018c..6596189 100644
--- a/applications/app_common/public/app_common/common.h
+++ b/applications/app_common/public/app_common/common.h
@@ -13,6 +13,7 @@
 // the License.
 #pragma once
 
+#include "kudzu_imu/imu.h"
 #include "pw_display/display.h"
 #include "pw_status/status.h"
 #include "pw_thread/thread.h"
@@ -33,6 +34,9 @@
   // Return an initialized display.
   static pw::display::Display& GetDisplay();
 
+  // Return an initialized display.
+  static kudzu::imu::PollingImu& GetImu();
+
   // Return an initialized touchscreen.
   static pw::touchscreen::Touchscreen& GetTouchscreen();
 
diff --git a/applications/app_common_impl/BUILD.gn b/applications/app_common_impl/BUILD.gn
index 3a9484b..2a54966 100644
--- a/applications/app_common_impl/BUILD.gn
+++ b/applications/app_common_impl/BUILD.gn
@@ -77,6 +77,7 @@
   "//applications/app_common:app_common.facade",
   "//lib/ft6236",
   "//lib/icm42670p",
+  "//lib/kudzu_imu_icm42670p",
   "//lib/max17948",
   "//lib/tca9535",
 ]
@@ -121,6 +122,7 @@
     "$dir_pw_thread:thread",
     "$dir_pw_thread_stl:thread",
     "//applications/app_common:app_common.facade",
+    "//lib/kudzu_imu_imgui",
     "//lib/pw_touchscreen_imgui",
   ]
   sources = [ "common_host_imgui.cc" ]
diff --git a/applications/app_common_impl/common_host_imgui.cc b/applications/app_common_impl/common_host_imgui.cc
index 26d5447..b9d3675 100644
--- a/applications/app_common_impl/common_host_imgui.cc
+++ b/applications/app_common_impl/common_host_imgui.cc
@@ -12,6 +12,7 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 #include "app_common/common.h"
+#include "kudzu_imu_imgui/imu.h"
 #include "pw_color/color.h"
 #include "pw_display_driver_imgui/display_driver.h"
 #include "pw_display_imgui/display.h"
@@ -71,6 +72,11 @@
   return s_touchscreen;
 }
 
+kudzu::imu::PollingImu& Common::GetImu() {
+  static kudzu::imu::PollingImuImGui s_imu = kudzu::imu::PollingImuImGui();
+  return s_imu;
+}
+
 const pw::thread::Options& Common::DisplayDrawThreadOptions() {
   static pw::thread::stl::Options display_draw_thread_options;
   return display_draw_thread_options;
diff --git a/applications/app_common_impl/common_pico.cc b/applications/app_common_impl/common_pico.cc
index 40192b9..0e1c43a 100644
--- a/applications/app_common_impl/common_pico.cc
+++ b/applications/app_common_impl/common_pico.cc
@@ -24,6 +24,7 @@
 #include "hardware/pwm.h"
 #include "hardware/vreg.h"
 #include "icm42670p/device.h"
+#include "kudzu_imu_icm42670p/imu.h"
 #include "max17948/device.h"
 #include "pico/stdlib.h"
 #include "pw_digital_io_rp2040/digital_io.h"
@@ -232,7 +233,7 @@
 pw::i2c::PicoInitiator i2c1_bus(ki2c1Config);
 
 pw::tca9535::Device io_expander(i2c1_bus);
-pw::icm42670p::Device imu(i2c0_bus);
+kudzu::icm42670p::Device imu(i2c0_bus);
 pw::max17948::Device fuel_guage(i2c0_bus);
 pw::ft6236::Device touch_screen_controller(i2c0_bus);
 
@@ -356,6 +357,11 @@
   return s_touchscreen;
 }
 
+kudzu::imu::PollingImu& Common::GetImu() {
+  static kudzu::imu::PollingImuICM42670P s_imu(&imu);
+  return s_imu;
+}
+
 const pw::thread::Options& Common::DisplayDrawThreadOptions() {
   static constexpr auto options =
       pw::thread::freertos::Options()
diff --git a/applications/badge/BUILD.gn b/applications/badge/BUILD.gn
index 3eb7243..49e8030 100644
--- a/applications/badge/BUILD.gn
+++ b/applications/badge/BUILD.gn
@@ -50,6 +50,7 @@
     "$pw_dir_third_party_32blit:32blit",
     "//applications/app_common",
     "//lib/framecounter",
+    "//lib/kudzu_imu",
     "//lib/random",
   ]
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
diff --git a/lib/icm42670p/BUILD.gn b/lib/icm42670p/BUILD.gn
index 73e8fdb..e0b632a 100644
--- a/lib/icm42670p/BUILD.gn
+++ b/lib/icm42670p/BUILD.gn
@@ -23,6 +23,7 @@
   public_configs = [ ":default_config" ]
   public_deps = [
     "$dir_pw_i2c:initiator",
+    "$dir_pw_result",
     "$dir_pw_status",
   ]
   public = [ "public/icm42670p/device.h" ]
@@ -30,6 +31,7 @@
     "$dir_pw_digital_io",
     "$dir_pw_i2c:register_device",
     "$dir_pw_log",
+    "//lib/kudzu_imu:kudzu_imu",
   ]
   sources = [ "device.cc" ]
   remove_configs = [ "$dir_pw_build:strict_warnings" ]
diff --git a/lib/icm42670p/device.cc b/lib/icm42670p/device.cc
index 5c78506..ab1b4c3 100644
--- a/lib/icm42670p/device.cc
+++ b/lib/icm42670p/device.cc
@@ -25,12 +25,25 @@
 #include "pw_i2c/address.h"
 #include "pw_i2c/register_device.h"
 #include "pw_log/log.h"
+#include "pw_result/result.h"
 #include "pw_status/status.h"
 
 using ::pw::Status;
 using namespace std::chrono_literals;
 
-namespace pw::icm42670p {
+namespace kudzu::icm42670p {
+
+enum Icm42670pRegister : uint8_t {
+  kWhoAmI = 0x75,
+  kPwrMgmt0 = 0x1f,
+  kGyroConfig0 = 0x20,
+  kAccelConfig0 = 0x21,
+  kAccelDataX1 = 0x0b,
+  kGyroDataX1 = 0x11,
+};
+
+constexpr int16_t GYRO_UI_FS_SEL = 1000;
+constexpr float ACCEL_UI_FS_SEL = 8.0f;
 
 namespace {
 
@@ -62,20 +75,128 @@
   }
 }
 
+pw::Result<kudzu::imu::ImuSample> ReadData(pw::i2c::RegisterDevice& device) {
+  // TODO: asadmemon - Check the status register before reading the values.
+
+  uint8_t data[12];
+  device.ReadRegisters8(Icm42670pRegister::kAccelDataX1,
+                        pw::span(data, 12),
+                        pw::chrono::SystemClock::for_at_least(10ms));
+
+  uint16_t accel_x =
+      pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[0]);
+  uint16_t accel_y =
+      pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[2]);
+  uint16_t accel_z =
+      pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[4]);
+
+  uint16_t gyro_x = pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[6]);
+  uint16_t gyro_y = pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[8]);
+  uint16_t gyro_z =
+      pw::bytes::ReadInOrder<uint16_t>(pw::endian::big, &data[10]);
+
+  float f_accel_x =
+      static_cast<float>((int16_t)accel_x) * ACCEL_UI_FS_SEL / INT16_MAX;
+  float f_accel_y =
+      static_cast<float>((int16_t)accel_y) * ACCEL_UI_FS_SEL / INT16_MAX;
+  float f_accel_z =
+      static_cast<float>((int16_t)accel_z) * ACCEL_UI_FS_SEL / INT16_MAX;
+
+  int16_t f_gyro_x =
+      static_cast<int16_t>((int16_t)gyro_x) * GYRO_UI_FS_SEL / INT16_MAX;
+  int16_t f_gyro_y =
+      static_cast<int16_t>((int16_t)gyro_y) * GYRO_UI_FS_SEL / INT16_MAX;
+  int16_t f_gyro_z =
+      static_cast<int16_t>((int16_t)gyro_z) * GYRO_UI_FS_SEL / INT16_MAX;
+
+  kudzu::imu::ImuSample sample = {
+      {f_accel_x, f_accel_y, f_accel_z},
+      {f_gyro_x, f_gyro_y, f_gyro_z},
+  };
+  return sample;
+}
+
 }  // namespace
 
 Device::Device(pw::i2c::Initiator& initiator)
     : initiator_(initiator),
       device_(initiator,
               kAddress,
-              endian::little,
+              pw::endian::little,
               pw::i2c::RegisterAddressSize::k1Byte) {}
 
-Status Device::Enable() {
+pw::Status Device::Enable() {
   device_.WriteRegister8(
       0x1f, 0x0f, pw::chrono::SystemClock::for_at_least(10ms));
 
-  return OkStatus();
+  /*
+  +----------------+-----------------------------------+
+  | ACCEL_CONFIG0  |                                   |
+  +----------------+-----------------------------------+
+  | Name           | ACCEL_CONFIG0                     |
+  | Address        | 33 (21h)                          |
+  | Serial IF      | R/W                               |
+  | Reset value    | 0x06                              |
+  +----------------+-----------------------------------+
+  | BIT  | NAME            | FUNCTION                  |
+  +----------------+-----------------------------------+
+  | 7    | -               | Reserved                  |
+  | 6:5  | ACCEL_UI_FS_SEL | 00: ±16g                  |
+  |      |                 | 01: ±8g                   |
+  |      |                 | 10: ±4g                   |
+  |      |                 | 11: ±2g                   |
+  | 4    | -               | Reserved                  |
+  | 3:0  | ACCEL_ODR       | 0101: 1.6k Hz (LN mode)   |
+  |      |                 | 0110: 800 Hz (LN mode)    |
+  |      |                 | 0111: 400 Hz (LP or LN)   |
+  |      |                 | 1000: 200 Hz (LP or LN)   |
+  |      |                 | 1001: 100 Hz (LP or LN)   |
+  |      |                 | 1010: 50 Hz (LP or LN)    |
+  |      |                 | 1011: 25 Hz (LP or LN)    |
+  |      |                 | 1100: 12.5 Hz (LP or LN)  |
+  |      |                 | 1101: 6.25 Hz (LP mode)   |
+  |      |                 | 1110: 3.125 Hz (LP mode)  |
+  |      |                 | 1111: 1.5625 Hz (LP mode) |
+  +----------------+-----------------------------------+
+  */
+  uint8_t new_accel_config = 0b00101000;
+  device_.WriteRegister8(Icm42670pRegister::kAccelConfig0,
+                         new_accel_config,
+                         pw::chrono::SystemClock::for_at_least(10ms));
+
+  /*
+  +--------------+------------------------+
+  | GYRO_CONFIG0 |                        |
+  +--------------+------------------------+
+  | Name         | GYRO_CONFIG0           |
+  | Address      | 32 (20h)               |
+  | Serial IF    | R/W                    |
+  | Reset value  | 0x06                   |
+  +--------------+------------------------+
+  | BIT  | NAME           | FUNCTION      |
+  +--------------+------------------------+
+  | 7    | -              | Reserved      |
+  | 6:5  | GYRO_UI_FS_SEL | 00: ±2000 dps |
+  |      |                | 01: ±1000 dps |
+  |      |                | 10: ±500 dps  |
+  |      |                | 11: ±250 dps  |
+  | 4    | -              | Reserved      |
+  | 3:0  | GYRO_ODR       | 0101: 1.6k Hz |
+  |      |                | 0110: 800 Hz  |
+  |      |                | 0111: 400 Hz  |
+  |      |                | 1000: 200 Hz  |
+  |      |                | 1001: 100 Hz  |
+  |      |                | 1010: 50 Hz   |
+  |      |                | 1011: 25 Hz   |
+  |      |                | 1100: 12.5 Hz |
+  +--------------+------------------------+
+  */
+  uint8_t new_gyro_config = 0b00101000;
+  device_.WriteRegister8(Icm42670pRegister::kGyroConfig0,
+                         new_gyro_config,
+                         pw::chrono::SystemClock::for_at_least(10ms));
+
+  return pw::OkStatus();
 }
 
 Status Device::Probe() {
@@ -94,10 +215,14 @@
   device_.WriteRegister8(
       0x1f, 0x0f, pw::chrono::SystemClock::for_at_least(10ms));
 
-  ImuReadReg(device_, 0x75, "WHO_AM_I");
-  ImuReadReg(device_, 0x1f, "PWR_MGMT0");
-  ImuReadReg(device_, 0x0c, "X0");
-  ImuReadReg(device_, 0x0b, "X1");
+  ImuReadReg(device_, Icm42670pRegister::kWhoAmI, "WHO_AM_I");
+  ImuReadReg(device_, Icm42670pRegister::kPwrMgmt0, "PWR_MGMT0");
+  ImuReadReg(device_, Icm42670pRegister::kGyroConfig0, "GYRO_CONFIG0");
+  ImuReadReg(device_, Icm42670pRegister::kAccelConfig0, "ACCEL_CONFIG0");
 }
 
-}  // namespace pw::icm42670p
+pw::Result<kudzu::imu::ImuSample> Device::ReadValues() {
+  return ReadData(device_);
+}
+
+}  // namespace kudzu::icm42670p
diff --git a/lib/icm42670p/public/icm42670p/device.h b/lib/icm42670p/public/icm42670p/device.h
index 288e7a1..f88e790 100644
--- a/lib/icm42670p/public/icm42670p/device.h
+++ b/lib/icm42670p/public/icm42670p/device.h
@@ -16,25 +16,27 @@
 #include <array>
 #include <cstdint>
 
+#include "kudzu_imu/imu.h"
 #include "pw_i2c/address.h"
 #include "pw_i2c/initiator.h"
 #include "pw_i2c/register_device.h"
 #include "pw_status/status.h"
 
-namespace pw::icm42670p {
+namespace kudzu::icm42670p {
 
 class Device {
  public:
   Device(pw::i2c::Initiator& initiator);
   ~Device() = default;
 
-  Status Enable();
-  Status Probe();
+  pw::Status Enable();
+  pw::Status Probe();
   void LogControllerInfo();
+  pw::Result<kudzu::imu::ImuSample> ReadValues();
 
  private:
   pw::i2c::Initiator& initiator_;
   pw::i2c::RegisterDevice device_;
 };
 
-}  // namespace pw::icm42670p
+}  // namespace kudzu::icm42670p
diff --git a/lib/kudzu_imu/BUILD.gn b/lib/kudzu_imu/BUILD.gn
new file mode 100644
index 0000000..68ef3c0
--- /dev/null
+++ b/lib/kudzu_imu/BUILD.gn
@@ -0,0 +1,27 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("public_includes") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_imu") {
+  public_configs = [ ":public_includes" ]
+  public = [ "public/kudzu_imu/imu.h" ]
+  public_deps = [ "$dir_pw_status" ]
+}
diff --git a/lib/kudzu_imu/public/kudzu_imu/imu.h b/lib/kudzu_imu/public/kudzu_imu/imu.h
new file mode 100644
index 0000000..d5c425a
--- /dev/null
+++ b/lib/kudzu_imu/public/kudzu_imu/imu.h
@@ -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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace kudzu::imu {
+
+struct AccelerometerData {
+  float x, y, z;  // g / axis
+};
+
+struct GyroscopeData {
+  int16_t x, y, z;  // dps / axis
+};
+
+struct ImuSample {
+  AccelerometerData accelerometer;
+  GyroscopeData gyroscope;
+};
+
+// Abstract base class for Polling IMU
+class PollingImu {
+ public:
+  virtual ~PollingImu() = default;
+
+  // Initialize the IMU controller.
+  virtual pw::Status Init() = 0;
+
+  // Return true if the controller is ready.
+  virtual bool IsAvailable() = 0;
+
+  // Retrieve IMU data. The concrete implementation would typically communicate
+  // with the actual IMU hardware to get this data.
+  virtual pw::Result<ImuSample> ReadData() = 0;
+};
+
+}  // namespace kudzu::imu
diff --git a/lib/kudzu_imu_icm42670p/BUILD.gn b/lib/kudzu_imu_icm42670p/BUILD.gn
new file mode 100644
index 0000000..fdc2bcb
--- /dev/null
+++ b/lib/kudzu_imu_icm42670p/BUILD.gn
@@ -0,0 +1,33 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_imu_icm42670p") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/kudzu_imu_icm42670p/imu.h" ]
+  deps = [
+    "$dir_pw_log",
+    "$dir_pw_result",
+    "//lib/icm42670p",
+    "//lib/kudzu_imu",
+  ]
+  sources = [ "imu.cc" ]
+}
diff --git a/lib/kudzu_imu_icm42670p/imu.cc b/lib/kudzu_imu_icm42670p/imu.cc
new file mode 100644
index 0000000..4d3cd12
--- /dev/null
+++ b/lib/kudzu_imu_icm42670p/imu.cc
@@ -0,0 +1,51 @@
+// 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 "kudzu_imu/imu.h"
+
+#include <math.h>
+
+#include <cinttypes>
+
+#define PW_LOG_MODULE_NAME "kudzu_imu_icm42670p"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "icm42670p/device.h"
+#include "kudzu_imu_icm42670p/imu.h"
+#include "pw_log/log.h"
+
+namespace kudzu::imu {
+
+PollingImuICM42670P::PollingImuICM42670P(
+    kudzu::icm42670p::Device* imu_controller)
+    : imu_controller_(imu_controller) {}
+
+pw::Status PollingImuICM42670P::Init() {
+  imu_controller_->Enable();
+  imu_controller_->LogControllerInfo();
+  return pw::OkStatus();
+}
+
+bool PollingImuICM42670P::IsAvailable() { return true; }
+
+pw::Result<ImuSample> PollingImuICM42670P::ReadData() {
+  pw::Result<ImuSample> data = imu_controller_->ReadValues();
+
+  if (data.ok()) {
+    last_data = data.value();
+  }
+  return data;
+}
+
+}  // namespace kudzu::imu
diff --git a/lib/kudzu_imu_icm42670p/public/kudzu_imu_icm42670p/imu.h b/lib/kudzu_imu_icm42670p/public/kudzu_imu_icm42670p/imu.h
new file mode 100644
index 0000000..a0a35ca
--- /dev/null
+++ b/lib/kudzu_imu_icm42670p/public/kudzu_imu_icm42670p/imu.h
@@ -0,0 +1,35 @@
+// 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 "kudzu_imu/imu.h"
+#include "pw_status/status.h"
+
+namespace kudzu::imu {
+
+class PollingImuICM42670P : public PollingImu {
+ public:
+  PollingImuICM42670P(kudzu::icm42670p::Device* imu_controller);
+
+  pw::Status Init() override;
+  bool IsAvailable() override;
+  pw::Result<ImuSample> ReadData() override;
+
+  ImuSample last_data;
+
+ private:
+  kudzu::icm42670p::Device* imu_controller_;
+};
+
+}  // namespace kudzu::imu
diff --git a/lib/kudzu_imu_imgui/BUILD.gn b/lib/kudzu_imu_imgui/BUILD.gn
new file mode 100644
index 0000000..c10a1a5
--- /dev/null
+++ b/lib/kudzu_imu_imgui/BUILD.gn
@@ -0,0 +1,32 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_source_set("kudzu_imu_imgui") {
+  public_configs = [ ":default_config" ]
+  public = [ "public/kudzu_imu_imgui/imu.h" ]
+  deps = [
+    "$dir_pw_log",
+    "$dir_pw_result",
+    "//lib/kudzu_imu:kudzu_imu",
+  ]
+  sources = [ "imu.cc" ]
+}
diff --git a/lib/kudzu_imu_imgui/imu.cc b/lib/kudzu_imu_imgui/imu.cc
new file mode 100644
index 0000000..f0aa56a
--- /dev/null
+++ b/lib/kudzu_imu_imgui/imu.cc
@@ -0,0 +1,43 @@
+// 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 "kudzu_imu/imu.h"
+
+#include <math.h>
+
+#include <cinttypes>
+
+#define PW_LOG_MODULE_NAME "kudzu_imu_imgui"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+
+#include "kudzu_imu_imgui/imu.h"
+#include "pw_log/log.h"
+
+namespace kudzu::imu {
+
+PollingImuImGui::PollingImuImGui() {}
+
+pw::Status PollingImuImGui::Init() { return pw::Status::Unimplemented(); }
+
+bool PollingImuImGui::IsAvailable() { return true; }
+
+pw::Result<ImuSample> PollingImuImGui::ReadData() {
+  ImuSample data = {.accelerometer = {1.0f, 2.0f, 3.0f},
+                    .gyroscope = {10, 20, 30}};
+
+  last_data = data;
+  return data;
+}
+
+}  // namespace kudzu::imu
diff --git a/lib/kudzu_imu_imgui/public/kudzu_imu_imgui/imu.h b/lib/kudzu_imu_imgui/public/kudzu_imu_imgui/imu.h
new file mode 100644
index 0000000..33215ed
--- /dev/null
+++ b/lib/kudzu_imu_imgui/public/kudzu_imu_imgui/imu.h
@@ -0,0 +1,32 @@
+// 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 "kudzu_imu/imu.h"
+#include "pw_status/status.h"
+
+namespace kudzu::imu {
+
+class PollingImuImGui : public PollingImu {
+ public:
+  PollingImuImGui();
+
+  pw::Status Init() override;
+  bool IsAvailable() override;
+  pw::Result<ImuSample> ReadData() override;
+
+  ImuSample last_data;
+};
+
+}  // namespace kudzu::imu