Add a translator for imu can values

This provides a translation layer between the raw CAN frames from the
IMU and a flatbuffer filled with the information being sent.

Signed-off-by: Maxwell Henderson <mxwhenderson@gmail.com>
Change-Id: Ifd295e7353da8ca622e334569ed58dcae0d59a70
diff --git a/frc971/imu_fdcan/BUILD b/frc971/imu_fdcan/BUILD
index deb6a7e..b47f367 100644
--- a/frc971/imu_fdcan/BUILD
+++ b/frc971/imu_fdcan/BUILD
@@ -1,3 +1,4 @@
+load("//aos:config.bzl", "aos_config")
 load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 
 static_flatbuffer(
@@ -5,3 +6,69 @@
     srcs = ["dual_imu.fbs"],
     visibility = ["//visibility:public"],
 )
+
+static_flatbuffer(
+    name = "can_translator_status_fbs",
+    srcs = ["can_translator_status.fbs"],
+    visibility = ["//visibility:public"],
+)
+
+cc_binary(
+    name = "can_translator",
+    srcs = ["can_translator_main.cc"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":can_translator_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
+
+cc_library(
+    name = "can_translator_lib",
+    srcs = [
+        "can_translator_lib.cc",
+    ],
+    hdrs = [
+        "can_translator_lib.h",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":can_translator_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:event_loop",
+        "//frc971/can_logger:can_logging_fbs",
+    ],
+)
+
+cc_test(
+    name = "can_translator_lib_test",
+    srcs = [
+        "can_translator_lib_test.cc",
+    ],
+    data = [
+        ":can_translator_test_config",
+    ],
+    deps = [
+        ":can_translator_lib",
+        ":can_translator_status_fbs",
+        ":dual_imu_fbs",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//frc971/can_logger:can_logging_fbs",
+        "@com_github_google_glog//:glog",
+    ],
+)
+
+aos_config(
+    name = "can_translator_test_config",
+    src = "can_translator_test_config_source.json",
+    flatbuffers = [
+        "//aos/logging:log_message_fbs",
+        ":dual_imu_fbs",
+        ":can_translator_status_fbs",
+        "//frc971/can_logger:can_logging_fbs",
+        "//aos/events:event_loop_fbs",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+)
diff --git a/frc971/imu_fdcan/can_translator_lib.cc b/frc971/imu_fdcan/can_translator_lib.cc
new file mode 100644
index 0000000..b5e5576
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib.cc
@@ -0,0 +1,125 @@
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+using frc971::imu_fdcan::CANTranslator;
+
+constexpr std::size_t kCanFrameSize = 64;
+constexpr int kImuCanId = 1;
+
+CANTranslator::CANTranslator(aos::EventLoop *event_loop,
+                             std::string_view canframe_channel)
+    : dual_imu_sender_(
+          event_loop->MakeSender<frc971::imu::DualImuStatic>("/imu")),
+      can_translator_status_sender_(
+          event_loop->MakeSender<frc971::imu::CanTranslatorStatusStatic>(
+              "/imu")) {
+  // TODO(max): Update this with a proper priority
+  event_loop->SetRuntimeRealtimePriority(15);
+
+  event_loop->MakeWatcher(
+      canframe_channel, [this](const frc971::can_logger::CanFrame &can_frame) {
+        if (can_frame.data()->size() / sizeof(uint8_t) != 64) {
+          invalid_packet_count_++;
+        }
+
+        if (can_frame.can_id() != kImuCanId) {
+          invalid_can_id_count_++;
+          return;
+        }
+
+        if (can_frame.data()->size() / sizeof(uint8_t) == 64) {
+          valid_packet_count_++;
+          HandleFrame(&can_frame);
+        }
+      });
+
+  event_loop->AddPhasedLoop(
+      [this](int) {
+        aos::Sender<frc971::imu::CanTranslatorStatusStatic>::StaticBuilder
+            status_builder = can_translator_status_sender_.MakeStaticBuilder();
+
+        status_builder->set_valid_packet_count(valid_packet_count_);
+        status_builder->set_invalid_packet_count(invalid_packet_count_);
+        status_builder->set_invalid_can_id_count(invalid_can_id_count_);
+
+        status_builder.CheckOk(status_builder.Send());
+      },
+      std::chrono::milliseconds(100));
+}
+
+// Gets the data from the span and iterates it to the next section of bytes.
+template <typename T>
+T GetAndPopDataFromBuffer(std::span<const uint8_t> &span) {
+  T value = 0;
+
+  std::memcpy(&value, span.data(), sizeof(T));
+  span = span.subspan(sizeof(T));
+
+  return value;
+}
+
+// Values from the data field mapping table in
+// https://docs.google.com/document/d/12AJUruW7DZ2pIrDzTyPC0qqFoia4QOSVlax6Jd7m4H0/edit?usp=sharing
+void CANTranslator::HandleFrame(const frc971::can_logger::CanFrame *can_frame) {
+  aos::Sender<frc971::imu::DualImuStatic>::StaticBuilder dual_imu_builder =
+      dual_imu_sender_.MakeStaticBuilder();
+
+  std::span can_data(can_frame->data()->data(), kCanFrameSize);
+
+  frc971::imu::SingleImuStatic *murata = dual_imu_builder->add_murata();
+
+  auto *murata_chip_states = murata->add_chip_states();
+  frc971::imu::ChipStateStatic *murata_uno_chip_state =
+      murata_chip_states->emplace_back();
+  frc971::imu::ChipStateStatic *murata_due_chip_state =
+      murata_chip_states->emplace_back();
+
+  frc971::imu::SingleImuStatic *tdk = dual_imu_builder->add_tdk();
+
+  auto tdk_chip_states = tdk->add_chip_states();
+  frc971::imu::ChipStateStatic *tdk_chip_state =
+      tdk_chip_states->emplace_back();
+
+  dual_imu_builder->set_board_timestamp_us(
+      GetAndPopDataFromBuffer<uint32_t>(can_data));
+
+  dual_imu_builder->set_packet_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+
+  tdk_chip_state->set_counter(GetAndPopDataFromBuffer<uint16_t>(can_data));
+  murata_uno_chip_state->set_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+  murata_due_chip_state->set_counter(
+      GetAndPopDataFromBuffer<uint16_t>(can_data));
+
+  tdk->set_accelerometer_x(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_accelerometer_y(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_accelerometer_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  tdk->set_gyro_x(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_gyro_y(GetAndPopDataFromBuffer<float>(can_data));
+  tdk->set_gyro_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  murata->set_accelerometer_x(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_accelerometer_y(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_accelerometer_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  murata->set_gyro_x(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_gyro_y(GetAndPopDataFromBuffer<float>(can_data));
+  murata->set_gyro_z(GetAndPopDataFromBuffer<float>(can_data));
+
+  tdk_chip_state->set_temperature(GetAndPopDataFromBuffer<uint8_t>(can_data));
+  murata_uno_chip_state->set_temperature(
+      GetAndPopDataFromBuffer<uint8_t>(can_data));
+  murata_due_chip_state->set_temperature(
+      GetAndPopDataFromBuffer<uint8_t>(can_data));
+
+  murata_uno_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  murata_due_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  tdk_chip_state->set_max_counter(std::numeric_limits<uint16_t>::max());
+  dual_imu_builder->set_max_packet_counter(
+      std::numeric_limits<uint16_t>::max());
+
+  dual_imu_builder->set_kernel_timestamp(can_frame->monotonic_timestamp_ns());
+
+  dual_imu_builder.CheckOk(dual_imu_builder.Send());
+}
diff --git a/frc971/imu_fdcan/can_translator_lib.h b/frc971/imu_fdcan/can_translator_lib.h
new file mode 100644
index 0000000..47a3638
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib.h
@@ -0,0 +1,30 @@
+#ifndef FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
+#define FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
+#include "aos/events/event_loop.h"
+#include "frc971/can_logger/can_logging_generated.h"
+#include "frc971/imu_fdcan/can_translator_status_static.h"
+#include "frc971/imu_fdcan/dual_imu_static.h"
+
+namespace frc971::imu_fdcan {
+
+// Translates the CanFrames from the IMU into a DualIMU message based on the
+// spec defined in this doc:
+// https://docs.google.com/document/d/12AJUruW7DZ2pIrDzTyPC0qqFoia4QOSVlax6Jd7m4H0/edit?usp=sharing
+class CANTranslator {
+ public:
+  CANTranslator(aos::EventLoop *event_loop, std::string_view canframe_channel);
+
+ private:
+  void HandleFrame(const can_logger::CanFrame *can_frame);
+
+  aos::Sender<imu::DualImuStatic> dual_imu_sender_;
+  aos::Sender<imu::CanTranslatorStatusStatic> can_translator_status_sender_;
+
+  uint64_t valid_packet_count_ = 0;
+  uint64_t invalid_packet_count_ = 0;
+  uint64_t invalid_can_id_count_ = 0;
+};
+
+}  // namespace frc971::imu_fdcan
+
+#endif  // FRC971_IMU_FDCAN_CAN_TRANSLATOR_LIB_H_
diff --git a/frc971/imu_fdcan/can_translator_lib_test.cc b/frc971/imu_fdcan/can_translator_lib_test.cc
new file mode 100644
index 0000000..ce9dd59
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_lib_test.cc
@@ -0,0 +1,191 @@
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+
+#include "aos/events/simulated_event_loop.h"
+#include "frc971/can_logger/can_logging_static.h"
+#include "frc971/imu_fdcan/can_translator_status_generated.h"
+#include "frc971/imu_fdcan/dual_imu_generated.h"
+
+class CANTranslatorTest : public ::testing::Test {
+ public:
+  CANTranslatorTest()
+      : config_(aos::configuration::ReadConfig(
+            "frc971/imu_fdcan/can_translator_test_config.json")),
+        event_loop_factory_(&config_.message()),
+        can_translator_event_loop_(
+            event_loop_factory_.MakeEventLoop("can_translator")),
+        can_frame_event_loop_(event_loop_factory_.MakeEventLoop("can_frame")),
+        dual_imu_fetcher_(
+            can_translator_event_loop_->MakeFetcher<frc971::imu::DualImu>(
+                "/imu")),
+        can_translator_status_fetcher_(
+            can_translator_event_loop_
+                ->MakeFetcher<frc971::imu::CanTranslatorStatus>("/imu")),
+        can_frame_sender_(
+            can_frame_event_loop_
+                ->MakeSender<frc971::can_logger::CanFrameStatic>("/can")),
+        can_translator_(can_translator_event_loop_.get(), "/can") {}
+
+ protected:
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+
+  std::unique_ptr<aos::EventLoop> can_translator_event_loop_;
+  std::unique_ptr<aos::EventLoop> can_frame_event_loop_;
+
+  aos::Fetcher<frc971::imu::DualImu> dual_imu_fetcher_;
+  aos::Fetcher<frc971::imu::CanTranslatorStatus> can_translator_status_fetcher_;
+
+  aos::Sender<frc971::can_logger::CanFrameStatic> can_frame_sender_;
+
+  frc971::imu_fdcan::CANTranslator can_translator_;
+};
+
+TEST_F(CANTranslatorTest, CheckValidFrame) {
+  can_frame_event_loop_->OnRun([this] {
+    aos::Sender<frc971::can_logger::CanFrameStatic>::StaticBuilder
+        can_frame_builder = can_frame_sender_.MakeStaticBuilder();
+
+    can_frame_builder->set_can_id(1);
+    can_frame_builder->set_monotonic_timestamp_ns(100);
+    auto can_data = can_frame_builder->add_data();
+    CHECK(can_data->reserve(sizeof(uint8_t) * 64));
+
+    CHECK(can_data->emplace_back(226));
+    CHECK(can_data->emplace_back(100));
+    CHECK(can_data->emplace_back(108));
+    CHECK(can_data->emplace_back(8));
+    CHECK(can_data->emplace_back(152));
+    CHECK(can_data->emplace_back(40));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(202));
+    CHECK(can_data->emplace_back(121));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(81));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(0));
+    CHECK(can_data->emplace_back(0));
+    CHECK(can_data->emplace_back(8));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(85));
+    CHECK(can_data->emplace_back(213));
+    CHECK(can_data->emplace_back(127));
+    CHECK(can_data->emplace_back(191));
+    CHECK(can_data->emplace_back(12));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(34));
+    CHECK(can_data->emplace_back(187));
+    CHECK(can_data->emplace_back(255));
+    CHECK(can_data->emplace_back(219));
+    CHECK(can_data->emplace_back(220));
+    CHECK(can_data->emplace_back(59));
+    CHECK(can_data->emplace_back(147));
+    CHECK(can_data->emplace_back(173));
+    CHECK(can_data->emplace_back(5));
+    CHECK(can_data->emplace_back(61));
+    CHECK(can_data->emplace_back(88));
+    CHECK(can_data->emplace_back(68));
+    CHECK(can_data->emplace_back(205));
+    CHECK(can_data->emplace_back(188));
+    CHECK(can_data->emplace_back(230));
+    CHECK(can_data->emplace_back(92));
+    CHECK(can_data->emplace_back(24));
+    CHECK(can_data->emplace_back(189));
+    CHECK(can_data->emplace_back(235));
+    CHECK(can_data->emplace_back(1));
+    CHECK(can_data->emplace_back(127));
+    CHECK(can_data->emplace_back(191));
+    CHECK(can_data->emplace_back(210));
+    CHECK(can_data->emplace_back(7));
+    CHECK(can_data->emplace_back(34));
+    CHECK(can_data->emplace_back(54));
+    CHECK(can_data->emplace_back(86));
+    CHECK(can_data->emplace_back(103));
+    CHECK(can_data->emplace_back(133));
+    CHECK(can_data->emplace_back(186));
+    CHECK(can_data->emplace_back(100));
+    CHECK(can_data->emplace_back(205));
+    CHECK(can_data->emplace_back(101));
+    CHECK(can_data->emplace_back(185));
+    CHECK(can_data->emplace_back(29));
+    CHECK(can_data->emplace_back(26));
+    CHECK(can_data->emplace_back(26));
+    CHECK(can_data->emplace_back(0));
+
+    can_frame_builder.CheckOk(can_frame_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(can_translator_status_fetcher_.Fetch());
+  ASSERT_TRUE(dual_imu_fetcher_.Fetch());
+
+  ASSERT_FALSE(can_translator_status_fetcher_->invalid_packet_count() > 0);
+  ASSERT_FALSE(can_translator_status_fetcher_->invalid_can_id_count() > 0);
+  EXPECT_EQ(can_translator_status_fetcher_->valid_packet_count(), 1);
+
+  EXPECT_EQ(dual_imu_fetcher_->board_timestamp_us(), 141321442);
+  EXPECT_EQ(dual_imu_fetcher_->packet_counter(), 10392);
+
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_x(), 2.41444e-06, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_y(), -0.00101779, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->gyro_z(), -0.000219157, 0.00001);
+
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_x(), -0.025057,
+              0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_y(), -0.037198,
+              0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->murata()->accelerometer_z(), -0.996123,
+              0.00001);
+
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(0)->counter(),
+            31178);
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(0)->temperature(),
+            26);
+
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(1)->counter(),
+            31178);
+  EXPECT_EQ(dual_imu_fetcher_->murata()->chip_states()->Get(1)->temperature(),
+            26);
+
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_x(), -0.00248319, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_y(), 0.00674009, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->gyro_z(), 0.0326362, 0.00001);
+
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_x(), -0.0511068, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_y(), -0.0332031, 0.00001);
+  EXPECT_NEAR(dual_imu_fetcher_->tdk()->accelerometer_z(), -0.999349, 0.00001);
+
+  EXPECT_EQ(dual_imu_fetcher_->tdk()->chip_states()->Get(0)->counter(), 31178);
+  EXPECT_EQ(dual_imu_fetcher_->tdk()->chip_states()->Get(0)->temperature(), 29);
+}
+
+TEST_F(CANTranslatorTest, CheckInvalidFrame) {
+  can_frame_event_loop_->OnRun([this] {
+    aos::Sender<frc971::can_logger::CanFrameStatic>::StaticBuilder
+        can_frame_builder = can_frame_sender_.MakeStaticBuilder();
+
+    can_frame_builder->set_can_id(2);
+    can_frame_builder->set_monotonic_timestamp_ns(100);
+    auto can_data = can_frame_builder->add_data();
+    CHECK(can_data->reserve(sizeof(uint8_t) * 1));
+
+    CHECK(can_data->emplace_back(0));
+
+    can_frame_builder.CheckOk(can_frame_builder.Send());
+  });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(200));
+
+  ASSERT_TRUE(can_translator_status_fetcher_.Fetch());
+  ASSERT_FALSE(dual_imu_fetcher_.Fetch());
+
+  EXPECT_EQ(can_translator_status_fetcher_->invalid_packet_count(), 1);
+  EXPECT_EQ(can_translator_status_fetcher_->invalid_can_id_count(), 1);
+}
diff --git a/frc971/imu_fdcan/can_translator_main.cc b/frc971/imu_fdcan/can_translator_main.cc
new file mode 100644
index 0000000..fdb7107
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_main.cc
@@ -0,0 +1,22 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "frc971/imu_fdcan/can_translator_lib.h"
+
+DEFINE_string(channel, "/can", "The CAN channel to use");
+
+using frc971::imu_fdcan::CANTranslator;
+
+int main(int argc, char **argv) {
+  ::aos::InitGoogle(&argc, &argv);
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig("aos_config.json");
+
+  ::aos::ShmEventLoop event_loop(&config.message());
+
+  CANTranslator translator(&event_loop, FLAGS_channel);
+
+  event_loop.Run();
+
+  return 0;
+}
diff --git a/frc971/imu_fdcan/can_translator_status.fbs b/frc971/imu_fdcan/can_translator_status.fbs
new file mode 100644
index 0000000..232f3ba
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_status.fbs
@@ -0,0 +1,12 @@
+namespace frc971.imu;
+
+table CanTranslatorStatus {
+  // Number of times we've gotten valid packets at 64 bytes
+  valid_packet_count: uint64 (id: 0);
+  // Number of times we've gotten packets under 64 bytes
+  invalid_packet_count: uint64 (id: 1);
+  // Number of times we've gotten an invalid can id
+  invalid_can_id_count: uint64 (id: 2);
+}
+
+root_type CanTranslatorStatus;
diff --git a/frc971/imu_fdcan/can_translator_test_config_source.json b/frc971/imu_fdcan/can_translator_test_config_source.json
new file mode 100644
index 0000000..e82c87d
--- /dev/null
+++ b/frc971/imu_fdcan/can_translator_test_config_source.json
@@ -0,0 +1,28 @@
+{
+  "channels": [
+    {
+      "name": "/aos",
+      "type": "aos.timing.Report"
+    },
+    {
+      "name": "/aos",
+      "type": "aos.logging.LogMessageFbs",
+      "frequency": 400
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.DualImu",
+      "frequency": 200
+    },
+    {
+      "name": "/can",
+      "type": "frc971.can_logger.CanFrame",
+      "frequency": 200
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.CanTranslatorStatus",
+      "frequency": 100
+    },
+  ]
+}
diff --git a/frc971/imu_fdcan/dual_imu.fbs b/frc971/imu_fdcan/dual_imu.fbs
index 7236acf..88f02df 100644
--- a/frc971/imu_fdcan/dual_imu.fbs
+++ b/frc971/imu_fdcan/dual_imu.fbs
@@ -1,5 +1,7 @@
 namespace frc971.imu;
 
+attribute "static_length";
+
 table ChipState {
   // Counter indicating how many samples have been taken on this IMU.
   // Note that because averaging occurs on the microcontroller, this
@@ -28,14 +30,14 @@
   accelerometer_z:double (id: 5);
 
   // State for the individual ASIC(s) for this IMU.
-  chip_states:[ChipState] (id: 6);
+  chip_states:[ChipState] (id: 6, static_length: 2);
 }
 
 table DualImu {
-  // Timestamp from the board corresponding to these samples.
+  // Timestamp from the board corresponding to these samples in nanoseconds.
   // Future changes may lead us to add per-chip timestamps, but for now
   // we treat them as being sampled at the same time.
-  board_timestamp_us:uint32 (id: 0);
+  kernel_timestamp:uint64 (id: 0);
   // Packet counter that should increment by 1 for every incoming packet.
   packet_counter:uint64 (id: 1);
   // Value at which the packet counter wraps, such that
@@ -45,6 +47,8 @@
   murata:SingleImu (id: 3);
   // Readings associated with the TDK IMU.
   tdk:SingleImu (id: 4);
+  // Timestamp from the IMU in microseconds
+  board_timestamp_us:uint32 (id: 5);
 }
 
 root_type DualImu;
diff --git a/y2024/BUILD b/y2024/BUILD
index d8d0db7..45ff96c 100644
--- a/y2024/BUILD
+++ b/y2024/BUILD
@@ -71,6 +71,7 @@
     ],
     start_binaries = [
         "//aos/events/logging:logger_main",
+        "//frc971/imu_fdcan:can_translator",
         "//aos/network:message_bridge_client",
         "//aos/network:message_bridge_server",
         "//aos/network:web_proxy_main",
@@ -111,6 +112,8 @@
     flatbuffers = [
         "//aos/network:message_bridge_client_fbs",
         "//aos/network:message_bridge_server_fbs",
+        "//frc971/imu_fdcan:dual_imu_fbs",
+        "//frc971/imu_fdcan:can_translator_status_fbs",
         "//y2024/constants:constants_fbs",
         "//frc971/can_logger:can_logging_fbs",
         "//aos/network:timestamp_fbs",
diff --git a/y2024/y2024_imu.json b/y2024/y2024_imu.json
index 5dd4710..6a611ff 100644
--- a/y2024/y2024_imu.json
+++ b/y2024/y2024_imu.json
@@ -160,6 +160,22 @@
       "max_size": 200
     },
     {
+      "name": "/imu",
+      "type": "frc971.imu.DualImu",
+      "source_node": "imu",
+      "frequency": 1100,
+      "num_senders": 1,
+      "max_size": 496
+    },
+    {
+      "name": "/imu",
+      "type": "frc971.imu.CanTranslatorStatus",
+      "source_node": "imu",
+      "frequency": 1000,
+      "num_senders": 1,
+      "max_size": 200
+    },
+    {
       "name": "/can/cana",
       "type": "frc971.can_logger.CanFrame",
       "source_node": "imu",
@@ -260,6 +276,17 @@
         "imu"
       ]
     },
+    // TODO(max): Update the channel value with whatever channel the IMU is on.
+    {
+      "name": "can_translator",
+      "executable_name": "can_translator",
+      "args": [
+          "--channel=/can/canb"
+      ],
+      "nodes": [
+        "imu"
+      ]
+    },
     {
       "name": "web_proxy",
       "executable_name": "web_proxy_main",