Merge "Update origin location on Spline GUI"
diff --git a/aos/network/message_bridge_protocol.cc b/aos/network/message_bridge_protocol.cc
index 9663edf..0a4a50e 100644
--- a/aos/network/message_bridge_protocol.cc
+++ b/aos/network/message_bridge_protocol.cc
@@ -28,7 +28,12 @@
       for (const Connection *connection : *channel->destination_nodes()) {
         if (connection->name()->string_view() == node_name &&
             channel->source_node()->string_view() == remote_name) {
-          channel_offsets.emplace_back(CopyFlatBuffer<Channel>(channel, &fbb));
+          // Remove the schema to save some space on the wire.
+          aos::FlatbufferDetachedBuffer<Channel> cleaned_channel =
+              CopyFlatBuffer<Channel>(channel);
+          cleaned_channel.mutable_message()->clear_schema();
+          channel_offsets.emplace_back(
+              CopyFlatBuffer<Channel>(&cleaned_channel.message(), &fbb));
         }
       }
     }
diff --git a/aos/network/message_bridge_server_lib.cc b/aos/network/message_bridge_server_lib.cc
index e5222fc..3607449 100644
--- a/aos/network/message_bridge_server_lib.cc
+++ b/aos/network/message_bridge_server_lib.cc
@@ -97,12 +97,12 @@
   for (Peer &peer : peers_) {
     logged_remotely = logged_remotely || peer.logged_remotely;
 
-    if (peer.sac_assoc_id != 0) {
-      server->Send(std::string_view(
-                       reinterpret_cast<const char *>(fbb.GetBufferPointer()),
-                       fbb.GetSize()),
-                   peer.sac_assoc_id, peer.stream,
-                   peer.connection->time_to_live() / 1000000);
+    if (peer.sac_assoc_id != 0 &&
+        server->Send(std::string_view(
+                         reinterpret_cast<const char *>(fbb.GetBufferPointer()),
+                         fbb.GetSize()),
+                     peer.sac_assoc_id, peer.stream,
+                     peer.connection->time_to_live() / 1000000)) {
       peer.server_connection_statistics->mutate_sent_packets(
           peer.server_connection_statistics->sent_packets() + 1);
       if (peer.logged_remotely) {
diff --git a/aos/network/sctp_server.cc b/aos/network/sctp_server.cc
index 093e3f0..6e32c8a 100644
--- a/aos/network/sctp_server.cc
+++ b/aos/network/sctp_server.cc
@@ -80,7 +80,7 @@
   return ReadSctpMessage(fd_, max_size_);
 }
 
-void SctpServer::Send(std::string_view data, sctp_assoc_t snd_assoc_id,
+bool SctpServer::Send(std::string_view data, sctp_assoc_t snd_assoc_id,
                       int stream, int timetolive) {
   struct iovec iov;
   iov.iov_base = const_cast<char *>(data.data());
@@ -116,11 +116,14 @@
   // And send.
   const ssize_t size = sendmsg(fd_, &outmsg, MSG_NOSIGNAL | MSG_DONTWAIT);
   if (size == -1) {
-    if (errno != EPIPE) {
-      PCHECK(size == static_cast<ssize_t>(data.size()));
+    if (errno == EPIPE || errno == EAGAIN) {
+      return false;
     }
+    PCHECK(size == static_cast<ssize_t>(data.size()));
+    return false;
   } else {
     CHECK_EQ(static_cast<ssize_t>(data.size()), size);
+    return true;
   }
 }
 
diff --git a/aos/network/sctp_server.h b/aos/network/sctp_server.h
index 25d81fc..8fa3d15 100644
--- a/aos/network/sctp_server.h
+++ b/aos/network/sctp_server.h
@@ -31,8 +31,9 @@
   // Receives the next packet from the remote.
   aos::unique_c_ptr<Message> Read();
 
-  // Sends a block of data to a client on a stream with a TTL.
-  void Send(std::string_view data, sctp_assoc_t snd_assoc_id, int stream,
+  // Sends a block of data to a client on a stream with a TTL.  Returns true on
+  // success.
+  bool Send(std::string_view data, sctp_assoc_t snd_assoc_id, int stream,
             int timetolive);
 
   int fd() { return fd_; }
diff --git a/y2020/BUILD b/y2020/BUILD
index 5b7560d..8beb799 100644
--- a/y2020/BUILD
+++ b/y2020/BUILD
@@ -138,21 +138,62 @@
     name = "config",
     src = "y2020.json",
     flatbuffers = [
-        ":setpoint_fbs",
         "//aos/network:message_bridge_client_fbs",
         "//aos/network:message_bridge_server_fbs",
         "//aos/network:timestamp_fbs",
-        "//y2019/control_loops/drivetrain:target_selector_fbs",
-        "//y2020/control_loops/superstructure:superstructure_goal_fbs",
-        "//y2020/control_loops/superstructure:superstructure_output_fbs",
-        "//y2020/control_loops/superstructure:superstructure_position_fbs",
-        "//y2020/control_loops/superstructure:superstructure_status_fbs",
         "//y2020/vision/sift:sift_fbs",
         "//y2020/vision/sift:sift_training_fbs",
         "//y2020/vision:vision_fbs",
     ],
     visibility = ["//visibility:public"],
     deps = [
+        ":config_pi1",
+        ":config_pi2",
+        ":config_pi3",
+        ":config_roborio",
+    ],
+)
+
+[
+    aos_config(
+        name = "config_" + pi,
+        src = "y2020_" + pi + ".json",
+        flatbuffers = [
+            "//aos/network:message_bridge_client_fbs",
+            "//aos/network:message_bridge_server_fbs",
+            "//aos/network:timestamp_fbs",
+            "//y2020/vision/sift:sift_fbs",
+            "//y2020/vision/sift:sift_training_fbs",
+            "//y2020/vision:vision_fbs",
+        ],
+        visibility = ["//visibility:public"],
+        deps = [
+            "//aos/events:config",
+        ],
+    )
+    for pi in [
+        "pi1",
+        "pi2",
+        "pi3",
+    ]
+]
+
+aos_config(
+    name = "config_roborio",
+    src = "y2020_roborio.json",
+    flatbuffers = [
+        ":setpoint_fbs",
+        "//aos/network:message_bridge_client_fbs",
+        "//aos/network:message_bridge_server_fbs",
+        "//aos/network:timestamp_fbs",
+        "//y2020/control_loops/superstructure:superstructure_goal_fbs",
+        "//y2019/control_loops/drivetrain:target_selector_fbs",
+        "//y2020/control_loops/superstructure:superstructure_output_fbs",
+        "//y2020/control_loops/superstructure:superstructure_position_fbs",
+        "//y2020/control_loops/superstructure:superstructure_status_fbs",
+    ],
+    deps = [
+        "//aos/events:config",
         "//aos/robot_state:config",
         "//frc971/autonomous:config",
         "//frc971/control_loops/drivetrain:config",
@@ -171,11 +212,11 @@
     srcs = ["web_proxy.sh"],
     data = [
         ":config.json",
-        "//y2020/www:field_main_bundle",
         "//aos/network:web_proxy_main",
+        "//y2020/www:camera_main_bundle",
+        "//y2020/www:field_main_bundle",
         "//y2020/www:files",
         "//y2020/www:flatbuffers",
-        "//y2020/www:camera_main_bundle",
     ],
 )
 
diff --git a/y2020/control_loops/drivetrain/localizer.cc b/y2020/control_loops/drivetrain/localizer.cc
index d1223b8..742c3ae 100644
--- a/y2020/control_loops/drivetrain/localizer.cc
+++ b/y2020/control_loops/drivetrain/localizer.cc
@@ -26,6 +26,38 @@
 // Indices of the pis to use.
 const std::array<std::string, 3> kPisToUse{"pi1", "pi2", "pi3"};
 
+// Calculates the pose implied by the camera target, just based on
+// distance/heading components.
+Eigen::Vector3d CalculateImpliedPose(const Localizer::State &X,
+                                     const Eigen::Matrix4d &H_field_target,
+                                     const Localizer::Pose &pose_robot_target) {
+  // This code overrides the pose sent directly from the camera code and
+  // effectively distills it down to just a distance + heading estimate, on
+  // the presumption that these signals will tend to be much lower noise and
+  // better-conditioned than other portions of the robot pose.
+  // As such, this code assumes that the current estimate of the robot
+  // heading is correct and then, given the heading from the camera to the
+  // target and the distance from the camera to the target, calculates the
+  // position that the robot would have to be at to make the current camera
+  // heading + distance correct. This X/Y implied robot position is then
+  // used as the measurement in the EKF, rather than the X/Y that is
+  // directly returned from the vision processing. This means that
+  // the cameras will not correct any drift in the robot heading estimate
+  // but will compensate for X/Y position in a way that prioritizes keeping
+  // an accurate distance + heading to the goal.
+
+  // Calculate the heading to the robot in the target's coordinate frame.
+  const double implied_heading_from_target = aos::math::NormalizeAngle(
+      pose_robot_target.heading() + M_PI + X(Localizer::StateIdx::kTheta));
+  const double implied_distance = pose_robot_target.xy_norm();
+  const Eigen::Vector4d robot_pose_in_target_frame(
+      implied_distance * std::cos(implied_heading_from_target),
+      implied_distance * std::sin(implied_heading_from_target), 0, 1);
+  const Eigen::Vector4d implied_pose =
+      H_field_target * robot_pose_in_target_frame;
+  return implied_pose.topRows<3>();
+}
+
 }  // namespace
 
 Localizer::Localizer(
@@ -178,42 +210,14 @@
     // reading.
     const Pose measured_pose(H_field_target *
                              (H_robot_camera * H_camera_target).inverse());
-    Eigen::Matrix<double, 3, 1> Z(measured_pose.rel_pos().x(),
-                                  measured_pose.rel_pos().y(),
-                                  measured_pose.rel_theta());
+    // This "Z" is the robot pose directly implied by the camera results.
+    // Currently, we do not actually use this result directly. However, it is
+    // kept around in case we want to quickly re-enable it.
+    const Eigen::Matrix<double, 3, 1> Z(measured_pose.rel_pos().x(),
+                                        measured_pose.rel_pos().y(),
+                                        measured_pose.rel_theta());
     // Pose of the target in the robot frame.
     Pose pose_robot_target(H_robot_camera * H_camera_target);
-    // This code overrides the pose sent directly from the camera code and
-    // effectively distills it down to just a distance + heading estimate, on
-    // the presumption that these signals will tend to be much lower noise and
-    // better-conditioned than other portions of the robot pose.
-    // As such, this code assumes that the current estimate of the robot
-    // heading is correct and then, given the heading from the camera to the
-    // target and the distance from the camera to the target, calculates the
-    // position that the robot would have to be at to make the current camera
-    // heading + distance correct. This X/Y implied robot position is then
-    // used as the measurement in the EKF, rather than the X/Y that is
-    // directly returned from the vision processing. This means that
-    // the cameras will not correct any drift in the robot heading estimate
-    // but will compensate for X/Y position in a way that prioritizes keeping
-    // an accurate distance + heading to the goal.
-    {
-      // TODO(james): This doesn't do time-compensation properly--it uses the
-      // current robot heading to calculate an implied pose, rather than using
-      // the heading from when the picture was taken.
-
-      // Calculate the heading to the robot in the target's coordinate frame.
-      const double implied_heading_from_target = aos::math::NormalizeAngle(
-          pose_robot_target.heading() + M_PI + theta());
-      const double implied_distance = pose_robot_target.xy_norm();
-      const Eigen::Vector4d robot_pose_in_target_frame(
-          implied_distance * std::cos(implied_heading_from_target),
-          implied_distance * std::sin(implied_heading_from_target), 0, 1);
-      const Eigen::Vector4d implied_pose =
-          H_field_target * robot_pose_in_target_frame;
-      Z.x() = implied_pose.x();
-      Z.y() = implied_pose.y();
-    }
     // TODO(james): Figure out how to properly handle calculating the
     // noise. Currently, the values are deliberately tuned so that image updates
     // will not be trusted overly much. In theory, we should probably also be
@@ -247,28 +251,35 @@
       AOS_LOG(WARNING, "Dropped image match due to heading mismatch.\n");
       continue;
     }
-    // Just in case we ever do encounter any, drop measurements if they have
-    // non-finite numbers.
-    if (!Z.allFinite()) {
-      AOS_LOG(WARNING, "Got measurement with infinites or NaNs.\n");
-      continue;
-    }
+    // For the correction step, instead of passing in the measurement directly,
+    // we pass in (0, 0, 0) as the measurement and then for the expected
+    // measurement (Zhat) we calculate the error between the implied and actual
+    // poses. This doesn't affect any of the math, it just makes the code a bit
+    // more convenient to write given the Correct() interface we already have.
     ekf_.Correct(
-        Z, nullptr, {},
-        [H, Z](const State &X, const Input &) {
-          Eigen::Vector3d Zhat = H * X;
-          // In order to deal with wrapping of the angle, calculate an expected
-          // angle that is in the range (Z(2) - pi, Z(2) + pi].
-          const double angle_error =
-              aos::math::NormalizeAngle(X(StateIdx::kTheta) - Z(2));
-          Zhat(2) = Z(2) + angle_error;
+        Eigen::Vector3d::Zero(), nullptr, {},
+        [H, H_field_target, pose_robot_target](
+            const State &X, const Input &) -> Eigen::Vector3d {
+          const Eigen::Vector3d Z =
+              CalculateImpliedPose(X, H_field_target, pose_robot_target);
+          // Just in case we ever do encounter any, drop measurements if they
+          // have non-finite numbers.
+          if (!Z.allFinite()) {
+            AOS_LOG(WARNING, "Got measurement with infinites or NaNs.\n");
+            return Eigen::Vector3d::Zero();
+          }
+          Eigen::Vector3d Zhat = H * X - Z;
+          // Rewrap angle difference to put it back in range. Note that this
+          // component of the error is currently ignored (see definition of H
+          // above).
+          Zhat(2) = aos::math::NormalizeAngle(Zhat(2));
           // If the measurement implies that we are too far from the current
           // estimate, then ignore it.
           // Note that I am not entirely sure how much effect this actually has,
           // because I primarily introduced it to make sure that any grossly
           // invalid measurements get thrown out.
-          if ((Zhat - Z).squaredNorm() > std::pow(10.0, 2)) {
-            return Z;
+          if (Zhat.squaredNorm() > std::pow(10.0, 2)) {
+            return Eigen::Vector3d::Zero();
           }
           return Zhat;
         },
diff --git a/y2020/control_loops/drivetrain/localizer_test.cc b/y2020/control_loops/drivetrain/localizer_test.cc
index c0995b0..e864330 100644
--- a/y2020/control_loops/drivetrain/localizer_test.cc
+++ b/y2020/control_loops/drivetrain/localizer_test.cc
@@ -443,10 +443,7 @@
 
   RunFor(chrono::seconds(3));
   VerifyNearGoal();
-  // Note: because the current localizer code doesn't do time-compensation
-  // correctly (see comments in localizer.cc), the "perfect" camera updates
-  // aren't actually handled perfectly.
-  EXPECT_TRUE(VerifyEstimatorAccurate(2e-2));
+  EXPECT_TRUE(VerifyEstimatorAccurate(2e-3));
 }
 
 // Tests that camera updates with a constant initial error in the position
diff --git a/y2020/y2020.json b/y2020/y2020.json
index 285a23d..a0a465a 100644
--- a/y2020/y2020.json
+++ b/y2020/y2020.json
@@ -1,580 +1,9 @@
 {
   "channel_storage_duration": 5000000000,
-  "channels":
-  [
-    {
-      "name": "/aos/roborio",
-      "type": "aos.JoystickState",
-      "source_node": "roborio",
-      "frequency": 75
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.RobotState",
-      "source_node": "roborio",
-      "frequency": 200,
-      "destination_nodes": [
-        {
-          "name": "pi1",
-          "priority": 2,
-          "timestamp_logger": "LOCAL_LOGGER",
-          "time_to_live": 10000000
-        },
-        {
-          "name": "pi2",
-          "priority": 2,
-          "timestamp_logger": "LOCAL_LOGGER",
-          "time_to_live": 10000000
-        },
-        {
-          "name": "pi3",
-          "priority": 2,
-          "timestamp_logger": "LOCAL_LOGGER",
-          "time_to_live": 10000000
-        }
-      ]
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.timing.Report",
-      "source_node": "roborio",
-      "frequency": 50,
-      "num_senders": 20,
-      "max_size": 2048
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.logging.LogMessageFbs",
-      "source_node": "roborio",
-      "frequency": 400,
-      "num_senders": 20
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.message_bridge.ServerStatistics",
-      "source_node": "roborio",
-      "frequency": 2,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.message_bridge.ClientStatistics",
-      "source_node": "roborio",
-      "frequency": 10,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "aos.message_bridge.Timestamp",
-      "source_node": "roborio",
-      "frequency": 10,
-      "num_senders": 2,
-      "max_size": 200,
-      "destination_nodes": [
-        {
-          "name": "pi1",
-          "priority": 1,
-          "time_to_live": 5000000
-        },
-        {
-          "name": "pi2",
-          "priority": 1,
-          "time_to_live": 5000000
-        },
-        {
-          "name": "pi3",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/aos/pi1",
-      "type": "aos.timing.Report",
-      "source_node": "pi1",
-      "frequency": 50,
-      "num_senders": 20,
-      "max_size": 2048
-    },
-    {
-      "name": "/aos/pi1",
-      "type": "aos.logging.LogMessageFbs",
-      "source_node": "pi1",
-      "frequency": 200,
-      "num_senders": 20
-    },
-    {
-      "name": "/aos/pi1",
-      "type": "aos.message_bridge.ServerStatistics",
-      "source_node": "pi1",
-      "frequency": 2,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi1",
-      "type": "aos.message_bridge.ClientStatistics",
-      "source_node": "pi1",
-      "frequency": 10,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi1",
-      "type": "aos.message_bridge.Timestamp",
-      "source_node": "pi1",
-      "frequency": 10,
-      "num_senders": 2,
-      "max_size": 200,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/aos/pi2",
-      "type": "aos.timing.Report",
-      "source_node": "pi2",
-      "frequency": 50,
-      "num_senders": 20,
-      "max_size": 2048
-    },
-    {
-      "name": "/aos/pi2",
-      "type": "aos.logging.LogMessageFbs",
-      "source_node": "pi2",
-      "frequency": 200,
-      "num_senders": 20
-    },
-    {
-      "name": "/aos/pi2",
-      "type": "aos.message_bridge.ServerStatistics",
-      "source_node": "pi2",
-      "frequency": 2,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi2",
-      "type": "aos.message_bridge.ClientStatistics",
-      "source_node": "pi2",
-      "frequency": 10,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi2",
-      "type": "aos.message_bridge.Timestamp",
-      "source_node": "pi2",
-      "frequency": 10,
-      "num_senders": 2,
-      "max_size": 200,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/aos/pi3",
-      "type": "aos.timing.Report",
-      "source_node": "pi3",
-      "frequency": 50,
-      "num_senders": 20,
-      "max_size": 2048
-    },
-    {
-      "name": "/aos/pi3",
-      "type": "aos.logging.LogMessageFbs",
-      "source_node": "pi3",
-      "frequency": 200,
-      "num_senders": 20
-    },
-    {
-      "name": "/aos/pi3",
-      "type": "aos.message_bridge.ServerStatistics",
-      "source_node": "pi3",
-      "frequency": 2,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi3",
-      "type": "aos.message_bridge.ClientStatistics",
-      "source_node": "pi3",
-      "frequency": 10,
-      "num_senders": 2
-    },
-    {
-      "name": "/aos/pi3",
-      "type": "aos.message_bridge.Timestamp",
-      "source_node": "pi3",
-      "frequency": 10,
-      "num_senders": 2,
-      "max_size": 200,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/superstructure",
-      "type": "y2020.control_loops.superstructure.Goal",
-      "source_node": "roborio",
-      "frequency": 200
-    },
-    {
-      "name": "/superstructure",
-      "type": "y2020.control_loops.superstructure.Status",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/superstructure",
-      "type": "y2020.control_loops.superstructure.Output",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/superstructure",
-      "type": "y2020.control_loops.superstructure.Position",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/superstructure",
-      "type": "y2020.joysticks.Setpoint",
-      "source_node": "roborio",
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.IMUValues",
-      "source_node": "roborio",
-      "frequency": 2000,
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.sensors.GyroReading",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.sensors.Uid",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Goal",
-      "source_node": "roborio",
-      "frequency": 200
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Position",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Status",
-      "source_node": "roborio",
-      "frequency": 200,
-      "max_size": 2000,
-      "num_senders": 2,
-      "destination_nodes": [
-        {
-          "name": "pi1",
-          "priority": 5,
-          "time_to_live": 5000000
-        },
-        {
-          "name": "pi2",
-          "priority": 5,
-          "time_to_live": 5000000
-        },
-        {
-          "name": "pi3",
-          "priority": 5,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.Output",
-      "source_node": "roborio",
-      "frequency": 200,
-      "num_senders": 2
-    },
-    {
-      "name": "/drivetrain",
-      "type": "frc971.control_loops.drivetrain.LocalizerControl",
-      "source_node": "roborio",
-      "frequency": 200
-    },
-    {
-      "name": "/drivetrain",
-      "type": "y2019.control_loops.drivetrain.TargetSelectorHint",
-      "source_node": "roborio"
-    },
-    {
-      "name": "/pi1/camera",
-      "type": "frc971.vision.CameraImage",
-      "source_node": "pi1",
-      "frequency": 25,
-      "max_size": 620000,
-      "num_senders": 18
-    },
-    {
-      "name": "/pi1/camera",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi1",
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_node": "roborio",
-      "frequency": 25,
-      "max_size": 10000,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/pi1/camera/detailed",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi1",
-      "frequency": 25,
-      "max_size": 1000000
-    },
-    {
-      "name": "/pi1/camera",
-      "type": "frc971.vision.sift.TrainingData",
-      "source_node": "pi1",
-      "frequency": 2,
-      "max_size": 2000000
-    },
-    {
-      "name": "/pi2/camera",
-      "type": "frc971.vision.CameraImage",
-      "source_node": "pi2",
-      "frequency": 25,
-      "max_size": 620000,
-      "num_senders": 18
-    },
-    {
-      "name": "/pi2/camera",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi2",
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_node": "roborio",
-      "frequency": 25,
-      "max_size": 300000,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/pi2/camera/detailed",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi2",
-      "frequency": 25,
-      "max_size": 1000000
-    },
-    {
-      "name": "/pi2/camera",
-      "type": "frc971.vision.sift.TrainingData",
-      "source_node": "pi2",
-      "frequency": 2,
-      "max_size": 2000000
-    },
-    {
-      "name": "/pi3/camera",
-      "type": "frc971.vision.CameraImage",
-      "source_node": "pi3",
-      "frequency": 25,
-      "max_size": 620000,
-      "num_senders": 18
-    },
-    {
-      "name": "/pi3/camera",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi3",
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_node": "roborio",
-      "frequency": 25,
-      "max_size": 10000,
-      "destination_nodes": [
-        {
-          "name": "roborio",
-          "priority": 1,
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/pi3/camera/detailed",
-      "type": "frc971.vision.sift.ImageMatchResult",
-      "source_node": "pi3",
-      "frequency": 25,
-      "max_size": 1000000
-    },
-    {
-      "name": "/pi3/camera",
-      "type": "frc971.vision.sift.TrainingData",
-      "source_node": "pi3",
-      "frequency": 2,
-      "max_size": 2000000
-    },
-    {
-      "name": "/autonomous",
-      "type": "aos.common.actions.Status",
-      "source_node": "roborio"
-    },
-    {
-      "name": "/autonomous",
-      "type": "frc971.autonomous.Goal",
-      "source_node": "roborio"
-    },
-    {
-      "name": "/autonomous",
-      "type": "frc971.autonomous.AutonomousMode",
-      "source_node": "roborio",
-      "frequency": 200
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "frc971.PDPValues",
-      "source_node": "roborio",
-      "frequency": 50
-    },
-    {
-      "name": "/aos/roborio",
-      "type": "frc971.wpilib.PneumaticsToLog",
-      "source_node": "roborio",
-      "frequency": 50
-    }
-  ],
-  "applications": [
-    {
-      "name": "drivetrain"
-    },
-    {
-      "name": "camera_reader"
-    }
-  ],
   "maps": [
     {
       "match": {
         "name": "/aos",
-        "source_node": "roborio"
-      },
-      "rename": {
-        "name": "/aos/roborio"
-      }
-    },
-    {
-      "match": {
-        "name": "/aos",
-        "source_node": "pi1"
-      },
-      "rename": {
-        "name": "/aos/pi1"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera",
-        "source_node": "pi1"
-      },
-      "rename": {
-        "name": "/pi1/camera"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera/detailed",
-        "source_node": "pi1"
-      },
-      "rename": {
-        "name": "/pi1/camera/detailed"
-      }
-    },
-    {
-      "match": {
-        "name": "/aos",
-        "source_node": "pi2"
-      },
-      "rename": {
-        "name": "/aos/pi2"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera",
-        "source_node": "pi2"
-      },
-      "rename": {
-        "name": "/pi2/camera"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera/detailed",
-        "source_node": "pi2"
-      },
-      "rename": {
-        "name": "/pi2/camera/detailed"
-      }
-    },
-    {
-      "match": {
-        "name": "/aos",
-        "source_node": "pi3"
-      },
-      "rename": {
-        "name": "/aos/pi3"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera",
-        "source_node": "pi3"
-      },
-      "rename": {
-        "name": "/pi3/camera"
-      }
-    },
-    {
-      "match": {
-        "name": "/camera/detailed",
-        "source_node": "pi3"
-      },
-      "rename": {
-        "name": "/pi3/camera/detailed"
-      }
-    },
-    {
-      "match": {
-        "name": "/aos",
         "type": "aos.RobotState"
       },
       "rename": {
@@ -582,50 +11,10 @@
       }
     }
   ],
-  "nodes": [
-    {
-      "name": "roborio",
-      "hostname": "roborio",
-      "hostnames": [
-        "roboRIO-971-FRC",
-        "roboRIO-7971-FRC",
-        "roboRIO-8971-FRC",
-        "roboRIO-9971-FRC"
-      ],
-      "port": 9971
-    },
-    {
-      "name": "pi1",
-      "hostname": "pi1",
-      "hostnames": [
-        "pi-971-1",
-        "pi-7971-1",
-        "pi-8971-1",
-        "pi-9971-1"
-      ],
-      "port": 9971
-    },
-    {
-      "name": "pi2",
-      "hostname": "pi2",
-      "hostnames": [
-        "pi-971-2",
-        "pi-7971-2",
-        "pi-8971-2",
-        "pi-9971-2"
-      ],
-      "port": 9971
-    },
-    {
-      "name": "pi3",
-      "hostname": "pi3",
-      "hostnames": [
-        "pi-971-3",
-        "pi-7971-3",
-        "pi-8971-3",
-        "pi-9971-3"
-      ],
-      "port": 9971
-    }
+  "imports": [
+    "y2020_roborio.json",
+    "y2020_pi1.json",
+    "y2020_pi2.json",
+    "y2020_pi3.json"
   ]
 }
diff --git a/y2020/y2020_pi1.json b/y2020/y2020_pi1.json
new file mode 100644
index 0000000..f14996c
--- /dev/null
+++ b/y2020/y2020_pi1.json
@@ -0,0 +1,132 @@
+{
+  "channels":
+  [
+    {
+      "name": "/aos/pi1",
+      "type": "aos.timing.Report",
+      "source_node": "pi1",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/aos/pi1",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi1",
+      "frequency": 200,
+      "num_senders": 20
+    },
+    {
+      "name": "/aos/pi1",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi1",
+      "frequency": 2,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi1",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi1",
+      "frequency": 10,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi1",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi1",
+      "frequency": 10,
+      "num_senders": 2,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "pi1",
+      "frequency": 25,
+      "max_size": 620000,
+      "num_senders": 18
+    },
+    {
+      "name": "/pi1/camera",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi1",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_node": "roborio",
+      "frequency": 25,
+      "max_size": 10000,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi1/camera/detailed",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi1",
+      "frequency": 25,
+      "max_size": 1000000
+    },
+    {
+      "name": "/pi1/camera",
+      "type": "frc971.vision.sift.TrainingData",
+      "source_node": "pi1",
+      "frequency": 2,
+      "max_size": 2000000
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/aos/pi1"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/camera"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera/detailed",
+        "source_node": "pi1"
+      },
+      "rename": {
+        "name": "/pi1/camera/detailed"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi1",
+      "hostname": "pi1",
+      "hostnames": [
+        "pi-971-1",
+        "pi-7971-1",
+        "pi-8971-1",
+        "pi-9971-1"
+      ],
+      "port": 9971
+    },
+    {
+      "name": "roborio"
+    }
+  ]
+}
diff --git a/y2020/y2020_pi2.json b/y2020/y2020_pi2.json
new file mode 100644
index 0000000..82a2bc9
--- /dev/null
+++ b/y2020/y2020_pi2.json
@@ -0,0 +1,132 @@
+{
+  "channels":
+  [
+    {
+      "name": "/aos/pi2",
+      "type": "aos.timing.Report",
+      "source_node": "pi2",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/aos/pi2",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi2",
+      "frequency": 200,
+      "num_senders": 20
+    },
+    {
+      "name": "/aos/pi2",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi2",
+      "frequency": 2,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi2",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi2",
+      "frequency": 10,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi2",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi2",
+      "frequency": 10,
+      "num_senders": 2,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "pi2",
+      "frequency": 25,
+      "max_size": 620000,
+      "num_senders": 18
+    },
+    {
+      "name": "/pi2/camera",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi2",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_node": "roborio",
+      "frequency": 25,
+      "max_size": 10000,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi2/camera/detailed",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi2",
+      "frequency": 25,
+      "max_size": 1000000
+    },
+    {
+      "name": "/pi2/camera",
+      "type": "frc971.vision.sift.TrainingData",
+      "source_node": "pi2",
+      "frequency": 2,
+      "max_size": 2000000
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/aos/pi2"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/camera"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera/detailed",
+        "source_node": "pi2"
+      },
+      "rename": {
+        "name": "/pi2/camera/detailed"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi2",
+      "hostname": "pi2",
+      "hostnames": [
+        "pi-971-2",
+        "pi-7971-2",
+        "pi-8971-2",
+        "pi-9971-2"
+      ],
+      "port": 9971
+    },
+    {
+      "name": "roborio"
+    }
+  ]
+}
diff --git a/y2020/y2020_pi3.json b/y2020/y2020_pi3.json
new file mode 100644
index 0000000..727e41d
--- /dev/null
+++ b/y2020/y2020_pi3.json
@@ -0,0 +1,132 @@
+{
+  "channels":
+  [
+    {
+      "name": "/aos/pi3",
+      "type": "aos.timing.Report",
+      "source_node": "pi3",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/aos/pi3",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "pi3",
+      "frequency": 200,
+      "num_senders": 20
+    },
+    {
+      "name": "/aos/pi3",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "pi3",
+      "frequency": 2,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi3",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "pi3",
+      "frequency": 10,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/pi3",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "pi3",
+      "frequency": 10,
+      "num_senders": 2,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "pi3",
+      "frequency": 25,
+      "max_size": 620000,
+      "num_senders": 18
+    },
+    {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi3",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_node": "roborio",
+      "frequency": 25,
+      "max_size": 10000,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi3/camera/detailed",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi3",
+      "frequency": 25,
+      "max_size": 1000000
+    },
+    {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.sift.TrainingData",
+      "source_node": "pi3",
+      "frequency": 2,
+      "max_size": 2000000
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/aos/pi3"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/pi3/camera"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera/detailed",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/pi3/camera/detailed"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "pi3",
+      "hostname": "pi3",
+      "hostnames": [
+        "pi-971-3",
+        "pi-7971-3",
+        "pi-8971-3",
+        "pi-9971-3"
+      ],
+      "port": 9971
+    },
+    {
+      "name": "roborio"
+    }
+  ]
+}
diff --git a/y2020/y2020_roborio.json b/y2020/y2020_roborio.json
new file mode 100644
index 0000000..86d9b7e
--- /dev/null
+++ b/y2020/y2020_roborio.json
@@ -0,0 +1,270 @@
+{
+  "channels":
+  [
+    {
+      "name": "/aos/roborio",
+      "type": "aos.JoystickState",
+      "source_node": "roborio",
+      "frequency": 75
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.RobotState",
+      "source_node": "roborio",
+      "frequency": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 2,
+          "timestamp_logger": "LOCAL_LOGGER",
+          "time_to_live": 10000000
+        },
+        {
+          "name": "pi2",
+          "priority": 2,
+          "timestamp_logger": "LOCAL_LOGGER",
+          "time_to_live": 10000000
+        },
+        {
+          "name": "pi3",
+          "priority": 2,
+          "timestamp_logger": "LOCAL_LOGGER",
+          "time_to_live": 10000000
+        }
+      ]
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.timing.Report",
+      "source_node": "roborio",
+      "frequency": 50,
+      "num_senders": 20,
+      "max_size": 2048
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.logging.LogMessageFbs",
+      "source_node": "roborio",
+      "frequency": 400,
+      "num_senders": 20
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.message_bridge.ServerStatistics",
+      "source_node": "roborio",
+      "frequency": 2,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.message_bridge.ClientStatistics",
+      "source_node": "roborio",
+      "frequency": 10,
+      "num_senders": 2
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "aos.message_bridge.Timestamp",
+      "source_node": "roborio",
+      "frequency": 10,
+      "num_senders": 2,
+      "max_size": 200,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 1,
+          "time_to_live": 5000000
+        },
+        {
+          "name": "pi2",
+          "priority": 1,
+          "time_to_live": 5000000
+        },
+        {
+          "name": "pi3",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/superstructure",
+      "type": "y2020.control_loops.superstructure.Goal",
+      "source_node": "roborio",
+      "frequency": 200
+    },
+    {
+      "name": "/superstructure",
+      "type": "y2020.control_loops.superstructure.Status",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/superstructure",
+      "type": "y2020.control_loops.superstructure.Output",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/superstructure",
+      "type": "y2020.control_loops.superstructure.Position",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/superstructure",
+      "type": "y2020.joysticks.Setpoint",
+      "source_node": "roborio",
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.IMUValues",
+      "source_node": "roborio",
+      "frequency": 2000,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.sensors.GyroReading",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.sensors.Uid",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.Goal",
+      "source_node": "roborio",
+      "frequency": 200
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.Position",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.Status",
+      "source_node": "roborio",
+      "frequency": 200,
+      "max_size": 2000,
+      "num_senders": 2,
+      "destination_nodes": [
+        {
+          "name": "pi1",
+          "priority": 5,
+          "time_to_live": 5000000
+        },
+        {
+          "name": "pi2",
+          "priority": 5,
+          "time_to_live": 5000000
+        },
+        {
+          "name": "pi3",
+          "priority": 5,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.Output",
+      "source_node": "roborio",
+      "frequency": 200,
+      "num_senders": 2
+    },
+    {
+      "name": "/drivetrain",
+      "type": "frc971.control_loops.drivetrain.LocalizerControl",
+      "source_node": "roborio",
+      "frequency": 200
+    },
+    {
+      "name": "/drivetrain",
+      "type": "y2019.control_loops.drivetrain.TargetSelectorHint",
+      "source_node": "roborio"
+    },
+    {
+      "name": "/autonomous",
+      "type": "aos.common.actions.Status",
+      "source_node": "roborio"
+    },
+    {
+      "name": "/autonomous",
+      "type": "frc971.autonomous.Goal",
+      "source_node": "roborio"
+    },
+    {
+      "name": "/autonomous",
+      "type": "frc971.autonomous.AutonomousMode",
+      "source_node": "roborio",
+      "frequency": 200
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "frc971.PDPValues",
+      "source_node": "roborio",
+      "frequency": 50
+    },
+    {
+      "name": "/aos/roborio",
+      "type": "frc971.wpilib.PneumaticsToLog",
+      "source_node": "roborio",
+      "frequency": 50
+    }
+  ],
+  "applications": [
+    {
+      "name": "drivetrain"
+    },
+    {
+      "name": "camera_reader"
+    }
+  ],
+  "maps": [
+    {
+      "match": {
+        "name": "/aos",
+        "source_node": "roborio"
+      },
+      "rename": {
+        "name": "/aos/roborio"
+      }
+    }
+  ],
+  "nodes": [
+    {
+      "name": "roborio",
+      "hostname": "roborio",
+      "hostnames": [
+        "roboRIO-971-FRC",
+        "roboRIO-7971-FRC",
+        "roboRIO-8971-FRC",
+        "roboRIO-9971-FRC"
+      ],
+      "port": 9971
+    },
+    {
+      "name": "pi1"
+    },
+    {
+      "name": "pi2"
+    },
+    {
+      "name": "pi3"
+    }
+  ]
+}