Send frames out on the queue

This also required passing the camera index through the Teensy.

Change-Id: I73d380a01fd129919dba5ccfa04e41b0f02da767
diff --git a/y2019/BUILD b/y2019/BUILD
index f00f5ba..3fae9e5 100644
--- a/y2019/BUILD
+++ b/y2019/BUILD
@@ -72,6 +72,7 @@
         "//frc971/wpilib:wpilib_robot_base",
         "//third_party:phoenix",
         "//third_party:wpilib",
+        "//y2019/control_loops/drivetrain:camera_queue",
         "//y2019/control_loops/superstructure:superstructure_queue",
         "//y2019/jevois:spi",
     ],
diff --git a/y2019/control_loops/drivetrain/BUILD b/y2019/control_loops/drivetrain/BUILD
index 677726e..de52215 100644
--- a/y2019/control_loops/drivetrain/BUILD
+++ b/y2019/control_loops/drivetrain/BUILD
@@ -88,6 +88,7 @@
     srcs = [
         "camera.q",
     ],
+    visibility = ["//visibility:public"],
 )
 
 cc_library(
diff --git a/y2019/control_loops/drivetrain/camera.q b/y2019/control_loops/drivetrain/camera.q
index 5add759..6ef6f49 100644
--- a/y2019/control_loops/drivetrain/camera.q
+++ b/y2019/control_loops/drivetrain/camera.q
@@ -1,7 +1,6 @@
 package y2019.control_loops.drivetrain;
 
-// These structures have a nearly one-to-one correspondence to those in
-// //y2019/jevois:structures.h. Please refer to that file for details.
+// See the Target structure in //y2019/jevois:structures.h for documentation.
 struct CameraTarget {
   float distance;
   float height;
@@ -10,17 +9,17 @@
 };
 
 message CameraFrame {
-  // monotonic time in nanoseconds at which frame was taken (note structure.h
-  // uses age).
+  // Number of nanoseconds since the aos::monotonic_clock epoch at which this
+  // frame was captured.
   int64_t timestamp;
 
   // Number of targets actually in this frame.
   uint8_t num_targets;
 
-  // Buffer for the targets
+  // Buffer for the targets.
   CameraTarget[3] targets;
 
-  // Index of the camera with which this frame was taken:
+  // Index of the camera position (not serial number) which this frame is from.
   uint8_t camera;
 };
 
diff --git a/y2019/jevois/spi.cc b/y2019/jevois/spi.cc
index b6e6632..a987ee7 100644
--- a/y2019/jevois/spi.cc
+++ b/y2019/jevois/spi.cc
@@ -32,9 +32,10 @@
 //     2. 8 bits distance
 //     3. 6 bits skew
 //     4. 6 bits height
-//     5. 1 bit target valid (a present frame has all-valid targets)
-//     6. 1 bit target present (a present frame can have from 0 to 3
-//          targets, depending on how many were found)
+//     5. 2 bits of quantity+index
+//   The 6 bits of quantity+index (between all three targets) are:
+//     1. 4 bits camera index + 1 (0 means this isn't a valid frame)
+//     2. 2 bits target count
 //   Note that empty frames are still sent to indicate that the camera is
 //   still working even though it doesn't see any targets.
 
@@ -110,26 +111,29 @@
                                               integer);
 }
 
-constexpr int valid_bits() { return 1; }
-constexpr int valid_offset() { return height_offset() + height_bits(); }
-void valid_pack(bool valid, gsl::span<char> destination) {
-  aos::PackBits<uint32_t, valid_bits(), valid_offset()>(valid, destination);
+constexpr int quantity_index_offset() { return height_offset() + height_bits(); }
+void camera_index_pack(int camera_index, gsl::span<char> destination) {
+  aos::PackBits<uint32_t, 2, quantity_index_offset()>(camera_index & 3,
+                                                      destination);
+  aos::PackBits<uint32_t, 2, quantity_index_offset() + 32>(
+      (camera_index >> 2) & 3, destination);
 }
-bool valid_unpack(gsl::span<const char> source) {
-  return aos::UnpackBits<uint32_t, valid_bits(), valid_offset()>(source);
+int camera_index_unpack(gsl::span<const char> source) {
+  int result = 0;
+  result |= aos::UnpackBits<uint32_t, 2, quantity_index_offset()>(source);
+  result |= aos::UnpackBits<uint32_t, 2, quantity_index_offset() + 32>(source)
+            << 2;
+  return result;
+}
+void target_count_pack(int target_count, gsl::span<char> destination) {
+  aos::PackBits<uint32_t, 2, quantity_index_offset() + 32 * 2>(target_count,
+                                                               destination);
+}
+int target_count_unpack(gsl::span<const char> source) {
+  return aos::UnpackBits<uint32_t, 2, quantity_index_offset() + 32 * 2>(source);
 }
 
-constexpr int present_bits() { return 1; }
-constexpr int present_offset() { return valid_offset() + valid_bits(); }
-void present_pack(bool present, gsl::span<char> destination) {
-  aos::PackBits<uint32_t, present_bits(), present_offset()>(present,
-                                                            destination);
-}
-bool present_unpack(gsl::span<const char> source) {
-  return aos::UnpackBits<uint32_t, present_bits(), present_offset()>(source);
-}
-
-constexpr int next_offset() { return present_offset() + present_bits(); }
+constexpr int next_offset() { return quantity_index_offset() + 2; }
 static_assert(next_offset() <= 32, "Target is too big");
 
 }  // namespace
@@ -138,14 +142,17 @@
   SpiTransfer transfer;
   gsl::span<char> remaining_space = transfer;
   for (int frame = 0; frame < 3; ++frame) {
-    for (int target = 0; target < 3; ++target) {
-      remaining_space[0] = 0;
-      remaining_space[1] = 0;
-      remaining_space[2] = 0;
-      remaining_space[3] = 0;
+    // Zero out all three targets and the age.
+    for (int i = 0; i < 3 * 4 + 1; ++i) {
+      remaining_space[i] = 0;
+    }
 
-      if (static_cast<int>(message.frames.size()) > frame) {
-        valid_pack(true, remaining_space);
+    if (static_cast<int>(message.frames.size()) > frame) {
+      camera_index_pack(message.frames[frame].camera_index + 1,
+                        remaining_space);
+      target_count_pack(message.frames[frame].targets.size(), remaining_space);
+
+      for (int target = 0; target < 3; ++target) {
         if (static_cast<int>(message.frames[frame].targets.size()) > target) {
           heading_pack(message.frames[frame].targets[target].heading,
                        remaining_space);
@@ -155,23 +162,16 @@
                     remaining_space);
           height_pack(message.frames[frame].targets[target].height,
                       remaining_space);
-          present_pack(true, remaining_space);
-        } else {
-          present_pack(false, remaining_space);
         }
-      } else {
-        valid_pack(false, remaining_space);
+        remaining_space = remaining_space.subspan(4);
       }
 
-      remaining_space = remaining_space.subspan(4);
-    }
-    if (static_cast<int>(message.frames.size()) > frame) {
       const uint8_t age_count = message.frames[frame].age.count();
       memcpy(&remaining_space[0], &age_count, 1);
+      remaining_space = remaining_space.subspan(1);
     } else {
-      remaining_space[0] = 0;
+      remaining_space = remaining_space.subspan(4 * 3 + 1);
     }
-    remaining_space = remaining_space.subspan(1);
   }
   {
     uint16_t crc = jevois_crc_init();
@@ -191,13 +191,14 @@
   TeensyToRoborio message;
   gsl::span<const char> remaining_input = transfer;
   for (int frame = 0; frame < 3; ++frame) {
-    const bool have_frame = valid_unpack(remaining_input);
-    if (have_frame) {
+    const int camera_index_plus = camera_index_unpack(remaining_input);
+    if (camera_index_plus > 0) {
       message.frames.push_back({});
-    }
-    for (int target = 0; target < 3; ++target) {
-      if (present_unpack(remaining_input)) {
-        if (have_frame) {
+      message.frames.back().camera_index = camera_index_plus - 1;
+
+      const int target_count = target_count_unpack(remaining_input);
+      for (int target = 0; target < 3; ++target) {
+        if (target < target_count) {
           message.frames.back().targets.push_back({});
           message.frames.back().targets.back().heading =
               heading_unpack(remaining_input);
@@ -208,16 +209,18 @@
           message.frames.back().targets.back().height =
               height_unpack(remaining_input);
         }
+        remaining_input = remaining_input.subspan(4);
       }
 
-      remaining_input = remaining_input.subspan(4);
+      {
+        uint8_t age_count;
+        memcpy(&age_count, &remaining_input[0], 1);
+        message.frames.back().age = camera_duration(age_count);
+      }
+      remaining_input = remaining_input.subspan(1);
+    } else {
+      remaining_input = remaining_input.subspan(4 * 3 + 1);
     }
-    if (have_frame) {
-      uint8_t age_count;
-      memcpy(&age_count, &remaining_input[0], 1);
-      message.frames.back().age = camera_duration(age_count);
-    }
-    remaining_input = remaining_input.subspan(1);
   }
   {
     uint16_t calculated_crc = jevois_crc_init();
diff --git a/y2019/jevois/spi_test.cc b/y2019/jevois/spi_test.cc
index 32551db..84938f0 100644
--- a/y2019/jevois/spi_test.cc
+++ b/y2019/jevois/spi_test.cc
@@ -60,10 +60,13 @@
   TeensyToRoborio input_message;
   input_message.frames.push_back({});
   input_message.frames.back().age = camera_duration(9);
+  input_message.frames.back().camera_index = 2;
   input_message.frames.push_back({});
   input_message.frames.back().age = camera_duration(7);
+  input_message.frames.back().camera_index = 5;
   input_message.frames.push_back({});
   input_message.frames.back().age = camera_duration(1);
+  input_message.frames.back().camera_index = 4;
 
   const SpiTransfer transfer = SpiPackToRoborio(input_message);
   const auto output_message = SpiUnpackToRoborio(transfer);
@@ -80,13 +83,16 @@
   input_message.frames.back().targets.back().height = 1;
   input_message.frames.back().targets.back().heading = 0.5;
   input_message.frames.back().targets.back().skew = -0.5;
+  input_message.frames.back().camera_index = 0;
   input_message.frames.push_back({});
   input_message.frames.back().targets.push_back({});
   input_message.frames.back().targets.push_back({});
+  input_message.frames.back().camera_index = 2;
   input_message.frames.push_back({});
   input_message.frames.back().targets.push_back({});
   input_message.frames.back().targets.push_back({});
   input_message.frames.back().targets.push_back({});
+  input_message.frames.back().camera_index = 3;
 
   const SpiTransfer transfer = SpiPackToRoborio(input_message);
   const auto output_message = SpiUnpackToRoborio(transfer);
@@ -103,6 +109,93 @@
               output_message->frames.back().targets.back().heading, 0.1);
   EXPECT_NEAR(input_message.frames.back().targets.back().skew,
               output_message->frames.back().targets.back().skew, 0.1);
+  for (int i = 0; i < 3; ++i) {
+    EXPECT_EQ(input_message.frames[i].camera_index,
+              output_message->frames[i].camera_index);
+  }
+}
+
+// Tests that packing and unpacking two targets results in the same number on
+// the other side.
+TEST(SpiToRoborioPackTest, TwoTargets) {
+  TeensyToRoborio input_message;
+  input_message.frames.push_back({});
+  input_message.frames.back().targets.push_back({});
+  input_message.frames.back().targets.back().distance = 9;
+  input_message.frames.back().targets.back().height = 1;
+  input_message.frames.back().targets.back().heading = 0.5;
+  input_message.frames.back().targets.back().skew = -0.5;
+  input_message.frames.back().targets.push_back({});
+  input_message.frames.back().targets.back().distance = 1;
+  input_message.frames.back().targets.back().height = 0.9;
+  input_message.frames.back().targets.back().heading = 0.4;
+  input_message.frames.back().targets.back().skew = -0.4;
+  input_message.frames.back().age = camera_duration(9);
+  input_message.frames.back().camera_index = 2;
+
+  const SpiTransfer transfer = SpiPackToRoborio(input_message);
+  const auto output_message = SpiUnpackToRoborio(transfer);
+  ASSERT_TRUE(output_message);
+  ASSERT_EQ(1u, output_message->frames.size());
+  ASSERT_EQ(2u, output_message->frames[0].targets.size());
+  for (int i = 0; i < 2; ++i) {
+    EXPECT_NEAR(input_message.frames.back().targets[i].distance,
+                output_message->frames.back().targets[i].distance, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].height,
+                output_message->frames.back().targets[i].height, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].heading,
+                output_message->frames.back().targets[i].heading, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].skew,
+                output_message->frames.back().targets[i].skew, 0.1);
+    EXPECT_EQ(input_message.frames.back().age,
+              output_message->frames.back().age);
+    EXPECT_EQ(input_message.frames.back().camera_index,
+              output_message->frames.back().camera_index);
+  }
+}
+
+// Tests that packing and unpacking three targets results in the same number on
+// the other side.
+TEST(SpiToRoborioPackTest, ThreeTargets) {
+  TeensyToRoborio input_message;
+  input_message.frames.push_back({});
+  input_message.frames.back().targets.push_back({});
+  input_message.frames.back().targets.back().distance = 9;
+  input_message.frames.back().targets.back().height = 1;
+  input_message.frames.back().targets.back().heading = 0.5;
+  input_message.frames.back().targets.back().skew = -0.5;
+  input_message.frames.back().targets.push_back({});
+  input_message.frames.back().targets.back().distance = 1;
+  input_message.frames.back().targets.back().height = 0.9;
+  input_message.frames.back().targets.back().heading = 0.4;
+  input_message.frames.back().targets.back().skew = -0.4;
+  input_message.frames.back().targets.push_back({});
+  input_message.frames.back().targets.back().distance = 2;
+  input_message.frames.back().targets.back().height = 0.7;
+  input_message.frames.back().targets.back().heading = 0.3;
+  input_message.frames.back().targets.back().skew = -0.3;
+  input_message.frames.back().age = camera_duration(1);
+  input_message.frames.back().camera_index = 1;
+
+  const SpiTransfer transfer = SpiPackToRoborio(input_message);
+  const auto output_message = SpiUnpackToRoborio(transfer);
+  ASSERT_TRUE(output_message);
+  ASSERT_EQ(1u, output_message->frames.size());
+  ASSERT_EQ(3u, output_message->frames[0].targets.size());
+  for (int i = 0; i < 3; ++i) {
+    EXPECT_NEAR(input_message.frames.back().targets[i].distance,
+                output_message->frames.back().targets[i].distance, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].height,
+                output_message->frames.back().targets[i].height, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].heading,
+                output_message->frames.back().targets[i].heading, 0.1);
+    EXPECT_NEAR(input_message.frames.back().targets[i].skew,
+                output_message->frames.back().targets[i].skew, 0.1);
+    EXPECT_EQ(input_message.frames.back().age,
+              output_message->frames.back().age);
+    EXPECT_EQ(input_message.frames.back().camera_index,
+              output_message->frames.back().camera_index);
+  }
 }
 
 // Tests packing and then unpacking an empty message.
diff --git a/y2019/jevois/structures.h b/y2019/jevois/structures.h
index 1f10dc0..b6db24e 100644
--- a/y2019/jevois/structures.h
+++ b/y2019/jevois/structures.h
@@ -85,8 +85,8 @@
 // The information extracted from a single camera frame.
 //
 // This is all the information sent from each camera to the Teensy.
-struct Frame {
-  bool operator==(const Frame &other) const {
+struct CameraFrame {
+  bool operator==(const CameraFrame &other) const {
     if (other.targets != targets) {
       return false;
     }
@@ -95,7 +95,7 @@
     }
     return true;
   }
-  bool operator!=(const Frame &other) const {
+  bool operator!=(const CameraFrame &other) const {
     return !(*this == other);
   }
 
@@ -106,6 +106,34 @@
   camera_duration age;
 };
 
+// The information extracted from a single camera frame, from a given camera.
+struct RoborioFrame {
+  bool operator==(const RoborioFrame &other) const {
+    if (other.targets != targets) {
+      return false;
+    }
+    if (other.age != age) {
+      return false;
+    }
+    if (other.camera_index != camera_index) {
+      return false;
+    }
+    return true;
+  }
+  bool operator!=(const RoborioFrame &other) const {
+    return !(*this == other);
+  }
+
+  // The top most interesting targets found in this frame.
+  aos::SizedArray<Target, 3> targets;
+
+  // How long ago from the current time this frame was captured.
+  camera_duration age;
+  // Which camera this is from (which position on the robot, not a serial
+  // number).
+  int camera_index;
+};
+
 enum class CameraCommand : char {
   // Stay in normal mode.
   kNormal,
@@ -170,7 +198,7 @@
 
   // The newest frames received from up to three cameras. These will be the
   // three earliest-received of all buffered frames.
-  aos::SizedArray<Frame, 3> frames;
+  aos::SizedArray<RoborioFrame, 3> frames;
 };
 
 // This is all the information the RoboRIO sends to the Teensy.
@@ -182,6 +210,12 @@
     if (other.light_rings != light_rings) {
       return false;
     }
+    if (other.realtime_now != realtime_now) {
+      return false;
+    }
+    if (other.camera_command != camera_command) {
+      return false;
+    }
     return true;
   }
   bool operator!=(const RoborioToTeensy &other) const {
diff --git a/y2019/jevois/teensy.cc b/y2019/jevois/teensy.cc
index 539d7b6..84b98b0 100644
--- a/y2019/jevois/teensy.cc
+++ b/y2019/jevois/teensy.cc
@@ -269,9 +269,10 @@
   FrameQueue(const FrameQueue &) = delete;
   FrameQueue &operator=(const FrameQueue &) = delete;
 
-  void UpdateFrame(int camera, const Frame &frame) {
+  void UpdateFrame(int camera, const CameraFrame &frame) {
     frames_[camera].targets = frame.targets;
     frames_[camera].capture_time = aos::monotonic_clock::now() - frame.age;
+    frames_[camera].camera_index = camera;
     const aos::SizedArray<int, 3> old_last_frames = last_frames_;
     last_frames_.clear();
     for (int index : old_last_frames) {
@@ -302,6 +303,7 @@
     aos::SizedArray<Target, 3> targets;
     aos::monotonic_clock::time_point capture_time =
         aos::monotonic_clock::min_time;
+    int camera_index;
   };
 
   std::array<FrameData, 5> frames_;
@@ -327,7 +329,7 @@
     const FrameData &frame = frames_[index];
     const auto age = aos::monotonic_clock::now() - frame.capture_time;
     const auto rounded_age = aos::time::round<camera_duration>(age);
-    message.frames.push_back({frame.targets, rounded_age});
+    message.frames.push_back({frame.targets, rounded_age, frame.camera_index});
     last_frames_.push_back(index);
   }
   return SpiPackToRoborio(message);
@@ -658,7 +660,6 @@
             UartUnpackToTeensy(packetizers[i].received_packet());
         packetizers[i].clear_received_packet();
         if (decoded) {
-          printf("got one with %d\n", (int)decoded->targets.size());
           frame_queue.UpdateFrame(i, *decoded);
         }
       }
diff --git a/y2019/jevois/uart.cc b/y2019/jevois/uart.cc
index 3621da8..63138a8 100644
--- a/y2019/jevois/uart.cc
+++ b/y2019/jevois/uart.cc
@@ -15,7 +15,7 @@
 namespace frc971 {
 namespace jevois {
 
-UartToTeensyBuffer UartPackToTeensy(const Frame &message) {
+UartToTeensyBuffer UartPackToTeensy(const CameraFrame &message) {
   std::array<char, uart_to_teensy_size()> buffer;
   gsl::span<char> remaining_space = buffer;
   remaining_space[0] = message.targets.size();
@@ -55,7 +55,7 @@
   return result;
 }
 
-tl::optional<Frame> UartUnpackToTeensy(gsl::span<const char> encoded_buffer) {
+tl::optional<CameraFrame> UartUnpackToTeensy(gsl::span<const char> encoded_buffer) {
   std::array<char, uart_to_teensy_size()> buffer;
   if (static_cast<size_t>(
           CobsDecode<uart_to_teensy_size()>(encoded_buffer, &buffer).size()) !=
@@ -63,7 +63,7 @@
     return tl::nullopt;
   }
 
-  Frame message;
+  CameraFrame message;
   gsl::span<const char> remaining_input = buffer;
   const int number_targets = remaining_input[0];
   remaining_input = remaining_input.subspan(1);
diff --git a/y2019/jevois/uart.h b/y2019/jevois/uart.h
index 2a4acec..b9e784b 100644
--- a/y2019/jevois/uart.h
+++ b/y2019/jevois/uart.h
@@ -29,8 +29,8 @@
 using UartToCameraBuffer =
     aos::SizedArray<char, CobsMaxEncodedSize(uart_to_camera_size())>;
 
-UartToTeensyBuffer UartPackToTeensy(const Frame &message);
-tl::optional<Frame> UartUnpackToTeensy(gsl::span<const char> buffer);
+UartToTeensyBuffer UartPackToTeensy(const CameraFrame &message);
+tl::optional<CameraFrame> UartUnpackToTeensy(gsl::span<const char> buffer);
 
 UartToCameraBuffer UartPackToCamera(const CameraCalibration &message);
 tl::optional<CameraCalibration> UartUnpackToCamera(
diff --git a/y2019/jevois/uart_test.cc b/y2019/jevois/uart_test.cc
index d669688..b8f25c1 100644
--- a/y2019/jevois/uart_test.cc
+++ b/y2019/jevois/uart_test.cc
@@ -10,7 +10,7 @@
 
 // Tests packing and then unpacking a message with arbitrary values.
 TEST(UartToTeensyTest, Basic) {
-  Frame input_message;
+  CameraFrame input_message;
   for (int i = 0; i < 3; ++i) {
     input_message.targets.push_back({});
     Target *const target = &input_message.targets.back();
@@ -29,7 +29,7 @@
 // Tests packing and then unpacking a message with arbitrary values and no
 // frames.
 TEST(UartToTeensyTest, NoFrames) {
-  Frame input_message;
+  CameraFrame input_message;
   input_message.age = camera_duration(123);
   const UartToTeensyBuffer buffer = UartPackToTeensy(input_message);
   const auto output_message = UartUnpackToTeensy(buffer);
@@ -39,7 +39,7 @@
 
 // Tests packing and then unpacking a message with just one frame.
 TEST(UartToTeensyTest, OneFrame) {
-  Frame input_message;
+  CameraFrame input_message;
   {
     input_message.targets.push_back({});
     Target *const target = &input_message.targets.back();
@@ -75,7 +75,7 @@
 
 // Tests that corrupting the data in various ways is handled properly.
 TEST(UartToTeensyTest, CorruptData) {
-  Frame input_message{};
+  CameraFrame input_message{};
   {
     UartToTeensyBuffer buffer = UartPackToTeensy(input_message);
     buffer[0]++;
diff --git a/y2019/vision/target_sender.cc b/y2019/vision/target_sender.cc
index ad42ce4..5f2afb9 100644
--- a/y2019/vision/target_sender.cc
+++ b/y2019/vision/target_sender.cc
@@ -334,7 +334,7 @@
 
     // TODO: Select top 3 (randomly?)
 
-    frc971::jevois::Frame frame{};
+    frc971::jevois::CameraFrame frame{};
 
     for (size_t i = 0; i < results.size() && i < frame.targets.max_size();
          ++i) {
diff --git a/y2019/wpilib_interface.cc b/y2019/wpilib_interface.cc
index 02269eb..564e8e1 100644
--- a/y2019/wpilib_interface.cc
+++ b/y2019/wpilib_interface.cc
@@ -28,6 +28,7 @@
 #include "aos/util/log_interval.h"
 #include "aos/util/phased_loop.h"
 #include "aos/util/wrapping_counter.h"
+#include "ctre/phoenix/motorcontrol/can/TalonSRX.h"
 #include "frc971/autonomous/auto.q.h"
 #include "frc971/control_loops/drivetrain/drivetrain.q.h"
 #include "frc971/wpilib/ADIS16448.h"
@@ -42,8 +43,8 @@
 #include "frc971/wpilib/pdp_fetcher.h"
 #include "frc971/wpilib/sensor_reader.h"
 #include "frc971/wpilib/wpilib_robot_base.h"
-#include "ctre/phoenix/motorcontrol/can/TalonSRX.h"
 #include "y2019/constants.h"
+#include "y2019/control_loops/drivetrain/camera.q.h"
 #include "y2019/control_loops/superstructure/superstructure.q.h"
 #include "y2019/jevois/spi.h"
 
@@ -319,7 +320,22 @@
       return;
     }
 
-    // TODO(Brian): Do something useful with the targets.
+    const auto now = aos::monotonic_clock::now();
+    for (const auto &received : unpacked->frames) {
+      auto to_send = control_loops::drivetrain::camera_frames.MakeMessage();
+      to_send->timestamp =
+          std::chrono::nanoseconds((now + received.age).time_since_epoch())
+              .count();
+      to_send->num_targets = received.targets.size();
+      for (size_t i = 0; i < received.targets.size(); ++i) {
+        to_send->targets[i].distance = received.targets[i].distance;
+        to_send->targets[i].height = received.targets[i].height;
+        to_send->targets[i].heading = received.targets[i].heading;
+        to_send->targets[i].skew = received.targets[i].skew;
+      }
+      to_send->camera = received.camera_index;
+      to_send.Send();
+    }
 
     if (dummy_spi_) {
       uint8_t dummy_send, dummy_receive;