Merge "show center of target"
diff --git a/WORKSPACE b/WORKSPACE
index eee3d96..2eba07b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -58,6 +58,14 @@
     "//debian:opencv_amd64.bzl",
     opencv_amd64_debs = "files",
 )
+load(
+    "//debian:gstreamer_amd64.bzl",
+    gstreamer_amd64_debs = "files"
+)
+load(
+    "//debian:gstreamer_armhf.bzl",
+    gstreamer_armhf_debs = "files"
+)
 load("//debian:packages.bzl", "generate_repositories_for_debs")
 
 generate_repositories_for_debs(python_debs)
@@ -88,6 +96,10 @@
 
 generate_repositories_for_debs(opencv_amd64_debs)
 
+generate_repositories_for_debs(gstreamer_amd64_debs)
+
+generate_repositories_for_debs(gstreamer_armhf_debs)
+
 http_archive(
     name = "python_repo",
     build_file = "@//debian:python.BUILD",
@@ -692,3 +704,17 @@
     type = "zip",
     url = "http://www.frc971.org/Build-Dependencies/opencv_contrib_python_nonfree-4.1.1.1-cp35-cp35m-manylinux1_x86_64.whl",
 )
+
+http_archive(
+    name = "gstreamer_k8",
+    build_file = "@//debian:gstreamer.BUILD",
+    sha256 = "4d74d4a82f7a73dc9fe9463d5fae409b17845eef7cd64ef9c4c4553816c53589",
+    url = "http://www.frc971.org/Build-Dependencies/gstreamer_amd64.tar.gz",
+)
+
+http_archive(
+    name = "gstreamer_armhf",
+    build_file = "@//debian:gstreamer.BUILD",
+    sha256 = "c5ac4c604952c274a50636e244f0d091bd1de302032446f24f0e9e03ae9c76f7",
+    url = "http://www.frc971.org/Build-Dependencies/gstreamer_armhf.tar.gz",
+)
diff --git a/aos/events/BUILD b/aos/events/BUILD
index a905728..d35f206 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -64,6 +64,7 @@
         "event_loop_tmpl.h",
     ],
     hdrs = [
+        "channel_preallocated_allocator.h",
         "event_loop.h",
     ],
     visibility = ["//visibility:public"],
diff --git a/aos/events/channel_preallocated_allocator.h b/aos/events/channel_preallocated_allocator.h
new file mode 100644
index 0000000..8a5d68f
--- /dev/null
+++ b/aos/events/channel_preallocated_allocator.h
@@ -0,0 +1,77 @@
+#ifndef AOS_EVENTS_CHANNEL_PREALLOCATED_ALLOCATOR_
+#define AOS_EVENTS_CHANNEL_PREALLOCATED_ALLOCATOR_
+
+#include "aos/configuration.h"
+#include "aos/configuration_generated.h"
+#include "flatbuffers/flatbuffers.h"
+
+namespace aos {
+
+class ChannelPreallocatedAllocator : public flatbuffers::Allocator {
+ public:
+  ChannelPreallocatedAllocator(uint8_t *data, size_t size,
+                               const Channel *channel)
+      : data_(data), size_(size), channel_(channel) {}
+
+  ChannelPreallocatedAllocator(const ChannelPreallocatedAllocator &) = delete;
+  ChannelPreallocatedAllocator(ChannelPreallocatedAllocator &&other)
+      : data_(other.data_), size_(other.size_), channel_(other.channel_) {
+    CHECK(!is_allocated());
+    CHECK(!other.is_allocated());
+  }
+
+  ChannelPreallocatedAllocator &operator=(
+      const ChannelPreallocatedAllocator &) = delete;
+  ChannelPreallocatedAllocator &operator=(
+      ChannelPreallocatedAllocator &&other) {
+    CHECK(!is_allocated());
+    CHECK(!other.is_allocated());
+    data_ = other.data_;
+    size_ = other.size_;
+    channel_ = other.channel_;
+    return *this;
+  }
+  ~ChannelPreallocatedAllocator() override { CHECK(!is_allocated_); }
+
+  // TODO(austin): Read the contract for these.
+  uint8_t *allocate(size_t /*size*/) override {
+    if (is_allocated_) {
+      LOG(FATAL) << "Can't allocate more memory with a fixed size allocator.  "
+                    "Increase the memory reserved.";
+    }
+
+    is_allocated_ = true;
+    return data_;
+  }
+
+  void deallocate(uint8_t *, size_t) override { is_allocated_ = false; }
+
+  uint8_t *reallocate_downward(uint8_t * /*old_p*/, size_t /*old_size*/,
+                               size_t new_size, size_t /*in_use_back*/,
+                               size_t /*in_use_front*/) override {
+    LOG(FATAL) << "Requested " << new_size << " bytes, max size "
+               << channel_->max_size() << " for channel "
+               << configuration::CleanedChannelToString(channel_)
+               << ".  Increase the memory reserved to at least " << new_size
+               << ".";
+    return nullptr;
+  }
+
+  void Reset() { is_allocated_ = false; }
+  bool is_allocated() const { return is_allocated_; }
+
+  bool allocated() { return is_allocated_; }
+
+  size_t size() const { return size_; }
+  const uint8_t *data() const { return data_; }
+
+ private:
+  bool is_allocated_ = false;
+  uint8_t *data_;
+  size_t size_;
+  const Channel *channel_;
+};
+
+}  // namespace aos
+
+#endif  // AOS_EVENTS_CHANNEL_PREALLOCATED_ALLOCATOR_
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index d0ca196..bc5a5ae 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -1,5 +1,4 @@
 #ifndef AOS_EVENTS_EVENT_LOOP_H_
-
 #define AOS_EVENTS_EVENT_LOOP_H_
 
 #include <atomic>
@@ -8,6 +7,7 @@
 
 #include "aos/configuration.h"
 #include "aos/configuration_generated.h"
+#include "aos/events/channel_preallocated_allocator.h"
 #include "aos/events/event_loop_event.h"
 #include "aos/events/event_loop_generated.h"
 #include "aos/events/timing_statistics.h"
@@ -145,8 +145,9 @@
 
   // Returns the associated flatbuffers-style allocator. This must be
   // deallocated before the message is sent.
-  PreallocatedAllocator *fbb_allocator() {
-    fbb_allocator_ = PreallocatedAllocator(data(), size());
+  ChannelPreallocatedAllocator *fbb_allocator() {
+    fbb_allocator_ = ChannelPreallocatedAllocator(
+        reinterpret_cast<uint8_t *>(data()), size(), channel());
     return &fbb_allocator_;
   }
 
@@ -176,7 +177,7 @@
 
   internal::RawSenderTiming timing_;
 
-  PreallocatedAllocator fbb_allocator_{nullptr, 0};
+  ChannelPreallocatedAllocator fbb_allocator_{nullptr, 0, nullptr};
 };
 
 // Fetches the newest message from a channel.
@@ -247,7 +248,7 @@
   // builder.Send(t_builder.Finish());
   class Builder {
    public:
-    Builder(RawSender *sender, PreallocatedAllocator *allocator)
+    Builder(RawSender *sender, ChannelPreallocatedAllocator *allocator)
         : fbb_(allocator->size(), allocator),
           allocator_(allocator),
           sender_(sender) {
@@ -283,7 +284,7 @@
 
    private:
     flatbuffers::FlatBufferBuilder fbb_;
-    PreallocatedAllocator *allocator_;
+    ChannelPreallocatedAllocator *allocator_;
     RawSender *sender_;
   };
 
@@ -302,9 +303,7 @@
   // Returns the name of the underlying queue.
   const Channel *channel() const { return sender_->channel(); }
 
-  operator bool() {
-    return sender_ ? true : false;
-  }
+  operator bool() { return sender_ ? true : false; }
 
   // Returns the time_points that the last message was sent at.
   aos::monotonic_clock::time_point monotonic_sent_time() const {
@@ -464,13 +463,33 @@
 
   // This will watch messages sent to the provided channel.
   //
-  // Watch is a functor that have a call signature like so:
-  // void Event(const MessageType& type);
+  // w must have a non-polymorphic operator() (aka it can only be called with a
+  // single set of arguments; no overloading or templates). It must be callable
+  // with this signature:
+  //   void(const MessageType &);
   //
-  // TODO(parker): Need to support ::std::bind.  For now, use lambdas.
-  // TODO(austin): Do we need a functor?  Or is a std::function good enough?
+  // Lambdas are a common form for w. A std::function will work too.
+  //
+  // Note that bind expressions have polymorphic call operators, so they are not
+  // allowed.
+  //
+  // We template Watch as a whole instead of using std::function<void(const T
+  // &)> to allow deducing MessageType from lambdas and other things which are
+  // implicitly convertible to std::function, but not actually std::function
+  // instantiations. Template deduction guides might allow solving this
+  // differently in newer versions of C++, but those have their own corner
+  // cases.
   template <typename Watch>
-  void MakeWatcher(const std::string_view name, Watch &&w);
+  void MakeWatcher(const std::string_view channel_name, Watch &&w);
+
+  // Like MakeWatcher, but doesn't have access to the message data. This may be
+  // implemented to use less resources than an equivalent MakeWatcher.
+  //
+  // The function will still have access to context(), although that will have
+  // its data field set to nullptr.
+  template <typename MessageType>
+  void MakeNoArgWatcher(const std::string_view channel_name,
+                        std::function<void()> w);
 
   // The passed in function will be called when the event loop starts.
   // Use this to run code once the thread goes into "real-time-mode",
@@ -518,6 +537,18 @@
       std::function<void(const Context &context, const void *message)>
           watcher) = 0;
 
+  // Watches channel (name, type) for new messages, without needing to extract
+  // the message contents. Default implementation simply re-uses MakeRawWatcher.
+  virtual void MakeRawNoArgWatcher(
+      const Channel *channel,
+      std::function<void(const Context &context)> watcher) {
+    MakeRawWatcher(channel, [watcher](const Context &context, const void *) {
+      Context new_context = context;
+      new_context.data = nullptr;
+      watcher(new_context);
+    });
+  }
+
   // Creates a raw sender for the provided channel.  This is used for reflection
   // based sending.
   // Note: this ignores any node constraints.  Ignore at your own peril.
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index 096d84a..f0fb3d3 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -43,6 +43,94 @@
   EXPECT_TRUE(happened);
 }
 
+// Verifies that a no-arg watcher will not have a data pointer.
+TEST_P(AbstractEventLoopTest, NoArgNoData) {
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
+
+  bool happened = false;
+
+  loop2->OnRun([&]() {
+    happened = true;
+
+    aos::Sender<TestMessage>::Builder msg = sender.MakeBuilder();
+    TestMessage::Builder builder = msg.MakeBuilder<TestMessage>();
+    ASSERT_TRUE(msg.Send(builder.Finish()));
+  });
+
+  loop2->MakeNoArgWatcher<TestMessage>("/test", [&]() {
+    EXPECT_GT(loop2->context().size, 0u);
+    EXPECT_EQ(nullptr, loop2->context().data);
+    this->Exit();
+  });
+
+  EXPECT_FALSE(happened);
+  Run();
+  EXPECT_TRUE(happened);
+}
+
+// Tests that no-arg watcher can receive messages from a sender.
+// Also tests that OnRun() works.
+TEST_P(AbstractEventLoopTest, BasicNoArg) {
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
+
+  bool happened = false;
+
+  loop2->OnRun([&]() {
+    happened = true;
+
+    aos::Sender<TestMessage>::Builder msg = sender.MakeBuilder();
+    TestMessage::Builder builder = msg.MakeBuilder<TestMessage>();
+    builder.add_value(200);
+    ASSERT_TRUE(msg.Send(builder.Finish()));
+  });
+
+  aos::Fetcher<TestMessage> fetcher = loop2->MakeFetcher<TestMessage>("/test");
+  loop2->MakeNoArgWatcher<TestMessage>("/test", [&]() {
+    ASSERT_TRUE(fetcher.Fetch());
+    EXPECT_EQ(fetcher->value(), 200);
+    this->Exit();
+  });
+
+  EXPECT_FALSE(happened);
+  Run();
+  EXPECT_TRUE(happened);
+}
+
+// Tests that a watcher can be created with an std::function.
+TEST_P(AbstractEventLoopTest, BasicFunction) {
+  auto loop1 = Make();
+  auto loop2 = MakePrimary();
+
+  aos::Sender<TestMessage> sender = loop1->MakeSender<TestMessage>("/test");
+
+  bool happened = false;
+
+  loop2->OnRun([&]() {
+    happened = true;
+
+    aos::Sender<TestMessage>::Builder msg = sender.MakeBuilder();
+    TestMessage::Builder builder = msg.MakeBuilder<TestMessage>();
+    builder.add_value(200);
+    ASSERT_TRUE(msg.Send(builder.Finish()));
+  });
+
+  loop2->MakeWatcher("/test", std::function<void(const TestMessage &)>(
+                                  [&](const TestMessage &message) {
+                                    EXPECT_EQ(message.value(), 200);
+                                    this->Exit();
+                                  }));
+
+  EXPECT_FALSE(happened);
+  Run();
+  EXPECT_TRUE(happened);
+}
+
 // Tests that watcher can receive messages from two senders.
 // Also tests that OnRun() works.
 TEST_P(AbstractEventLoopTest, BasicTwoSenders) {
@@ -463,6 +551,9 @@
   EXPECT_DEATH(
       { loop->MakeWatcher("/test/invalid", [&](const TestMessage &) {}); },
       "/test/invalid");
+  EXPECT_DEATH(
+      { loop->MakeNoArgWatcher<TestMessage>("/test/invalid", [&]() {}); },
+      "/test/invalid");
 }
 
 // Verify that registering a watcher twice for "/test" fails.
@@ -471,6 +562,16 @@
   loop->MakeWatcher("/test", [&](const TestMessage &) {});
   EXPECT_DEATH(loop->MakeWatcher("/test", [&](const TestMessage &) {}),
                "/test");
+  EXPECT_DEATH(loop->MakeNoArgWatcher<TestMessage>("/test", [&]() {}), "/test");
+}
+
+// Verify that registering a no-arg watcher twice for "/test" fails.
+TEST_P(AbstractEventLoopDeathTest, TwoNoArgWatcher) {
+  auto loop = Make();
+  loop->MakeNoArgWatcher<TestMessage>("/test", [&]() {});
+  EXPECT_DEATH(loop->MakeWatcher("/test", [&](const TestMessage &) {}),
+               "/test");
+  EXPECT_DEATH(loop->MakeNoArgWatcher<TestMessage>("/test", [&]() {}), "/test");
 }
 
 // Verify that SetRuntimeRealtimePriority fails while running.
@@ -512,6 +613,16 @@
   EXPECT_DEATH(Run(), "running");
 }
 
+// Verify that we can't create a no-arg watcher inside OnRun.
+TEST_P(AbstractEventLoopDeathTest, NoArgWatcherInOnRun) {
+  auto loop1 = MakePrimary();
+
+  loop1->OnRun(
+      [&]() { loop1->MakeNoArgWatcher<TestMessage>("/test", [&]() {}); });
+
+  EXPECT_DEATH(Run(), "running");
+}
+
 // Verify that Quit() works when there are multiple watchers.
 TEST_P(AbstractEventLoopTest, MultipleWatcherQuit) {
   auto loop1 = Make();
@@ -722,7 +833,7 @@
       "Channel pointer not found in configuration\\(\\)->channels\\(\\)");
 }
 
-// Verify that the send time on a message is roughly right.
+// Verify that the send time on a message is roughly right when using a watcher.
 TEST_P(AbstractEventLoopTest, MessageSendTime) {
   auto loop1 = MakePrimary();
   auto loop2 = Make();
@@ -737,7 +848,7 @@
   });
 
   bool triggered = false;
-  loop1->MakeWatcher("/test", [&triggered, &loop1](const TestMessage &msg) {
+  loop1->MakeWatcher("/test", [&](const TestMessage &msg) {
     // Confirm that the data pointer makes sense from a watcher, and all the
     // timestamps look right.
     EXPECT_GT(&msg, loop1->context().data);
@@ -770,7 +881,92 @@
 
   EXPECT_TRUE(triggered);
 
-  EXPECT_TRUE(fetcher.Fetch());
+  ASSERT_TRUE(fetcher.Fetch());
+
+  monotonic_clock::duration monotonic_time_offset =
+      fetcher.context().monotonic_event_time -
+      (loop1->monotonic_now() - ::std::chrono::seconds(1));
+  realtime_clock::duration realtime_time_offset =
+      fetcher.context().realtime_event_time -
+      (loop1->realtime_now() - ::std::chrono::seconds(1));
+
+  EXPECT_EQ(fetcher.context().realtime_event_time,
+            fetcher.context().realtime_remote_time);
+  EXPECT_EQ(fetcher.context().monotonic_event_time,
+            fetcher.context().monotonic_remote_time);
+
+  EXPECT_TRUE(monotonic_time_offset > ::std::chrono::milliseconds(-500))
+      << ": Got "
+      << fetcher.context().monotonic_event_time.time_since_epoch().count()
+      << " expected " << loop1->monotonic_now().time_since_epoch().count();
+  // Confirm that the data pointer makes sense.
+  EXPECT_GT(fetcher.get(), fetcher.context().data);
+  EXPECT_LT(fetcher.get(),
+            reinterpret_cast<void *>(
+                reinterpret_cast<char *>(fetcher.context().data) +
+                fetcher.context().size));
+  EXPECT_TRUE(monotonic_time_offset < ::std::chrono::milliseconds(500))
+      << ": Got "
+      << fetcher.context().monotonic_event_time.time_since_epoch().count()
+      << " expected " << loop1->monotonic_now().time_since_epoch().count();
+
+  EXPECT_TRUE(realtime_time_offset > ::std::chrono::milliseconds(-500))
+      << ": Got "
+      << fetcher.context().realtime_event_time.time_since_epoch().count()
+      << " expected " << loop1->realtime_now().time_since_epoch().count();
+  EXPECT_TRUE(realtime_time_offset < ::std::chrono::milliseconds(500))
+      << ": Got "
+      << fetcher.context().realtime_event_time.time_since_epoch().count()
+      << " expected " << loop1->realtime_now().time_since_epoch().count();
+}
+
+// Verify that the send time on a message is roughly right when using a no-arg
+// watcher. To get a message, we need to use a fetcher to actually access the
+// message. This is also the main use case for no-arg fetchers.
+TEST_P(AbstractEventLoopTest, MessageSendTimeNoArg) {
+  auto loop1 = MakePrimary();
+  auto loop2 = Make();
+  auto sender = loop2->MakeSender<TestMessage>("/test");
+  auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
+
+  auto test_timer = loop1->AddTimer([&sender]() {
+    aos::Sender<TestMessage>::Builder msg = sender.MakeBuilder();
+    TestMessage::Builder builder = msg.MakeBuilder<TestMessage>();
+    builder.add_value(200);
+    ASSERT_TRUE(msg.Send(builder.Finish()));
+  });
+
+  bool triggered = false;
+  loop1->MakeNoArgWatcher<TestMessage>("/test", [&]() {
+    // Confirm that we can indeed use a fetcher on this channel from this
+    // context, and it results in a sane data pointer and timestamps.
+    ASSERT_TRUE(fetcher.Fetch());
+
+    EXPECT_EQ(loop1->context().monotonic_remote_time,
+              loop1->context().monotonic_event_time);
+    EXPECT_EQ(loop1->context().realtime_remote_time,
+              loop1->context().realtime_event_time);
+
+    const aos::monotonic_clock::time_point monotonic_now =
+        loop1->monotonic_now();
+    const aos::realtime_clock::time_point realtime_now = loop1->realtime_now();
+
+    EXPECT_LE(loop1->context().monotonic_event_time, monotonic_now);
+    EXPECT_LE(loop1->context().realtime_event_time, realtime_now);
+    EXPECT_GE(loop1->context().monotonic_event_time + chrono::milliseconds(500),
+              monotonic_now);
+    EXPECT_GE(loop1->context().realtime_event_time + chrono::milliseconds(500),
+              realtime_now);
+
+    triggered = true;
+  });
+
+  test_timer->Setup(loop1->monotonic_now() + ::std::chrono::seconds(1));
+
+  EndEventLoop(loop1.get(), ::std::chrono::seconds(2));
+  Run();
+
+  ASSERT_TRUE(triggered);
 
   monotonic_clock::duration monotonic_time_offset =
       fetcher.context().monotonic_event_time -
@@ -1336,6 +1532,19 @@
       [](const Context &, const void *) {});
 }
 
+// Tests that no-arg watchers work with a node setup.
+TEST_P(AbstractEventLoopTest, NodeNoArgWatcher) {
+  EnableNodes("me");
+
+  auto loop1 = Make();
+  auto loop2 = Make();
+  loop1->MakeWatcher("/test", [](const TestMessage &) {});
+  loop2->MakeRawNoArgWatcher(
+      configuration::GetChannel(configuration(), "/test", "aos.TestMessage", "",
+                                nullptr),
+      [](const Context &) {});
+}
+
 // Tests that fetcher work with a node setup.
 TEST_P(AbstractEventLoopTest, NodeFetcher) {
   EnableNodes("me");
@@ -1370,6 +1579,16 @@
             [](const Context &, const void *) {});
       },
       "node");
+  EXPECT_DEATH({ loop1->MakeNoArgWatcher<TestMessage>("/test", []() {}); },
+               "node");
+  EXPECT_DEATH(
+      {
+        loop2->MakeRawNoArgWatcher(
+            configuration::GetChannel(configuration(), "/test",
+                                      "aos.TestMessage", "", nullptr),
+            [](const Context &) {});
+      },
+      "node");
 }
 
 // Tests that fetchers fail when created on the wrong node.
diff --git a/aos/events/event_loop_tmpl.h b/aos/events/event_loop_tmpl.h
index 1daf88c..5534c83 100644
--- a/aos/events/event_loop_tmpl.h
+++ b/aos/events/event_loop_tmpl.h
@@ -6,13 +6,16 @@
 #include "glog/logging.h"
 
 namespace aos {
+namespace event_loop_internal {
 
-// From a watch functor, this will extract the message type of the argument.
-// This is the template forward declaration, and it extracts the call operator
-// as a PTMF to be used by the following specialization.
+// From a watch functor, specializations of this will extract the message type
+// of the template argument. If T is not a valid message type, there will be no
+// matching specialization.
+//
+// This is just the forward declaration, which will be used by one of the
+// following specializations to match valid argument types.
 template <class T>
-struct watch_message_type_trait
-    : watch_message_type_trait<decltype(&T::operator())> {};
+struct watch_message_type_trait;
 
 // From a watch functor, this will extract the message type of the argument.
 // This is the template specialization.
@@ -21,6 +24,8 @@
   using message_type = typename std::decay<A1>::type;
 };
 
+}  // namespace event_loop_internal
+
 template <typename T>
 typename Sender<T>::Builder Sender<T>::MakeBuilder() {
   return Builder(sender_.get(), sender_->fbb_allocator());
@@ -28,19 +33,38 @@
 
 template <typename Watch>
 void EventLoop::MakeWatcher(const std::string_view channel_name, Watch &&w) {
-  using T = typename watch_message_type_trait<Watch>::message_type;
+  using MessageType =
+      typename event_loop_internal::watch_message_type_trait<decltype(
+          &Watch::operator())>::message_type;
   const Channel *channel = configuration::GetChannel(
-      configuration_, channel_name, T::GetFullyQualifiedName(), name(), node());
+      configuration_, channel_name, MessageType::GetFullyQualifiedName(),
+      name(), node());
 
   CHECK(channel != nullptr)
       << ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
-      << T::GetFullyQualifiedName() << "\" } not found in config.";
+      << MessageType::GetFullyQualifiedName() << "\" } not found in config.";
 
-  return MakeRawWatcher(
-      channel, [this, w](const Context &context, const void *message) {
-        context_ = context;
-        w(*flatbuffers::GetRoot<T>(reinterpret_cast<const char *>(message)));
-      });
+  MakeRawWatcher(channel,
+                 [this, w](const Context &context, const void *message) {
+                   context_ = context;
+                   w(*flatbuffers::GetRoot<MessageType>(
+                       reinterpret_cast<const char *>(message)));
+                 });
+}
+
+template <typename MessageType>
+void EventLoop::MakeNoArgWatcher(const std::string_view channel_name,
+                                 std::function<void()> w) {
+  const Channel *channel = configuration::GetChannel(
+      configuration_, channel_name, MessageType::GetFullyQualifiedName(),
+      name(), node());
+  CHECK(channel != nullptr)
+      << ": Channel { \"name\": \"" << channel_name << "\", \"type\": \""
+      << MessageType::GetFullyQualifiedName() << "\" } not found in config.";
+  MakeRawNoArgWatcher(channel, [this, w](const Context &context) {
+    context_ = context;
+    w();
+  });
 }
 
 inline bool RawFetcher::FetchNext() {
@@ -194,7 +218,9 @@
   // context.
   void DoCallCallback(std::function<monotonic_clock::time_point()> get_time,
                       Context context) {
-    CheckChannelDataAlignment(context.data, context.size);
+    if (context.data) {
+      CheckChannelDataAlignment(context.data, context.size);
+    }
     const monotonic_clock::time_point monotonic_start_time = get_time();
     {
       const float start_latency =
diff --git a/aos/events/logging/log_stats.cc b/aos/events/logging/log_stats.cc
index 88bf15c..8243d9f 100644
--- a/aos/events/logging/log_stats.cc
+++ b/aos/events/logging/log_stats.cc
@@ -93,34 +93,39 @@
   int it = 0;  // iterate through the channel_stats
   for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
     const aos::Channel *channel = channels->Get(i);
-    if (channel->name()->string_view().find(FLAGS_name) != std::string::npos) {
-      // Add a record to the stats vector.
-      channel_stats.push_back({channel});
-      // Lambda to read messages and parse for information
-      stats_event_loop->MakeRawWatcher(
-          channel,
-          [&logfile_stats, &channel_stats, it](const aos::Context &context,
-                                               const void * /* message */) {
-            channel_stats[it].max_message_size =
-                std::max(channel_stats[it].max_message_size, context.size);
-            channel_stats[it].total_message_size += context.size;
-            channel_stats[it].total_num_messages++;
-            // asume messages are send in sequence per channel
-            channel_stats[it].channel_end_time = context.realtime_event_time;
-            channel_stats[it].first_message_time =
-                std::min(channel_stats[it].first_message_time,
-                         context.monotonic_event_time);
-            channel_stats[it].current_message_time =
-                context.monotonic_event_time;
-            // update the overall logfile statistics
-            logfile_stats.logfile_length += context.size;
-          });
-      it++;
-      // TODO (Stephan): Frequency of messages per second
-      // - Sliding window
-      // - Max / Deviation
-      found_channel = true;
+    if (!aos::configuration::ChannelIsReadableOnNode(
+            channel, stats_event_loop->node())) {
+      continue;
     }
+
+    if (channel->name()->string_view().find(FLAGS_name) == std::string::npos) {
+      continue;
+    }
+
+    // Add a record to the stats vector.
+    channel_stats.push_back({channel});
+    // Lambda to read messages and parse for information
+    stats_event_loop->MakeRawNoArgWatcher(
+        channel,
+        [&logfile_stats, &channel_stats, it](const aos::Context &context) {
+          channel_stats[it].max_message_size =
+              std::max(channel_stats[it].max_message_size, context.size);
+          channel_stats[it].total_message_size += context.size;
+          channel_stats[it].total_num_messages++;
+          // asume messages are send in sequence per channel
+          channel_stats[it].channel_end_time = context.realtime_event_time;
+          channel_stats[it].first_message_time =
+              std::min(channel_stats[it].first_message_time,
+                       context.monotonic_event_time);
+          channel_stats[it].current_message_time = context.monotonic_event_time;
+          // update the overall logfile statistics
+          logfile_stats.logfile_length += context.size;
+        });
+    it++;
+    // TODO (Stephan): Frequency of messages per second
+    // - Sliding window
+    // - Max / Deviation
+    found_channel = true;
   }
   if (!found_channel) {
     LOG(FATAL) << "Could not find any channels";
@@ -171,7 +176,7 @@
   }
   std::cout << std::setfill('-') << std::setw(80) << "-"
             << "\nLogfile statistics for: " << FLAGS_logfile << "\n"
-            << "Log starts at:\t" << reader.realtime_start_time() << "\n"
+            << "Log starts at:\t" << reader.realtime_start_time(node) << "\n"
             << "Log ends at:\t" << logfile_stats.logfile_end_time << "\n"
             << "Log file size:\t" << logfile_stats.logfile_length << "\n"
             << "Total messages:\t" << logfile_stats.total_log_messages << "\n";
diff --git a/aos/events/logging/logfile_utils.cc b/aos/events/logging/logfile_utils.cc
index f062ad3..a7238ba 100644
--- a/aos/events/logging/logfile_utils.cc
+++ b/aos/events/logging/logfile_utils.cc
@@ -8,6 +8,7 @@
 
 #include <vector>
 
+#include "absl/strings/escaping.h"
 #include "aos/configuration.h"
 #include "aos/events/logging/logger_generated.h"
 #include "aos/flatbuffer_merge.h"
@@ -181,6 +182,15 @@
   const size_t data_size =
       flatbuffers::GetPrefixedSize(data_.data() + consumed_data_) +
       sizeof(flatbuffers::uoffset_t);
+  if (data_size == sizeof(flatbuffers::uoffset_t)) {
+    LOG(ERROR) << "Size of data is zero.  Log file end is corrupted, skipping.";
+    LOG(ERROR) << "  Rest of log file is "
+               << absl::BytesToHexString(std::string_view(
+                      reinterpret_cast<const char *>(data_.data() +
+                                                     consumed_data_),
+                      data_.size() - consumed_data_));
+    return absl::Span<const uint8_t>();
+  }
   while (data_.size() < consumed_data_ + data_size) {
     if (!ReadBlock()) {
       return absl::Span<const uint8_t>();
diff --git a/aos/events/shm_event_loop.cc b/aos/events/shm_event_loop.cc
index 57bcb1c..afd65a3 100644
--- a/aos/events/shm_event_loop.cc
+++ b/aos/events/shm_event_loop.cc
@@ -81,16 +81,16 @@
     // already exist and we need to create it.  Start by trying to create it. If
     // that fails, the file has already been created and we can open it
     // normally..  Once the file has been created it wil never be deleted.
-    fd_ = open(path.c_str(), O_RDWR | O_CREAT | O_EXCL,
+    int fd = open(path.c_str(), O_RDWR | O_CREAT | O_EXCL,
                O_CLOEXEC | FLAGS_permissions);
-    if (fd_ == -1 && errno == EEXIST) {
+    if (fd == -1 && errno == EEXIST) {
       VLOG(1) << path << " already created.";
       // File already exists.
-      fd_ = open(path.c_str(), O_RDWR, O_CLOEXEC);
-      PCHECK(fd_ != -1) << ": Failed to open " << path;
+      fd = open(path.c_str(), O_RDWR, O_CLOEXEC);
+      PCHECK(fd != -1) << ": Failed to open " << path;
       while (true) {
         struct stat st;
-        PCHECK(fstat(fd_, &st) == 0);
+        PCHECK(fstat(fd, &st) == 0);
         if (st.st_size != 0) {
           CHECK_EQ(static_cast<size_t>(st.st_size), size_)
               << ": Size of " << path
@@ -105,19 +105,19 @@
       }
     } else {
       VLOG(1) << "Created " << path;
-      PCHECK(fd_ != -1) << ": Failed to open " << path;
-      PCHECK(ftruncate(fd_, size_) == 0);
+      PCHECK(fd != -1) << ": Failed to open " << path;
+      PCHECK(ftruncate(fd, size_) == 0);
     }
 
-    data_ = mmap(NULL, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
+    data_ = mmap(NULL, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
     PCHECK(data_ != MAP_FAILED);
+    PCHECK(close(fd) == 0);
 
     ipc_lib::InitializeLocklessQueueMemory(memory(), config_);
   }
 
   ~MMapedQueue() {
     PCHECK(munmap(data_, size_) == 0);
-    PCHECK(close(fd_) == 0);
   }
 
   ipc_lib::LocklessQueueMemory *memory() const {
@@ -133,8 +133,6 @@
  private:
   ipc_lib::LocklessQueueConfiguration config_;
 
-  int fd_;
-
   size_t size_;
   void *data_;
 };
@@ -166,17 +164,19 @@
 
 class SimpleShmFetcher {
  public:
-  explicit SimpleShmFetcher(EventLoop *event_loop, const Channel *channel)
+  explicit SimpleShmFetcher(EventLoop *event_loop, const Channel *channel,
+                            bool copy_data)
       : channel_(channel),
         lockless_queue_memory_(
             channel,
             chrono::ceil<chrono::seconds>(chrono::nanoseconds(
                 event_loop->configuration()->channel_storage_duration()))),
         lockless_queue_(lockless_queue_memory_.memory(),
-                        lockless_queue_memory_.config()),
-        data_storage_(static_cast<char *>(malloc(channel->max_size() +
-                                                 kChannelDataAlignment - 1)),
-                      &free) {
+                        lockless_queue_memory_.config()) {
+    if (copy_data) {
+      data_storage_.reset(static_cast<char *>(
+          malloc(channel->max_size() + kChannelDataAlignment - 1)));
+    }
     context_.data = nullptr;
     // Point the queue index at the next index to read starting now.  This
     // makes it such that FetchNext will read the next message sent after
@@ -219,8 +219,12 @@
       if (context_.realtime_remote_time == aos::realtime_clock::min_time) {
         context_.realtime_remote_time = context_.realtime_event_time;
       }
-      context_.data = data_storage_start() +
-                      lockless_queue_.message_data_size() - context_.size;
+      if (copy_data()) {
+        context_.data = data_storage_start() +
+                        lockless_queue_.message_data_size() - context_.size;
+      } else {
+        context_.data = nullptr;
+      }
       actual_queue_index_ = actual_queue_index_.Increment();
     }
 
@@ -269,8 +273,12 @@
       if (context_.realtime_remote_time == aos::realtime_clock::min_time) {
         context_.realtime_remote_time = context_.realtime_event_time;
       }
-      context_.data = data_storage_start() +
-                      lockless_queue_.message_data_size() - context_.size;
+      if (copy_data()) {
+        context_.data = data_storage_start() +
+                        lockless_queue_.message_data_size() - context_.size;
+      } else {
+        context_.data = nullptr;
+      }
       actual_queue_index_ = queue_index.Increment();
     }
 
@@ -310,8 +318,10 @@
 
  private:
   char *data_storage_start() {
+    if (!copy_data()) return nullptr;
     return RoundChannelData(data_storage_.get(), channel_->max_size());
   }
+  bool copy_data() const { return static_cast<bool>(data_storage_); }
 
   const Channel *const channel_;
   MMapedQueue lockless_queue_memory_;
@@ -320,7 +330,8 @@
   ipc_lib::QueueIndex actual_queue_index_ =
       ipc_lib::LocklessQueue::empty_queue_index();
 
-  std::unique_ptr<char, decltype(&free)> data_storage_;
+  // This being empty indicates we're not going to copy data.
+  std::unique_ptr<char, decltype(&free)> data_storage_{nullptr, &free};
 
   Context context_;
 };
@@ -329,7 +340,7 @@
  public:
   explicit ShmFetcher(EventLoop *event_loop, const Channel *channel)
       : RawFetcher(event_loop, channel),
-        simple_shm_fetcher_(event_loop, channel) {}
+        simple_shm_fetcher_(event_loop, channel, true) {}
 
   ~ShmFetcher() { context_.data = nullptr; }
 
@@ -408,11 +419,12 @@
  public:
   WatcherState(
       ShmEventLoop *event_loop, const Channel *channel,
-      std::function<void(const Context &context, const void *message)> fn)
+      std::function<void(const Context &context, const void *message)> fn,
+      bool copy_data)
       : aos::WatcherState(event_loop, channel, std::move(fn)),
         event_loop_(event_loop),
         event_(this),
-        simple_shm_fetcher_(event_loop, channel) {}
+        simple_shm_fetcher_(event_loop, channel, copy_data) {}
 
   ~WatcherState() override { event_loop_->RemoveEvent(&event_); }
 
@@ -617,7 +629,18 @@
   TakeWatcher(channel);
 
   NewWatcher(::std::unique_ptr<WatcherState>(
-      new internal::WatcherState(this, channel, std::move(watcher))));
+      new internal::WatcherState(this, channel, std::move(watcher), true)));
+}
+
+void ShmEventLoop::MakeRawNoArgWatcher(
+    const Channel *channel,
+    std::function<void(const Context &context)> watcher) {
+  TakeWatcher(channel);
+
+  NewWatcher(::std::unique_ptr<WatcherState>(new internal::WatcherState(
+      this, channel,
+      [watcher](const Context &context, const void *) { watcher(context); },
+      false)));
 }
 
 TimerHandler *ShmEventLoop::AddTimer(::std::function<void()> callback) {
diff --git a/aos/events/shm_event_loop.h b/aos/events/shm_event_loop.h
index fa870b8..d3f1295 100644
--- a/aos/events/shm_event_loop.h
+++ b/aos/events/shm_event_loop.h
@@ -52,6 +52,9 @@
       const Channel *channel,
       std::function<void(const Context &context, const void *message)> watcher)
       override;
+  void MakeRawNoArgWatcher(
+      const Channel *channel,
+      std::function<void(const Context &context)> watcher) override;
 
   TimerHandler *AddTimer(std::function<void()> callback) override;
   aos::PhasedLoopHandler *AddPhasedLoop(
diff --git a/aos/events/simulated_network_bridge.cc b/aos/events/simulated_network_bridge.cc
index 1f56e46..0ab366a 100644
--- a/aos/events/simulated_network_bridge.cc
+++ b/aos/events/simulated_network_bridge.cc
@@ -174,9 +174,8 @@
     }
 
     // And register every delayer to be poked when a new message shows up.
-    source_event_loop->second->MakeRawWatcher(
-        channel,
-        [captured_delayers = delayers.get()](const Context &, const void *) {
+    source_event_loop->second->MakeRawNoArgWatcher(
+        channel, [captured_delayers = delayers.get()](const Context &) {
           for (std::unique_ptr<RawMessageDelayer> &delayer :
                *captured_delayers) {
             delayer->Schedule();
diff --git a/aos/init.cc b/aos/init.cc
index 5d62868..c40f22c 100644
--- a/aos/init.cc
+++ b/aos/init.cc
@@ -24,6 +24,8 @@
 using FLAG__namespace_do_not_use_directly_use_DECLARE_double_instead::
     FLAGS_tcmalloc_release_rate;
 
+DEFINE_bool(coredump, false, "If true, write core dumps on failure.");
+
 namespace aos {
 namespace logging {
 namespace internal {
@@ -39,7 +41,9 @@
 // non-realtime initialization sequences. May be called twice.
 void InitStart() {
   ::aos::logging::Init();
-  WriteCoreDumps();
+  if (FLAGS_coredump) {
+    WriteCoreDumps();
+  }
   google::InstallFailureSignalHandler();
 }
 
diff --git a/aos/ipc_lib/lockless_queue.cc b/aos/ipc_lib/lockless_queue.cc
index 7126ffd..f31d80d 100644
--- a/aos/ipc_lib/lockless_queue.cc
+++ b/aos/ipc_lib/lockless_queue.cc
@@ -792,7 +792,9 @@
   }
   *monotonic_remote_time = m->header.monotonic_remote_time;
   *realtime_remote_time = m->header.realtime_remote_time;
-  memcpy(data, m->data(memory_->message_data_size()), message_data_size());
+  if (data) {
+    memcpy(data, m->data(memory_->message_data_size()), message_data_size());
+  }
   *length = m->header.length;
 
   // And finally, confirm that the message *still* points to the queue index we
diff --git a/aos/ipc_lib/lockless_queue.h b/aos/ipc_lib/lockless_queue.h
index 0384aa8..550485f 100644
--- a/aos/ipc_lib/lockless_queue.h
+++ b/aos/ipc_lib/lockless_queue.h
@@ -162,6 +162,8 @@
   // element newer than QueueSize() from the current message, we consider it
   // behind by a large amount and return TOO_OLD.  If the message is modified
   // out from underneath us as we read it, return OVERWROTE.
+  //
+  // data may be nullptr to indicate the data should not be copied.
   enum class ReadResult { TOO_OLD, GOOD, NOTHING_NEW, OVERWROTE };
   ReadResult Read(uint32_t queue_index,
                   ::aos::monotonic_clock::time_point *monotonic_sent_time,
diff --git a/aos/network/sctp_server.cc b/aos/network/sctp_server.cc
index 5fd9f53..093e3f0 100644
--- a/aos/network/sctp_server.cc
+++ b/aos/network/sctp_server.cc
@@ -10,6 +10,7 @@
 #include <string.h>
 #include <sys/socket.h>
 #include <memory>
+#include <thread>
 
 #include "aos/network/sctp_lib.h"
 #include "aos/unique_malloc_ptr.h"
@@ -19,51 +20,60 @@
 namespace message_bridge {
 
 SctpServer::SctpServer(std::string_view local_host, int local_port)
-    : sockaddr_local_(ResolveSocket(local_host, local_port)),
-      fd_(socket(sockaddr_local_.ss_family, SOCK_SEQPACKET, IPPROTO_SCTP)) {
-  LOG(INFO) << "socket(" << Family(sockaddr_local_)
-            << ", SOCK_SEQPACKET, IPPROTOSCTP) = " << fd_;
-  PCHECK(fd_ != -1);
+    : sockaddr_local_(ResolveSocket(local_host, local_port)) {
+  while (true) {
+    fd_ = socket(sockaddr_local_.ss_family, SOCK_SEQPACKET, IPPROTO_SCTP);
+    LOG(INFO) << "socket(" << Family(sockaddr_local_)
+              << ", SOCK_SEQPACKET, IPPROTOSCTP) = " << fd_;
+    PCHECK(fd_ != -1);
 
-  {
-    struct sctp_event_subscribe subscribe;
-    memset(&subscribe, 0, sizeof(subscribe));
-    subscribe.sctp_data_io_event = 1;
-    subscribe.sctp_association_event = 1;
-    subscribe.sctp_send_failure_event = 1;
-    subscribe.sctp_partial_delivery_event = 1;
+    {
+      struct sctp_event_subscribe subscribe;
+      memset(&subscribe, 0, sizeof(subscribe));
+      subscribe.sctp_data_io_event = 1;
+      subscribe.sctp_association_event = 1;
+      subscribe.sctp_send_failure_event = 1;
+      subscribe.sctp_partial_delivery_event = 1;
 
-    PCHECK(setsockopt(fd_, SOL_SCTP, SCTP_EVENTS, (char *)&subscribe,
-                      sizeof(subscribe)) == 0);
+      PCHECK(setsockopt(fd_, SOL_SCTP, SCTP_EVENTS, (char *)&subscribe,
+                        sizeof(subscribe)) == 0);
+    }
+    {
+      // Enable recvinfo when a packet arrives.
+      int on = 1;
+      PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_RECVRCVINFO, &on,
+                        sizeof(int)) == 0);
+    }
+    {
+      // Allow one packet on the wire to have multiple source packets.
+      int full_interleaving = 2;
+      PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE,
+                        &full_interleaving, sizeof(full_interleaving)) == 0);
+    }
+    {
+      // Turn off the NAGLE algorithm.
+      int on = 1;
+      PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_NODELAY, &on, sizeof(int)) ==
+             0);
+    }
+
+    // And go!
+    if (bind(fd_, (struct sockaddr *)&sockaddr_local_,
+             sockaddr_local_.ss_family == AF_INET6
+                 ? sizeof(struct sockaddr_in6)
+                 : sizeof(struct sockaddr_in)) != 0) {
+      PLOG(ERROR) << "Failed to bind, retrying";
+      close(fd_);
+      std::this_thread::sleep_for(std::chrono::seconds(5));
+      continue;
+    }
+    LOG(INFO) << "bind(" << fd_ << ", " << Address(sockaddr_local_) << ")";
+
+    PCHECK(listen(fd_, 100) == 0);
+
+    SetMaxSize(1000);
+    break;
   }
-  {
-    // Enable recvinfo when a packet arrives.
-    int on = 1;
-    PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_RECVRCVINFO, &on, sizeof(int)) ==
-           0);
-  }
-  {
-    // Allow one packet on the wire to have multiple source packets.
-    int full_interleaving = 2;
-    PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE,
-                      &full_interleaving, sizeof(full_interleaving)) == 0);
-  }
-  {
-    // Turn off the NAGLE algorithm.
-    int on = 1;
-    PCHECK(setsockopt(fd_, IPPROTO_SCTP, SCTP_NODELAY, &on, sizeof(int)) == 0);
-  }
-
-  // And go!
-  PCHECK(bind(fd_, (struct sockaddr *)&sockaddr_local_,
-              sockaddr_local_.ss_family == AF_INET6
-                  ? sizeof(struct sockaddr_in6)
-                  : sizeof(struct sockaddr_in)) == 0);
-  LOG(INFO) << "bind(" << fd_ << ", " << Address(sockaddr_local_) << ")";
-
-  PCHECK(listen(fd_, 100) == 0);
-
-  SetMaxSize(1000);
 }
 
 aos::unique_c_ptr<Message> SctpServer::Read() {
diff --git a/debian/BUILD b/debian/BUILD
index 7f3b7f1..dee25cb 100644
--- a/debian/BUILD
+++ b/debian/BUILD
@@ -56,6 +56,14 @@
     ":opencv_amd64.bzl",
     opencv_amd64_debs = "files",
 )
+load(
+    ":gstreamer_amd64.bzl",
+    gstreamer_amd64_debs = "files",
+)
+load(
+    ":gstreamer_armhf.bzl",
+    gstreamer_armhf_debs = "files",
+)
 load("//debian:packages.bzl", "download_packages", "generate_deb_tarball")
 
 filegroup(
@@ -306,6 +314,16 @@
     files = opencv_amd64_debs,
 )
 
+generate_deb_tarball(
+    name = "gstreamer_amd64",
+    files = gstreamer_amd64_debs,
+)
+
+generate_deb_tarball(
+    name = "gstreamer_armhf",
+    files = gstreamer_armhf_debs,
+)
+
 exports_files([
     "ssh_wrapper.sh",
 ])
diff --git a/debian/download_packages.py b/debian/download_packages.py
index 5ad46e1..6331102 100755
--- a/debian/download_packages.py
+++ b/debian/download_packages.py
@@ -29,6 +29,30 @@
     if package == b'python3-numpy-abi9':
       yield b'python3-numpy'
       continue
+    if package == b'libjack-0.125':
+      yield b'libjack-jackd2-0'
+      continue
+    if package == b'fonts-freefont':
+      yield b'fonts-freefont-ttf'
+      continue
+    if package == b'gsettings-backend':
+      yield b'dconf-gsettings-backend'
+      continue
+    if package == b'gdal-abi-2-4-0':
+      yield b'libgdal20'
+      continue
+    if package == b'libglu1':
+      yield b'libglu1-mesa'
+      continue
+    if package == b'liblapack.so.3':
+      yield b'liblapack3'
+      continue
+    if package == b'libopencl1':
+      yield b'ocl-icd-libopencl1'
+      continue
+    if package == b'libblas.so.3':
+      yield b'libblas3'
+      continue
     yield package
 
 def download_deps(packages, excludes, force_includes):
@@ -65,8 +89,10 @@
   print("}")
 
 _ALWAYS_EXCLUDE = [
+    "dbus-session-bus",
     "debconf",
     "debconf-2.0",
+    "default-dbus-session-bus",
     "dpkg",
     "install-info",
     "libc-dev",
diff --git a/debian/gstreamer.BUILD b/debian/gstreamer.BUILD
new file mode 100644
index 0000000..a3d1b82
--- /dev/null
+++ b/debian/gstreamer.BUILD
@@ -0,0 +1,191 @@
+load("@//tools/build_rules:select.bzl", "cpu_select")
+
+cc_library(
+    name = "gstreamer",
+    srcs = cpu_select({
+        "amd64": [
+            "lib/x86_64-linux-gnu/libblkid.so.1",
+            "lib/x86_64-linux-gnu/libcom_err.so.2",
+            "lib/x86_64-linux-gnu/libexpat.so.1",
+            "lib/x86_64-linux-gnu/libkeyutils.so.1",
+            "lib/x86_64-linux-gnu/liblzma.so.5",
+            "lib/x86_64-linux-gnu/libmount.so.1",
+            "lib/x86_64-linux-gnu/libpcre.so.3",
+            "lib/x86_64-linux-gnu/libselinux.so.1",
+            "lib/x86_64-linux-gnu/libudev.so.1",
+            "lib/x86_64-linux-gnu/libuuid.so.1",
+            "lib/x86_64-linux-gnu/libz.so.1",
+            "usr/lib/x86_64-linux-gnu/libEGL.so.1",
+            "usr/lib/x86_64-linux-gnu/libGL.so.1",
+            "usr/lib/x86_64-linux-gnu/libGLX.so.0",
+            "usr/lib/x86_64-linux-gnu/libGLdispatch.so.0",
+            "usr/lib/x86_64-linux-gnu/libX11-xcb.so.1",
+            "usr/lib/x86_64-linux-gnu/libX11.so.6",
+            "usr/lib/x86_64-linux-gnu/libXau.so.6",
+            "usr/lib/x86_64-linux-gnu/libXdmcp.so.6",
+            "usr/lib/x86_64-linux-gnu/libatomic.so.1",
+            "usr/lib/x86_64-linux-gnu/libbsd.so.0",
+            "usr/lib/x86_64-linux-gnu/libdrm.so.2",
+            "usr/lib/x86_64-linux-gnu/libffi.so.6",
+            "usr/lib/x86_64-linux-gnu/libgbm.so.1",
+            "usr/lib/x86_64-linux-gnu/libgio-2.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libglib-2.0.so",
+            "usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgmp.so.10",
+            "usr/lib/x86_64-linux-gnu/libgnutls.so.30",
+            "usr/lib/x86_64-linux-gnu/libgobject-2.0.so",
+            "usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2",
+            "usr/lib/x86_64-linux-gnu/libgssdp-1.0.so.3",
+            "usr/lib/x86_64-linux-gnu/libgstallocators-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstapp-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstaudio-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstbadvideo-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstbase-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstgl-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstrtp-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstsdp-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgsttag-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstvideo-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgstwebrtc-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgthread-2.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgudev-1.0.so.0",
+            "usr/lib/x86_64-linux-gnu/libgupnp-1.0.so.4",
+            "usr/lib/x86_64-linux-gnu/libgupnp-igd-1.0.so.4",
+            "usr/lib/x86_64-linux-gnu/libhogweed.so.4",
+            "usr/lib/x86_64-linux-gnu/libicudata.so.63",
+            "usr/lib/x86_64-linux-gnu/libicui18n.so.63",
+            "usr/lib/x86_64-linux-gnu/libicuuc.so.63",
+            "usr/lib/x86_64-linux-gnu/libidn2.so.0",
+            "usr/lib/x86_64-linux-gnu/libjpeg.so.62",
+            "usr/lib/x86_64-linux-gnu/libk5crypto.so.3",
+            "usr/lib/x86_64-linux-gnu/libkrb5.so.3",
+            "usr/lib/x86_64-linux-gnu/libkrb5support.so.0",
+            "usr/lib/x86_64-linux-gnu/libnettle.so.6",
+            "usr/lib/x86_64-linux-gnu/libnice.so.10",
+            "usr/lib/x86_64-linux-gnu/libopencv_core.so.3.2",
+            "usr/lib/x86_64-linux-gnu/liborc-0.4.so.0",
+            "usr/lib/x86_64-linux-gnu/libp11-kit.so.0",
+            "usr/lib/x86_64-linux-gnu/libpsl.so.5",
+            "usr/lib/x86_64-linux-gnu/libsoup-2.4.so.1",
+            "usr/lib/x86_64-linux-gnu/libsqlite3.so.0",
+            "usr/lib/x86_64-linux-gnu/libtasn1.so.6",
+            "usr/lib/x86_64-linux-gnu/libtbb.so.2",
+            "usr/lib/x86_64-linux-gnu/libunistring.so.2",
+            "usr/lib/x86_64-linux-gnu/libvpx.so.5",
+            "usr/lib/x86_64-linux-gnu/libwayland-client.so.0",
+            "usr/lib/x86_64-linux-gnu/libwayland-egl.so.1",
+            "usr/lib/x86_64-linux-gnu/libwayland-server.so.0",
+            "usr/lib/x86_64-linux-gnu/libx265.so.165",
+            "usr/lib/x86_64-linux-gnu/libxcb.so.1",
+            "usr/lib/x86_64-linux-gnu/libxml2.so.2",
+        ],
+        "roborio": [
+        ],
+        "armhf": [
+            "lib/arm-linux-gnueabihf/libblkid.so.1",
+            "lib/arm-linux-gnueabihf/libcom_err.so.2",
+            "lib/arm-linux-gnueabihf/libexpat.so.1",
+            "lib/arm-linux-gnueabihf/libkeyutils.so.1",
+            "lib/arm-linux-gnueabihf/liblzma.so.5",
+            "lib/arm-linux-gnueabihf/libmount.so.1",
+            "lib/arm-linux-gnueabihf/libpcre.so.3",
+            "lib/arm-linux-gnueabihf/libselinux.so.1",
+            "lib/arm-linux-gnueabihf/libudev.so.1",
+            "lib/arm-linux-gnueabihf/libuuid.so.1",
+            "lib/arm-linux-gnueabihf/libz.so.1",
+            "usr/lib/arm-linux-gnueabihf/libEGL.so.1",
+            "usr/lib/arm-linux-gnueabihf/libGL.so.1",
+            "usr/lib/arm-linux-gnueabihf/libGLX.so.0",
+            "usr/lib/arm-linux-gnueabihf/libGLdispatch.so.0",
+            "usr/lib/arm-linux-gnueabihf/libX11-xcb.so.1",
+            "usr/lib/arm-linux-gnueabihf/libX11.so.6",
+            "usr/lib/arm-linux-gnueabihf/libXau.so.6",
+            "usr/lib/arm-linux-gnueabihf/libXdmcp.so.6",
+            "usr/lib/arm-linux-gnueabihf/libatomic.so.1",
+            "usr/lib/arm-linux-gnueabihf/libbsd.so.0",
+            "usr/lib/arm-linux-gnueabihf/libdrm.so.2",
+            "usr/lib/arm-linux-gnueabihf/libffi.so.6",
+            "usr/lib/arm-linux-gnueabihf/libgbm.so.1",
+            "usr/lib/arm-linux-gnueabihf/libgio-2.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libglib-2.0.so",
+            "usr/lib/arm-linux-gnueabihf/libgmodule-2.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgmp.so.10",
+            "usr/lib/arm-linux-gnueabihf/libgnutls.so.30",
+            "usr/lib/arm-linux-gnueabihf/libgobject-2.0.so",
+            "usr/lib/arm-linux-gnueabihf/libgssapi_krb5.so.2",
+            "usr/lib/arm-linux-gnueabihf/libgssdp-1.0.so.3",
+            "usr/lib/arm-linux-gnueabihf/libgstallocators-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstapp-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstaudio-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstbadvideo-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstbase-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstgl-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstreamer-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstrtp-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstsdp-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgsttag-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstvideo-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgstwebrtc-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgthread-2.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgudev-1.0.so.0",
+            "usr/lib/arm-linux-gnueabihf/libgupnp-1.0.so.4",
+            "usr/lib/arm-linux-gnueabihf/libgupnp-igd-1.0.so.4",
+            "usr/lib/arm-linux-gnueabihf/libhogweed.so.4",
+            "usr/lib/arm-linux-gnueabihf/libicudata.so.63",
+            "usr/lib/arm-linux-gnueabihf/libicui18n.so.63",
+            "usr/lib/arm-linux-gnueabihf/libicuuc.so.63",
+            "usr/lib/arm-linux-gnueabihf/libidn2.so.0",
+            "usr/lib/arm-linux-gnueabihf/libjpeg.so.62",
+            "usr/lib/arm-linux-gnueabihf/libk5crypto.so.3",
+            "usr/lib/arm-linux-gnueabihf/libkrb5.so.3",
+            "usr/lib/arm-linux-gnueabihf/libkrb5support.so.0",
+            "usr/lib/arm-linux-gnueabihf/libnettle.so.6",
+            "usr/lib/arm-linux-gnueabihf/libnice.so.10",
+            "usr/lib/arm-linux-gnueabihf/libopencv_core.so.3.2",
+            "usr/lib/arm-linux-gnueabihf/liborc-0.4.so.0",
+            "usr/lib/arm-linux-gnueabihf/libp11-kit.so.0",
+            "usr/lib/arm-linux-gnueabihf/libpsl.so.5",
+            "usr/lib/arm-linux-gnueabihf/libsoup-2.4.so.1",
+            "usr/lib/arm-linux-gnueabihf/libsqlite3.so.0",
+            "usr/lib/arm-linux-gnueabihf/libtasn1.so.6",
+            "usr/lib/arm-linux-gnueabihf/libtbb.so.2",
+            "usr/lib/arm-linux-gnueabihf/libunistring.so.2",
+            "usr/lib/arm-linux-gnueabihf/libvpx.so.5",
+            "usr/lib/arm-linux-gnueabihf/libwayland-client.so.0",
+            "usr/lib/arm-linux-gnueabihf/libwayland-egl.so.1",
+            "usr/lib/arm-linux-gnueabihf/libwayland-server.so.0",
+            "usr/lib/arm-linux-gnueabihf/libx265.so.165",
+            "usr/lib/arm-linux-gnueabihf/libxcb.so.1",
+            "usr/lib/arm-linux-gnueabihf/libxml2.so.2",
+        ],
+        "cortex-m": [],
+    }),
+    hdrs = glob([
+        "usr/lib/x86_64-linux-gnu/glib-2.0/include/**/*.h",
+        "usr/include/gstreamer-1.0/**/*.h",
+        "usr/include/glib-2.0/**/*.h",
+        "usr/lib/arm-linux-gnueabihf/glib-2.0/include/**/*.h",
+    ]),
+    includes = cpu_select({
+        "amd64": [
+            "usr/lib/x86_64-linux-gnu/glib-2.0/include",
+            "usr/include/glib-2.0",
+            "usr/include/gstreamer-1.0",
+        ],
+        "armhf": [
+            "usr/lib/arm-linux-gnueabihf/glib-2.0/include",
+            "usr/include/glib-2.0",
+            "usr/include/gstreamer-1.0",
+        ],
+        "roborio": [
+        ],
+        "cortex-m": [
+        ],
+    }),
+    linkopts = [
+        "-ldl",
+        "-lresolv",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/debian/gstreamer_amd64.bzl b/debian/gstreamer_amd64.bzl
new file mode 100644
index 0000000..65c03af
--- /dev/null
+++ b/debian/gstreamer_amd64.bzl
@@ -0,0 +1,511 @@
+files = {
+    "adduser_3.118_all.deb": "bd71dd1ab8dcd6005390708f23741d07f1913877affb7604dfd55f85d009aa2b",
+    "adwaita-icon-theme_3.30.1-1_all.deb": "698b3f0fa337bb36ea4fe072a37a32a1c81875db13042368677490bb087ccb93",
+    "coreutils_8.30-3_amd64.deb": "ae6e5cd6e9aaf74d66edded3931a7a6c916625b8b890379189c75574f6856bf4",
+    "dconf-gsettings-backend_0.30.1-2_amd64.deb": "8dd9f676ed51db557cfdbb107542bf5406627dc1c83ded565149f02abb60e268",
+    "dconf-service_0.30.1-2_amd64.deb": "1adc68353e17f12ceb3f2e01bb0cb4e5d11b547b9436a89fa0209c46cf028c51",
+    "fontconfig-config_2.13.1-2_all.deb": "9f5d34ba20eb156ef62d8126866a376be985c6a83fdcfb33f12cd83acac480c2",
+    "fontconfig_2.13.1-2_amd64.deb": "efbc7d9a8cf245e31429d3bda3e560df275f6b7302367aabe83503ca734ac0fd",
+    "fonts-dejavu-core_2.37-1_all.deb": "58d21a255606191e6512cca51f32c4480e7a798945cc980623377696acfa3cfc",
+    "fonts-freefont-ttf_20120503-9_all.deb": "13489f628898f01ad4ab12807309d814cf6df678a2ae9c1e49a426d1c916a1c5",
+    "fonts-liberation_1.07.4-9_all.deb": "c936aebbfd0af7851399ae5ab08bb01744f5e3381f7678fb87cc77114f95ef53",
+    "gdal-data_2.4.0+dfsg-1_all.deb": "6e0fce32cf2e85ad2539482087d712bf2258d05e1838f3586a17ad2dc6bb7410",
+    "gir1.2-glib-2.0_1.58.3-2_amd64.deb": "21843844415a72bda1f3f524dfcff94550128b082f31b850f5149552ae171264",
+    "gir1.2-gst-plugins-bad-1.0_1.14.4-1+b1_amd64.deb": "a3d18fa2e46485f192acc72d3f5e8062c541a4f68864e3f27ea4d6f599c67b2c",
+    "gir1.2-gst-plugins-base-1.0_1.14.4-2_amd64.deb": "9ace033e35ee9a6b6a42ab3bd28c26c32afc98401e933599fd4a25a009e27f29",
+    "gir1.2-gstreamer-1.0_1.14.4-1_amd64.deb": "b66c3872d9cb4e3c61c9863741ca9c551edbd9ba88347b3bd3f74d116e9d9072",
+    "glib-networking-common_2.58.0-2_all.deb": "79831fd09fc96dc5729e8ed360563b05100d6bff70b03f3badf4e0f4759bb7ec",
+    "glib-networking-services_2.58.0-2_amd64.deb": "fc3bbb1b8f14c7aaa2f873cc006c0d2f8c3a4c712bc067d749adafa82635e094",
+    "glib-networking_2.58.0-2_amd64.deb": "21854f3921469776a598369a5b5b5264112bf49de1559c219fbe36cbd702628f",
+    "gsettings-desktop-schemas_3.28.1-1_all.deb": "a75aed8781a781c4b819b2d1e952791b123580b1a02a4bb35fdbbba2e3ab8310",
+    "gstreamer1.0-plugins-bad_1.14.4-1+b1_amd64.deb": "8ec300a7c1485ef8128b75d4015eef29e8e56eb8a8005d9bfba78a33bd19afa8",
+    "gstreamer1.0-plugins-base_1.14.4-2_amd64.deb": "c62e46eae5b671176285e50268f342432b73293e7ebf149f036357e577f3f4fc",
+    "gstreamer1.0-plugins-good_1.14.4-1_amd64.deb": "5788cd63e8479309e75a395e951067088df9a934aba6d992261e819f4cd2decb",
+    "gstreamer1.0-plugins-ugly_1.14.4-1_amd64.deb": "94af175fc2f304771a962b89c5fe4d05e098eae48a145e36953f204893f003cd",
+    "gtk-update-icon-cache_3.24.5-1_amd64.deb": "ca87a8eaa7a662049e2a95f3405d8affb7715a9dbdcba6fa186ae0bcc8981847",
+    "hicolor-icon-theme_0.17-2_all.deb": "20304d34b85a734ec1e4830badf3a3a70a5dc5f9c1afc0b2230ecd760c81b5e0",
+    "iso-codes_4.2-1_all.deb": "72d0589fc065005574232bf103c7d34acf5e4c805f2b0d19e6aeb639438ff253",
+    "liba52-0.7.4_0.7.4-19_amd64.deb": "e9e40412ef190d42a7f42c1ebbf6198b3cc46a19a9b42d24fd6c49c9a1970192",
+    "libaa1_1.4p5-46_amd64.deb": "2311c88fcd1662319576f4b265c665c08cdf061cff7e4557885f32f6678eeed8",
+    "libaec0_1.0.2-1_amd64.deb": "18193039d49a7944623f7a175af6d3d0c01ff39f724ee42041c2f6511acfcc9a",
+    "libaom0_1.0.0-3_amd64.deb": "22f071d9b5fbc62bdb01aa49fa6a423948ffdafec40eda52b9f0de0d332278f1",
+    "libarmadillo9_9.200.7+dfsg-1_amd64.deb": "1f5ba72600963a7a4cd6f2035f032ef59a8c3169e85e1f3e7f12271a2d1ebd62",
+    "libarpack2_3.7.0-2_amd64.deb": "08b83c081ba569bd61ee67ff39da490690389eb15c52d0a3f8d12a51f9debc90",
+    "libatk-bridge2.0-0_2.30.0-5_amd64.deb": "52ed3333fd0e1430b573343fc65d594a075ee5f493b8cbff0f64d5f41f6f3f8f",
+    "libatk1.0-0_2.30.0-2_amd64.deb": "51603cc054baa82cee4cd50ac41578266e1321ef1c74bccbb78a3dcf1729d168",
+    "libatk1.0-data_2.30.0-2_all.deb": "cf0c94611ff2245ae31d12a5a43971eb4ca628f42e93b0e003fd2c4c0de5e533",
+    "libatomic1_8.3.0-6_amd64.deb": "f3aed76145c49f0b6be3eb6840abc4245eebf24448b55c8ed0736fc1d45e5f8a",
+    "libatspi2.0-0_2.30.0-7_amd64.deb": "8ff1ab1508799679e3209188c5f4765c3da16a7876e220c49d805ef03cced397",
+    "libaudit-common_2.8.4-3_all.deb": "4e51dc247cde083528d410f525c6157b08be8b69511891cf972bc87025311371",
+    "libaudit1_2.8.4-3_amd64.deb": "21f2b3dfbe7db9e15ff9c01e1ad8db35a0adf41b70a3aa71e809f7631fc2253d",
+    "libavahi-client3_0.7-4+b1_amd64.deb": "ab80126c56e9aa8fd3b2ef5991b547bd15eb41cd78919fa55c55d221a9d349e9",
+    "libavahi-common-data_0.7-4+b1_amd64.deb": "388bc38faf2ad715b9c383daa7c358c656c05de33886c2da7e3641db6bdf5512",
+    "libavahi-common3_0.7-4+b1_amd64.deb": "82ee379d69de764c2fc1663535e43fabad7e31247a6fae7d492b5a0446a730c3",
+    "libavc1394-0_0.5.4-5_amd64.deb": "f96e824b1a7e8d9ae1254c51dfaea40cdde45072be68e2025eb109faba10f3dc",
+    "libavcodec-dev_4.1.4-1~deb10u1_amd64.deb": "e7704b27d393f99c4d31b2dbdd45de7aebadefdcc0760d5c496931f815b3e373",
+    "libavcodec58_4.1.4-1~deb10u1_amd64.deb": "06a7c8232bb6ae761713bb409a7007691f68c8fa1a06d7e172cc1af2a0b248d5",
+    "libavformat-dev_4.1.4-1~deb10u1_amd64.deb": "74f8f220f91b09514aee4082c6517d82e2903ae3a93c5182a9d5172071b4bce3",
+    "libavformat58_4.1.4-1~deb10u1_amd64.deb": "4898fe7f8bf5d759ced3c30045e12784295ec3dbda430cdd05c5733e80448009",
+    "libavresample-dev_4.1.4-1~deb10u1_amd64.deb": "d33a192f435f1ca2a2e8d1c76f3a745423666c5e57c4041249743adc6917dcc1",
+    "libavresample4_4.1.4-1~deb10u1_amd64.deb": "22c4dbc129e1f67e8c9b0a50c05aafe4ed21eed1b825a39f356e3d2b2a4cbcd4",
+    "libavutil-dev_4.1.4-1~deb10u1_amd64.deb": "c835b7e0ab29b0b22611b0e373e8a4a4be653a547c50ff27a5aedd5755900aff",
+    "libavutil56_4.1.4-1~deb10u1_amd64.deb": "cec6a4827449633d9ce36a1f97e109ae38792fea5b39381da8c38431a3796863",
+    "libblas3_3.8.0-2_amd64.deb": "7161d85be1e755bb605b2a3f65d7c556c5851ed0379b723b3f9d54a5eada5fd5",
+    "libblkid-dev_2.33.1-0.1_amd64.deb": "996914d6cec1bac151c766d5b65197888b464c3505f63f7abbc129d74e5f28ac",
+    "libblkid1_2.33.1-0.1_amd64.deb": "0b15f3eb3cf2fbe540f99ae1c9fd5ec1730f2245b99e31c91755de71b967343a",
+    "libbluray2_1.1.0-1_amd64.deb": "df82a7141cd2f3c212ad805e83be01b04e90cf095638bf11fbf7c790d4027452",
+    "libbsd0_0.9.1-2_amd64.deb": "0827321e85d36200759e3ec621fc05154c752534c330ffc5472ad75bbb8eb913",
+    "libcaca0_0.99.beta19-2.1_amd64.deb": "2f21fa1c7691803c0250930140d07f1c4671574063bcc977e97b92a26a465c47",
+    "libcairo-gobject2_1.16.0-4_amd64.deb": "5579ae5d311dbd71556dc36edf2bb39ba73f5aa65a8a5367bd93e96a4c98f69e",
+    "libcairo2_1.16.0-4_amd64.deb": "32fcf6fb0cffe41263927f692a88911796c9006402b6cc2137ca78db3c96068e",
+    "libcap-ng0_0.7.9-2_amd64.deb": "4f9caf61638db6dcf79529ef756a2d36c7aae6604d486a5055bb3212d823b691",
+    "libcap2-bin_2.25-2_amd64.deb": "3c8c5b1410447356125fd8f5af36d0c28853b97c072037af4a1250421008b781",
+    "libcap2_2.25-2_amd64.deb": "8f93459c99e9143dfb458353336c5171276860896fd3e10060a515cd3ea3987b",
+    "libcdio18_2.0.0-2_amd64.deb": "caafbabaffef90d57250196e26357070fcc739387f41f5d9b8d7340696438157",
+    "libcdparanoia0_3.10.2+debian-13_amd64.deb": "df50318f333569808c1bad8448212113be8f3dba62d508f2211672dd88fc3d6b",
+    "libcharls2_2.0.0+dfsg-1_amd64.deb": "04489cf5717651fb958c923950b185616d468c615fc1fcdd138ba1abd549c9b4",
+    "libchromaprint1_1.4.3-3_amd64.deb": "22eab07031e801fa1d450247451cac9023494221583760b31faa9e67aa051d32",
+    "libcodec2-0.8.1_0.8.1-2_amd64.deb": "49603d461161ed9e77d848f3f203b5ad49ab7b6498202492ceb03c948a4481b3",
+    "libcolord2_1.4.3-4_amd64.deb": "2fd78fc761cc8465702ce4ec03bc6922b172e47f524c7c64312dcf2ad0db1489",
+    "libcom-err2_1.44.5-1+deb10u3_amd64.deb": "e5ea8e6db9453ed13199f4cbfe8e29d76c579eb6f678ab9bb4bebd7d12c1936e",
+    "libcroco3_0.6.12-3_amd64.deb": "1acb00996b7477687e4f3f12de7fbf4b635866a6167671f2201ea3e67af05336",
+    "libcrystalhd3_0.0~git20110715.fdd2f19-13_amd64.deb": "e2dab38126a0b92abdc149397307f9af3f04f5aa26ae08a1ed461a0178f4b1fe",
+    "libcups2_2.2.10-6+deb10u2_amd64.deb": "d5dea9519516290f4f424099add8b6bb77d77c188ccca23073d855517c13b79c",
+    "libcurl3-gnutls_7.64.0-4_amd64.deb": "da6d0e4c58d09d767e10c5d7653504c778fe8a6dcd1accf0fa713102d17338a9",
+    "libdap25_3.20.3-1_amd64.deb": "f897c1f533b513da49fee93d9c912b791b809833fe8ad7dbf6505f62e8f2d47e",
+    "libdapclient6v5_3.20.3-1_amd64.deb": "80cabdf76dead855c54e583848b366994590ebf321fc21c133ec46beabdc67a7",
+    "libdapserver7v5_3.20.3-1_amd64.deb": "49c7c5f18b78bbcf73c298469ea8fbc12f5c154b3d1b926584df1b087d6d1659",
+    "libdatrie1_0.2.12-2_amd64.deb": "7159a08f4a40f74e4582ebd62db0fb48b3ba8e592655ac2ab44f7bfacbca12f3",
+    "libdb5.3_5.3.28+dfsg1-0.5_amd64.deb": "c7f0e9a423840731362ee52d4344c0bcf84318fbc06dad4fefe0e61d9e7062bc",
+    "libdbus-1-3_1.12.16-1_amd64.deb": "b6667d3d29f2a4b5efb3f7368eb750582341ab0554213246d2d6713af09e552f",
+    "libdc1394-22-dev_2.2.5-1_amd64.deb": "2ed94c28030329432642aff77553f843c264ed391becc6d72ab2da52088ddac1",
+    "libdc1394-22_2.2.5-1_amd64.deb": "ba076aaa8e60f2203fbd1734c822558c5875eab35c8731fb6e42a2244390ffa2",
+    "libdconf1_0.30.1-2_amd64.deb": "22775563fd803db3dafe4fcc93950f72acf04e3d87b51b3dd5c107b21105a5ff",
+    "libdpkg-perl_1.19.7_all.deb": "1cb272a8168138e9b8334e87cc26388259f232b74667b3a7f3856f227adcc4ba",
+    "libdrm-amdgpu1_2.4.97-1_amd64.deb": "283bff4909f50da051f057cf6b8e84c590675ede91e57ce7414d2f1d4097b691",
+    "libdrm-common_2.4.97-1_all.deb": "eea378d3dab56923e06871331838aecc38a35aad997da7fc96a5e8c4e36081a2",
+    "libdrm-dev_2.4.97-1_amd64.deb": "8b5197c40c5feb0a096befae273880ff904d44cd4853c136c8a5ff9ea7d24558",
+    "libdrm-intel1_2.4.97-1_amd64.deb": "d5cb66f82681192ae14157370c98fc12bac0331283a8afd6b2c9c1a70c910a57",
+    "libdrm-nouveau2_2.4.97-1_amd64.deb": "875b604283ad5b56fb0ae0ec28b4e52ba3055ce9116e71d4bcec7854b67ba7b6",
+    "libdrm-radeon1_2.4.97-1_amd64.deb": "e7e98f7beedfb326a3dc4d2cef3eff144c7cfe22bef99c2004708c1aa5cceb8c",
+    "libdrm2_2.4.97-1_amd64.deb": "759caef1fbf885c515ae7273cdf969d185cf7276b432a813c46651e468c57489",
+    "libdv4_1.0.0-12_amd64.deb": "cbff9d3d56bcc5f30227ce893b5250fd0008092b30c7c004f1abe98229dd88d5",
+    "libdvdread4_6.0.1-1_amd64.deb": "68621b27c8d2334c225a9d2de48acca21e51f0a7517674ba1a699e9d81a168f3",
+    "libedit2_3.1-20181209-1_amd64.deb": "ccd6cdf5ec28a92744a79f3f210f071679d12deb36917d4e8d17ae7587f218cc",
+    "libegl-mesa0_18.3.6-2+deb10u1_amd64.deb": "cdfd118d0dcb5fcc0eeb3a57c7c630acc51644328a67c0fe06baaf87f701527b",
+    "libegl1-mesa-dev_18.3.6-2+deb10u1_amd64.deb": "626a86cda8833cf45ac20299237ca8e7d831e04acb3716b846581ed567b367f6",
+    "libegl1_1.1.0-1_amd64.deb": "a5d56bc4fc21a87f95b45ca9665abae5390809bc8d2d8fd3a3c3fc15eeeb1918",
+    "libelf1_0.176-1.1_amd64.deb": "cc7496ca986aa77d01e136b8ded5a3e371ec8f248b331b4124d1fd2cbeaec3ef",
+    "libepoxy0_1.5.3-0.1_amd64.deb": "968295ae7382be0fc06e535f2a1408f54b0b29096e0142618d185da1c7a42ed0",
+    "libepsilon1_0.9.2+dfsg-4_amd64.deb": "f8908300afd1436d471f0b14da2078f7ceeb5171748ab24b32b77b7c83039295",
+    "libevent-2.1-6_2.1.8-stable-4_amd64.deb": "ffebc078745662d2308c0026cc50e37cb54344bde61b1f92b979a2a4e8138efe",
+    "libevent-core-2.1-6_2.1.8-stable-4_amd64.deb": "a96168d513725033c6558c49b191ae192a0eb3b92dd574f540b163ce19549323",
+    "libevent-pthreads-2.1-6_2.1.8-stable-4_amd64.deb": "d2012b6f09029fd2c9a8d7a423fc7afb2fcc86c1b4b1dd46659b7e08f20e5a68",
+    "libexif-dev_0.6.21-5.1_amd64.deb": "0da4fda369950ab0e8cd6d743e294ef42cd75b5a7e4c7cec0559bf44e50d39ba",
+    "libexif12_0.6.21-5.1_amd64.deb": "75052c4cd070441379acfe1332bed3200ed55f92852c71b8253608766347af86",
+    "libexpat1_2.2.6-2+deb10u1_amd64.deb": "d60dee1f402ee0fba6d44df584512ae9ede73e866048e8476de55d9b78fa2da1",
+    "libfabric1_1.6.2-3_amd64.deb": "71358ea6a57ec41309f23826cfff4e83aabe569cec50e6ce635740c63e2b8654",
+    "libffi-dev_3.2.1-9_amd64.deb": "643bf19e859c9bf8f61033e48d7ba73c114039efbe851f407356edab396af317",
+    "libffi6_3.2.1-9_amd64.deb": "d4d748d897e8e53aa239ead23a18724a1a30085cc6ca41a8c31b3b1e1b3452f4",
+    "libflac8_1.3.2-3_amd64.deb": "e736a3b67bd9e5a627dbfe44c08c84d59c650e8ec3b6b7f24839ff740944d7d0",
+    "libfontconfig1_2.13.1-2_amd64.deb": "6766d0bcfc615fb15542efb5235d38237ccaec4c219beb84dbd22d1662ccea8f",
+    "libfreetype6_2.9.1-3+deb10u1_amd64.deb": "9ebd43849f483b79f6f1e0d9ba398faafb23f55a33ea916821b13991ba821a2a",
+    "libfreexl1_1.0.5-3_amd64.deb": "5e41fb4438c7c655894b111eced2b9697fb5f5bab6ddf12d7cb7fb680725c17e",
+    "libfribidi0_1.0.5-3.1+deb10u1_amd64.deb": "9844b02a3bfa8c9f89a077cc5208122f9245a6a6301cbf5fdc66b1a76f163c08",
+    "libfyba0_4.1.1-6_amd64.deb": "70da7c23ef1b12f01d0e5a5062c9ee0bbeec2b87d6c517db9bfa34def51601bf",
+    "libgbm1_18.3.6-2+deb10u1_amd64.deb": "74c468c77990d871243ae31c108b7b770f5a0ff02fd36316f70affe52bce1999",
+    "libgcrypt20_1.8.4-5_amd64.deb": "1b826517b328e29a441cc89e5c427896182ffc946713329f50accc8417856136",
+    "libgd3_2.2.5-5.2_amd64.deb": "ee49ded27e44a8fd04710458413c0203704a2fd4e30497d5eb64f46695816633",
+    "libgdal20_2.4.0+dfsg-1+b1_amd64.deb": "4fb22452c0ee831156373d77d8950f578d24a08f3b009ed416df148ef0e5b0b4",
+    "libgdcm2-dev_2.8.8-9_amd64.deb": "0461bf270ea31e4c1c8d0366cbcd01948b2f0bf9dbcafe400dbe6c82df0703ff",
+    "libgdcm2.8_2.8.8-9_amd64.deb": "002349ae3eb032c6594d1269e66048b6f430989049213c83a206695b74d22e95",
+    "libgdk-pixbuf2.0-0_2.38.1+dfsg-1_amd64.deb": "90e1842771968ffae4b4c28f1ad6a8bf77ff3a57616b799abed93354b860edc8",
+    "libgdk-pixbuf2.0-common_2.38.1+dfsg-1_all.deb": "1310e3f0258866eb4d0e95f140d5d9025cf6be1e3e2c375f4a426ccc2e78cf68",
+    "libgeos-3.7.1_3.7.1-1_amd64.deb": "5db308a68fa4d3f92f718cdfa3bccdab0bc81e955eb68b739f93395fcd551f5f",
+    "libgeos-c1v5_3.7.1-1_amd64.deb": "f9e0dd7cdcbf071840f2f95e5c913dfc3256111f4ba0faa772a4f60a80176fa2",
+    "libgeotiff2_1.4.3-1_amd64.deb": "9d1a005e1268e71fe64a0087f66750ec661967107307da6738647ac31ff845a6",
+    "libgfortran5_8.3.0-6_amd64.deb": "c76cb39bb3da74c5315e0d9577adc45bd39bf2d21fb7885e724429e5b4ed0ffe",
+    "libgif7_5.1.4-3_amd64.deb": "a7d7610a798cf3d72bf5ef9f6e44c4b0669f5df3e4a0014e83f9d788ce47f9a9",
+    "libgirepository-1.0-1_1.58.3-2_amd64.deb": "6db170195856b430d580e6ecf528b2efaf66233a98884368658fbef5abe4eaa5",
+    "libgl1-mesa-dev_18.3.6-2+deb10u1_amd64.deb": "fd7b17e82f1ccf95d5a2da726a3e183a5f01f7c3b0aac36c70b2ebc5d1903fcd",
+    "libgl1-mesa-dri_18.3.6-2+deb10u1_amd64.deb": "964968e2914e86eca243c9a316529a4d2f8b6e000f981e9a0891ac3c3550be32",
+    "libgl1_1.1.0-1_amd64.deb": "79420dd0cdb5b9dab9d3266c8c052036c93e363708e27738871692e0e163e5a2",
+    "libgl2ps1.4_1.4.0+dfsg1-2_amd64.deb": "b24681fc4d4594d5ff999f63367a952eb93dd10822b7acbbaf315ba03470907b",
+    "libglapi-mesa_18.3.6-2+deb10u1_amd64.deb": "400fa15a8da369359328ad41ac893c4cb51686514ee6a9456dbbfd12e8836ec3",
+    "libgles1_1.1.0-1_amd64.deb": "cc5e1e0f2b0f1f82b5f3c79ae76870e105f132785491efe21fd2d6fd080e25b5",
+    "libgles2-mesa-dev_18.3.6-2+deb10u1_amd64.deb": "eb29ba1d8050688984490cd53e1c6727c7d9cdb79946b0c45dfbbe79aed474c0",
+    "libgles2_1.1.0-1_amd64.deb": "58e9ff1026d81fc38994b12877f6c383e39f599bd98f5c8e5c6bf5716da81a45",
+    "libglib2.0-0_2.58.3-2+deb10u2_amd64.deb": "9b2d2c420beed1bb115b05c7766e981eab3865f9e9509d22fc621389614d2528",
+    "libglib2.0-bin_2.58.3-2+deb10u2_amd64.deb": "306e0cd7d18e7dc6cf19ffea23843ec8f38ebd52f3751b070ef50f88125e531e",
+    "libglib2.0-data_2.58.3-2+deb10u2_all.deb": "19982dfe0de4571ec99f683bd62c74e66c2422a00b0502089247d86c1c08ce92",
+    "libglib2.0-dev-bin_2.58.3-2+deb10u2_amd64.deb": "3066bd946d86fd52b04477a76c7f142243b76b91aa36a146d4488fbc8a1fa907",
+    "libglib2.0-dev_2.58.3-2+deb10u2_amd64.deb": "d83ab43d30e7131f6f893a0ef7a33bc7e9bea9748861dec29c5f29f457e6e1c6",
+    "libglu1-mesa_9.0.0-2.1+b3_amd64.deb": "5eaed67b0a425117601d36a7f2d1d299a45bb6848d1a71d938ae34522deed98d",
+    "libglvnd-core-dev_1.1.0-1_amd64.deb": "2bae9473e270936a87bc4d7756dfb40643c48c77520d9743b0e1ed92f65ba52a",
+    "libglvnd-dev_1.1.0-1_amd64.deb": "60380b7d1919960b453f66155defb341803adb6b5619884c969dfa85fd050fc0",
+    "libglvnd0_1.1.0-1_amd64.deb": "4247b31689649f12d7429f337d038ce73cb8394d7a3a25eac466536a008f00c6",
+    "libglx-mesa0_18.3.6-2+deb10u1_amd64.deb": "0d25475d75cf870387a70afb2809aa79c33c7d05fe333bc9b2e1c4a258489ce7",
+    "libglx0_1.1.0-1_amd64.deb": "cd370a004c0ddec213b34423963e74c98420f08d45c1dec8f4355ff6c0e9d905",
+    "libgme0_0.6.2-1_amd64.deb": "5ca59f1b731b73c06aa9e232ca297e384f2712f691534dd7a539e91788dc3ac0",
+    "libgmp10_6.1.2+dfsg-4_amd64.deb": "d9c9661c7d4d686a82c29d183124adacbefff797f1ef5723d509dbaa2e92a87c",
+    "libgnutls30_3.6.7-4+deb10u2_amd64.deb": "00d35db0d553ba4852546a30b890efb25b6cb565fee5b3d786fe90c5ef6db640",
+    "libgomp1_8.3.0-6_amd64.deb": "909fcd28491d7ebecf44ee2e8d0269b600271b0b6d236b19f2c0469cde162d21",
+    "libgpg-error0_1.35-1_amd64.deb": "996b67baf6b5c6fda0db2df27cce15701b122403d0a7f30e9a1f50d07205450a",
+    "libgphoto2-6_2.5.22-3_amd64.deb": "35971fed6001e039c8512c7bf06a8ffec276a25fd7cf86f4234e45af615f337e",
+    "libgphoto2-dev_2.5.22-3_amd64.deb": "3d7abfbf9b2b288ebcde53b8da27a919d593160847239be5244bee0b8d0c34f3",
+    "libgphoto2-port12_2.5.22-3_amd64.deb": "ff365d9c5350662a78a6e1224febc0fbd173e2abefadc8280499d94b67918940",
+    "libgpm2_1.20.7-5_amd64.deb": "2ff7fbe9078ed8ed9535b4cd8388ed6eb2767e6071a26007383a520e3da0232c",
+    "libgraphite2-3_1.3.13-7_amd64.deb": "f79bfdcfe09285cccee68c070171888b98adbf3e7bd3e8f6afcb6caef5623179",
+    "libgsm1_1.0.18-2_amd64.deb": "a763da85a8d66c222a74edeb0a58abca813eae02d5bf53b09159869c104817eb",
+    "libgssapi-krb5-2_1.17-3_amd64.deb": "49a2e7f290ab0006dbc139bfe6784f71bf38d1b14feebc22c14808bbe3748f6d",
+    "libgssdp-1.0-3_1.0.2-4_amd64.deb": "573f3c556d56747cccb88b8d4293997557176306ea3378670be541cc123be425",
+    "libgstreamer-gl1.0-0_1.14.4-2_amd64.deb": "41b702e8700a87daafdc7758bb1e4c43849babf36d982bbfec79e0585f023c64",
+    "libgstreamer-opencv1.0-0_1.14.4-1+b1_amd64.deb": "85564c3087b0ae3c30d48ef688e67a67383e33c150b3b8e0538aa3c129f141e2",
+    "libgstreamer-plugins-bad1.0-0_1.14.4-1+b1_amd64.deb": "d85e3ba9dc98b212f3cdc2581ee077abb72c67b6f7fa77932d6c35045126da45",
+    "libgstreamer-plugins-bad1.0-dev_1.14.4-1+b1_amd64.deb": "9d91289a04c31aa89c232595ab4f3e4c27d13956195d9f165e287d262d8177e6",
+    "libgstreamer-plugins-base1.0-0_1.14.4-2_amd64.deb": "be0fea48d5ff9bc178d0af25f9b8cf4dbc9cd915368ea79c848e636d46c6b85a",
+    "libgstreamer-plugins-base1.0-dev_1.14.4-2_amd64.deb": "b4fa8fb012ce3db5179e0ccab722770b607524450cbb380372b7150e75a096c8",
+    "libgstreamer1.0-0_1.14.4-1_amd64.deb": "b12567b65cd1dd951845d7d441ef383b0f1a22756b7a203032d548d0619226ef",
+    "libgstreamer1.0-dev_1.14.4-1_amd64.deb": "0b4a50310a1f042ed1f29c40a6ba857292dccef930a15594a6dc0fd2f98c3aec",
+    "libgtk-3-0_3.24.5-1_amd64.deb": "e652e04b04cc8a67c24c5773180a7fdd65a6cfc55a2777722e80825a56a33729",
+    "libgtk-3-common_3.24.5-1_all.deb": "1e1c979ec882542ce09b40c0f7246a7f348b42d9bec6f31eb2614a8ddccd4874",
+    "libgudev-1.0-0_232-2_amd64.deb": "e7d198d67d0d29a482f0f88a7f2480a4696e1d3ee0612a7ca6be22e6866ea26c",
+    "libgupnp-1.0-4_1.0.3-3_amd64.deb": "d316a0e47251ce5d69bad9c0e3292a4cf6d859a783d618715b9b04e434becdf4",
+    "libgupnp-igd-1.0-4_0.2.5-3_amd64.deb": "4478c4fa1f9f0e44b7a34c8ec389738936127fe5204269d50c923afc471580c8",
+    "libharfbuzz0b_2.3.1-1_amd64.deb": "aee1dd6f9884c1acdd1b6d6f49bd419235decd00f49cd927e4be4c37af2ecdab",
+    "libhdf4-0-alt_4.2.13-4_amd64.deb": "4884c473170273a3cf0e83ec0cb2f1a907c5bbe57b998f0240d5e6aecf20a398",
+    "libhdf5-103_1.10.4+repack-10_amd64.deb": "1236ee56593adf5f06ea6e407d5d7d77c782b9b4c71cada16fe2b867c95f8cd7",
+    "libhdf5-openmpi-103_1.10.4+repack-10_amd64.deb": "9b96bdec653349fd89f4cb6b17fd835b3fb0d0924b9b8e9b9d6346a53d2e567c",
+    "libhogweed4_3.4.1-1_amd64.deb": "a938f22b6cead37c5f980a59330e71e2df1df4af890ea6b3432485c0da96ea58",
+    "libhwloc-plugins_1.11.12-3_amd64.deb": "eb1dc47ac594f102005a8614413adebad0ae56d68785aac6773a05c81c8e1afc",
+    "libhwloc5_1.11.12-3_amd64.deb": "4306a4cfbaf3db358120ba57720cf1c90c512c4aa4e0c1b72f142ac93883bbd8",
+    "libibverbs1_22.1-1_amd64.deb": "681dbe4dafb9dec6ce0d3c987a11bd166babefac91aaf32142defcba394f8981",
+    "libice6_1.0.9-2_amd64.deb": "5ab658c7efc05094b69f6d0950486a70df617305fab10983b7d885ab0a750f21",
+    "libicu63_63.1-6_amd64.deb": "ccf205dfb840a9cdf8d4775febb32ac9bf08e17735920d91f5c39a9cf9c642c5",
+    "libidn2-0_2.0.5-1+deb10u1_amd64.deb": "13c3129c4930cd8b1255dbc5da7068c036f217218d1017634b83847a659fad16",
+    "libiec61883-0_1.2.0-3_amd64.deb": "b31c37a0b954f0aac8e93667b2023a3f399d115d2a91ec02bea0a862ac0cdc34",
+    "libilmbase-dev_2.2.1-2_amd64.deb": "2060c2aec18a71728f4e0abf4b8918771d2dc55e680660ed4f2a7bacd49d3de0",
+    "libilmbase23_2.2.1-2_amd64.deb": "4e0e265a1eb33cc6e6cfcb15581604df4fe252b73b7a353ed2cfe15505fbdbd3",
+    "libjack-jackd2-0_1.9.12~dfsg-2_amd64.deb": "4d9aaaa070e00def2944cb4fe06a6442e4ceb983c27b1f1745467f13b924ca33",
+    "libjbig-dev_2.1-3.1+b2_amd64.deb": "6ca760f67d2f482d269d4e1d4cfc5f9c5f7247afb012266db40e773a63ef7048",
+    "libjbig0_2.1-3.1+b2_amd64.deb": "9646d69eefce505407bf0437ea12fb7c2d47a3fd4434720ba46b642b6dcfd80f",
+    "libjpeg-dev_1.5.2-2_all.deb": "71b42025bdeb9fcc30054b54c84c4306da59466fbd419f46471f15ec54d435aa",
+    "libjpeg62-turbo-dev_1.5.2-2+b1_amd64.deb": "26f02e34181d7d76d3bdf932444f3f003690e3b8ddbec2ce0617f3ca7c8afd66",
+    "libjpeg62-turbo_1.5.2-2+b1_amd64.deb": "19fa4d492c59e051f00334b1a13bcd3579b3c199623a23e68476cb46d5b1d590",
+    "libjson-c3_0.12.1+ds-2_amd64.deb": "5b0194dac67efa04ef6df15e3080bd53448b0209f6cf25ff6a46c6ba8dccc354",
+    "libjson-glib-1.0-0_1.4.4-2_amd64.deb": "58f872df6bc521a7ef4990c2a4b3264b1a1fab15440297a7e92ef88067e308ed",
+    "libjson-glib-1.0-common_1.4.4-2_all.deb": "c27dbb0cf9c73e2a09d5c774fb46ecf6d2b634facaf3b37b20a4654d9c549187",
+    "libjsoncpp1_1.7.4-3_amd64.deb": "c1d8c2c8943f95511811eea4c85ee72cec9cf1442d16db18e135e589a77dc050",
+    "libk5crypto3_1.17-3_amd64.deb": "b9ded0026e9d0e006eb6d3e697919d9b2a8f7bf607d8acdebf03588e2b96b771",
+    "libkeyutils1_1.6-6_amd64.deb": "0c199af9431db289ba5b34a4f21e30a4f1b6c5305203da9298096fce1cdcdb97",
+    "libkmlbase1_1.3.0-7_amd64.deb": "6bd25218052f42b46c85d20dec2ecddc40cf31be51177b82b8e848a0063abe64",
+    "libkmlconvenience1_1.3.0-7_amd64.deb": "c473db7982aaa5bd51abd50b7c59b7d7ad38a03a2a077ef3bf6b70393388d8c5",
+    "libkmldom1_1.3.0-7_amd64.deb": "a2c279ba0354dba90ca8a7a3f53b4880f3bfbc309b52bd97f78a2e2be11b3ff6",
+    "libkmlengine1_1.3.0-7_amd64.deb": "926353a83536421f6a8edcfc5530c1be7dd62f0a202ae6978d7aeeb8bb22d7b7",
+    "libkmlregionator1_1.3.0-7_amd64.deb": "d7f211d0443aae8648f4e5320815f23a6d3efa26041b69d3e66fe1a3a5d98f3d",
+    "libkmlxsd1_1.3.0-7_amd64.deb": "f6fed1c2774053cb41bde7fe7ae631999af226b24ac8cb904b5e1a3bd3efc097",
+    "libkrb5-3_1.17-3_amd64.deb": "042967b8267ee537ed9a1bf012533622847aab433362e3b57c9108a53bfcb99a",
+    "libkrb5support0_1.17-3_amd64.deb": "e0e9d331643755db339e321c38889be13a8284cbba8ed0b7bfc062f8a68a0974",
+    "liblapack3_3.8.0-2_amd64.deb": "29f7df1fb03bc42b38872d37f2d1fc43ac0943b117dd766d8771247363ab4419",
+    "liblcms2-2_2.9-3_amd64.deb": "6dd806a326519b98ed9e54b184b4da2d256c4d516e75d0a38f2f6059e14eb325",
+    "libldap-2.4-2_2.4.47+dfsg-3+deb10u1_amd64.deb": "780b7e3f4d5780a705bf5bbb6b3d1d7e93cb822e831ec4a3d0da5ffd6fc39c40",
+    "libldap-common_2.4.47+dfsg-3+deb10u1_all.deb": "ee6a95d9e8a88de8770b9279239ba7bcdc754edab7b06220d960ba6eb3aaf306",
+    "liblept5_1.76.0-1_amd64.deb": "fd136eb4907d04382f46bdf75a4fadd8d589a6bd6eb19609c186a1c774cf98ca",
+    "libllvm7_7.0.1-8_amd64.deb": "353d119fd3852c168bafdf73565d4030cdf9c580fd341b3ef9e77e49720bdf30",
+    "libltdl7_2.4.6-9_amd64.deb": "d5fc0ab86db9a6a02c2ad517671788c08cf86cfa0186bac1b5c863b14e2e7eb6",
+    "liblz4-1_1.8.3-1_amd64.deb": "826203ecea7e8cb87aebfbb7bd2afc9f7e519f4c0f578c0404e21416572d1005",
+    "liblzma5_5.2.4-1_amd64.deb": "292dfe85defad3a08cca62beba85e90b0231d16345160f4a66aba96399c85859",
+    "liblzma-dev_5.2.4-1_amd64.deb": "df1c6d0290e42418df9ed76c0e17c507a12bfd590c0a17e580675555e99e51ea",
+    "libmariadb3_10.3.22-0+deb10u1_amd64.deb": "d1ea3bbf04124a8d0aab4541956bd30c8854259fe8d1b761ad1b3107a45ce3c3",
+    "libminizip1_1.1-8+b1_amd64.deb": "9141e2d8195e920e1e7a55611b75e4a8cf007f19322432c08c21422574262983",
+    "libmount-dev_2.33.1-0.1_amd64.deb": "d98985a29d705146cddffed1442980549d8bf0d5148fbf03fbc413bdd3aec8ca",
+    "libmount1_2.33.1-0.1_amd64.deb": "b8b28669dc4995a7a48d47d9199d1806d4fce9c4051277279d4dcc514c086ba3",
+    "libmp3lame0_3.100-2+b1_amd64.deb": "9743322c11e89a9c4ca00fc383522ec01d59819c61b126cf9b9690528d348592",
+    "libmpdec2_2.4.2-2_amd64.deb": "9ca85e6e2645a5e660431294320658ec7a2910d9fed90ca4e648c1211a2b844b",
+    "libmpeg2-4_0.5.1-8_amd64.deb": "395454259a0a1bbb94da9dfb50c072909e0699144371866e7f24241504d2359b",
+    "libmpg123-0_1.25.10-2_amd64.deb": "aad76b14331161db35a892d211f892e8ceda7e252a05dca98b51c00ae59d1b33",
+    "libncurses6_6.1+20181013-2+deb10u2_amd64.deb": "25cc6d68d36b13b54ca5a1c2933703681bf4694a66ee29a555616620a482fe0d",
+    "libncursesw6_6.1+20181013-2+deb10u2_amd64.deb": "7dffe9602586300292960f2e3cf4301acfc64a91aed6fa41ea2e719ae75788b3",
+    "libnetcdf-c++4_4.2-11_amd64.deb": "36391f3fd7d4e390366f4abc0f359bc824c60531994544eace2c7c7579b11a22",
+    "libnetcdf13_4.6.2-1_amd64.deb": "70755c490c8f430ff2428872a9d4742098526e3907e19a53fed32fd45bdec571",
+    "libnettle6_3.4.1-1_amd64.deb": "5a384c773ae68b0c7905ecc0abf5e45925794b679674866d7783d88786ffb0d2",
+    "libnghttp2-14_1.36.0-2+deb10u1_amd64.deb": "6980055df5f62aea9a32c6cc44fe231ca66cc9a251b091bd0b7e3274f4ce2a19",
+    "libnice10_0.1.14-1_amd64.deb": "225c4955256cfb8bc74c32e4cd0d136bf02af53914f37d7664044ec0b8853dd7",
+    "libnl-3-200_3.4.0-1_amd64.deb": "4d381ab32378d599b963d6418fc89ca0c7ae7d00277c80e08ac103bae6109ca9",
+    "libnl-route-3-200_3.4.0-1_amd64.deb": "0704ba113c8a3f8b348de8e88f4dc877578c51c194090cea07b869ee3a3fdbc8",
+    "libnspr4_4.20-1_amd64.deb": "e6188fdd91ec215d12d4eca5211c2406874eb17f5b1c09d6355641a349adcec0",
+    "libnss3_3.42.1-1+deb10u2_amd64.deb": "e9a421a3ca17274eb471d55034b13a5845306c55967619d2b1f3c5ee54bfa891",
+    "libnuma1_2.0.12-1_amd64.deb": "ab2277a2af54056f7c2b01f98c0ac9ea546753a35de00e74285b7a0f667ea7e7",
+    "libodbc1_2.3.6-0.1_amd64.deb": "04fd35fe0afe55ef8d0b9523edd569242815b0d7a9f21de1da812c458dd8c2cd",
+    "libogdi3.2_3.2.1+ds-4_amd64.deb": "e3ad75566b51255c04ff96a4c0e19c25ea36b21d679371446bf6c00b1d426f36",
+    "libogg0_1.3.2-1+b1_amd64.deb": "fd8e4b0e1ce171daff480eafd862d8e3f37343dc7adb60a85229f39e45192663",
+    "libopencore-amrnb0_0.1.3-2.1+b2_amd64.deb": "5561b98a6f46ca93950872fcb5a098a4af067e1bf4d1052d9f1c3365ec4d2d07",
+    "libopencore-amrwb0_0.1.3-2.1+b2_amd64.deb": "eb76ef7aecc6fc92077d4da1a00fdadd1109e1bcaedb8fe8fff329e80b9712c3",
+    "libopencv-calib3d-dev_3.2.0+dfsg-6_amd64.deb": "27d9496c13ecdc4e163a956ed27bead3c32c8855eda60061df8613a51631a512",
+    "libopencv-calib3d3.2_3.2.0+dfsg-6_amd64.deb": "82127fc7f416ebe777d418a7ca1971dbd1c5efde739ef0bb4ec45cda64d5f2be",
+    "libopencv-contrib-dev_3.2.0+dfsg-6_amd64.deb": "7d2ea9425942e8fe845912c9ec6566b7aff119a309461b9c31f5ee2765b9286b",
+    "libopencv-contrib3.2_3.2.0+dfsg-6_amd64.deb": "6a9ef938a4e27616556bb70ab12ee23aa703b5a02ab1fa21600811c7f41db762",
+    "libopencv-core-dev_3.2.0+dfsg-6_amd64.deb": "65e19e74938c8e76f9e37ae1112751edd130ab985fb9f7ef0720f6600d7582c6",
+    "libopencv-core3.2_3.2.0+dfsg-6_amd64.deb": "32bdd13bab61af2315b5c5e19989162192a44301f42871c85c988d1a010910d3",
+    "libopencv-dev_3.2.0+dfsg-6_amd64.deb": "39d8a36c3bdcec1218cc2f7db1a11db283b793c864913f9cb53d33d5b383723b",
+    "libopencv-features2d-dev_3.2.0+dfsg-6_amd64.deb": "7cd7d7f8c0fb713a3879413ab9249d0a0ce42065f1a44ab3e2f274aa6a151b39",
+    "libopencv-features2d3.2_3.2.0+dfsg-6_amd64.deb": "30596fcb4998bfd25bfcbe99803bb6622da8523d9585c8b89f75b4b984d26841",
+    "libopencv-flann-dev_3.2.0+dfsg-6_amd64.deb": "823f5ccdac8b5be341724d51bd3462b6c93078dd406cc47bbe2f79f2dc7e804f",
+    "libopencv-flann3.2_3.2.0+dfsg-6_amd64.deb": "8ffceaddd6d8a24d8e0b4869e29a8aff39ef17f943a69862a00562bad2ad1025",
+    "libopencv-highgui-dev_3.2.0+dfsg-6_amd64.deb": "be31c1e23123f05a764436e63f73c693fd33dfc7d2118a8749e92366edcce842",
+    "libopencv-highgui3.2_3.2.0+dfsg-6_amd64.deb": "b4959b56fb3de46f1d5a7b09360558ab2469d2eeee2241090924a5e85bcba06a",
+    "libopencv-imgcodecs-dev_3.2.0+dfsg-6_amd64.deb": "434689bcc78706e97e545e76ea60a876287c6105b5d9848e161a99752cabad75",
+    "libopencv-imgcodecs3.2_3.2.0+dfsg-6_amd64.deb": "cd945a6301c7fd8ce50643392c413cf2d2b870be539fceb5d259c30a571d42c1",
+    "libopencv-imgproc-dev_3.2.0+dfsg-6_amd64.deb": "b85d61d0dca625eab589d22d69132bb4b6c1cf1eb49e4499e576c8e991f7d83c",
+    "libopencv-imgproc3.2_3.2.0+dfsg-6_amd64.deb": "b4e5edf3385d233627a47b97bd1549c27d3c2ac6a9d10c6225a2ea3cb4f84ccd",
+    "libopencv-ml-dev_3.2.0+dfsg-6_amd64.deb": "274de19ab04749d41c9791e2ae5821ff45f437a2d11b516e276f5554f34ca5d8",
+    "libopencv-ml3.2_3.2.0+dfsg-6_amd64.deb": "8399ee0c46d1b0ad6e2dd580255daec319055a51423d8506a833e4e24530b02f",
+    "libopencv-objdetect-dev_3.2.0+dfsg-6_amd64.deb": "deeeefa46c326a0040b414e43df050eb903eb9c847275f0b72cf961c17169f5b",
+    "libopencv-objdetect3.2_3.2.0+dfsg-6_amd64.deb": "1f435a5f2f3cff3c29a8c30cbef0cb53d9dcfc6908e8dea045dc13436821c6cc",
+    "libopencv-photo-dev_3.2.0+dfsg-6_amd64.deb": "764e7aca50104588726843db0e1178aaad2b591d5f1c234fb2302a321123eca5",
+    "libopencv-photo3.2_3.2.0+dfsg-6_amd64.deb": "5f9441de0b3b2d43196763f4027603cbdb7fb44a83c872d8e913560563282e3b",
+    "libopencv-shape-dev_3.2.0+dfsg-6_amd64.deb": "521c63b1f251477238e1ca9e3aae0a3a1cb822fc3f939f958deb2e0111e98275",
+    "libopencv-shape3.2_3.2.0+dfsg-6_amd64.deb": "b410c1e5b71dfcee0dff145a9c6a91532f4c56d62a363da4ae5cf8fd8eb223b0",
+    "libopencv-stitching-dev_3.2.0+dfsg-6_amd64.deb": "3a440bd217e48166b875b298ea554e730716814bc465b8dc3f80f6c950de8551",
+    "libopencv-stitching3.2_3.2.0+dfsg-6_amd64.deb": "e754dc2df8a3381839dd6378d387542898d55a2b7b64869fbc604686e150f704",
+    "libopencv-superres-dev_3.2.0+dfsg-6_amd64.deb": "fd5bedfac07e4b68949e988c89e192b33ccc863407944706faad0a72963b84cf",
+    "libopencv-superres3.2_3.2.0+dfsg-6_amd64.deb": "ffdc92dd75005126eb52533d62eaefc4672c8093141340a4e7928e050443d901",
+    "libopencv-ts-dev_3.2.0+dfsg-6_amd64.deb": "0dfd2eef637818eda7d31750c70b015644bc0456783d456baa6cd2ee10a062b2",
+    "libopencv-video-dev_3.2.0+dfsg-6_amd64.deb": "3d1676dcca48cb25769492c0458dc18e7c73dfbc8972bb392752e609e5fae39a",
+    "libopencv-video3.2_3.2.0+dfsg-6_amd64.deb": "28f1b40bda8a0a14c196cd7a81f901fc15c9ee45da10f736da90ccf0b1dbdcbc",
+    "libopencv-videoio-dev_3.2.0+dfsg-6_amd64.deb": "b4d9d179d542dd1472dc40ad6fb7fea3f593b408b16eecab9998f2a33c391da3",
+    "libopencv-videoio3.2_3.2.0+dfsg-6_amd64.deb": "4f2eadcd5ce4bc8d0ab2f58a7762cbfd52f64a101e71ce8be772b598c20e098b",
+    "libopencv-videostab-dev_3.2.0+dfsg-6_amd64.deb": "f2885e518d007a65fc88e5eb97cd2a19d6ed10530d2b025ab835db888329d00a",
+    "libopencv-videostab3.2_3.2.0+dfsg-6_amd64.deb": "290d0ac910701ba468fecc2c8cb2ba82b918c1a51494d7dd902e3b4dde12944c",
+    "libopencv-viz-dev_3.2.0+dfsg-6_amd64.deb": "f9ad9aea38b1684addae5db528d875bdba5865a6cc79df5d2811300248daa781",
+    "libopencv-viz3.2_3.2.0+dfsg-6_amd64.deb": "ac1dc5ef101bd4328fbecec8582c390ccdf54efd7fb7c79391f0c37338cf0c98",
+    "libopencv3.2-java_3.2.0+dfsg-6_all.deb": "6a177762d8dbe7e2a54cfc03aa523802848e0567ded674314d1919652b07f81b",
+    "libopencv3.2-jni_3.2.0+dfsg-6_amd64.deb": "038a30852d113505629350e7c16a13af2f61ffda4118e4b82cf601726adefae3",
+    "libopenexr-dev_2.2.1-4.1_amd64.deb": "bde724ab34b97892ae41e19874bb3f23f650f36df1eaece4d0657f5297f11bea",
+    "libopenexr23_2.2.1-4.1_amd64.deb": "21c7979a6b08b5090fa368b4a933518a158cb242073ba85b7e0ebc22033d199d",
+    "libopengl0_1.1.0-1_amd64.deb": "52db2523f92e299c0002cf73d225c62bad50a54d2e88174176f6310b48b3b67c",
+    "libopenjp2-7_2.3.0-2+deb10u1_amd64.deb": "be133e48ac8894d4824b6106fe361a1b46acbcef8232b3b98dc04455da90e02a",
+    "libopenmpi3_3.1.3-11_amd64.deb": "02db5446521cdbd3833ae483600c8fb6adc555c5f7141480f8a0d287a142cd50",
+    "libopenmpt0_0.4.3-1_amd64.deb": "fa1acaede28cae58f0dbac63ce05743409575cb3eecd621043205c3fb04966ad",
+    "libopus0_1.3-1_amd64.deb": "78d2d72932f9749012cf356e8699f5f56c4a707eeb1f18c44b59928af7ac5876",
+    "liborc-0.4-0_0.4.28-3.1_amd64.deb": "50d46ca045ab1a5a4e17aab20f890b6704297c44019ac4e9ad3bf48498ef69ab",
+    "liborc-0.4-dev-bin_0.4.28-3.1_amd64.deb": "48a5c4dae4bf7c48401a2c414f6854de5eb63dc809e1a5f2fb817980246b6831",
+    "liborc-0.4-dev_0.4.28-3.1_amd64.deb": "05ce2269e6c711368d8fa78a2caaead32468caf406396a76cf9d3b0a75e304db",
+    "libp11-kit0_0.23.15-2_amd64.deb": "4b677eab958f55e5e701c9d8bbdc27f4e7afdb07756a5f8746e97251ee66907b",
+    "libpam-modules-bin_1.3.1-5_amd64.deb": "9ba6ca27c6d4077846c2ec3489c30b8d699391393fa0c0de28a1de8cffbf118e",
+    "libpam-modules_1.3.1-5_amd64.deb": "bc8a1c2e17c0855a3ecef398299d88696ed6d8254cc03cce3800c4a4063f7d7d",
+    "libpam0g_1.3.1-5_amd64.deb": "b480fef838d01dc647170fdbde8d44c12e05e04da989b3bffd44223457cee0dc",
+    "libpango-1.0-0_1.42.4-7~deb10u1_amd64.deb": "2426547312628b7c1585f7043f316271d97fe32d663b1fe5b1afae8d71aa4186",
+    "libpangocairo-1.0-0_1.42.4-7~deb10u1_amd64.deb": "deccaebf49890ae9569ab94b1dbf97dfb84553c71d140d2d493f2984306c5233",
+    "libpangoft2-1.0-0_1.42.4-7~deb10u1_amd64.deb": "31e25bb0553175cb1ae81c89fcf084c6222d9c66b3d801385a25d73c300a21d5",
+    "libpciaccess0_0.14-1_amd64.deb": "5f6cc48ee748200858ab56f43a47534731f5012c2c7c936a364b5c52c0cbe809",
+    "libpcre16-3_8.39-12_amd64.deb": "843e6f8cbe5545582e8f5fcbd4acdc17fafa81a396249381782a0f89ac097f05",
+    "libpcre3_8.39-12_amd64.deb": "5496ea46b812b1a00104fc97b30e13fc5f8f6e9ec128a8ff4fd2d66a80cc6bee",
+    "libpcre3-dev_8.39-12_amd64.deb": "e2b3c3dd3e23a70f9488c31d53c88aae90f84d574b6fdb015dd6de4a9cc853fe",
+    "libpcre32-3_8.39-12_amd64.deb": "7a082633a4288af6cdec0ca2c2b9c908e2d1c3640c0cca4c16890942823fa0ab",
+    "libpcrecpp0v5_8.39-12_amd64.deb": "06b470c5dc5c29600f34b7b9a802ff804644b990a064159487ccde73d0309f13",
+    "libpixman-1-0_0.36.0-1_amd64.deb": "4382ebfc5c52623d917dc0f63c22fbf7a791d00f5b303cd56a44bf9616fa5fbe",
+    "libpmix2_3.1.2-3_amd64.deb": "8bb028cd0e3e2dcb3ed39e68b0e2b15ea757945989201832d671d2be0f9d44b5",
+    "libpng-dev_1.6.36-6_amd64.deb": "43c90b368979af1aaf2baa239892250203b24f1da0814266e009bae0a850763d",
+    "libpng16-16_1.6.36-6_amd64.deb": "82a252478465521cde9d5af473df01ed79f16e912effc5971892a574e9113500",
+    "libpoppler82_0.71.0-5_amd64.deb": "803a32bab6406429fefe53b9502386e2f831a347562eddf490b2a4c5b6fb410f",
+    "libpopt0_1.16-12_amd64.deb": "6eab4706e8f484eefcd708b0fb26a1ae27c01442a6ca2fc1affb0197afbadab1",
+    "libpq5_11.6-0+deb10u1_amd64.deb": "3407ca0dd5fae698d56faa924e735301ea5e5d901282b57400a8135433126801",
+    "libproj13_5.2.0-1_amd64.deb": "8795d816010fe3f940e842b0bf0283ec584587013eb2ace82db6676908f2c114",
+    "libproxy1v5_0.4.15-5_amd64.deb": "0e782aa0488d7effd7c3b937eeed7a604f846093bb7215467177c22bb6471011",
+    "libpsl5_0.20.2-2_amd64.deb": "290fc88e99d21586164d51f8562c3b4c6a3bfabdbb626d91b6541896d76a582b",
+    "libpsm-infinipath1_3.3+20.604758e7-6_amd64.deb": "aa453566f8efa394b7f8d6dba30ba684647f11147cec4fbe0faaa6ebb598425b",
+    "libpsm2-2_11.2.78-1_amd64.deb": "a4cdf6398189d96fbb235e6223b2f3421b1d4605da4a5d482f285491a971e2ff",
+    "libpthread-stubs0-dev_0.4-1_amd64.deb": "54632f160e1e8a43656a87195a547391038c4ca0f53291b849cd4457ba5dfde9",
+    "libpython2.7-minimal_2.7.16-2+deb10u1_amd64.deb": "8a54dfa6c30ced68dafc159d88adb8c096697a993023bb5e31f2dfd93e386474",
+    "libpython2.7-stdlib_2.7.16-2+deb10u1_amd64.deb": "96c9e7ad71da07f47b7356b416b7f5d6d9e8eda1404b2c8a8ba8edda3799177b",
+    "libpython2.7_2.7.16-2+deb10u1_amd64.deb": "e5dcd5ff5be854e9c7645f1a349701e809078051ef88dd119dc55d07c2e1f7bb",
+    "libpython3-stdlib_3.7.3-1_amd64.deb": "4f8883d378e698aa89b7bd4b68ce8e7cca01c961d3df87fafe4c079bb4668f5b",
+    "libpython3.7-minimal_3.7.3-2+deb10u1_amd64.deb": "b3d45767c2f6ff022bc76f9a1bedd445f7e90584f844e459604c856d91193fdd",
+    "libpython3.7-stdlib_3.7.3-2+deb10u1_amd64.deb": "993d6e8bad12cea70257c96f2f76f1c4a5afe7506992971dd9b6924fcb924657",
+    "libqhull7_2015.2-4_amd64.deb": "1bae4f773f67a27a9de59eb387f8dc425d62a46baf2e1ca86f3b0e50ca88e1f2",
+    "libquadmath0_8.3.0-6_amd64.deb": "766684a231a740b434468e1c7146353fcddff7b8e14644a82672299459c53c34",
+    "libraw1394-11_2.1.2-1+b1_amd64.deb": "83542d8989a81b222cada2b47eaeee11beebf35e8031dcb55ae741d00a076139",
+    "libraw1394-dev_2.1.2-1+b1_amd64.deb": "7b19568a113a488246913f1072f41ce7532d942fd211748c96296985a018059c",
+    "librdmacm1_22.1-1_amd64.deb": "59851755a31fd3f8731451923f4edddfacc161f929b1966df68530e3e662b9e5",
+    "libreadline7_7.0-5_amd64.deb": "01e99d68427722e64c603d45f00063c303b02afb53d85c8d1476deca70db64c6",
+    "librest-0.7-0_0.8.1-1_amd64.deb": "17d25479dd8fb0bfc7fd92ca92d7c063e9d0a22f43cb90e2de243b89111cde93",
+    "librsvg2-2_2.44.10-2.1_amd64.deb": "181188485d646e0ac29e79df67d8fa3ca7a984bb65024b06b36e917b4e282e21",
+    "librsvg2-common_2.44.10-2.1_amd64.deb": "c873d99436da50dfcc23104d827bd73e5063d9ee5742f39ffeb44ba1145af5e1",
+    "librtmp1_2.4+20151223.gitfa8646d.1-2_amd64.deb": "506fc9e1fc66f34e6f3f79555619cc12a15388c3bdd5387c1e89d78b19d1b5dc",
+    "libsamplerate0_0.1.9-2_amd64.deb": "8a3cf8a4405de7f5e8474c8f5e298dfd817a0217f113f4292dd7a7c378be2f60",
+    "libsasl2-2_2.1.27+dfsg-1+deb10u1_amd64.deb": "4a3fb6e0953789f3de455ad7c921294978d734e6395bc45bd6039dcd9634d263",
+    "libsasl2-modules-db_2.1.27+dfsg-1+deb10u1_amd64.deb": "c99437674b33964f44eb54b1a4d8cb5bbca0293989cd3d426bcb54e9f54d88db",
+    "libselinux1_2.8-1+b1_amd64.deb": "05238a8c13c32418511a965e7b756ab031c140ef154ca0b3b2a1bb7a14e2faab",
+    "libselinux1-dev_2.8-1+b1_amd64.deb": "d98880fbaa0fa1035d684ec8912e8f0f9c2d1d738bf03eb0ca052be548bc8297",
+    "libsemanage-common_2.8-2_all.deb": "fa3c50e11afa9250f823218898084bdefea73c7cd1995ef5ed5e7c12e7b46331",
+    "libsemanage1_2.8-2_amd64.deb": "ebc5346a40336fb481865e48a2a5356b5124fc868269dc2c1fbab2bdc2ac495e",
+    "libsensors-config_3.5.0-3_all.deb": "a064dbafa1590562e979852aca9802fc10ecfb6fda5403369c903fb38fa9802a",
+    "libsensors5_3.5.0-3_amd64.deb": "363ea208bfe6bf3dd1f66914eae5a15373fef0d72f84df013eb6d60633866c50",
+    "libsepol1-dev_2.8-1_amd64.deb": "fff0388005333eae45f2b27b9daf8d52b4392aa27becdabc304e70925b9fbac8",
+    "libsepol1_2.8-1_amd64.deb": "5e4ebf890bab2422d3caff579006c02cc3b153e98a61b8c548a951e24c0693f2",
+    "libshine3_3.1.1-2_amd64.deb": "6b09f0d577f515feffc071048218a26cdde5346d6e2661f861009897db0204d2",
+    "libshout3_2.4.1-2_amd64.deb": "76cb50f044716523a7531c9a89457ed042f1b5ee3266f6eb3644990d0437c26f",
+    "libsidplay1v5_1.36.59-11_amd64.deb": "dd5fa21ffa1257f33637fe29d1b1a0efe64e15b6bb2dfd53985a4f584c96362a",
+    "libslang2_2.3.2-2_amd64.deb": "d94c51ea5cdf253019b67867bf4b0a5116ab224e97fd767614f0af31c63477bd",
+    "libsm6_1.2.3-1_amd64.deb": "22a420890489023346f30fecef14ea900a0788e7bf959ef826aabb83944fccfb",
+    "libsnappy1v5_1.1.7-1_amd64.deb": "e791ed82f816844219a27e3680ed50753a893a34f38f3e69ed08c4abc389cbf8",
+    "libsocket++1_1.12.13-10_amd64.deb": "6611c010f2eb12786f82b80ed7029ca48b2c3ed675d693ffa38e629d33a4e1e2",
+    "libsoup-gnome2.4-1_2.64.2-2_amd64.deb": "33c571659e0fe2ba55214d2c68b15d883215c6c0e08e6037173da92585f9a623",
+    "libsoup2.4-1_2.64.2-2_amd64.deb": "db9918e3937eb4f92068665a9b42ea33b0860da602fa5c2f0e80e5cb15a556c4",
+    "libsoxr0_0.1.2-3_amd64.deb": "e8af4d04065bcca876f0e2bb1824bb0ce710a2ec10a9b1a320e210bebbc3dba7",
+    "libspatialite7_4.3.0a-5+b2_amd64.deb": "f22d5a7da9fa1358737007e12da8cb073f1d8db5cf02b1213437eed707cef656",
+    "libspeex1_1.2~rc1.2-1+b2_amd64.deb": "228dfcfa7dd3fd85aa3bb60c21de45489e3ce3f2274a80cac3992797ef8e542e",
+    "libsqlite3-0_3.27.2-3_amd64.deb": "ff247b1c0527cc7322af8d47260268db079e94284ee12352b31be912d30ce2a1",
+    "libssh-gcrypt-4_0.8.7-1_amd64.deb": "b18787dbff57eba507ae9b282688d90208e818357a76130b6195eb3d68faefc9",
+    "libssh2-1_1.8.0-2.1_amd64.deb": "0226c5853f5e48d7e99796c2e6332591383e9c337ac588e1b689f537abd0a891",
+    "libssl1.1_1.1.1d-0+deb10u2_amd64.deb": "31c15130e0e4b2c907ef7cd92e50be23320a22c0c3b54e130b5258fe6bd8df2d",
+    "libstdc++6_8.3.0-6_amd64.deb": "5cc70625329655ff9382580971d4616db8aa39af958b7c995ee84598f142a4ee",
+    "libsuperlu5_5.2.1+dfsg1-4_amd64.deb": "475d366d3a322c10111785b3e6d6f519d35831490388d1eea11e430d6e2fa711",
+    "libswresample-dev_4.1.4-1~deb10u1_amd64.deb": "f3f1a10ba6f95b35d2c2c272cf66d51fc166bf589044f8cbf8376008b12cce38",
+    "libswresample3_4.1.4-1~deb10u1_amd64.deb": "41950f8346f92b2208fe013fec0b722448be6bc4d20d4f161a8aa3b13edd4a74",
+    "libswscale-dev_4.1.4-1~deb10u1_amd64.deb": "d445fcb352b8810f0c6f844a0bc2dab8e7ccdb14846677a35489951543c6b969",
+    "libswscale5_4.1.4-1~deb10u1_amd64.deb": "1f74aaa422e55fe2ac43633a9034e432d8c9ba986d53bea361ac82152c364ea3",
+    "libsystemd0_241-7~deb10u3_amd64.deb": "ff64489d01d4fdba32f55e251dcb5e5f5f26c4fe4f43f96c094fbda9323bafee",
+    "libsz2_1.0.2-1_amd64.deb": "1cfe425dbc24e2143549ba4f18e53f9b45e8645298c2d1388a649d7108ae3604",
+    "libtag1v5-vanilla_1.11.1+dfsg.1-0.3_amd64.deb": "2cf03256786f232323c882cc7853dcd5179c9306819feaf22b9079c1398e5181",
+    "libtag1v5_1.11.1+dfsg.1-0.3_amd64.deb": "4ed09f8e76d19e59e487e4784ce192e58b7ff4414d7b05233f0c3002718596e7",
+    "libtasn1-6_4.13-3_amd64.deb": "2771ea1ba49d30f033e67e708f71da9b031649c7c13d2ce04cb3ec913ac3b839",
+    "libtbb-dev_2018~U6-4_amd64.deb": "8c3236b7ee59e6a529a6a6be4a89622a034d13a841595f2ce63ee562531934e0",
+    "libtbb2_2018~U6-4_amd64.deb": "39df49a8f732da2088369326f2f0f53f99baa0c2d1ce9f3ceb8654ebb0bbc676",
+    "libtcl8.6_8.6.9+dfsg-2_amd64.deb": "7b5d095b83e13b9b571cfecde55834b770735e29ff23a52d45e9f4692d4c64a1",
+    "libtesseract4_4.0.0-2_amd64.deb": "8e96d37eceff951c9e89f328577cb177faf6813bbd76a8c4a7deede72f73a680",
+    "libthai-data_0.1.28-2_all.deb": "267d6b251f77c17fb1415ac0727675cb978c895cc1c77d7540e7133125614366",
+    "libthai0_0.1.28-2_amd64.deb": "40e7fbd1ed27185879836b43fb8a739c8991a6d589fef9fb2b3b63e188a537ae",
+    "libtheora0_1.1.1+dfsg.1-15_amd64.deb": "ca02e9d81aac51287601f7886db67f746fff83a8b744afc4647c34a09881aae2",
+    "libtiff-dev_4.1.0+git191117-2~deb10u1_amd64.deb": "0ca4c0388ca816ab348050e10dc7c82f53ec4c39a55a0e79702e8b0ef46be174",
+    "libtiff5_4.1.0+git191117-2~deb10u1_amd64.deb": "3fe1a515b8be7987aecc8bfde57066e6f008289e86493bbd676d1ebd8e40cd7e",
+    "libtiffxx5_4.1.0+git191117-2~deb10u1_amd64.deb": "e8172e3beb684c171c8705047418570903502fee90a3e965f2ddfe66c65611b9",
+    "libtinfo6_6.1+20181013-2+deb10u2_amd64.deb": "7f39c7a7b02c3373a427aa276830a6e1e0c4cc003371f34e2e50e9992aa70e1a",
+    "libtk8.6_8.6.9-2_amd64.deb": "a250aba06a5fc9c90622b6e1c3560ff351f945ed7234f61267ec3688370d1770",
+    "libtwolame0_0.3.13-4_amd64.deb": "b22893a3a1fa5a98b75405efb27ca07f96454d9ac16cccc91160ea67a0c18102",
+    "libudev1_241-7~deb10u3_amd64.deb": "77f123122993ad99dbbdb352822a50abec400bd8a353e84f0647b6a355c31893",
+    "libunistring2_0.9.10-1_amd64.deb": "bc3961271c9f78e7ef93dec3bf7c1047f2cde73dfc3e2b0c475b6115b76780f8",
+    "liburiparser1_0.9.1-1_amd64.deb": "005564c21755fcaae2e1c10c277b43c94eec546c52797eb6d053977cebea2d8b",
+    "libusb-1.0-0_1.0.22-2_amd64.deb": "37be9e682f0fd7533b7bb9d91af802a5070ad68eb9434036af5bc2815efb2615",
+    "libuuid1_2.33.1-0.1_amd64.deb": "90b90bef4593d4f347fb1e74a63c5609daa86d4c5003b14e85f58628d6c118b2",
+    "libv4l-0_1.16.3-3_amd64.deb": "6864b565634e51d2f1cc51a95f42a79ff51c83915aa556a827b2b71227dbdc08",
+    "libv4lconvert0_1.16.3-3_amd64.deb": "52c3ad5548fb2a0b16b36a64b37ca86fd76cb49e76369281e59923be4038c596",
+    "libva-drm2_2.4.0-1_amd64.deb": "6790e8d48840780f93a9f4566f99f53ae6bf95597fddfe183526885a7b49911f",
+    "libva-x11-2_2.4.0-1_amd64.deb": "d4b632c6f216759ccd4052ef7ee95bc2f32d6aea21bbdb8cfe370f189193c32f",
+    "libva2_2.4.0-1_amd64.deb": "40a89587dea750033d0c03dbc6580c54872400a3e8254d8f0bd1ab93e3d5379d",
+    "libvdpau1_1.1.1-10_amd64.deb": "405f005d362c260bd044dbe2780212bd94e6a9225220fe29126edcf3ff5a345d",
+    "libvisual-0.4-0_0.4.0-15_amd64.deb": "e8bbfaf5d5172ca84dfb8002f51d59f35e59ca98a3e3aad28744f69bad305995",
+    "libvorbis0a_1.3.6-2_amd64.deb": "ca25986889a378ffe90c977739f8184858cc9c3c0fd287c2f29e19369855c6f3",
+    "libvorbisenc2_1.3.6-2_amd64.deb": "04ffe79e724c230778bf8a9875a455bd24d8a15c3b453b2a1b5125120c4a5044",
+    "libvorbisfile3_1.3.6-2_amd64.deb": "7bb4e70414c9f8a9cfdc86a64eb8659e889fa74e1f54a8cfc050ec5b6c9faace",
+    "libvpx5_1.7.0-3+deb10u1_amd64.deb": "72d8466a4113dd97d2ca96f778cad6c72936914165edafbed7d08ad3a1679fec",
+    "libvtk6.3_6.3.0+dfsg2-2+b5_amd64.deb": "d455661b50ecfcbd5305dc03f249b24032dc3e6b5d86f429a80835bea7adc4b1",
+    "libwavpack1_5.1.0-6_amd64.deb": "5007546fc6cefb212c894dd6f463aaf9227c0b82f9442ecb0e257adeb9d3a0c8",
+    "libwayland-bin_1.16.0-1_amd64.deb": "a394cff3d6b7b4fd4e7c401ff2b56beb110e4aa3f2e1566a339da2d987de5e11",
+    "libwayland-client0_1.16.0-1_amd64.deb": "826fdd1a6a5ffa01415f138e238da858aae22ac4f4835cedfecab76dd0dcb01b",
+    "libwayland-cursor0_1.16.0-1_amd64.deb": "eee990ea0ad68ac409986ebf92106b8deada54cc2cfd19293177f5f938c35690",
+    "libwayland-dev_1.16.0-1_amd64.deb": "7d978fe21ea070b9f1a80327292d90a8644f50da27284c245e54c0b7f5602999",
+    "libwayland-egl1_1.16.0-1_amd64.deb": "a021e9aa9a92270fa259211a0ca69b5e8428f32c6e800a4a93f0766b0a48a5c6",
+    "libwayland-server0_1.16.0-1_amd64.deb": "93c7bb9dfd107f1e7d2b3e0a9af6efc653d75cc5e58f653cfd14afc12d125655",
+    "libwebp6_0.6.1-2_amd64.deb": "7d9cb5e08149327731e84380e454a56f148c517ec2ecad30900c6837d0b1b76a",
+    "libwebpmux3_0.6.1-2_amd64.deb": "89d70c819420ddc636994b18ec4ad35b8edea49567b59304a4bc88701168cd9f",
+    "libx11-6_1.6.7-1_amd64.deb": "9e6592d7fc9ef52f3c88c68ce3788341af6c8a90868928ab8416f7d35e28aed6",
+    "libx11-data_1.6.7-1_all.deb": "eb9e373fa57bf61fe3a3ecb2e869deb639aab5c7a53c90144ce903da255f7431",
+    "libx11-dev_1.6.7-1_amd64.deb": "9d13c5ee3119679d14aa809e637a2d868c72003345cf7d25fc04557a58ec5f27",
+    "libx11-xcb-dev_1.6.7-1_amd64.deb": "52116465eb2b886f3df9ea86659b09204114e39104e84ab1c538cc42c9cc1a20",
+    "libx11-xcb1_1.6.7-1_amd64.deb": "576a174412174def47014ddd71443cb65588ba243196e0001b3186273361d857",
+    "libx264-155_0.155.2917+git0a84d98-2_amd64.deb": "37c8b5be1ddaddf6abc92d284372474584c5e1ceb2f2ec12a4296a7c55ea5b60",
+    "libx265-165_2.9-4_amd64.deb": "3c90c5a6b2a69a3de321c3781ebf6640ecd693651bd99526c5f18664fbb16f63",
+    "libxau-dev_1.0.8-1+b2_amd64.deb": "5a994d70f36e0cafe99b38f681e584971db6cc932df0380ebca0fec0c32a4295",
+    "libxau6_1.0.8-1+b2_amd64.deb": "a7857b726c3e0d16cda2fbb9020d42e024a3160d54ef858f58578612276683e8",
+    "libxcb-dri2-0-dev_1.13.1-2_amd64.deb": "e58f8fb9e7b58f17aba45167ea9dd019ee6dd75413f6327f716a988fb7a78a7f",
+    "libxcb-dri2-0_1.13.1-2_amd64.deb": "1604da91e88a88395add6588d8b6227098acc2680ee1f234697219036f4d22b1",
+    "libxcb-dri3-0_1.13.1-2_amd64.deb": "931d9c7be021a45ae69fb99f72fde393402f3d38355ecbcf8c1742e19749a0df",
+    "libxcb-dri3-dev_1.13.1-2_amd64.deb": "7919943a87bcae91e99dd79da0cd85265b9f291cfdd48e3e9f8dd56b24a9c578",
+    "libxcb-glx0-dev_1.13.1-2_amd64.deb": "cff2a773888d9ea84358210bd4a12223cc9a1f3d05b8357269309d8194a2fa83",
+    "libxcb-glx0_1.13.1-2_amd64.deb": "ba58285fe011506fed6e2401e5623d924542864362eb68d5e724555af5195d11",
+    "libxcb-present-dev_1.13.1-2_amd64.deb": "b778a671ef3e89d44009fc9cd10d234c6d2c9b0312ef48351bd6cd3e27261773",
+    "libxcb-present0_1.13.1-2_amd64.deb": "fb531c51237c2371bc9a9924f3e70b15fb004181444473bc932b7ad9263500cb",
+    "libxcb-randr0-dev_1.13.1-2_amd64.deb": "b428af3473515445d598ed1b49159039bcfcaeca7bd097f8366d2ba7294aae0a",
+    "libxcb-randr0_1.13.1-2_amd64.deb": "7d5f213e48f27bd1c4b3a4b5e46f4519cf0d2d33403c19dbf3891fbb2a599fc4",
+    "libxcb-render0-dev_1.13.1-2_amd64.deb": "64249d7ac1bc82b202906433fdba7c8b18130f51c1561bdfc7c1886c32d74e5e",
+    "libxcb-render0_1.13.1-2_amd64.deb": "7bd78eb3d27de76d43185d68914e938d60f233737f7a05586888072695cab6fb",
+    "libxcb-shape0-dev_1.13.1-2_amd64.deb": "aa451e4d713bbc37f327b6f6b9767372dd298e6c3fc4e6287349c5c45836fa40",
+    "libxcb-shape0_1.13.1-2_amd64.deb": "971be06832051730a59ef0db4ed49f49efa0539d36f5acf5d1ee0a6e67a1e3bf",
+    "libxcb-shm0_1.13.1-2_amd64.deb": "a7a9927c9b656c253fe6f61497b94aa7332e2270cc30ca67c2925a3ecb61d742",
+    "libxcb-sync-dev_1.13.1-2_amd64.deb": "32680293f03a28af11271aa4b8a39de047fdad329f5125dd53054da360fb5382",
+    "libxcb-sync1_1.13.1-2_amd64.deb": "991807437dc07687ae2622f0e6ee8aff87695e13003921f469e5b6a495f55e3b",
+    "libxcb-xfixes0-dev_1.13.1-2_amd64.deb": "a0f5bd931af6941688a024b74cf0d29122a09a074e65599b4e13bc87fbd24d24",
+    "libxcb-xfixes0_1.13.1-2_amd64.deb": "ade907976731e9c80dd87149647eb94197ae5a6d534712f1c18c58192b6a8be4",
+    "libxcb1-dev_1.13.1-2_amd64.deb": "237e0336b66c96e651b04221f03fe021c3052438bf9bedb85440e7cb48dfa890",
+    "libxcb1_1.13.1-2_amd64.deb": "87d9ed9340dc3cb6d7ce024d2e046a659d91356863083715d2c428a32e908833",
+    "libxcomposite1_0.4.4-2_amd64.deb": "043c878356954f4521c401b160d554809115c472ca384d9f793c1c7542316eb9",
+    "libxcursor1_1.1.15-2_amd64.deb": "5c5c3c5020b3e963afcf45af21ad8c0c14375ae35f6c649a05a22790503bf24c",
+    "libxdamage-dev_1.1.4-3+b3_amd64.deb": "4fbfdf647a2a082112ad9a0b84a0780fac2d0fbfabf1b7cb2cc6325fc096ac4a",
+    "libxdamage1_1.1.4-3+b3_amd64.deb": "e9539838d47cb10b4273c320f8e885ef85df7bd3a95f0ea9bcbc144db82c03ae",
+    "libxdmcp-dev_1.1.2-3_amd64.deb": "c6733e5f6463afd261998e408be6eb37f24ce0a64b63bed50a87ddb18ebc1699",
+    "libxdmcp6_1.1.2-3_amd64.deb": "ecb8536f5fb34543b55bb9dc5f5b14c9dbb4150a7bddb3f2287b7cab6e9d25ef",
+    "libxerces-c3.2_3.2.2+debian-1+b1_amd64.deb": "486d1ec47054ca3c25796c7615ecdd431dbc045aa006ae8a36bf2b5f41375447",
+    "libxext-dev_1.3.3-1+b2_amd64.deb": "86ffd581902086ab0bf4381b28d8fbe66fc378de2db02a96a25705eeabee0c7b",
+    "libxext6_1.3.3-1+b2_amd64.deb": "724901105792e983bd0e7c2b46960cd925dd6a2b33b5ee999b4e80aaf624b082",
+    "libxfixes-dev_5.0.3-1_amd64.deb": "ea11a513c722e0587f4bf0d1433cb9825acbdf7a873a6fe82c7b8d9dd23ff738",
+    "libxfixes3_5.0.3-1_amd64.deb": "3b307490c669accd52dc627ad4dc269a03632ca512fbc7b185b572f76608ff4e",
+    "libxft2_2.3.2-2_amd64.deb": "cd71384b4d511cba69bcee29af326943c7ca12450765f44c40d246608c779aad",
+    "libxi6_1.7.9-1_amd64.deb": "fe26733adf2025f184bf904caf088a5d3f6aa29a8863b616af9cafaad85b1237",
+    "libxinerama1_1.1.4-2_amd64.deb": "f692c854935571ee44fe313541d8a9f678a4f11dc513bc43b9d0a501c6dff0bd",
+    "libxkbcommon0_0.8.2-1_amd64.deb": "a93729f1d325598ad9c6a7ffe00c464fbe276181a3a124855041c1e303175f0c",
+    "libxml2_2.9.4+dfsg1-7+b3_amd64.deb": "401c65a9d435a26d1f9ea6e58be55253f5c3a9e32610e23edd3e103cc4ada0b4",
+    "libxpm4_3.5.12-1_amd64.deb": "49e64f0923cdecb2aaf6c93f176c25f63b841da2a501651ae23070f998967aa7",
+    "libxrandr2_1.5.1-1_amd64.deb": "8fdd8ba4a8ad819731d6bbd903b52851a2ec2f9ef4139d880e9be421ea61338c",
+    "libxrender1_0.9.10-1_amd64.deb": "3ea17d07b5aa89012130e2acd92f0fc0ea67314e2f5eab6e33930ef688f48294",
+    "libxshmfence-dev_1.3-1_amd64.deb": "7fcd7bb466a106e6f79a0cdf403559857c0fc20e6a55bdc2b963cfccee0256f9",
+    "libxshmfence1_1.3-1_amd64.deb": "1a38142e40e3d32dc4f9a326bf5617363b7d9b4bb762fdcdd262f2192092024d",
+    "libxss1_1.2.3-1_amd64.deb": "85cce16368f08a878fa892fbc54520fc654d00769cde6d300b8b802734a993c0",
+    "libxt6_1.1.5-1+b3_amd64.deb": "5c474aa7c6bef9c8b0a4cf5cb9102c29ba8c5b2b19a59269ab6e2f0a47a5ec59",
+    "libxvidcore4_1.3.5-1_amd64.deb": "da357124b48e97c7c55d836b093688ef708de7446a6ba1c54708f1307f7b7a16",
+    "libxxf86vm-dev_1.1.4-1+b2_amd64.deb": "2fb753a7c2c2fd6b74b8429d180b259af9c2b4201f8c777621ac88c63d43cea7",
+    "libxxf86vm1_1.1.4-1+b2_amd64.deb": "6f4ca916aaec26d7000fa7f58de3f71119309ab7590ce1f517abfe1825a676c7",
+    "libzstd1_1.3.8+dfsg-3_amd64.deb": "0f780477b30c7996ca370563aee75117531cea64182f847583ee7028a4d7a2e8",
+    "libzvbi-common_0.2.35-16_all.deb": "5eea3f86857626ce4371a8a70ba9ce89f1abfc47ed033d1de12ebc0c7d1dd3ea",
+    "libzvbi0_0.2.35-16_amd64.deb": "8f4a22c99ed7dce92964b8635574701753bffedf44c78704f701f895f78f2604",
+    "lsb-base_10.2019051400_all.deb": "2dd69416c4e8decda8a9ed56e36275df7645aea7851b05eb16d42fed61b6a12f",
+    "mariadb-common_10.3.22-0+deb10u1_all.deb": "54fb0fcefe4c0e74e6ad57cea0f47d5b585c2e9597423d8f0205aee8b0982975",
+    "mesa-common-dev_18.3.6-2+deb10u1_amd64.deb": "7b195dd2a78cdd5ad9979212f3757916a61b41c1016d05b3c1a4382344635664",
+    "mime-support_3.62_all.deb": "776efd686af26fa26325450280e3305463b1faef75d82b383bb00da61893d8ca",
+    "mysql-common_5.8+1.0.5_all.deb": "340c68aaf03b9c4372467a907575b6a7c980c6d31f90f1d6abc6707a0630608a",
+    "ocl-icd-libopencl1_2.2.12-2_amd64.deb": "f6841f09eb7b30d5c77b8ff07c2d0051f1c2ca201fdb86ae7a7afdbcd76a463a",
+    "odbcinst1debian2_2.3.6-0.1_amd64.deb": "9faa42382d24940361117a44ad7e95c5cc556a79e1a3623aeba4e9dfc1057fd1",
+    "odbcinst_2.3.6-0.1_amd64.deb": "b355185cb8559b8c1e733874c37c5ffc73431122b04ffb5264db59654e99aad6",
+    "passwd_4.5-1.1_amd64.deb": "23af4a550da375cefbac02484e49ed1c2e6717c0f76533137b3f2fa2cc277cf2",
+    "perl_5.28.1-6_amd64.deb": "dcd5b010f41c636822e8d4e51ccee48d66dd3cc663a61cc316ac489887e210e2",
+    "pkg-config_0.29-6_amd64.deb": "61fc3d4e34671d05f097e4aee5c03223b66de4fcbc76887ad1dbc55885c3965b",
+    "proj-data_5.2.0-1_all.deb": "fa7126aa00742ccf75f0e9867b54ea70f733436b97f600bec39408c5d3c54bd2",
+    "python3-distutils_3.7.3-1_all.deb": "6918af11061d3141990e78f5ad0530ec0f9a188cac27113d9de2896203efc13f",
+    "python3-lib2to3_3.7.3-1_all.deb": "227e2a2d12922c00dee9e55d8c5b889cfc5e72a54b85c2a509fa1664c2e9e137",
+    "python3-minimal_3.7.3-1_amd64.deb": "9c937923b35ac24f5cb6be81626f00dd6b810fc0889e5b3b08b7ffc9d179ff1b",
+    "python3.7-minimal_3.7.3-2+deb10u1_amd64.deb": "b5501e882d51b5f39c0d2858750a4d479072b24cd48adfbd42727331abb3426d",
+    "python3.7_3.7.3-2+deb10u1_amd64.deb": "c4fe8e46da0c8dccee47780993709c2ede46cc32d720a5f673e295fb68a9e640",
+    "python3_3.7.3-1_amd64.deb": "eb7862c7ad2cf5b86f3851c7103f72f8fa45b48514ddcf371a8e4ba8f02a79e5",
+    "readline-common_7.0-5_all.deb": "153d8a5ddb04044d10f877a8955d944612ec9035f4c73eec99d85a92c3816712",
+    "sensible-utils_0.0.12_all.deb": "2043859f8bf39a20d075bf52206549f90dcabd66665bb9d6837273494fc6a598",
+    "shared-mime-info_1.10-1_amd64.deb": "6a19f62c59788ba3a52c8b08750a263edde89ac98e63c7e4ccfb14b40eafaf51",
+    "ttf-bitstream-vera_1.10-8_all.deb": "328def7f581bf94b3b06d21e641f3e5df9a9b2e84e93b4206bc952fe8e80f38a",
+    "tzdata_2019c-0+deb10u1_all.deb": "59b295b0e669af26d494ed2803eb9011408f768a77395db2573e67c388e8a509",
+    "ucf_3.0038+nmu1_all.deb": "d02a82455faab988a52121f37d97c528a4f967ed75e9398e1d8db571398c12f9",
+    "uuid-dev_2.33.1-0.1_amd64.deb": "68ddfcc9e87d448b646dab275eef9dbf3eac9ac8187c38fa0e66faf6182fecdc",
+    "x11-common_7.7+19_all.deb": "221b2e71e0e98b8cafa4fbc674b3fbe293db031c51d35570a3c8cdfb02a5a155",
+    "x11proto-core-dev_2018.4-4_all.deb": "8bdb72e48cac24f5a6b284fea4d2bd6cb11cbe5fba2345ce57d8017ac40243cb",
+    "x11proto-damage-dev_2018.4-4_all.deb": "33b9b97e107f01f537108143aca6c087eef71ebf46c0f2dd3194b02bf3ef6450",
+    "x11proto-dev_2018.4-4_all.deb": "aa0237467fcb5ccabf6a93fc19fae4d76d8c6dfbf9e449edda5f6393e50d8674",
+    "x11proto-fixes-dev_2018.4-4_all.deb": "c4bb58f037c0de0ccd9c1021c3bd89de9236f5ac0db64fcd4cb8b636b14a5d64",
+    "x11proto-input-dev_2018.4-4_all.deb": "364edce8bb7cf3187c1e81d37ea5b8f6b6ddd3a74c4c82efa1810dd451bbddbf",
+    "x11proto-kb-dev_2018.4-4_all.deb": "14df9b61bf65d8cb8b6053c2bc8f993454e8076d2a5ebc4e8d2bfe671c0592e3",
+    "x11proto-xext-dev_2018.4-4_all.deb": "fd5c04fa8f10bdaeadb7bde8b14534265b38fdf18de708476eefcb229a70cbc0",
+    "x11proto-xf86vidmode-dev_2018.4-4_all.deb": "ab251198b55dc453015a596ed3195e7ad8a5b930f0baf4b68b72e0104c9c1501",
+    "xkb-data_2.26-2_all.deb": "17d21564c940dd8d89e0a1b69d6fea0144d057e4698902378f5c83500612b779",
+    "xorg-sgml-doctools_1.11-1_all.deb": "359dc76bf7b19fbbdb0b9e3ca3077e415b5b9ca8ff85162ccc889f9974493600",
+    "xtrans-dev_1.3.5-1_all.deb": "cadb720a2f8f0a2b2ad2dd82172d59e105e37885cedb3a93f6de9c78478c72d0",
+    "zlib1g_1.2.11.dfsg-1_amd64.deb": "61bc9085aadd3007433ce6f560a08446a3d3ceb0b5e061db3fc62c42fbfe3eff",
+    "zlib1g-dev_1.2.11.dfsg-1_amd64.deb": "ad2fa6d373ab18c3fc729e3a477e8b999ad33480170bd0d8966e9c7fd4843837",
+}
diff --git a/debian/gstreamer_armhf.bzl b/debian/gstreamer_armhf.bzl
new file mode 100644
index 0000000..d204ccb
--- /dev/null
+++ b/debian/gstreamer_armhf.bzl
@@ -0,0 +1,483 @@
+files = {
+    "adduser_3.118_all.deb": "bd71dd1ab8dcd6005390708f23741d07f1913877affb7604dfd55f85d009aa2b",
+    "adwaita-icon-theme_3.30.1-1_all.deb": "698b3f0fa337bb36ea4fe072a37a32a1c81875db13042368677490bb087ccb93",
+    "coreutils_8.30-3_armhf.deb": "6a578920fe016ce628065f4c7a2639a6ffc3d52637e4b4f20a46ea76fcc05539",
+    "dconf-gsettings-backend_0.30.1-2_armhf.deb": "61bd02ba2da9e549e245ab2f5152baa2f27ea40fd0b0cde5873c63048feaa708",
+    "dconf-service_0.30.1-2_armhf.deb": "2ddc0eddff21e18afda15379d6414ffea5ea21a10e958c2ddc85625feab5cf70",
+    "fontconfig-config_2.13.1-2_all.deb": "9f5d34ba20eb156ef62d8126866a376be985c6a83fdcfb33f12cd83acac480c2",
+    "fontconfig_2.13.1-2_armhf.deb": "f2d17a9588f37d149e2777bdeb6acbc8bad203b814c8983b34ddee24ce316421",
+    "fonts-dejavu-core_2.37-1_all.deb": "58d21a255606191e6512cca51f32c4480e7a798945cc980623377696acfa3cfc",
+    "fonts-freefont-ttf_20120503-9_all.deb": "13489f628898f01ad4ab12807309d814cf6df678a2ae9c1e49a426d1c916a1c5",
+    "fonts-liberation_1.07.4-9_all.deb": "c936aebbfd0af7851399ae5ab08bb01744f5e3381f7678fb87cc77114f95ef53",
+    "gdal-data_2.4.0+dfsg-1_all.deb": "6e0fce32cf2e85ad2539482087d712bf2258d05e1838f3586a17ad2dc6bb7410",
+    "gir1.2-glib-2.0_1.58.3-2_armhf.deb": "9db56815153e9db7a235454a6a49bc5080227d285b13645cd0a7479e6531d599",
+    "gir1.2-gst-plugins-bad-1.0_1.14.4-1+b1_armhf.deb": "a8c9a005c1e91a7ceeeb15e0bc2a6f42a3b032e3d616301a2544065c39e0aa7b",
+    "gir1.2-gst-plugins-base-1.0_1.14.4-2_armhf.deb": "f03297871471f50628d89634fd19cf80d87f8e0f42ae221ce40ec9bdb3bfb901",
+    "gir1.2-gstreamer-1.0_1.14.4-1_armhf.deb": "4509ccdb92fcddd98ffe5807eb46553ea3c0324dcfca6987a37f32f786f8b815",
+    "glib-networking-common_2.58.0-2_all.deb": "79831fd09fc96dc5729e8ed360563b05100d6bff70b03f3badf4e0f4759bb7ec",
+    "glib-networking-services_2.58.0-2_armhf.deb": "f901428286b7d10acac92159d6e0b9c3e09691dbe7d5ec4848f962491f0805d6",
+    "glib-networking_2.58.0-2_armhf.deb": "713ec6741147cc75468f8c16cda12185aa5b11ec79cfcc5786790febf1664aaf",
+    "gsettings-desktop-schemas_3.28.1-1_all.deb": "a75aed8781a781c4b819b2d1e952791b123580b1a02a4bb35fdbbba2e3ab8310",
+    "gstreamer1.0-plugins-bad_1.14.4-1+b1_armhf.deb": "8e19a98c55d43b1524b1a8ce7658e396e1010f1581608a9fb9267e145848b484",
+    "gstreamer1.0-plugins-base_1.14.4-2_armhf.deb": "bbcad1ee299ce211edcbc6b16a1b4e5c2f046c74d6f3f733b9ba1df1e61c1a86",
+    "gstreamer1.0-plugins-good_1.14.4-1+rpt1_armhf.deb": "f091ec762f6ac05bd76b20300b4e242fe64fa71e41244fdc92f85f6b95900cf9",
+    "gstreamer1.0-plugins-ugly_1.14.4-1_armhf.deb": "3b3479c2fcde1c0e6b7931ff463181b59a9347d6a2f53111655a9e49a9696ee8",
+    "gtk-update-icon-cache_3.24.5-1+rpt2_armhf.deb": "8ed36c27190370354987249416dd4d19545a9e82e6020f271499bf72db067e8b",
+    "hicolor-icon-theme_0.17-2_all.deb": "20304d34b85a734ec1e4830badf3a3a70a5dc5f9c1afc0b2230ecd760c81b5e0",
+    "iso-codes_4.2-1_all.deb": "72d0589fc065005574232bf103c7d34acf5e4c805f2b0d19e6aeb639438ff253",
+    "liba52-0.7.4_0.7.4-19_armhf.deb": "b0a3b3277e6a8b8a3644f451675f17b16c29daf7c4f78c90aa93bb60f3d4fb73",
+    "libaa1_1.4p5-46_armhf.deb": "d8f2bfb7889d8bafa002047dd7bf5eef0893b8c826974411f3f5a7e1c0f37ba6",
+    "libaec0_1.0.2-1_armhf.deb": "6a8107a0253577259ccadd5c274664bf7cb7f0d6e67a694d7ff0da7af850a7e9",
+    "libaom0_1.0.0-3_armhf.deb": "eca4cfebdc6f8afcdf141e17097c37c4094da61bb220c5c6fdf7faf2ef9badd6",
+    "libarmadillo9_9.200.7+dfsg-1_armhf.deb": "30e835a8de5c42bcede50b98f10ac2292d2677de12fbb44349b4611cc8803ad8",
+    "libarpack2_3.7.0-2_armhf.deb": "a1a466a360902651a8722539a39521787afe10df067a47b8d6b070e3cdc35b60",
+    "libatk-bridge2.0-0_2.30.0-5_armhf.deb": "7c7900d671e6a04cada9f36162a7a2e411763b19b08a2cd6b81ec9c249d24445",
+    "libatk1.0-0_2.30.0-2_armhf.deb": "c2c32c8784a995894da3b76d9c5e9269f64cb2cf3c28971a1cbede53190605c2",
+    "libatk1.0-data_2.30.0-2_all.deb": "cf0c94611ff2245ae31d12a5a43971eb4ca628f42e93b0e003fd2c4c0de5e533",
+    "libatomic1_8.3.0-6+rpi1_armhf.deb": "f0c29af98f8358dc7d38a25f12a1f82ee8f876336ca459c25c08f42754b03865",
+    "libatspi2.0-0_2.30.0-7_armhf.deb": "05980a3e666d895433b8bd24306d67d2362ead58199253ce2e699f9dc4e8fa5d",
+    "libaudit-common_2.8.4-3_all.deb": "4e51dc247cde083528d410f525c6157b08be8b69511891cf972bc87025311371",
+    "libaudit1_2.8.4-3_armhf.deb": "25378f4115b0c71b352ea91095e85d5ddced7ceb7b46448abb8cb53a0bc02da9",
+    "libavahi-client3_0.7-4+b1_armhf.deb": "8555f041940308d4bb24558d8eed6c506287d95ea151814c9fb5572ef68b9797",
+    "libavahi-common-data_0.7-4+b1_armhf.deb": "064992922f2ff006f0dea327fb5c38e1328fe58e17eb55a6c2ceac4dc531c46d",
+    "libavahi-common3_0.7-4+b1_armhf.deb": "6d40047dc95d3d24c75763288367eb651cac1e93ad167d9c4cae6eb6ffc7fa59",
+    "libavc1394-0_0.5.4-5_armhf.deb": "aa01717d78dd53f1a99f465df74ae2610b3703e7750336145e1e3e04baf883f9",
+    "libavcodec-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "fe6ee3da336a5a8f5365362b9aa7728eaa0b36bfc34a8104e1e0df8f4344f7d8",
+    "libavcodec58_4.1.4-1+rpt7~deb10u1_armhf.deb": "b2184abb8435e836de2ff024d3d61b0c8320761a15c38aff40de3221bacd0bb6",
+    "libavformat-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "0c421d613cbe64e7ed87f1c578419ef616bb3dc7b325d7e3dee807755c637200",
+    "libavformat58_4.1.4-1+rpt7~deb10u1_armhf.deb": "06684df7e331441a4babd7e6277d3812e505aaef880beef934c18c28261767e5",
+    "libavresample-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "03cb25f0b809a5b3c68b900ca7ec283e0b128ddfb11242ab3e6f2491d971fed2",
+    "libavresample4_4.1.4-1+rpt7~deb10u1_armhf.deb": "dd21854cf27f3c4272dc7557692ccd3977e226abc06a0c39a42f1ac1080badae",
+    "libavutil-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "6f1420ce4d3be31857eb6b280839c0e5e83d57eed643db09f87a376f3f283668",
+    "libavutil56_4.1.4-1+rpt7~deb10u1_armhf.deb": "812597883472bcf6d08be76d64cf21a94ae457a53e8d80e4978a6b6d385ac78f",
+    "libblas3_3.8.0-2_armhf.deb": "4231b6d249eb60cb13e9fc65e4378bc9e7908407a3b49a6fcdd4e88eb5df9f3d",
+    "libblkid-dev_2.33.1-0.1_armhf.deb": "1274ba8a74c2144ddd3d6124cc81d1bffad228af6a5553e7e85dc72d515bc46f",
+    "libblkid1_2.33.1-0.1_armhf.deb": "92801e35c3dbe24f2cb45d76e0837bc3928ecf2c1016ab07e827097634afa2c0",
+    "libbluray2_1.1.0-1_armhf.deb": "670a11be9d786fa07e472928893f78ba57140cd7caecfb1e396802d3ef8863dd",
+    "libbsd0_0.9.1-2_armhf.deb": "49164a38e2aa95b45e154dafa9974765554bad662c6ee865244a1b115d568b94",
+    "libcaca0_0.99.beta19-2.1_armhf.deb": "64ceea26af598559ad86340934cdeec15e24a9bf41c7612d485c72efb6649b73",
+    "libcairo-gobject2_1.16.0-4+rpt1_armhf.deb": "e3fdc6667bb647e0804cbad1eb369949d7caa8a711592786b158d0bc62c576cf",
+    "libcairo2_1.16.0-4+rpt1_armhf.deb": "1f651a306f87337b4b493ba248a09813a6da8acf60bea8ad3669a06d9d522c9f",
+    "libcap-ng0_0.7.9-2_armhf.deb": "5218a1d5d264620e027391df53c66ddc3cb5715e0aa6065238118fa3515b5e7b",
+    "libcap2-bin_2.25-2_armhf.deb": "6df8f6dcb1bf24ede50229db5c608f25be4062dddf05236325ca4294daecda6d",
+    "libcap2_2.25-2_armhf.deb": "7551421c6d90142f9d50a107d3788ee049a1bd2d0352b625878689d26d5a21a1",
+    "libcdio18_2.0.0-2_armhf.deb": "baaf8dd1247da5b90ff5c784e4b3cdb5374f7e4b7ff3ab643af78c91435da081",
+    "libcdparanoia0_3.10.2+debian-13_armhf.deb": "649c4fd73c996d730e95eb5712897dc61aaf1d6358459e5b9c20063c4ce1556a",
+    "libcharls2_2.0.0+dfsg-1_armhf.deb": "fa95753cbe407167cf4959f9ac02ac5db804fbaad1aaf5e04fc0bc2839502ee4",
+    "libchromaprint1_1.4.3-3_armhf.deb": "ae8106e0e758a423a89443fd1abb180d5ba3e6b208c93fc4f4a619390331abd1",
+    "libcodec2-0.8.1_0.8.1-2_armhf.deb": "21b065c1587dfa8ca1d8b9cbf0c797c56fa19738b2f697886091305cbdd891e6",
+    "libcolord2_1.4.3-4_armhf.deb": "4eec3912d5e1df92ef8d71a9c72aad2d7cc31c05edde8b583f3ee94c0181fe25",
+    "libcom-err2_1.44.5-1+deb10u3_armhf.deb": "9681e00461b693393d7dcc5d095aa4856a0325ed8c1757f138369b2158ccddd1",
+    "libcroco3_0.6.12-3_armhf.deb": "45940fc83e016ab6921a17001125b4fd0743ff37a87d2cf503c97a9b73e50f3b",
+    "libcups2_2.2.10-6+deb10u2_armhf.deb": "c7410b0a8b08e0b5a32c6b1cb5f29dd7b87fd6f8caeb87a358fce68f3b94f094",
+    "libcurl3-gnutls_7.64.0-4_armhf.deb": "1f52639539ccc0b358c4bac4d4044fe95db30acbaa2d27765c7132632a11047e",
+    "libdap25_3.20.3-1_armhf.deb": "e4a7a8502e233d355eadbcfad793d23a6e2b5dfbfda813aef3b76e91da6178f6",
+    "libdapclient6v5_3.20.3-1_armhf.deb": "beee7cb7642fcfd2d550908ae019a833195eb46ae5e5fac3732ab8208e0626a9",
+    "libdapserver7v5_3.20.3-1_armhf.deb": "cb4b57096e13161c5368db9d2ae868ba377a177a17dbb47503bfc1d022019b6e",
+    "libdatrie1_0.2.12-2_armhf.deb": "8e57fcfce1f6cad89e48332931d3ac3e7d65838ef36108dcb8cb9b8689268704",
+    "libdb5.3_5.3.28+dfsg1-0.5_armhf.deb": "e9bfd3904dfbdab095f24f4e3d2736c1cabd0fc0a13c06239fc795dc3fd394fa",
+    "libdbus-1-3_1.12.16-1_armhf.deb": "8956d26ed5e36da1827b295af1db20c4c3326c2fb6286df0611af1fddadfe148",
+    "libdc1394-22-dev_2.2.5-1_armhf.deb": "e1181d30983e2388f30168a06e347af77f63dadd472d7f10a06c0453c9854492",
+    "libdc1394-22_2.2.5-1_armhf.deb": "dc9d04ccaaf4c9d8e789a81b5eee65f3b7dbc16dedd492106d5494a14cc024f6",
+    "libdconf1_0.30.1-2_armhf.deb": "6ede031759943492bbc180e8d370b68d22279158a73ed692501b0e2347491cde",
+    "libdpkg-perl_1.19.7_all.deb": "1cb272a8168138e9b8334e87cc26388259f232b74667b3a7f3856f227adcc4ba",
+    "libdrm-amdgpu1_2.4.100-4+rpi1~bpo10+1_armhf.deb": "04f8d385d9cd48a9c3a3640b86efc5d482314c2bf89d82aaa9ef0ac46e2c4a98",
+    "libdrm-common_2.4.100-4+rpi1~bpo10+1_all.deb": "7db0fb4943ca45892e8a144db2d3f8dd0e89bdeceef27417e91a2ddabae53524",
+    "libdrm-nouveau2_2.4.100-4+rpi1~bpo10+1_armhf.deb": "8284f53f4be3f0c8e77b6b9ba2113bf9488d6dabb2f1da71eecc66feb4e52c6c",
+    "libdrm-radeon1_2.4.100-4+rpi1~bpo10+1_armhf.deb": "bccfbe5e722378fe3cd0f47bea54a0d624f267fddfa9bddfd3e9c7460ee0d300",
+    "libdrm2_2.4.100-4+rpi1~bpo10+1_armhf.deb": "e6c6246deca3edd815a471ad520ce0d35b1a20d1d722dfb77c77c5fdb03dd5c6",
+    "libdv4_1.0.0-12_armhf.deb": "d9e1418bbda8da1681dc2deb742979fd5c2ef647351ae38937d8e77fe0a33fd3",
+    "libdvdread4_6.0.1-1_armhf.deb": "e58e2dcae4e4438773c6acd760f45be9f114192ec981f60a758783d2c39c1930",
+    "libedit2_3.1-20181209-1_armhf.deb": "0d35fbdf5952df7a78dbc4776b89361a2fef8006342a94ab9d602483ad3578da",
+    "libegl-dev_1.3.0-7~bpo10+1_armhf.deb": "ed5321aaacf2ba419c00af1f87f3035ad884370680b34464f8d01eb99f81d04e",
+    "libegl-mesa0_19.3.2-1~bpo10+1~rpt1_armhf.deb": "6594901be722b2db115c0adb2f5aa64f5b951b1ead97de4f0c4d0640540f1069",
+    "libegl1-mesa-dev_19.3.2-1~bpo10+1~rpt1_armhf.deb": "a13a14175ffcd7b7071559ad447636523ae289cf617c738d9159bfc0fab1470c",
+    "libegl1_1.3.0-7~bpo10+1_armhf.deb": "da6da10535c26d24a05fd3ce72295d19761c19dcf096398f7fe3ff9b349ff878",
+    "libelf1_0.176-1.1_armhf.deb": "013c168e64666e33e431701f7b1d683d2234f39caa2d801e389ef69266e88167",
+    "libepoxy0_1.5.3-0.1_armhf.deb": "1c09ff3084a59f3fbb519ca43b3d5d2bd32eb2835ec1b8f5703d02ddfefef2fc",
+    "libepsilon1_0.9.2+dfsg-4_armhf.deb": "22394acdfe3159dbb6a17aca4fa4edc641c1d6f04c5eed09802b10f0cbd24a29",
+    "libevent-2.1-6_2.1.8-stable-4_armhf.deb": "b8bca67f980502877981d8891e751fa0bd879e785c63e2dd25b61ef629442adc",
+    "libevent-core-2.1-6_2.1.8-stable-4_armhf.deb": "24cd3b8e29650bd0e4b4efe6c1d770b1e75df9682c07eb3276fa22eb55276c44",
+    "libevent-pthreads-2.1-6_2.1.8-stable-4_armhf.deb": "cb009ff0d23de8d1de1972b712c210899fd5e4380027d9ac6846d5bb3b7e8c25",
+    "libexif-dev_0.6.21-5.1+deb10u1_armhf.deb": "526dd289e8d99bb3f49201b683a61b5ac7647324d8768c1bf03331ded08786b7",
+    "libexif12_0.6.21-5.1+deb10u1_armhf.deb": "e5cee943f2890b6678d87cba21a5b99326f81f7882ffd87db1b8939ce5c10a97",
+    "libexpat1_2.2.6-2+deb10u1_armhf.deb": "869f0de1b5548c13e395554f454dcd17398479e12b5b1e7079fd8e5246113365",
+    "libffi-dev_3.2.1-9_armhf.deb": "094236c2efc98622b1ea267a9e32b1d9204fb99f703d632c7291322ed67c410f",
+    "libffi6_3.2.1-9_armhf.deb": "dd24f5e9fa15e16c80c8a13869d63f1a1fbef153b63c628d09f9bc4ed513112e",
+    "libflac8_1.3.2-3_armhf.deb": "27801797c7ef803e2c8dcb78c3af09ef0eac5498c2f69cf74d162d2b2d549c34",
+    "libfontconfig1_2.13.1-2_armhf.deb": "3c9b6ab7c53742599ba2d43f67181b01b77442c0bd48539466e3a117c555e094",
+    "libfreetype6_2.9.1-3+deb10u1_armhf.deb": "84a520466752a39ac67acd32403fd00b18f41bf5477044e8475d643fdfaefd78",
+    "libfreexl1_1.0.5-3_armhf.deb": "42a5ad5b00b79271a9651cd0fa220e488bc700b691e3e9789b7b0d0c27219a5e",
+    "libfribidi0_1.0.5-3.1+deb10u1_armhf.deb": "c1fd57da1608f48bd699d853f0149e47bb21baa4d7328be5f87fb0f908a5ed54",
+    "libfyba0_4.1.1-6_armhf.deb": "331d150a88b29e2cc16139dc2ba3c1c77ab0fd577be4f2f08de603bbaec0e59b",
+    "libgbm1_19.3.2-1~bpo10+1~rpt1_armhf.deb": "482834d6f9e315327baf5cf8fb53e43c22dd26a9031d248e57cebaa56350c2ec",
+    "libgcrypt20_1.8.4-5_armhf.deb": "19ec0ba3e4d133ade463dedc1ca4f2b37344eab058213cc384ea14488a7272d5",
+    "libgd3_2.2.5-5.2_armhf.deb": "77e8999b903a4b576ae05f3c3776f69a0277a8200857aba6fa3bc8fb290c874c",
+    "libgdal20_2.4.0+dfsg-1+b2_armhf.deb": "7b4f71a576320aecbeadd11dbd507c5a6f7c9c519606bd30efa5e192183112c4",
+    "libgdcm2-dev_2.8.8-9_armhf.deb": "8b27b3ab3e2c265bd92c78aa39bc485f692da499bcf5ed0e2d9ff6b52d5d6eff",
+    "libgdcm2.8_2.8.8-9_armhf.deb": "e07feea0e5724c4ea9ab24af5dae2ba5bc0d3be20d6c9596b09f7067dd037768",
+    "libgdk-pixbuf2.0-0_2.38.1+dfsg-1_armhf.deb": "68dc44a106ef642247847657567890d7f36a4eeed16d2b7d1e7e733a0442a265",
+    "libgdk-pixbuf2.0-common_2.38.1+dfsg-1_all.deb": "1310e3f0258866eb4d0e95f140d5d9025cf6be1e3e2c375f4a426ccc2e78cf68",
+    "libgeos-3.7.1_3.7.1-1_armhf.deb": "2fd2fc54180965df1f3921ced9186de9e97204bc08f05558a48de4fcfcec69e3",
+    "libgeos-c1v5_3.7.1-1_armhf.deb": "6efa1978880f24e97214163972ff29f652ffcb8a2cebff3d17235704e204f57b",
+    "libgeotiff2_1.4.3-1_armhf.deb": "044798114f725f781ec3f2415bdf12bba85c4e762e6a2d93fff0508ab8fa2346",
+    "libgfortran5_8.3.0-6+rpi1_armhf.deb": "a5f5a383d8e617a11316ec335f83ee5bafade9cc7de5c9d83dc79f5c5858f9ad",
+    "libgif7_5.1.4-3_armhf.deb": "b88a0b203bf0f88264dd932ee13a52d28b1e92eb76bfbc7e62a124eae71f9de5",
+    "libgirepository-1.0-1_1.58.3-2_armhf.deb": "098e26c851ba98e524d1d1d653bdbf3b1a8b9db2e89bcb64002edd67c9598e26",
+    "libgl-dev_1.3.0-7~bpo10+1_armhf.deb": "6f175572bf7dab31f7126048d7db6f55b5436520152470ed4b7d5ba0ebc9810b",
+    "libgl1-mesa-dev_19.3.2-1~bpo10+1~rpt1_armhf.deb": "5adea93723a650262ec985c973c20be17a82270396bdd77713f9f80af0875026",
+    "libgl1-mesa-dri_19.3.2-1~bpo10+1~rpt1_armhf.deb": "b38ba38d29ebc3b1a4d32169d80d38d3a573acc8b2bcad7e66165984e64d75c9",
+    "libgl1-mesa-glx_19.3.2-1~bpo10+1~rpt1_armhf.deb": "2379d7521f06e2c8a63cec396e67e6cf29894b5e3c40bd67dfda4bb48711e670",
+    "libgl1_1.3.0-7~bpo10+1_armhf.deb": "213a14ca4796ccb33b855471827d7fcdc882180200704b05a7c75b6c4c46d267",
+    "libgl2ps1.4_1.4.0+dfsg1-2_armhf.deb": "61aa9ebb7c7205365cf6adb2318b4d8376e30f6dbed9271bdda63135e6d57c37",
+    "libglapi-mesa_19.3.2-1~bpo10+1~rpt1_armhf.deb": "71b954a60ceecdb99910b104a1bc2de63efcd7e50a241f8c9084940f685a85d7",
+    "libgles-dev_1.3.0-7~bpo10+1_armhf.deb": "116de498552e39c00bb1eb25a8d4772b9d13ef795e224dec6a5c29c06b0275f8",
+    "libgles1_1.3.0-7~bpo10+1_armhf.deb": "3fc494445ad07c6e03e7b12c090299ae1c1a231bd3615702e7bba10c6b0dfc17",
+    "libgles2-mesa-dev_19.3.2-1~bpo10+1~rpt1_armhf.deb": "fea4ca62a9c5fd22ae478ba25fe508fca6f0b80dc356e05826e2ef9fac44fdc9",
+    "libgles2_1.3.0-7~bpo10+1_armhf.deb": "d877d872bcc1980622f89704de4870399fa2788635e2c6ac3ccf7ded8e1ba4d8",
+    "libglib2.0-0_2.58.3-2+deb10u2_armhf.deb": "6c9edefc08726bc9e63b31e487c022db6b55dd710fe3e022e1a414a17f33328f",
+    "libglib2.0-bin_2.58.3-2+deb10u2_armhf.deb": "2427db6ecd7aab23ebbeff37d85fb955d181fa6fe07f03d4203dc994b0fa26be",
+    "libglib2.0-data_2.58.3-2+deb10u2_all.deb": "19982dfe0de4571ec99f683bd62c74e66c2422a00b0502089247d86c1c08ce92",
+    "libglib2.0-dev-bin_2.58.3-2+deb10u2_armhf.deb": "a708579413f3e56e358047cbcc0b4a64fcee8d40379e5c7b65a2a5e97e8db9be",
+    "libglib2.0-dev_2.58.3-2+deb10u2_armhf.deb": "c7c649b1a207e2ecd5f87d68a81722555ac0fc30a6de7456707cbbf33c14e2ab",
+    "libglu1-mesa_9.0.0-2.1_armhf.deb": "1a3aaf79151e412a4af3316873e8ae5f73a8e78ef7e361b37e49aed186470e91",
+    "libglvnd0_1.3.0-7~bpo10+1_armhf.deb": "716ae09c8196bfa9d23567464711869a79e8b32239504d5d344d8a89ca12ba2e",
+    "libglx-dev_1.3.0-7~bpo10+1_armhf.deb": "077828dc0770a2c5b0878a5110f767ebc345b2fd455689df5151bf3479ab1347",
+    "libglx-mesa0_19.3.2-1~bpo10+1~rpt1_armhf.deb": "c2f547332b4ccdc7aaf9816f0b5801deddc2a5fcce46a4d6820258d38dcd3e61",
+    "libglx0_1.3.0-7~bpo10+1_armhf.deb": "6a5592b4469520bae2a4465fbe903a9e17eab5a54cca3871a2da657a1bc41347",
+    "libgme0_0.6.2-1_armhf.deb": "09a4d473e20c1cbd76119c45a42d00fe953ea7a58cda45abaf65897bea82e21d",
+    "libgmp10_6.1.2+dfsg-4_armhf.deb": "ca3cd65e915de80716dd976fd9e6b9588342e39117ec07ac5a00e60bcb1a27df",
+    "libgnutls30_3.6.7-4+deb10u2_armhf.deb": "825f79632abeb0ac3c5c6de342a5c85fac0d804d82621a6abda3a88e2a7fa26f",
+    "libgomp1_8.3.0-6+rpi1_armhf.deb": "243f49f947c8a992ecb8c38a301288661254bc10099d27c98eafd2e05fe88100",
+    "libgpg-error0_1.35-1_armhf.deb": "6549092b313862bf3959fa4a0903a885ff81a777bed2b4925ab85df03588eee2",
+    "libgphoto2-6_2.5.22-3_armhf.deb": "b20dd5e04ecce954151680f4aa35e6f5a2c8c7f09a8bbfc5a769345d99481861",
+    "libgphoto2-dev_2.5.22-3_armhf.deb": "30b98ae9bb8fa42ce16e2231f509bf68c425d1bc81003ef66ed8fda1774fa135",
+    "libgphoto2-port12_2.5.22-3_armhf.deb": "0eae07c8307af1d629d475c9a50570e032941c6ef065b2551684b03dbc0e7c46",
+    "libgpm2_1.20.7-5_armhf.deb": "ecbbfd27d35af29066c8f0af5a0178742bd4121db6cc3a73c36f7d92ac087da6",
+    "libgraphite2-3_1.3.13-7_armhf.deb": "461cc0fec95f74dae2c031e7c7123774877e8bb4f0341b607d163ee0e58d1186",
+    "libgsm1_1.0.18-2_armhf.deb": "148dd82999418b9d1e70b412b5fe2d2e1d4de7407cc23ec7e2c485a7fd73ef57",
+    "libgssapi-krb5-2_1.17-3_armhf.deb": "63a06b5943840f841aacc34032974f228a3c0023fca05d9b4b6329650390361a",
+    "libgssdp-1.0-3_1.0.2-4_armhf.deb": "e9d0bc5dc4f7adf6708dabeed47fdaa20ff084d69354631320aece8bd1cdaf6d",
+    "libgstreamer-gl1.0-0_1.14.4-2_armhf.deb": "e1ec18f4cbdb6b73edb27e3a31df024f9841055faf2ba72daa0620515df3dffe",
+    "libgstreamer-opencv1.0-0_1.14.4-1+b1_armhf.deb": "e5ec50fd7dc603220480517f3e0c7dfcc41ec8cdc2102d83edb278fdd5db26b9",
+    "libgstreamer-plugins-bad1.0-0_1.14.4-1+b1_armhf.deb": "5ad37aebe6abdd5c64f13d3f4d2d07bcb99e236062997eef59eb27001030c480",
+    "libgstreamer-plugins-bad1.0-dev_1.14.4-1+b1_armhf.deb": "7fc2423d435d36c0ebdc7bd95ec65ca6f2d05e3d0b08db3e110fab21660972a6",
+    "libgstreamer-plugins-base1.0-0_1.14.4-2_armhf.deb": "617414a8a437506f147993f53fc512c6be6654ac9db842a0cdbdd9df0933fdcf",
+    "libgstreamer-plugins-base1.0-dev_1.14.4-2_armhf.deb": "4d6e6533676ad4239b4bccf3832ef041fff28fd3bbcb4d1625c830a1f2702f03",
+    "libgstreamer1.0-0_1.14.4-1_armhf.deb": "a02653fa3ca6b0c274360ab93ca68a4c313a29391017a641743a3259556c423d",
+    "libgstreamer1.0-dev_1.14.4-1_armhf.deb": "2dc305a1839f7eb92cf902865339f24af07756d0108be2676989d5f29b68033f",
+    "libgtk-3-0_3.24.5-1+rpt2_armhf.deb": "3ee5f878748e4a6301a631a1fc1e5afad3a0bc07cf5d2485e12511545609732b",
+    "libgtk-3-common_3.24.5-1+rpt2_all.deb": "eb65d4610b98e5927a23ce4f36bfd76e2d53e1303b94c6d2a0c634d8fe4506fd",
+    "libgudev-1.0-0_232-2+rpi1_armhf.deb": "6e7458021ddf02a5627569f2f67fc38eb0a5896080ad5b8e558b2813f0036201",
+    "libgupnp-1.0-4_1.0.3-3_armhf.deb": "6e38b555d6c945f37e0a56d8133007b448562a0a64e3998844c138c6c2e2a0a0",
+    "libgupnp-igd-1.0-4_0.2.5-3_armhf.deb": "fb0a64162c01d6dcd3d22d0af42839c112996d2f3cc983c9174512194dcca35c",
+    "libharfbuzz0b_2.3.1-1_armhf.deb": "cb57cfe0e2c3e36a9cdbf032eed11269eeda8ae5c66203fb95c19cc8c2fa1ed0",
+    "libhdf4-0-alt_4.2.13-4+b1_armhf.deb": "df376e0f0413e52cd59ddc937a4c9fde565cc4d5cf56cdc63f9c32c709ac8053",
+    "libhdf5-103_1.10.4+repack-10_armhf.deb": "ebe9eff643cb5e5fb0f78038ee81ae8a7ee29bd2e1d34eeb92452c3c172291ff",
+    "libhdf5-openmpi-103_1.10.4+repack-10_armhf.deb": "eb53ab0db447b08d50f3c8a5d22f8c643f65075ace448efe86d6bea5e194352a",
+    "libhogweed4_3.4.1-1_armhf.deb": "9eafecd38110191e285293a770eb13ef7a291cea2e0ff18cf511c6cf21b947b6",
+    "libhwloc-plugins_1.11.12-3+rpi1_armhf.deb": "5703cbe54214331b879aa8bc07577dc7e4e3c748df6a9c8f89af9e6e6e5cb20d",
+    "libhwloc5_1.11.12-3+rpi1_armhf.deb": "a9c20eeaa0f5abff444a3f12639ccb8554fae05d97cef1840e5de54c7d3c394b",
+    "libibverbs1_22.1-1_armhf.deb": "37aebd2d0c1cffe2b9a8678bbde786ae57b9e04ca8977fce5baa334378e661f7",
+    "libice6_1.0.9-2_armhf.deb": "92374e7e8076ad0062638c7438c886d0c23000b1a8a9b361a057d0625dc20a65",
+    "libicu63_63.1-6_armhf.deb": "94010cc7c8ce2de49ad2dcdf2d70eccb32b890a8d5e9b30ec5ba3ce72f681fdc",
+    "libidn2-0_2.0.5-1+deb10u1_armhf.deb": "48ca6a20d9901a78b714dc926b74371786b03ece5956ab4f78a148a74f4ad56f",
+    "libiec61883-0_1.2.0-3_armhf.deb": "8e06dd60a6e73bbddbcf2a4ec0c2ad72d97525c95481e1f6f92b2eff37b7d020",
+    "libilmbase-dev_2.2.1-2_armhf.deb": "bcd411d9f601549cbbb343b672e6ce0be2704c701f2cc6cdbc254cc8a8b61bce",
+    "libilmbase23_2.2.1-2_armhf.deb": "7d8995d3db7cfe4ff6705553d00679682f55cd4021436e7bd2e83bb56d23d8c2",
+    "libjack-jackd2-0_1.9.12~dfsg-2_armhf.deb": "3b67f2727dd9020c370ec00c693f575bb8540b7ac3f1f27e1e4285449ae3ccea",
+    "libjbig-dev_2.1-3.1+b2_armhf.deb": "8324e57714c0e44ed47235ef3510cd4f1acc8b098eb2140b7773935cfdd4a7e6",
+    "libjbig0_2.1-3.1+b2_armhf.deb": "b50783fe5974f648125b6ce2487ba05f99e4f11929f5b75bdc5baa94890a563f",
+    "libjpeg-dev_1.5.2-2_all.deb": "71b42025bdeb9fcc30054b54c84c4306da59466fbd419f46471f15ec54d435aa",
+    "libjpeg62-turbo-dev_1.5.2-2+b1_armhf.deb": "c8b85c158cff2deb164da3e67eba70fa13cfddc40ef7e721eaa4bf0c770f9194",
+    "libjpeg62-turbo_1.5.2-2+b1_armhf.deb": "bc28dbc5b68fe0268aa7692562bb0a39908e1bd0901be1990affd585fec773b3",
+    "libjson-c3_0.12.1+ds-2_armhf.deb": "ca3de6f029fb22f0efb576734f27a97583ebd9b9137b1c7cfd0f6228fae44423",
+    "libjson-glib-1.0-0_1.4.4-2_armhf.deb": "a790c43ed7957d646337df29628b17e812869b1e087a59002f5b1b97a42b400f",
+    "libjson-glib-1.0-common_1.4.4-2_all.deb": "c27dbb0cf9c73e2a09d5c774fb46ecf6d2b634facaf3b37b20a4654d9c549187",
+    "libjsoncpp1_1.7.4-3_armhf.deb": "25674de33c2105228048b9380b71045faf0716e63c3f901f4d9bc67ed4579c8a",
+    "libk5crypto3_1.17-3_armhf.deb": "abcc38ec1ec6f0c84feb2cb14b8a96517957cbcbdc20f6183e7fe3c0e133975c",
+    "libkeyutils1_1.6-6_armhf.deb": "ee0948ea16c2520d5a8612ba74c95c820966ed8dba78334729aef37571161d58",
+    "libkmlbase1_1.3.0-7_armhf.deb": "7ffa17e6e3487fd5745d32416ff82dba541b926b9eaab2e16ac7811a38de2486",
+    "libkmlconvenience1_1.3.0-7_armhf.deb": "4bfcc0187e12a3eef08372c3b8be8205d4eecddaaf4d7467ce29585466bc2365",
+    "libkmldom1_1.3.0-7_armhf.deb": "168b96f0e36b863517afc16ea6a37f00acb20dac80a40ffe2a6039412db0630d",
+    "libkmlengine1_1.3.0-7_armhf.deb": "d1d5df02935b20105d94e9ea8d4d1b186d3592f9197d9bea36d69b2cc2952d80",
+    "libkmlregionator1_1.3.0-7_armhf.deb": "3eba2098651bd33e7a51e7c54f9996ac11f0167c133ce59ddea4415ad7f5cecc",
+    "libkmlxsd1_1.3.0-7_armhf.deb": "820b7705568f69c54b7ac30feb9bc36935aecbbcaac55a801b6675f1bfe1a599",
+    "libkrb5-3_1.17-3_armhf.deb": "eb91711bd2f1606354c27216c89cef3c85d78407902b750ee228018f9134f8a1",
+    "libkrb5support0_1.17-3_armhf.deb": "5b0d26f4a7f8a0991087b917b2a9d93d353c4c9cc18f6a345db45e1c06391564",
+    "liblapack3_3.8.0-2_armhf.deb": "b6b2d62fe5f607efbb383d5b39edffa414a1bdad89cb886a60e0b5ee55c8ecbd",
+    "liblcms2-2_2.9-3_armhf.deb": "6d771698dd7b90af8f53d744775ad0f8a669be7a5ee8bf2c285f7bced0c64822",
+    "libldap-2.4-2_2.4.47+dfsg-3+rpi1+deb10u1_armhf.deb": "b61e759ffe122e843dd2b5117a421fcd344deac94c75b1892e338ab6042ce4a9",
+    "libldap-common_2.4.47+dfsg-3+rpi1+deb10u1_all.deb": "16f2cc9f5faaf9a539697d8adf05c0f460d274d785497aa8027dca6b0e9236d0",
+    "liblept5_1.76.0-1_armhf.deb": "9eb19fa5d74b861bdca63d195e9f23c90f359e6702ab2140df36804d7098f495",
+    "libllvm9_9.0.1-6+rpi1~bpo10+1_armhf.deb": "5b56393c34757cce6e0a6e825ef49aada4cdf3b676758dd5e8b1756ee63bfacb",
+    "libltdl7_2.4.6-9_armhf.deb": "0109cd8ee5f2216584d21dcbb8b85786d5d38cd3d88fa8602c756088c38ba29a",
+    "liblz4-1_1.8.3-1_armhf.deb": "99661a8b347d55fc0212b8176f691eaee1e13e2ee75aa316c549ac669fe77925",
+    "liblzma5_5.2.4-1_armhf.deb": "825babb4ce905472493d6f26a5ec6dfa055447f3a9f4b3418cec9e0d56681f03",
+    "liblzma-dev_5.2.4-1_armhf.deb": "94c1b419a70af792590eb26582f3ab5fd6908ee0f045ee649c65523503290bd4",
+    "libmariadb3_10.3.22-0+deb10u1_armhf.deb": "8128c49aa661b270d351afe881e9a8081e3fb6706ae66b5b2977ba51acd5dca3",
+    "libminizip1_1.1-8+b1_armhf.deb": "7ac58a7fb21b641d00d5485c0068ab4aca024f795ee220eec5ac1501cbfe6b7c",
+    "libmount-dev_2.33.1-0.1_armhf.deb": "24c59b7b33df451c7c3eca4b1d39a25c2e26eca4d4ee6b47331382175307dc11",
+    "libmount1_2.33.1-0.1_armhf.deb": "9443056463d7ddedde9bf28e1f2b6486198b68143fa0b7a2688e3edf823d566d",
+    "libmp3lame0_3.100-2+b1_armhf.deb": "1b5334f976afe0a16c0faa29832ff35e6d442beca23062b9f385079a120d4017",
+    "libmpdec2_2.4.2-2_armhf.deb": "6e02dd1398652b5d49defa6e2e05bd209d47ca08ff7c2d5801afe044f2a7792f",
+    "libmpeg2-4_0.5.1-8_armhf.deb": "0d2ce617542f7f56073c12c9b51562059b8d10c191016274d4c5f582b94e4d62",
+    "libmpg123-0_1.25.10-2_armhf.deb": "e552789597110f8cc7300ca34578a2e93700db189ee4732c2adce39a339ad617",
+    "libncurses6_6.1+20181013-2+deb10u2_armhf.deb": "89e79e7be6f1a61e8b5f3d05f12933e9b9f9b0b728ecfa91bf64ae03e3bc421a",
+    "libncursesw6_6.1+20181013-2+deb10u2_armhf.deb": "2435c3c7d6f27d907584a36583da629927eec4c2d8e2deff7bc8d814ff2b97b6",
+    "libnetcdf-c++4_4.2-11_armhf.deb": "ddbc876f3a37f78386f7d4611ad9ef095dce27a8dffa95f65a64c10381324d27",
+    "libnetcdf13_4.6.2-1+b1_armhf.deb": "0323f376ec2d0be39683adfdebaba1a0ee062d4387a4b1cd5946b389b6fd0409",
+    "libnettle6_3.4.1-1_armhf.deb": "49010bb7544c086eb20d5330fd1b1bce61bf29f66f0bfe7da5381f1ddcc6abf0",
+    "libnghttp2-14_1.36.0-2+deb10u1_armhf.deb": "3e47c770b48f555decbb31bc38f38b985c8d6009f39f7923c0fc7973bac99348",
+    "libnice10_0.1.14-1_armhf.deb": "5804c3e12b944271420f92f8df0bb32dc78bbc0a11058cbfc2a614d5a9108866",
+    "libnl-3-200_3.4.0-1_armhf.deb": "61c07f733be04122faa5f86e50138f27b639b10852fa19c5109b63ce7b4f1d8d",
+    "libnl-route-3-200_3.4.0-1_armhf.deb": "3761f4d6c6b255873b5ddf9c615ff9c396f00b212bde9d81cf83a86373316b44",
+    "libnspr4_4.20-1_armhf.deb": "1a5c311c0b2d3de1d53bb8bed8034c475dcd8293319e69f4bea2d765f00c87ee",
+    "libnss3_3.42.1-1+deb10u2_armhf.deb": "bf52021aac6e4c10183f10155fb554286831c75639e766c19c0f4946cde76718",
+    "libodbc1_2.3.6-0.1_armhf.deb": "07ce132f8fc2dab2e11f6988896cfdaf2e865b81da96456f42fde8f5e1e1708f",
+    "libogdi3.2_3.2.1+ds-4_armhf.deb": "f2089377ed36ef36327e8a982ea3fdde736806fd7288be67da19b69a7d1f6bb1",
+    "libogg0_1.3.2-1+b2_armhf.deb": "2518b3214e3c709eb0df6bb71127d1b9e24fc642513f6a8a9e729de98f789d50",
+    "libopencore-amrnb0_0.1.3-2.1_armhf.deb": "59818224c4950a1ea304368783dbfc5a8b4497566cc42af6b5e0884134579f23",
+    "libopencore-amrwb0_0.1.3-2.1_armhf.deb": "a687b84cd6479fbc1a74c96556f6e468e7d2f46a9f674bccf1ac4005a6d1184c",
+    "libopencv-calib3d-dev_3.2.0+dfsg-6_armhf.deb": "8c7f3ca52bf148f39483a6e1ed864c36107f5461fed78934b9ae3cecf027e48a",
+    "libopencv-calib3d3.2_3.2.0+dfsg-6_armhf.deb": "a7ce99efec8b00d8db399352a7099ab78153d5aec72fb1f53d3570b6213d1c4c",
+    "libopencv-contrib-dev_3.2.0+dfsg-6_armhf.deb": "b733091fe4bafb84f40b1221af788ec5df7f64108bd15c700575054a9846d1b0",
+    "libopencv-contrib3.2_3.2.0+dfsg-6_armhf.deb": "41ce0e6af765c46d2fbfd9d9b8ecca678537e53ac637dbc28775f26b22b18d49",
+    "libopencv-core-dev_3.2.0+dfsg-6_armhf.deb": "a83d0f59ce1e23cfa2f7c400e669f12b905acbbc9349998b9eab451c78ce91ca",
+    "libopencv-core3.2_3.2.0+dfsg-6_armhf.deb": "d2b7ecda65da3ba6610711dc9ab95f7bc8f90a6dead77ad06f93a082b5ae36f2",
+    "libopencv-dev_3.2.0+dfsg-6_armhf.deb": "aa519c3e572b655f039803117c737bc7c0f0638fbc86ff8989294b5df294a8fb",
+    "libopencv-features2d-dev_3.2.0+dfsg-6_armhf.deb": "b614de275538bcb5a12ed7bc9cf7dc644572b1792cab7f5c821d329fbf05ae54",
+    "libopencv-features2d3.2_3.2.0+dfsg-6_armhf.deb": "2acd7864a39c01528f87b8d34fb5b620004e04cc287200c74ca44e0712a161b2",
+    "libopencv-flann-dev_3.2.0+dfsg-6_armhf.deb": "b81522782181f3d39d48a6b61b3b61fa45388c305ab7952d98fd7b6084314e5d",
+    "libopencv-flann3.2_3.2.0+dfsg-6_armhf.deb": "097da5fec0d3828e7e2de1bd1d38cccf86c3e0f866a94e9ae0f116fe69afdace",
+    "libopencv-highgui-dev_3.2.0+dfsg-6_armhf.deb": "20c31e48c84f7ac8ff74e603aa91453671fe0f13292f5c05370ce7c984eaeb76",
+    "libopencv-highgui3.2_3.2.0+dfsg-6_armhf.deb": "f52232ca0db2aec76bf55605f7268eb3f5969524d0dd4627a5e0c75900655b38",
+    "libopencv-imgcodecs-dev_3.2.0+dfsg-6_armhf.deb": "055d2f33c8b1b20edc0d989a6ff047d0b9eea0347237febbcf27f9380fc1b843",
+    "libopencv-imgcodecs3.2_3.2.0+dfsg-6_armhf.deb": "3a87d0dd4a0d534242394de932526365a9e080f9fe783a1caaaeb72b909762c0",
+    "libopencv-imgproc-dev_3.2.0+dfsg-6_armhf.deb": "0069eb4e75c2133ca12b94f979e5c2ec1c4be31b7ca42e39a90200bbb287f4c8",
+    "libopencv-imgproc3.2_3.2.0+dfsg-6_armhf.deb": "2561604f98264ade28e5ecb31b1e5590fab4c861bb170de41e8533d09dd868c6",
+    "libopencv-ml-dev_3.2.0+dfsg-6_armhf.deb": "f576a5bd460c64914e6091d5229ecf5a021a9541319d5dae472105f2d3c6e3aa",
+    "libopencv-ml3.2_3.2.0+dfsg-6_armhf.deb": "f187fa8c4cb52c982a19c5199b4df4d311bb19a561744e9086a46b4f40de69f9",
+    "libopencv-objdetect-dev_3.2.0+dfsg-6_armhf.deb": "5a1e49091efe0af562cbbc3fcd5035c4770d076503f4266360528b3aacd170ba",
+    "libopencv-objdetect3.2_3.2.0+dfsg-6_armhf.deb": "a908f56acfd787d76ab91bf3e10374c6c34070fe73acf0b58683d15d845bd5ce",
+    "libopencv-photo-dev_3.2.0+dfsg-6_armhf.deb": "6f93654b78007bb54b6294a6294d2ee1b2c8c34f9557f7fb6f51caf8a996680c",
+    "libopencv-photo3.2_3.2.0+dfsg-6_armhf.deb": "2dbb93becd211a9543711c99c32a9f6e7480f554c068a945bd76c20111756207",
+    "libopencv-shape-dev_3.2.0+dfsg-6_armhf.deb": "a56b63c6706af19b3f6e556ee7cac51dd80af48d948f7226bc5d60d4d57dda16",
+    "libopencv-shape3.2_3.2.0+dfsg-6_armhf.deb": "a6906df95067c61b18ebdbe36e30e836752c2a4c1b6a4160b1dc991b347cbe34",
+    "libopencv-stitching-dev_3.2.0+dfsg-6_armhf.deb": "d343a82f4190e0cf10ba72846d01fd38a132f7e22024f8e8ba19171c0a65feab",
+    "libopencv-stitching3.2_3.2.0+dfsg-6_armhf.deb": "e8abf81b53d28ee5e68ae5d6e423a1278e0619b02920efbc247665b5b7bbe497",
+    "libopencv-superres-dev_3.2.0+dfsg-6_armhf.deb": "24511a36a510be5943842931933577334b4e8410ded4be3fe6ad634831b5baba",
+    "libopencv-superres3.2_3.2.0+dfsg-6_armhf.deb": "81e8882fbeeae7d07665c6c07e7885b5f43fdd7d536b7007e298cdeea4acc510",
+    "libopencv-ts-dev_3.2.0+dfsg-6_armhf.deb": "2e9520d3c83ac2ef3af690ff4241c68a8cad067c0cc50d3ec9395ac7d75e29dc",
+    "libopencv-video-dev_3.2.0+dfsg-6_armhf.deb": "c7ae7ced16c7cf6aa1a1647758b0c1cf01e38bf84444dbd3baf568d4328c6f86",
+    "libopencv-video3.2_3.2.0+dfsg-6_armhf.deb": "e6d079903ce88b25558046b4ce94261eafd2fb5de0617e7fcffbbef02521ac59",
+    "libopencv-videoio-dev_3.2.0+dfsg-6_armhf.deb": "f712aa3626fd0efc42240c1b500e05a37f8e05bfab459046a6a1cf2364541127",
+    "libopencv-videoio3.2_3.2.0+dfsg-6_armhf.deb": "ed38ad4aded75bc4b5ee1a2e7acc67fc0a7a0484d9d5fe46e56f5a9edaafeb57",
+    "libopencv-videostab-dev_3.2.0+dfsg-6_armhf.deb": "942519168d7be208736394024612285bacc9c41f66edcce5a33eb86aedae6dfc",
+    "libopencv-videostab3.2_3.2.0+dfsg-6_armhf.deb": "a4dae91092fe9e9e60b2c185d610a15452f7f9df9b9b635e8deaa3b0aa93cbbf",
+    "libopencv-viz-dev_3.2.0+dfsg-6_armhf.deb": "64a9b47eb603860c60fe62f4024b3f23a4df23fe7a0e185090ba730a32ec7fc2",
+    "libopencv-viz3.2_3.2.0+dfsg-6_armhf.deb": "e3a859dc1426c7eddfb181f0e37c8c20bebced557fabf5161de795195c12c9b4",
+    "libopencv3.2-java_3.2.0+dfsg-6_all.deb": "6a177762d8dbe7e2a54cfc03aa523802848e0567ded674314d1919652b07f81b",
+    "libopencv3.2-jni_3.2.0+dfsg-6_armhf.deb": "4fae611a082c059c2344d4f39ad60d3c00c79e243b3e218d6ac5fa5a829c63bb",
+    "libopenexr-dev_2.2.1-4.1_armhf.deb": "fb634227cc4fb67662fd45a8a5e741dd9609a2872a841856cd21c20aa7f4d0e8",
+    "libopenexr23_2.2.1-4.1_armhf.deb": "437f125bc53e5749d32de0625f8aaa4eb3c06df096ce147889cf9bd380876dde",
+    "libopenjp2-7_2.3.0-2+deb10u1_armhf.deb": "74219a03fad8c3a64cebbf9d4d82f30e365ad7e80dd5c100eae0a8d63ea1b5b0",
+    "libopenmpi3_3.1.3-11+rpi1_armhf.deb": "b6d19977698ae4860690574ce43dd947462e41ab96498f6cc557c4a122ad2cb7",
+    "libopenmpt0_0.4.3-1_armhf.deb": "85eb98a60a45992c9345583c5869a124a71e6d9179737bc7ad5597c615b08530",
+    "libopus0_1.3-1_armhf.deb": "69cd56d03aaa51a4d62ad8f98d2ff487ea062bbdfe13e983afcefa99cb0c010e",
+    "liborc-0.4-0_0.4.28-3.1_armhf.deb": "9c66d1482f30f0fa8550c0c0dc4931ee756c3ce6a09519bc1c65080c51f3c470",
+    "liborc-0.4-dev-bin_0.4.28-3.1_armhf.deb": "a25a84b79c4127e7065706e2e06966cd358d53ed245b1ac8a80f93c2169d1dc8",
+    "liborc-0.4-dev_0.4.28-3.1_armhf.deb": "d24d100bdd75dde0f0338a80b8293200f7a838a0fb1d3760e887e6e3fa0d163a",
+    "libp11-kit0_0.23.15-2_armhf.deb": "56de64f62447b20b4f24f3c1d5cf2036f0971f22e1e820e25ff16b8cf59a7490",
+    "libpam-modules-bin_1.3.1-5+rpt1_armhf.deb": "d10f1ff2fa6e1b486e2d1443793ee86eecaa15db9487f553e659696d4a9c7e01",
+    "libpam-modules_1.3.1-5+rpt1_armhf.deb": "a7294f87afe55e0972ed7bba8269f62226b53824a6e0f25a8348776173be0907",
+    "libpam0g_1.3.1-5+rpt1_armhf.deb": "3f85873f6bda68084c597ccc7ec985cb5406b5335eaf0fd225ecce892d7c24dc",
+    "libpango-1.0-0_1.42.4-7~deb10u1_armhf.deb": "23d2b3f5e3ba20bc858adcd1e1718e1794ab34e7d50050d8af0f22c64d4c2afd",
+    "libpangocairo-1.0-0_1.42.4-7~deb10u1_armhf.deb": "a66aa6ac56c5d0f62d90c3015f2c9d8b6d40bbe00d0e2edc3f7ee14b030ae400",
+    "libpangoft2-1.0-0_1.42.4-7~deb10u1_armhf.deb": "40cd486567b4207f2fe367d704a9ad6224c3e032129df5d6cb625bd3435a3bb8",
+    "libpciaccess0_0.14-1_armhf.deb": "37f01b81f204bfd7ab1ffbd3e4f2ef1355dd0f65167e8081ac3639bf12af912b",
+    "libpcre16-3_8.39-12_armhf.deb": "74a5968f6a046b48f020d171c0c67a9eea9614b29cb94facc72355529f4bb86b",
+    "libpcre3_2.8.39-12_armhf.deb": "394b0ce553f25fe1bcca1ab367ac86e374c30688c213f95c50f62d0c9101a9df",
+    "libpcre3-dev_8.39-12_armhf.deb": "6f7fe34a15f0c3522c9517ec79444c2aa0a4d952011a40dcaa124ba42ee95ae7",
+    "libpcre32-3_8.39-12_armhf.deb": "8fcd332bd8b2a8f1f4df2120bd33587f8366c708ce09da4904004dcdefbd933a",
+    "libpcrecpp0v5_8.39-12_armhf.deb": "286b0295ac923a822307a03bdc0ad7d408633fb4da4af3311ea74af54cf960b2",
+    "libpixman-1-0_0.36.0-1+rpt1_armhf.deb": "e24b5249c31dcccc246a88df767cc1b05ad47c98d484773f9e18982e1b3c2739",
+    "libpmix2_3.1.2-3_armhf.deb": "dc28717bcaffa242bc81a4e55d37819fdc73d6e204303555cf836f85973ab1e4",
+    "libpng-dev_1.6.36-6_armhf.deb": "91d8e235856389d40018e6a1568cf23c7f22c8a8fb076e9d9515ffec7159a676",
+    "libpng16-16_1.6.36-6_armhf.deb": "e5d547ed5bcc30045e8812602324c41a8e59536bed73d2af0405cbe3b596eb44",
+    "libpoppler82_0.71.0-5_armhf.deb": "78add7ce54ba679fcba6a87545ff99ed4a586c506642982caad8b529f60a6cb6",
+    "libpopt0_1.16-12_armhf.deb": "260b2ba983c6489f86cbfa590316b6e4fa3ba7501bfe9475f00c46fbf3ee76e4",
+    "libpq5_11.7-0+deb10u1_armhf.deb": "7a147de888adf44c51d604fbca0bb4341284154571cbcc1c83744f4536ebe970",
+    "libproj13_5.2.0-1_armhf.deb": "01226abbfaf179ce9f19397ee085bced5a29ee89e8af012b817196b8d173a857",
+    "libproxy1v5_0.4.15-5_armhf.deb": "6786d3190e0ab7069b207679d93b9d2f204aeb091aa87cbf0b899902521c7407",
+    "libpsl5_0.20.2-2_armhf.deb": "e4d0c0fc1b232cc3aee36351a474d55e56c45c587edbb4e3b4ce58ef399bdc3e",
+    "libpthread-stubs0-dev_0.4-1_armhf.deb": "296cbc4e83aa79186551dcd8dabafed34fc92eb376b425b93fc01b3aa02b9791",
+    "libpython2.7-minimal_2.7.16-2+deb10u1_armhf.deb": "e84407a0d58e7dff3adac497db1519dbdeba952a7caabd4f7ea2a14abb0e346d",
+    "libpython2.7-stdlib_2.7.16-2+deb10u1_armhf.deb": "dd2479f925a3da9b605b8dfb5a14ff58642e450252d7d4d99f05ca249c0d0280",
+    "libpython2.7_2.7.16-2+deb10u1_armhf.deb": "d5617bddfb0927d53471aee0ce553f22786fa488725ed09c22c13ffd8d97d884",
+    "libpython3-stdlib_3.7.3-1_armhf.deb": "9e9d450c4563f8401ac61572ce8ab64224c50c70fb2167462ae91b65ae5be5c4",
+    "libpython3.7-minimal_3.7.3-2+deb10u1_armhf.deb": "c3ba76c44d24cc3c43476dde183a7b622aad7ce3c1417512b9247604e030449d",
+    "libpython3.7-stdlib_3.7.3-2+deb10u1_armhf.deb": "d08ef59f65a9254fc0ea3c2ab4969177f8f88a21279ed8ff404f3d9b519c846f",
+    "libqhull7_2015.2-4_armhf.deb": "498f825e3c31489dc47fb9312110333ebf8bad5f1e1fd850a312fff4694f6a92",
+    "libraspberrypi0_1.20200212-1_armhf.deb": "8d5b7a46b7896e1f533f2c69384e1aedfc49e3c88d04f949fbe72504dd25bd18",
+    "libraw1394-11_2.1.2-1+b1_armhf.deb": "e7691347dfdc9096a69068f22a2f88a81f132e1cb0d1619cce89177a79fd02aa",
+    "libraw1394-dev_2.1.2-1+b1_armhf.deb": "23620ea90abf64a75431beb2939a129473fb9de4ab1f6b6fe9a414f85abc7b53",
+    "libreadline7_7.0-5_armhf.deb": "f655bfd17328343631ea6dd3fc7d885184a518fa397881f4d32f2a30b1e8fcb5",
+    "librest-0.7-0_0.8.1-1_armhf.deb": "ac4b777c967ae0f31b6d1ff51c32c8098c9d17e742ebdd2cbaa152b6f375e820",
+    "librsvg2-2_2.44.10-2.1+rpi1_armhf.deb": "9bfade393582432caa8f96868cd2a67b974ff04b9dc94a266e1bf578d14b124b",
+    "librsvg2-common_2.44.10-2.1+rpi1_armhf.deb": "02d96caf56f77643744d9d902c0d413b50224ad1a95757da65ddc2471dbb6cd0",
+    "librtmp1_2.4+20151223.gitfa8646d.1-2_armhf.deb": "c4adf4780f3e19b55fba417a0edc8d0d3b40be6d61c996a23d1a60cc3d1a3980",
+    "libsamplerate0_0.1.9-2_armhf.deb": "0476547c88c784cac2be1d75a5c7aeb4485a2e94e2d2b27b2c21fbb686bf0744",
+    "libsasl2-2_2.1.27+dfsg-1+deb10u1_armhf.deb": "49d240ab934a6433ee4a4d5130bb0147004cb2ae7752777bdc592dd920384b67",
+    "libsasl2-modules-db_2.1.27+dfsg-1+deb10u1_armhf.deb": "8e85cc998b555833ca226637bdde423667ed3eae531221063a2598c0203aa74f",
+    "libselinux1_2.8-1+b1_armhf.deb": "cc2fee966330b3d362358434ae60fa08dd7dcec81b606f4ac94ce83dd6097a39",
+    "libselinux1-dev_2.8-1+b1_armhf.deb": "662c07974f8f417921d39f23e2cadfcbc4fd386e3d1d763578572a134794c1ac",
+    "libsemanage-common_2.8-2_all.deb": "fa3c50e11afa9250f823218898084bdefea73c7cd1995ef5ed5e7c12e7b46331",
+    "libsemanage1_2.8-2_armhf.deb": "7f403dccd00375eb91786db9fbea41496833cb9391f78bd6ea1136d83203b325",
+    "libsensors-config_3.5.0-3_all.deb": "a064dbafa1590562e979852aca9802fc10ecfb6fda5403369c903fb38fa9802a",
+    "libsensors5_3.5.0-3_armhf.deb": "695f500a247e8a7762fe044c6fd9081db2e9806818eb4cd0593075de53ad5f5f",
+    "libsepol1-dev_2.8-1_armhf.deb": "bb83006408fa724bd51bb7c680bba624db2201e36dd546167ea40ced0a4b3aa6",
+    "libsepol1_2.8-1_armhf.deb": "9941f76c1d378ed0322cb413e0340455fe812f6b7451cf86a78065b2e5db69ef",
+    "libshine3_3.1.1-2+b1_armhf.deb": "7853acc136660422b7d3423caaf2ed5cb526001bb5064f932dfedf702fb5a35b",
+    "libshout3_2.4.1-2_armhf.deb": "436495384e51adf81b73943e46d1447adc7bac54bf03a2af6a6f7fe932380f42",
+    "libsidplay1v5_1.36.59-11_armhf.deb": "37f89d02cc1cb5fad7617d933eae81ee55261807fdaf45a01b49ec10595adccc",
+    "libslang2_2.3.2-2_armhf.deb": "b282110d4662728174929b22fe52d2a43a58791b6c967de29fb22c589578640c",
+    "libsm6_1.2.3-1_armhf.deb": "92eccccb771f738c18bec33b38c2bf95a281e216608e91341a7e2dbb1f8703fd",
+    "libsnappy1v5_1.1.7-1_armhf.deb": "8b87dfb35872804edd1fd002383762e593e54e3860123d0089a5b7bb26b8aef9",
+    "libsocket++1_1.12.13-10_armhf.deb": "64af2171d010a98d3cb8ca200ce24738a8fb92ef057f6b4d0c059885bf233531",
+    "libsoup-gnome2.4-1_2.64.2-2_armhf.deb": "f36e6e41e88d1ea5e203fcd1a833ef0198693a2156a6abc4a29baae817746073",
+    "libsoup2.4-1_2.64.2-2_armhf.deb": "6ef422515aa22db398887e4b0efaaeeb980a3e0d27ec1dbe3199a186d6ac19fc",
+    "libsoxr0_0.1.2-3_armhf.deb": "82c3a5098221423a3213cb7754758034e24ff114ca4e8562bf37038efc7e8afd",
+    "libspatialite7_4.3.0a-5+b2_armhf.deb": "26db41b6b1f2fee9a39673e624fe74172cc4a12b4324737dd7c066b2ae205098",
+    "libspeex1_1.2~rc1.2-1+b2_armhf.deb": "a05502ef24e63edcb3410bce0fb654c3d5a8d3129df7cfe60e0e2a330ddbc114",
+    "libsqlite3-0_3.27.2-3_armhf.deb": "09efeaead3ce02fe3b390952a30c2207be518acdcf0210715595c486212dbe53",
+    "libssh-gcrypt-4_0.8.7-1_armhf.deb": "6e8ee452c5c3fede30ee89ab80f95532f63614a76afe3d138213e33986df768b",
+    "libssh2-1_1.8.0-2.1_armhf.deb": "ab4159f8bbd8491349d75231d09bc2fdca61f91756abaa2bac95210cfc21d310",
+    "libssl1.1_1.1.1d-0+deb10u2+rpt1_armhf.deb": "56d621416c4ee9accbd92739b513c72c3d984dcfc77ef3c066d1e1a4fe177ece",
+    "libstdc++6_8.3.0-6+rpi1_armhf.deb": "bfc0533cc7d6a4d8adfb62205b39a79ee6df7c2f7c48a1dc6ff15f5af519aed4",
+    "libsuperlu5_5.2.1+dfsg1-4_armhf.deb": "f4d797c904bbedb0ea341bd7667661137004403a86a2a8b3e7d1c2365d08dc35",
+    "libswresample-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "47aa45f988faa21fd8815ffea72fa4dc086a9e7992e32f1d9522d424ab1f4256",
+    "libswresample3_4.1.4-1+rpt7~deb10u1_armhf.deb": "d50c0cbac33b70edb77e01844b9eda8f26a02d8e060ba8ea010e638407e11de9",
+    "libswscale-dev_4.1.4-1+rpt7~deb10u1_armhf.deb": "566ca37b17fc19bccbdaeeb350dadd0081428c369cf344e36f34fd3060366d79",
+    "libswscale5_4.1.4-1+rpt7~deb10u1_armhf.deb": "cd1659a4fffca19203a7f018ff167b2aa4fb2ea0fcaccdb2340b16ae41c14d28",
+    "libsystemd0_241-7~deb10u3+rpi1_armhf.deb": "a339e885c19f2922deb7dd5f212ab4f15c209a5fe99c8333041d5f19abfbf475",
+    "libsz2_1.0.2-1_armhf.deb": "ce5347b6d722e01899fc49a39073da7a16985ceadcf8238985e8109617a2a11b",
+    "libtag1v5-vanilla_1.11.1+dfsg.1-0.3_armhf.deb": "0a1bc26966e18900082e2fb6c730a4c10d7995c01599ae0ee378a153965bdfbe",
+    "libtag1v5_1.11.1+dfsg.1-0.3_armhf.deb": "e6b60a8a839d5df43ec2487ad3643d0c796acb51ecafe5ed6fc4c65ef02e5365",
+    "libtasn1-6_4.13-3_armhf.deb": "594f82946858a332bfbe55ddb2b10247a52486b8b183fd818231fef8a70ff682",
+    "libtbb-dev_2018~U6-4_armhf.deb": "ff7b27eae8c89056677a0479667448c0a2d8e20f75ed84862ccc183d9739ae7c",
+    "libtbb2_2018~U6-4_armhf.deb": "4ed379b2c64bdc16b6cf1cff7b0b859c125bfc311ebfa933f17c8f6efb8f65af",
+    "libtcl8.6_8.6.9+dfsg-2_armhf.deb": "b0f0b25f4bdbb95020ed1476fbc9a84e9a22b3d5278c0dd3df4a5963b5daf3f1",
+    "libtesseract4_4.0.0-2_armhf.deb": "1f46f21a995d76aa42c83ea6272876292520d04a51936fbd4752811ea5e73be1",
+    "libthai-data_0.1.28-2_all.deb": "267d6b251f77c17fb1415ac0727675cb978c895cc1c77d7540e7133125614366",
+    "libthai0_0.1.28-2_armhf.deb": "dad127d817507db95d10a5010db28cef882b51567d5fae58da97fc7bed55f7ae",
+    "libtheora0_1.1.1+dfsg.1-15_armhf.deb": "92f9de0685e30d293e113072b429651a6b2f783c23ffdbdc430da851e9f48236",
+    "libtiff-dev_4.1.0+git191117-2~deb10u1_armhf.deb": "814eae7eac7d49370ac8bcbfe0d129bc9c57ebbd0df8b0da4cdbabec68a8415a",
+    "libtiff5_4.1.0+git191117-2~deb10u1_armhf.deb": "723b155bbe6844483a7a3dc6b3dbd391d18eb5ba405e1b2fa6d3886864a89cc6",
+    "libtiffxx5_4.1.0+git191117-2~deb10u1_armhf.deb": "2533170a4fe4b0d67924819941384eb21ef9790ec8f88f9e621aad95d0c50d5b",
+    "libtinfo6_6.1+20181013-2+deb10u2_armhf.deb": "48f25a4a8c6629126aa77d9665030b83867f520e50cf8317249e22d8ec204963",
+    "libtk8.6_8.6.9-2_armhf.deb": "d15d84339d668d91cc78e66122265fbccbb56f2ab5b37f2792f3112e44b9dded",
+    "libtwolame0_0.3.13-4_armhf.deb": "2fc0bb23e5ba08b77fce5651d9c3b536478eebfd00ff8078633187538b8bdb4a",
+    "libudev1_241-7~deb10u3+rpi1_armhf.deb": "14484556224a1c659202f91f7903166b899870e9f4602117944d69fcd8849e51",
+    "libunistring2_0.9.10-1_armhf.deb": "7e9a8046fde4a3471e9f5477bdcecd079e423aff2b916791e0d4a224e5a6c999",
+    "liburiparser1_0.9.1-1_armhf.deb": "ed680831b4a4236a27707cd50d4649fd812876eccf1f1bfec772bb9255f65cba",
+    "libusb-1.0-0_1.0.22-2_armhf.deb": "11df519acc304a52083bbcdf018bc842510fa9f6621ac957c0e3e994dc6a1843",
+    "libuuid1_2.33.1-0.1_armhf.deb": "31dd55f3044d29370d22f956aa86965b085a201f133771aed5a44631bf989791",
+    "libv4l-0_1.16.3-3_armhf.deb": "b556c9d2765cc20d09a30251b5304cca079ff39bba458ca51b165c426edad096",
+    "libv4lconvert0_1.16.3-3_armhf.deb": "a92e37e6a5447e64506e08ff462359e1428a40685fa66393c159f28df15ca905",
+    "libva-drm2_2.4.0-1_armhf.deb": "2475f97e6e91b6c5afb81ffa0ec00e57727ab44fcbc0eb6947d4ae3dabecd397",
+    "libva-x11-2_2.4.0-1_armhf.deb": "96a84184a734f4795ff0553b1ccb31c29641024b2967327c121f46dc794d9dd1",
+    "libva2_2.4.0-1_armhf.deb": "f4a11116c295ff059b74f2aab5b0156b6e5de493595ede9ccdca21dd2a0b6d24",
+    "libvdpau1_1.1.1-10_armhf.deb": "174cc3df89c9cce18253b832f896dfe4189b765d7122f3dfe8efc48d4b9f2528",
+    "libvisual-0.4-0_0.4.0-15_armhf.deb": "33cf6fff8030c82c5c5ebc78840166a2ce8ad9ab844d113f2fa83dc03d44af7e",
+    "libvorbis0a_1.3.6-2_armhf.deb": "10c7ef81708ea3382fa08dd9185d7f633788362e08e9d5e7549604d6c54bc33c",
+    "libvorbisenc2_1.3.6-2_armhf.deb": "5274a1593ea161d8a4511e4f771eaf83234cc40a383857209d8f38637dee2489",
+    "libvorbisfile3_1.3.6-2_armhf.deb": "22803a4d65a855683ce59f4d95663b786a75a35c2fff78574bdcd70d113318b5",
+    "libvpx5_1.7.0-3+deb10u1_armhf.deb": "44339d7f9ee6a467524aca298a71009092680ff17af4c50b654a0e4ea081f12b",
+    "libvtk6.3_6.3.0+dfsg2-2+b6_armhf.deb": "6f0a4ea94d410d4543fa1f3345b0481960bae5969405c177212c179a177ccf15",
+    "libwavpack1_5.1.0-6_armhf.deb": "d5f7a739bd2ec74e224d205ef2dd331ced7044f687636922c0c3da6250af94a0",
+    "libwayland-client0_1.16.0-1_armhf.deb": "384c3b3288e9a1ecd1014cdb62aece060b47383cb564a001a056bb78f66b2c09",
+    "libwayland-cursor0_1.16.0-1_armhf.deb": "384fd0dbcd9760d62348b5426f3d3072e582a99fd83218ac9d4a91d1758fd40c",
+    "libwayland-egl1_1.16.0-1_armhf.deb": "6270413558873bd434d112e789796d6cba5e0d8703ae19903db0234db2c71924",
+    "libwayland-server0_1.16.0-1_armhf.deb": "9e18a42346475eb28e7b17d1eefb7dddead21fd16226b68f227360fb77ea7bab",
+    "libwebp6_0.6.1-2_armhf.deb": "979fc61f16f7887e4ad602a7df402ed8f12d461fda376fde31de90873920494f",
+    "libwebpmux3_0.6.1-2_armhf.deb": "6237227b67a31609eeaa20c164028447c8db0f07c6aba29da0c0d08d2f758375",
+    "libx11-6_1.6.7-1_armhf.deb": "40450a640133af52c6ca90c150cbb6ff549d3ad0e81c80f8916bc57f6af5d918",
+    "libx11-data_1.6.7-1_all.deb": "eb9e373fa57bf61fe3a3ecb2e869deb639aab5c7a53c90144ce903da255f7431",
+    "libx11-dev_1.6.7-1_armhf.deb": "24dfe78e3adf39ce9c5f95476ef8e8b190071bea85f2d32eb357061a6da19b24",
+    "libx11-xcb1_1.6.7-1_armhf.deb": "13085f3f991abfab2fd17176c0cd97c9ade0856cd864cdb1d560451ee903b967",
+    "libx264-155_0.155.2917+git0a84d98-2+rpi1_armhf.deb": "307de7bd1053117095523c7b4cfa3ca3843490a6f10023beb77c7201143691ab",
+    "libx265-165_2.9-4_armhf.deb": "aeb74dbd170aee841a1908444e6d6997c81da92fc532c41f3908595ea86dd090",
+    "libxau-dev_1.0.8-1+b2_armhf.deb": "0a7803f2807e3912b6cb0641fc77ca142af9721724a4e971bf6c08d3020369ad",
+    "libxau6_1.0.8-1+b2_armhf.deb": "1d1c083edfc29fa3a0c7539565357fcf2f90488dee919444a155afee59ca85eb",
+    "libxcb-dri2-0_1.13.1-2_armhf.deb": "dd81a9718fec85632b80fbac71f2b03972c1c588ed570f4a6c26b7de15ba0914",
+    "libxcb-dri3-0_1.13.1-2_armhf.deb": "7760da9fec785977eea7a1dad02601d7db1841ee36bdba1d05ee8dfd5c65a11a",
+    "libxcb-glx0_1.13.1-2_armhf.deb": "d787c79efcad262895de9fa662cf7646448c1c447b4c8603daa5ac2e49d56aaa",
+    "libxcb-present0_1.13.1-2_armhf.deb": "00d64156b4710ff5621fa95c33a95d608fb59c22cb293dee26c0a09e701b80b2",
+    "libxcb-render0_1.13.1-2_armhf.deb": "842d08da35fd84d9c52d189bb412fc238ada6391da803f4e8a3bc8f9dddeded0",
+    "libxcb-shm0_1.13.1-2_armhf.deb": "d6d35c9e57153832d88a521eb22acb19639e80003de7f3d9c834162fe8e4b5da",
+    "libxcb-sync1_1.13.1-2_armhf.deb": "3a150594eb919886708a37a3c4ad13383ad798780db9175632fd442510fc436b",
+    "libxcb-xfixes0_1.13.1-2_armhf.deb": "11003793d07f6e7383727cb6d45f282d4ce4bc473a3959b4e3fa2ccc9bc60e53",
+    "libxcb1-dev_1.13.1-2_armhf.deb": "04668c3f6bdcf66ab70210ba281603b32a22cfeff8fb8f100d8c1682813915a8",
+    "libxcb1_1.13.1-2_armhf.deb": "9be3930e901f475e377dd0b3fb598d785826699be1e0e4cb1b4c24ed0ad3a46d",
+    "libxcomposite1_0.4.4-2_armhf.deb": "8550a66e62a33368988efbf9c77008e3b030a03a21421a96b595584481b15331",
+    "libxcursor1_1.1.15-2_armhf.deb": "c7ac382c659528b58c053a0c552d5cc9f26aded0caf2e2e3fcd602d978380fe4",
+    "libxdamage1_1.1.4-3+b3_armhf.deb": "51339efb637c4a3bf800ed0e605158e330732cd01c9ff6b8de94f2edc5bc9b29",
+    "libxdmcp-dev_1.1.2-3_armhf.deb": "0b6ec5b04e8118c3d664e7dd7b7efe884a7c116776aa98b89bf6ce648996ea11",
+    "libxdmcp6_1.1.2-3_armhf.deb": "c871d428ca79b15b31967a8e2f19507f06c4af10bcc29429a903a175634af6e4",
+    "libxerces-c3.2_3.2.2+debian-1+b1_armhf.deb": "df1a22c853bf85b6e9afa79751860c57280406d8b40a098ac3bc8f66eceb3255",
+    "libxext6_1.3.3-1+b2_armhf.deb": "4cff4cba6aae865ca4d5e72061d51c16c87985de0232751afce0d05909c755cc",
+    "libxfixes3_5.0.3-1_armhf.deb": "92ee46160bc288442c8e8cd7e0eb2a4dd24e69904333f49371b703af8a9e1b94",
+    "libxft2_2.3.2-2_armhf.deb": "502631a6a91f4a8fccbde895aeedcb518a54e11987f97d20866c325b2eeef220",
+    "libxi6_1.7.9-1_armhf.deb": "f03478e7a8bcf4c144e46d416fb01e74352bddb57a737f3ce78da308784f9639",
+    "libxinerama1_1.1.4-2_armhf.deb": "fb715bf6feefd3695dbaf963191673864a8f73976aa3f52f1197a551af66010e",
+    "libxkbcommon0_0.8.2-1_armhf.deb": "6a45884e50e7e7438e58b6c8387dfeed5f571b79cc8a3e9dc373ffcd6f4a76de",
+    "libxml2_2.9.4+dfsg1-7+b3_armhf.deb": "2629f83a6a430149ed091098e25e22884fb163df01a1f1a3a19765bd205b1a8b",
+    "libxpm4_3.5.12-1_armhf.deb": "f1a677cb3ef3b45e2262e326af71d939ff67dcd0fa3c7a6741706836900612fd",
+    "libxrandr2_1.5.1-1_armhf.deb": "5668f1bf32b9c1d3fe13a90ffb0a15aa5b6445029d24d1718865c08b08581d8a",
+    "libxrender1_0.9.10-1_armhf.deb": "82343e14e073be48577ae1c2c5f95886bc2dddf9a1966b77ba76a827a8e62e44",
+    "libxshmfence1_1.3-1_armhf.deb": "4c9c872c366037d4535e2b5749f34bae782e19171efec6eaaf8c14c9f2486225",
+    "libxss1_1.2.3-1_armhf.deb": "8ce41b86c573c234016450b188551001f7c7da606f090d865adde9c326e1cbc1",
+    "libxt6_1.1.5-1+b3_armhf.deb": "20e1bfa25f403a7014bb3c096a2140b5a6b4db0d370b30774965fc23bb7db122",
+    "libxvidcore4_1.3.5-1_armhf.deb": "caf1801fb13ee60bdc12235f5cd4138a5479b3769be598d29e1864dd7ffd5160",
+    "libxxf86vm1_1.1.4-1+b2_armhf.deb": "cbe30a767f4decb6203bc09661e877579a8adff99ccf73459c698ad0de8efce7",
+    "libzstd1_1.3.8+dfsg-3+rpi1_armhf.deb": "250e609240c682a90b85f2d90024acc63bd0b3f586699929246c1a5d4ba0458c",
+    "libzvbi-common_0.2.35-16_all.deb": "5eea3f86857626ce4371a8a70ba9ce89f1abfc47ed033d1de12ebc0c7d1dd3ea",
+    "libzvbi0_0.2.35-16_armhf.deb": "b8e412ce669fde535a3250714eda0a446c6791771bb6360f93f676efa3d6376d",
+    "lsb-base_10.2019051400+rpi1_all.deb": "b3e203037786d00dd83a5fa9412c8395090921d373e914cb166b395ee2aedaa4",
+    "mariadb-common_10.3.22-0+deb10u1_all.deb": "54fb0fcefe4c0e74e6ad57cea0f47d5b585c2e9597423d8f0205aee8b0982975",
+    "mime-support_3.62_all.deb": "776efd686af26fa26325450280e3305463b1faef75d82b383bb00da61893d8ca",
+    "multiarch-support_2.28-10+rpi1_armhf.deb": "f322ddda60acb6d026fa94e4378e912fbe5e5adb828bb3659b1a6c5e6465f00c",
+    "mysql-common_5.8+1.0.5_all.deb": "340c68aaf03b9c4372467a907575b6a7c980c6d31f90f1d6abc6707a0630608a",
+    "ocl-icd-libopencl1_2.2.12-2_armhf.deb": "634dd778eb0a073609a773b4af463999be6c77b7a757b270ba2759d52e28f16d",
+    "odbcinst1debian2_2.3.6-0.1_armhf.deb": "a2fa334961f985d37602f2eb8ec2a69338897a8e0cba6438b55d365e06624f4c",
+    "odbcinst_2.3.6-0.1_armhf.deb": "81f2678332309805a18b7120dca0c0d76e29ba4e67cca1a629c100893d65a19c",
+    "passwd_4.5-1.1_armhf.deb": "beae91f59bddfe2ca8bf99a70131263d120ada1bdee6d1b3bb46cf96093c44b3",
+    "perl_5.28.1-6_armhf.deb": "464d3c3c46d40e18ebb233106d83a1855931b01b02bd761e72217b161e87ec48",
+    "pkg-config_0.29-6_armhf.deb": "cd1b397b846e4a8b815be6a8e1edbf9a3f509b924030a008c07f2fa3ddd20911",
+    "proj-data_5.2.0-1_all.deb": "fa7126aa00742ccf75f0e9867b54ea70f733436b97f600bec39408c5d3c54bd2",
+    "python3-distutils_3.7.3-1_all.deb": "6918af11061d3141990e78f5ad0530ec0f9a188cac27113d9de2896203efc13f",
+    "python3-lib2to3_3.7.3-1_all.deb": "227e2a2d12922c00dee9e55d8c5b889cfc5e72a54b85c2a509fa1664c2e9e137",
+    "python3-minimal_3.7.3-1_armhf.deb": "8766848b4fec22c1e89a778f29cd5fb6e247c57442a0db65623a85d8be5d25df",
+    "python3.7-minimal_3.7.3-2+deb10u1_armhf.deb": "c467edb5f3d35d94a2c47b9df6ae07e1a1f3d9b7e0022670978417bdba9f597f",
+    "python3.7_3.7.3-2+deb10u1_armhf.deb": "fba9f6b9e93ae7ae037ad6f834651db1a22fd9d74bbdf2be5d61cee25bd32ba8",
+    "python3_3.7.3-1_armhf.deb": "ea96636f2c722bbf9d0cbc3aa3d884ca043fa8196e3e84a490ae867d3750efa4",
+    "raspberrypi-bootloader_1.20200212-1_armhf.deb": "dfcfe58456603f6be487ed49737b54254d235e5c8b99890b3e76fcc07cda3bef",
+    "readline-common_7.0-5_all.deb": "153d8a5ddb04044d10f877a8955d944612ec9035f4c73eec99d85a92c3816712",
+    "sensible-utils_0.0.12_all.deb": "2043859f8bf39a20d075bf52206549f90dcabd66665bb9d6837273494fc6a598",
+    "shared-mime-info_1.10-1_armhf.deb": "9cc1069b361b8c229b4e2afa4c5b7014e0258cca867204f2b9d4735cb7941e68",
+    "ttf-bitstream-vera_1.10-8_all.deb": "328def7f581bf94b3b06d21e641f3e5df9a9b2e84e93b4206bc952fe8e80f38a",
+    "tzdata_2019c-0+deb10u1_all.deb": "59b295b0e669af26d494ed2803eb9011408f768a77395db2573e67c388e8a509",
+    "ucf_3.0038+nmu1_all.deb": "d02a82455faab988a52121f37d97c528a4f967ed75e9398e1d8db571398c12f9",
+    "uuid-dev_2.33.1-0.1_armhf.deb": "76bd2830e577411840c79cf28560eb8041ddafc1f157d2cb9bde71ed1fbfbb33",
+    "x11-common_7.7+19_all.deb": "221b2e71e0e98b8cafa4fbc674b3fbe293db031c51d35570a3c8cdfb02a5a155",
+    "x11proto-core-dev_2018.4-4_all.deb": "8bdb72e48cac24f5a6b284fea4d2bd6cb11cbe5fba2345ce57d8017ac40243cb",
+    "x11proto-dev_2018.4-4_all.deb": "aa0237467fcb5ccabf6a93fc19fae4d76d8c6dfbf9e449edda5f6393e50d8674",
+    "x11proto-input-dev_2018.4-4_all.deb": "364edce8bb7cf3187c1e81d37ea5b8f6b6ddd3a74c4c82efa1810dd451bbddbf",
+    "x11proto-kb-dev_2018.4-4_all.deb": "14df9b61bf65d8cb8b6053c2bc8f993454e8076d2a5ebc4e8d2bfe671c0592e3",
+    "xkb-data_2.26-2_all.deb": "17d21564c940dd8d89e0a1b69d6fea0144d057e4698902378f5c83500612b779",
+    "xorg-sgml-doctools_1.11-1_all.deb": "359dc76bf7b19fbbdb0b9e3ca3077e415b5b9ca8ff85162ccc889f9974493600",
+    "xtrans-dev_1.3.5-1_all.deb": "cadb720a2f8f0a2b2ad2dd82172d59e105e37885cedb3a93f6de9c78478c72d0",
+    "zlib1g_1.2.11.dfsg-1_armhf.deb": "c4e5a709233034080b5f8ec7b73c83f26fc8e921326e926aef4a22a6d07415ac",
+    "zlib1g-dev_1.2.11.dfsg-1_armhf.deb": "51561e557bd16f56e1e28b276184f0a6d82617afce987fc8d5322369ab0da478",
+}
diff --git a/frc971/analysis/plot_configs/turret_plot.pb b/frc971/analysis/plot_configs/turret_plot.pb
new file mode 100644
index 0000000..8aee8d7
--- /dev/null
+++ b/frc971/analysis/plot_configs/turret_plot.pb
@@ -0,0 +1,128 @@
+channel {
+  name: "/aos/roborio"
+  type: "aos.JoystickState"
+  alias: "JoystickState"
+}
+channel {
+  name: "/superstructure"
+  type: "y2020.control_loops.superstructure.Status"
+  alias: "Status"
+}
+channel {
+  name: "/superstructure"
+  type: "y2020.control_loops.superstructure.Output"
+  alias: "Output"
+}
+channel {
+  name: "/superstructure"
+  type: "y2020.control_loops.superstructure.Position"
+  alias: "Position"
+}
+channel {
+  name: "/superstructure"
+  type: "y2020.control_loops.superstructure.Goal"
+  alias: "Goal"
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.goal_velocity"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.unprofiled_goal_velocity"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "aimer.turret_velocity"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.velocity"
+      }
+    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.goal_position"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.unprofiled_goal_position"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "aimer.turret_position"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.position"
+      }
+    }
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "aimer.aiming_for_inner_port"
+      }
+    }
+#    line {
+#      y_signal {
+#        channel: "JoystickState"
+#        field: "alliance"
+#      }
+#    }
+  }
+  axes {
+    line {
+      y_signal {
+        channel: "Status"
+        field: "aimer.shot_distance"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "aimer.target_distance"
+      }
+    }
+  }
+}
+
+figure {
+  axes {
+    line {
+      y_signal {
+        channel: "Output"
+        field: "turret_voltage"
+      }
+    }
+    line {
+      y_signal {
+        channel: "Status"
+        field: "turret.voltage_error"
+      }
+    }
+  }
+}
diff --git a/frc971/control_loops/drivetrain/BUILD b/frc971/control_loops/drivetrain/BUILD
index d223d13..8c81440 100644
--- a/frc971/control_loops/drivetrain/BUILD
+++ b/frc971/control_loops/drivetrain/BUILD
@@ -1,6 +1,6 @@
 package(default_visibility = ["//visibility:public"])
 
-load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_ts_library")
 load("//aos:config.bzl", "aos_config")
 load("//tools:environments.bzl", "mcu_cpus")
 load("//tools/build_rules:select.bzl", "compiler_select", "cpu_select")
@@ -35,6 +35,12 @@
     includes = ["//frc971/control_loops:control_loops_fbs_includes"],
 )
 
+flatbuffer_ts_library(
+    name = "drivetrain_status_ts_fbs",
+    srcs = ["drivetrain_status.fbs"],
+    includes = ["//frc971/control_loops:control_loops_fbs_includes"],
+)
+
 genrule(
     name = "drivetrain_goal_float_fbs_generated",
     srcs = ["drivetrain_goal.fbs"],
diff --git a/frc971/control_loops/drivetrain/splinedrivetrain.h b/frc971/control_loops/drivetrain/splinedrivetrain.h
index 051e71a..1ce0e31 100644
--- a/frc971/control_loops/drivetrain/splinedrivetrain.h
+++ b/frc971/control_loops/drivetrain/splinedrivetrain.h
@@ -135,6 +135,7 @@
 
   // TODO(alex): pull this out of dt_config.
   const ::Eigen::DiagonalMatrix<double, 5> Q =
+      0.2 *
       (::Eigen::DiagonalMatrix<double, 5>().diagonal()
            << 1.0 / ::std::pow(0.12, 2),
        1.0 / ::std::pow(0.12, 2), 1.0 / ::std::pow(0.1, 2),
diff --git a/frc971/control_loops/drivetrain/trajectory.cc b/frc971/control_loops/drivetrain/trajectory.cc
index 4da3dee..70ceb1a 100644
--- a/frc971/control_loops/drivetrain/trajectory.cc
+++ b/frc971/control_loops/drivetrain/trajectory.cc
@@ -538,15 +538,11 @@
     // collect more info about when this breaks down from logs.
     K = ::Eigen::Matrix<double, 2, 5>::Zero();
   }
-  ::Eigen::EigenSolver<::Eigen::Matrix<double, 5, 5>> eigensolver(A - B * K);
-  const auto eigenvalues = eigensolver.eigenvalues();
-  AOS_LOG(DEBUG,
-          "Eigenvalues: (%f + %fj), (%f + %fj), (%f + %fj), (%f + %fj), (%f + "
-          "%fj)\n",
-          eigenvalues(0).real(), eigenvalues(0).imag(), eigenvalues(1).real(),
-          eigenvalues(1).imag(), eigenvalues(2).real(), eigenvalues(2).imag(),
-          eigenvalues(3).real(), eigenvalues(3).imag(), eigenvalues(4).real(),
-          eigenvalues(4).imag());
+  if (VLOG_IS_ON(1)) {
+    ::Eigen::EigenSolver<::Eigen::Matrix<double, 5, 5>> eigensolver(A - B * K);
+    const auto eigenvalues = eigensolver.eigenvalues();
+    LOG(INFO) << "Eigenvalues: " << eigenvalues;
+  }
   return K;
 }
 
diff --git a/frc971/control_loops/python/control_loop.py b/frc971/control_loops/python/control_loop.py
index 9c36c90..81f7deb 100644
--- a/frc971/control_loops/python/control_loop.py
+++ b/frc971/control_loops/python/control_loop.py
@@ -5,20 +5,24 @@
 
 class Constant(object):
 
-    def __init__(self, name, formatt, value):
+    def __init__(self, name, formatt, value, comment=None):
         self.name = name
         self.formatt = formatt
         self.value = value
         self.formatToType = {}
         self.formatToType['%f'] = "double"
         self.formatToType['%d'] = "int"
+        if comment is None:
+            self.comment = ""
+        else:
+            self.comment = comment + "\n"
 
     def Render(self, loop_type):
         typestring = self.formatToType[self.formatt]
         if loop_type == 'float' and typestring == 'double':
             typestring = loop_type
-        return str("\nstatic constexpr %s %s = "+ self.formatt +";\n") % \
-            (typestring, self.name, self.value)
+        return str("\n%sstatic constexpr %s %s = "+ self.formatt +";\n") % \
+            (self.comment, typestring, self.name, self.value)
 
 
 class ControlLoopWriter(object):
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index ddc38ac..c186711 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -72,6 +72,8 @@
         self.inValue = None
         self.startSet = False
 
+        self.module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
+
     """set extents on images"""
 
     def reinit_extents(self):
@@ -320,35 +322,45 @@
             self.spline_edit = self.points.updates_for_mouse_move(
                 self.index_of_edit, self.spline_edit, self.x, self.y, difs)
 
+    def export_json(self, file_name):
+        self.path_to_export = os.path.join(self.module_path,
+                                           "spline_jsons/" + file_name)
+        if file_name[-5:] != ".json":
+            print("Error: Filename doesn't end in .json")
+        else:
+            # Will export to json file
+            self.mode = Mode.kEditing
+            exportList = [l.tolist() for l in self.points.getSplines()]
+            with open(self.path_to_export, mode='w') as points_file:
+                json.dump(exportList, points_file)
+
+    def import_json(self, file_name):
+        self.path_to_export = os.path.join(self.module_path,
+                                           "spline_jsons/" + file_name)
+        if file_name[-5:] != ".json":
+            print("Error: Filename doesn't end in .json")
+        else:
+            # import from json file
+            self.mode = Mode.kEditing
+            self.points.resetPoints()
+            self.points.resetSplines()
+            print("LOADING LOAD FROM " + file_name) # Load takes a few seconds
+            with open(self.path_to_export) as points_file:
+                self.points.setUpSplines(json.load(points_file))
+
+            self.points.update_lib_spline()
+            print("SPLINES LOADED")
+
     def do_key_press(self, event, file_name):
         keyval = Gdk.keyval_to_lower(event.keyval)
-        module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
-        self.path_to_export = os.path.join(module_path,
-                                           "spline_jsons/" + file_name)
         if keyval == Gdk.KEY_q:
             print("Found q key and exiting.")
             quit_main_loop()
-        file_name_end = file_name[-5:]
-        if file_name_end != ".json":
-            print("Error: Filename doesn't end in .json")
-        else:
-            if keyval == Gdk.KEY_e:
-                # Will export to json file
-                self.mode = Mode.kEditing
-                print('out to: ', self.path_to_export)
-                exportList = [l.tolist() for l in self.points.getSplines()]
-                with open(self.path_to_export, mode='w') as points_file:
-                    json.dump(exportList, points_file)
+        if keyval == Gdk.KEY_e:
+            export_json(file_name)
 
-            if keyval == Gdk.KEY_i:
-                # import from json file
-                self.mode = Mode.kEditing
-                self.points.resetPoints()
-                self.points.resetSplines()
-                with open(self.path_to_export) as points_file:
-                    self.points.setUpSplines(json.load(points_file))
-
-                self.points.update_lib_spline()
+        if keyval == Gdk.KEY_i:
+            import_json(file_name)
 
         if keyval == Gdk.KEY_p:
             self.mode = Mode.kPlacing
diff --git a/frc971/control_loops/python/spline_graph.py b/frc971/control_loops/python/spline_graph.py
index 94ee683..7885ec1 100755
--- a/frc971/control_loops/python/spline_graph.py
+++ b/frc971/control_loops/python/spline_graph.py
@@ -42,10 +42,13 @@
     def configure(self, event):
         self.drawing_area.window_shape = (event.width, event.height)
 
-    # handle submitting a constraint
-    def on_submit_click(self, widget):
-        self.drawing_area.inConstraint = int(self.constraint_box.get_text())
-        self.drawing_area.inValue = int(self.value_box.get_text())
+    def output_json_clicked(self, button):
+        print("OUTPUT JSON CLICKED")
+        self.drawing_area.export_json(self.file_name_box.get_text())
+
+    def input_json_clicked(self, button):
+        print("INPUT JSON CLICKED")
+        self.drawing_area.import_json(self.file_name_box.get_text())
 
     def __init__(self):
         Gtk.Window.__init__(self)
@@ -89,6 +92,17 @@
 
         container.put(self.file_name_box, 0, 0)
 
+        self.output_json = Gtk.Button.new_with_label("Output")
+        self.output_json.set_size_request(100, 40)
+        self.output_json.connect("clicked", self.output_json_clicked)
+
+        self.input_json = Gtk.Button.new_with_label("Import")
+        self.input_json.set_size_request(100, 40)
+        self.input_json.connect("clicked", self.input_json_clicked)
+
+        container.put(self.output_json, 210, 0)
+        container.put(self.input_json, 320, 0)
+
         self.show_all()
 
 
diff --git a/frc971/wpilib/sensor_reader.cc b/frc971/wpilib/sensor_reader.cc
index 0d970e8..2c5e563 100644
--- a/frc971/wpilib/sensor_reader.cc
+++ b/frc971/wpilib/sensor_reader.cc
@@ -118,8 +118,9 @@
   }
 
   if (pwm_trigger_) {
-    AOS_LOG(DEBUG, "PWM wakeup delta: %lld\n",
-            (monotonic_now - last_monotonic_now_).count());
+    // TODO(austin): Put this in a status message.
+    VLOG(1) << "PWM wakeup delta: "
+            << (monotonic_now - last_monotonic_now_).count();
     last_monotonic_now_ = monotonic_now;
 
     monotonic_clock::time_point last_tick_timepoint = GetPWMStartTime();
diff --git a/frc971/zeroing/BUILD b/frc971/zeroing/BUILD
index 75d610a..5955f8b 100644
--- a/frc971/zeroing/BUILD
+++ b/frc971/zeroing/BUILD
@@ -59,9 +59,18 @@
 cc_library(
     name = "zeroing",
     srcs = [
-        "zeroing.cc",
+        "absolute_encoder.cc",
+        "hall_effect_and_position.cc",
+        "pot_and_absolute_encoder.cc",
+        "pot_and_index.cc",
+        "pulse_index.cc",
     ],
     hdrs = [
+        "absolute_encoder.h",
+        "hall_effect_and_position.h",
+        "pot_and_absolute_encoder.h",
+        "pot_and_index.h",
+        "pulse_index.h",
         "zeroing.h",
     ],
     deps = [
@@ -76,11 +85,16 @@
 cc_test(
     name = "zeroing_test",
     srcs = [
-        "zeroing_test.cc",
+        "absolute_encoder_test.cc",
+        "hall_effect_and_position_test.cc",
+        "pot_and_absolute_encoder_test.cc",
+        "pot_and_index_test.cc",
+        "pulse_index_test.cc",
+        "relative_encoder_test.cc",
+        "zeroing_test.h",
     ],
     deps = [
         ":zeroing",
-        "//aos:die",
         "//aos/testing:googletest",
         "//aos/testing:test_shm",
         "//frc971/control_loops:control_loops_fbs",
diff --git a/frc971/zeroing/absolute_encoder.cc b/frc971/zeroing/absolute_encoder.cc
new file mode 100644
index 0000000..ffdf9da
--- /dev/null
+++ b/frc971/zeroing/absolute_encoder.cc
@@ -0,0 +1,178 @@
+#include "frc971/zeroing/absolute_encoder.h"
+
+#include <cmath>
+#include <numeric>
+
+#include "glog/logging.h"
+
+#include "frc971/zeroing/wrap.h"
+
+namespace frc971 {
+namespace zeroing {
+
+AbsoluteEncoderZeroingEstimator::AbsoluteEncoderZeroingEstimator(
+    const constants::AbsoluteEncoderZeroingConstants &constants)
+    : constants_(constants), move_detector_(constants_.moving_buffer_size) {
+  relative_to_absolute_offset_samples_.reserve(constants_.average_filter_size);
+  Reset();
+}
+
+void AbsoluteEncoderZeroingEstimator::Reset() {
+  zeroed_ = false;
+  error_ = false;
+  first_offset_ = 0.0;
+  offset_ = 0.0;
+  samples_idx_ = 0;
+  position_ = 0.0;
+  nan_samples_ = 0;
+  relative_to_absolute_offset_samples_.clear();
+  move_detector_.Reset();
+}
+
+
+// The math here is a bit backwards, but I think it'll be less error prone that
+// way and more similar to the version with a pot as well.
+//
+// We start by unwrapping the absolute encoder using the relative encoder.  This
+// puts us in a non-wrapping space and lets us average a bit easier.  From
+// there, we can compute an offset and wrap ourselves back such that we stay
+// close to the middle value.
+//
+// To guard against the robot moving while updating estimates, buffer a number
+// of samples and check that the buffered samples are not different than the
+// zeroing threshold. At any point that the samples differ too much, do not
+// update estimates based on those samples.
+void AbsoluteEncoderZeroingEstimator::UpdateEstimate(
+    const AbsolutePosition &info) {
+  // Check for Abs Encoder NaN value that would mess up the rest of the zeroing
+  // code below. NaN values are given when the Absolute Encoder is disconnected.
+  if (::std::isnan(info.absolute_encoder())) {
+    if (zeroed_) {
+      VLOG(1) << "NAN on absolute encoder.";
+      error_ = true;
+    } else {
+      ++nan_samples_;
+      VLOG(1) << "NAN on absolute encoder while zeroing " << nan_samples_;
+      if (nan_samples_ >= constants_.average_filter_size) {
+        error_ = true;
+        zeroed_ = true;
+      }
+    }
+    // Throw some dummy values in for now.
+    filtered_absolute_encoder_ = info.absolute_encoder();
+    position_ = offset_ + info.encoder();
+    return;
+  }
+
+  const bool moving = move_detector_.Update(info, constants_.moving_buffer_size,
+                                            constants_.zeroing_threshold);
+
+  if (!moving) {
+    const PositionStruct &sample = move_detector_.GetSample();
+
+    // Compute the average offset between the absolute encoder and relative
+    // encoder.  If we have 0 samples, assume it is 0.
+    double average_relative_to_absolute_offset =
+        relative_to_absolute_offset_samples_.size() == 0
+            ? 0.0
+            : ::std::accumulate(relative_to_absolute_offset_samples_.begin(),
+                                relative_to_absolute_offset_samples_.end(),
+                                0.0) /
+                  relative_to_absolute_offset_samples_.size();
+
+    // Now, compute the estimated absolute position using the previously
+    // estimated offset and the incremental encoder.
+    const double adjusted_incremental_encoder =
+        sample.encoder + average_relative_to_absolute_offset;
+
+    // Now, compute the absolute encoder value nearest to the offset relative
+    // encoder position.
+    const double adjusted_absolute_encoder =
+        UnWrap(adjusted_incremental_encoder,
+               sample.absolute_encoder - constants_.measured_absolute_position,
+               constants_.one_revolution_distance);
+
+    // We can now compute the offset now that we have unwrapped the absolute
+    // encoder.
+    const double relative_to_absolute_offset =
+        adjusted_absolute_encoder - sample.encoder;
+
+    // Add the sample and update the average with the new reading.
+    const size_t relative_to_absolute_offset_samples_size =
+        relative_to_absolute_offset_samples_.size();
+    if (relative_to_absolute_offset_samples_size <
+        constants_.average_filter_size) {
+      average_relative_to_absolute_offset =
+          (average_relative_to_absolute_offset *
+               relative_to_absolute_offset_samples_size +
+           relative_to_absolute_offset) /
+          (relative_to_absolute_offset_samples_size + 1);
+
+      relative_to_absolute_offset_samples_.push_back(
+          relative_to_absolute_offset);
+    } else {
+      average_relative_to_absolute_offset -=
+          relative_to_absolute_offset_samples_[samples_idx_] /
+          relative_to_absolute_offset_samples_size;
+      relative_to_absolute_offset_samples_[samples_idx_] =
+          relative_to_absolute_offset;
+      average_relative_to_absolute_offset +=
+          relative_to_absolute_offset /
+          relative_to_absolute_offset_samples_size;
+    }
+
+    // Drop the oldest sample when we run this function the next time around.
+    samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
+
+    // And our offset is the offset that gives us the position within +- ord/2
+    // of the middle position.
+    offset_ = Wrap(constants_.middle_position,
+                   average_relative_to_absolute_offset + sample.encoder,
+                   constants_.one_revolution_distance) -
+              sample.encoder;
+
+    // Reverse the math for adjusted_absolute_encoder to compute the absolute
+    // encoder. Do this by taking the adjusted encoder, and then subtracting off
+    // the second argument above, and the value that was added by Wrap.
+    filtered_absolute_encoder_ =
+        ((sample.encoder + average_relative_to_absolute_offset) -
+         (-constants_.measured_absolute_position +
+          (adjusted_absolute_encoder -
+           (sample.absolute_encoder - constants_.measured_absolute_position))));
+
+    if (offset_ready()) {
+      if (!zeroed_) {
+        first_offset_ = offset_;
+      }
+
+      if (::std::abs(first_offset_ - offset_) >
+          constants_.allowable_encoder_error *
+              constants_.one_revolution_distance) {
+        VLOG(1) << "Offset moved too far. Initial: " << first_offset_
+                << ", current " << offset_ << ", allowable change: "
+                << constants_.allowable_encoder_error *
+                       constants_.one_revolution_distance;
+        error_ = true;
+      }
+
+      zeroed_ = true;
+    }
+  }
+
+  // Update the position.
+  position_ = offset_ + info.encoder();
+}
+
+flatbuffers::Offset<AbsoluteEncoderZeroingEstimator::State>
+AbsoluteEncoderZeroingEstimator::GetEstimatorState(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  State::Builder builder(*fbb);
+  builder.add_error(error_);
+  builder.add_zeroed(zeroed_);
+  builder.add_position(position_);
+  builder.add_absolute_position(filtered_absolute_encoder_);
+  return builder.Finish();
+}
+
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/absolute_encoder.h b/frc971/zeroing/absolute_encoder.h
new file mode 100644
index 0000000..0021e13
--- /dev/null
+++ b/frc971/zeroing/absolute_encoder.h
@@ -0,0 +1,93 @@
+#ifndef FRC971_ZEROING_ABSOLUTE_ENCODER_H_
+#define FRC971_ZEROING_ABSOLUTE_ENCODER_H_
+
+#include <vector>
+
+#include "flatbuffers/flatbuffers.h"
+
+#include "frc971/zeroing/zeroing.h"
+
+namespace frc971 {
+namespace zeroing {
+
+// Estimates the position with an absolute encoder which also reports
+// incremental counts.  The absolute encoder can't spin more than one
+// revolution.
+class AbsoluteEncoderZeroingEstimator
+    : public ZeroingEstimator<AbsolutePosition,
+                              constants::AbsoluteEncoderZeroingConstants,
+                              AbsoluteEncoderEstimatorState> {
+ public:
+  explicit AbsoluteEncoderZeroingEstimator(
+      const constants::AbsoluteEncoderZeroingConstants &constants);
+
+  // Resets the internal logic so it needs to be re-zeroed.
+  void Reset() override;
+
+  // Updates the sensor values for the zeroing logic.
+  void UpdateEstimate(const AbsolutePosition &info) override;
+
+  void TriggerError() override { error_ = true; }
+
+  bool zeroed() const override { return zeroed_; }
+
+  double offset() const override { return offset_; }
+
+  bool error() const override { return error_; }
+
+  // Returns true if the sample buffer is full.
+  bool offset_ready() const override {
+    return relative_to_absolute_offset_samples_.size() ==
+           constants_.average_filter_size;
+  }
+
+  // Returns information about our current state.
+  virtual flatbuffers::Offset<State> GetEstimatorState(
+      flatbuffers::FlatBufferBuilder *fbb) const override;
+
+ private:
+  struct PositionStruct {
+    PositionStruct(const AbsolutePosition &position_buffer)
+        : absolute_encoder(position_buffer.absolute_encoder()),
+          encoder(position_buffer.encoder()) {}
+    double absolute_encoder;
+    double encoder;
+  };
+
+  // The zeroing constants used to describe the configuration of the system.
+  const constants::AbsoluteEncoderZeroingConstants constants_;
+
+  // True if the mechanism is zeroed.
+  bool zeroed_;
+  // Marker to track whether an error has occurred.
+  bool error_;
+  // The first valid offset we recorded. This is only set after zeroed_ first
+  // changes to true.
+  double first_offset_;
+
+  // The filtered absolute encoder.  This is used in the status for calibration.
+  double filtered_absolute_encoder_ = 0.0;
+
+  // Samples of the offset needed to line the relative encoder up with the
+  // absolute encoder.
+  ::std::vector<double> relative_to_absolute_offset_samples_;
+
+  MoveDetector<PositionStruct, AbsolutePosition> move_detector_;
+
+  // Estimated start position of the mechanism
+  double offset_ = 0;
+  // The next position in 'relative_to_absolute_offset_samples_' and
+  // 'encoder_samples_' to be used to store the next sample.
+  int samples_idx_ = 0;
+
+  // Number of NANs we've seen in a row.
+  size_t nan_samples_ = 0;
+
+  // The filtered position.
+  double position_ = 0.0;
+};
+
+}  // namespace zeroing
+}  // namespace frc971
+
+#endif  // FRC971_ZEROING_ABSOLUTE_ENCODER_H_
diff --git a/frc971/zeroing/absolute_encoder_test.cc b/frc971/zeroing/absolute_encoder_test.cc
new file mode 100644
index 0000000..38ce069
--- /dev/null
+++ b/frc971/zeroing/absolute_encoder_test.cc
@@ -0,0 +1,145 @@
+#include "frc971/zeroing/absolute_encoder.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+using constants::AbsoluteEncoderZeroingConstants;
+
+class AbsoluteEncoderZeroingTest : public ZeroingTest {
+ protected:
+  void MoveTo(PositionSensorSimulator *simulator,
+              AbsoluteEncoderZeroingEstimator *estimator, double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<AbsolutePosition>(&fbb));
+  }
+};
+
+// Makes sure that using an absolute encoder lets us zero without moving.
+TEST_F(AbsoluteEncoderZeroingTest, TestAbsoluteEncoderZeroingWithoutMovement) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double kMiddlePosition = 2.5;
+  const double start_pos = 2.1;
+  double measured_absolute_position = 0.3 * index_diff;
+
+  AbsoluteEncoderZeroingConstants constants{
+      kSampleSize,        index_diff, measured_absolute_position,
+      kMiddlePosition,    0.1,        kMovingBufferSize,
+      kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  AbsoluteEncoderZeroingEstimator estimator(constants);
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_TRUE(estimator.zeroed());
+  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
+}
+
+// Makes sure that we ignore a NAN if we get it, but will correctly zero
+// afterwards.
+TEST_F(AbsoluteEncoderZeroingTest, TestAbsoluteEncoderZeroingIgnoresNAN) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double start_pos = 2.1;
+  double measured_absolute_position = 0.3 * index_diff;
+  const double kMiddlePosition = 2.5;
+
+  AbsoluteEncoderZeroingConstants constants{
+      kSampleSize,        index_diff, measured_absolute_position,
+      kMiddlePosition,    0.1,        kMovingBufferSize,
+      kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  AbsoluteEncoderZeroingEstimator estimator(constants);
+
+  // We tolerate a couple NANs before we start.
+  FBB fbb;
+  fbb.Finish(CreateAbsolutePosition(
+      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN()));
+  const auto sensor_values =
+      flatbuffers::GetRoot<AbsolutePosition>(fbb.GetBufferPointer());
+  for (size_t i = 0; i < kSampleSize - 1; ++i) {
+    estimator.UpdateEstimate(*sensor_values);
+  }
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_TRUE(estimator.zeroed());
+  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
+}
+
+// Makes sure that using an absolute encoder doesn't let us zero while moving.
+TEST_F(AbsoluteEncoderZeroingTest, TestAbsoluteEncoderZeroingWithMovement) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double start_pos = 10 * index_diff;
+  double measured_absolute_position = 0.3 * index_diff;
+  const double kMiddlePosition = 2.5;
+
+  AbsoluteEncoderZeroingConstants constants{
+      kSampleSize,        index_diff, measured_absolute_position,
+      kMiddlePosition,    0.1,        kMovingBufferSize,
+      kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  AbsoluteEncoderZeroingEstimator estimator(constants);
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos + i * index_diff);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+  MoveTo(&sim, &estimator, start_pos + 10 * index_diff);
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_FALSE(estimator.zeroed());
+}
+
+// Makes sure we detect an error if the ZeroingEstimator gets sent a NaN.
+TEST_F(AbsoluteEncoderZeroingTest, TestAbsoluteEncoderZeroingWithNaN) {
+  AbsoluteEncoderZeroingConstants constants{
+      kSampleSize, 1, 0.3, 1.0, 0.1, kMovingBufferSize, kIndexErrorFraction};
+
+  AbsoluteEncoderZeroingEstimator estimator(constants);
+
+  FBB fbb;
+  fbb.Finish(CreateAbsolutePosition(
+      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN()));
+  const auto sensor_values =
+      flatbuffers::GetRoot<AbsolutePosition>(fbb.GetBufferPointer());
+  for (size_t i = 0; i < kSampleSize - 1; ++i) {
+    estimator.UpdateEstimate(*sensor_values);
+  }
+  ASSERT_FALSE(estimator.error());
+
+  estimator.UpdateEstimate(*sensor_values);
+  ASSERT_TRUE(estimator.error());
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/hall_effect_and_position.cc b/frc971/zeroing/hall_effect_and_position.cc
new file mode 100644
index 0000000..e074668
--- /dev/null
+++ b/frc971/zeroing/hall_effect_and_position.cc
@@ -0,0 +1,127 @@
+#include "frc971/zeroing/hall_effect_and_position.h"
+
+#include <algorithm>
+#include <cmath>
+#include <limits>
+
+#include "glog/logging.h"
+
+namespace frc971 {
+namespace zeroing {
+
+HallEffectAndPositionZeroingEstimator::HallEffectAndPositionZeroingEstimator(
+    const ZeroingConstants &constants)
+    : constants_(constants) {
+  Reset();
+}
+
+void HallEffectAndPositionZeroingEstimator::Reset() {
+  offset_ = 0.0;
+  min_low_position_ = ::std::numeric_limits<double>::max();
+  max_low_position_ = ::std::numeric_limits<double>::lowest();
+  zeroed_ = false;
+  initialized_ = false;
+  last_used_posedge_count_ = 0;
+  cycles_high_ = 0;
+  high_long_enough_ = false;
+  first_start_pos_ = 0.0;
+  error_ = false;
+  current_ = 0.0;
+  first_start_pos_ = 0.0;
+}
+
+void HallEffectAndPositionZeroingEstimator::TriggerError() {
+  if (!error_) {
+    VLOG(1) << "Manually triggered zeroing error.\n";
+    error_ = true;
+  }
+}
+
+void HallEffectAndPositionZeroingEstimator::StoreEncoderMaxAndMin(
+    const HallEffectAndPosition &info) {
+  // If we have a new posedge.
+  if (!info.current()) {
+    if (last_hall_) {
+      min_low_position_ = max_low_position_ = info.encoder();
+    } else {
+      min_low_position_ = ::std::min(min_low_position_, info.encoder());
+      max_low_position_ = ::std::max(max_low_position_, info.encoder());
+    }
+  }
+  last_hall_ = info.current();
+}
+
+void HallEffectAndPositionZeroingEstimator::UpdateEstimate(
+    const HallEffectAndPosition &info) {
+  // We want to make sure that we encounter at least one posedge while zeroing.
+  // So we take the posedge count from the first sample after reset and wait for
+  // that count to change and for the hall effect to stay high before we
+  // consider ourselves zeroed.
+  if (!initialized_) {
+    last_used_posedge_count_ = info.posedge_count();
+    initialized_ = true;
+    last_hall_ = info.current();
+  }
+
+  StoreEncoderMaxAndMin(info);
+
+  if (info.current()) {
+    cycles_high_++;
+  } else {
+    cycles_high_ = 0;
+    last_used_posedge_count_ = info.posedge_count();
+  }
+
+  high_long_enough_ = cycles_high_ >= constants_.hall_trigger_zeroing_length;
+
+  bool moving_backward = false;
+  if (constants_.zeroing_move_direction) {
+    moving_backward = info.encoder() > min_low_position_;
+  } else {
+    moving_backward = info.encoder() < max_low_position_;
+  }
+
+  // If there are no posedges to use or we don't have enough samples yet to
+  // have a well-filtered starting position then we use the filtered value as
+  // our best guess.
+  if (last_used_posedge_count_ != info.posedge_count() && high_long_enough_ &&
+      moving_backward) {
+    // Note the offset and the current posedge count so that we only run this
+    // logic once per posedge. That should be more resilient to corrupted
+    // intermediate data.
+    offset_ = -info.posedge_value();
+    if (constants_.zeroing_move_direction) {
+      offset_ += constants_.lower_hall_position;
+    } else {
+      offset_ += constants_.upper_hall_position;
+    }
+    last_used_posedge_count_ = info.posedge_count();
+
+    // Save the first starting position.
+    if (!zeroed_) {
+      first_start_pos_ = offset_;
+      VLOG(2) << "latching start position" << first_start_pos_;
+    }
+
+    // Now that we have an accurate starting position we can consider ourselves
+    // zeroed.
+    zeroed_ = true;
+  }
+
+  position_ = info.encoder() - offset_;
+}
+
+flatbuffers::Offset<HallEffectAndPositionZeroingEstimator::State>
+HallEffectAndPositionZeroingEstimator::GetEstimatorState(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  State::Builder builder(*fbb);
+  builder.add_error(error_);
+  builder.add_zeroed(zeroed_);
+  builder.add_encoder(position_);
+  builder.add_high_long_enough(high_long_enough_);
+  builder.add_offset(offset_);
+  return builder.Finish();
+}
+
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/hall_effect_and_position.h b/frc971/zeroing/hall_effect_and_position.h
new file mode 100644
index 0000000..f03a442
--- /dev/null
+++ b/frc971/zeroing/hall_effect_and_position.h
@@ -0,0 +1,90 @@
+#ifndef FRC971_ZEROING_HALL_EFFECT_AND_POSITION_H_
+#define FRC971_ZEROING_HALL_EFFECT_AND_POSITION_H_
+
+#include "flatbuffers/flatbuffers.h"
+
+#include "frc971/zeroing/zeroing.h"
+
+namespace frc971 {
+namespace zeroing {
+
+// Estimates the position with an incremental encoder and a hall effect sensor.
+class HallEffectAndPositionZeroingEstimator
+    : public ZeroingEstimator<HallEffectAndPosition,
+                              constants::HallEffectZeroingConstants,
+                              HallEffectAndPositionEstimatorState> {
+ public:
+  explicit HallEffectAndPositionZeroingEstimator(
+      const ZeroingConstants &constants);
+
+  // Update the internal logic with the next sensor values.
+  void UpdateEstimate(const Position &info) override;
+
+  // Reset the internal logic so it needs to be re-zeroed.
+  void Reset() override;
+
+  // Manually trigger an internal error. This is used for testing the error
+  // logic.
+  void TriggerError() override;
+
+  bool error() const override { return error_; }
+
+  bool zeroed() const override { return zeroed_; }
+
+  double offset() const override { return offset_; }
+
+  bool offset_ready() const override { return zeroed_; }
+
+  // Returns information about our current state.
+  virtual flatbuffers::Offset<State> GetEstimatorState(
+      flatbuffers::FlatBufferBuilder *fbb) const override;
+
+ private:
+  // Sets the minimum and maximum posedge position values.
+  void StoreEncoderMaxAndMin(const HallEffectAndPosition &info);
+
+  // The zeroing constants used to describe the configuration of the system.
+  const ZeroingConstants constants_;
+
+  // The estimated state of the hall effect.
+  double current_ = 0.0;
+  // The estimated position.
+  double position_ = 0.0;
+  // The smallest and largest positions of the last set of encoder positions
+  // while the hall effect was low.
+  double min_low_position_;
+  double max_low_position_;
+  // If we've seen the hall effect high for enough times without going low, then
+  // we can be sure it isn't a false positive.
+  bool high_long_enough_;
+  size_t cycles_high_;
+
+  bool last_hall_ = false;
+
+  // The estimated starting position of the mechanism. We also call this the
+  // 'offset' in some contexts.
+  double offset_;
+  // Flag for triggering logic that takes note of the current posedge count
+  // after a reset. See `last_used_posedge_count_'.
+  bool initialized_;
+  // After a reset we keep track of the posedge count with this. Only after the
+  // posedge count changes (i.e. increments at least once or wraps around) will
+  // we consider the mechanism zeroed. We also use this to store the most recent
+  // `HallEffectAndPosition::posedge_count' value when the start position
+  // was calculated. It helps us calculate the start position only on posedges
+  // to reject corrupted intermediate data.
+  int32_t last_used_posedge_count_;
+  // Marker to track whether we're fully zeroed yet or not.
+  bool zeroed_;
+  // Marker to track whether an error has occurred. This gets reset to false
+  // whenever Reset() is called.
+  bool error_ = false;
+  // Stores the position "start_pos" variable the first time the program
+  // is zeroed.
+  double first_start_pos_;
+};
+
+}  // namespace zeroing
+}  // namespace frc971
+
+#endif  // FRC971_ZEROING_HALL_EFFECT_AND_POSITION_H_
diff --git a/frc971/zeroing/hall_effect_and_position_test.cc b/frc971/zeroing/hall_effect_and_position_test.cc
new file mode 100644
index 0000000..c9ddf65
--- /dev/null
+++ b/frc971/zeroing/hall_effect_and_position_test.cc
@@ -0,0 +1,135 @@
+#include "frc971/zeroing/hall_effect_and_position.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+class HallEffectAndPositionZeroingTest : public ZeroingTest {
+ protected:
+  // The starting position of the system.
+  static constexpr double kStartPosition = 2.0;
+
+  HallEffectAndPositionZeroingTest()
+      : constants_(MakeConstants()), sim_(constants_.index_difference) {
+    // Start the system out at the starting position.
+    sim_.InitializeHallEffectAndPosition(kStartPosition,
+                                         constants_.lower_hall_position,
+                                         constants_.upper_hall_position);
+  }
+
+  void MoveTo(PositionSensorSimulator *simulator,
+              HallEffectAndPositionZeroingEstimator *estimator,
+              double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<HallEffectAndPosition>(&fbb));
+  }
+
+  // Returns a reasonable set of test constants.
+  static constants::HallEffectZeroingConstants MakeConstants() {
+    constants::HallEffectZeroingConstants constants;
+    constants.lower_hall_position = 0.25;
+    constants.upper_hall_position = 0.75;
+    constants.index_difference = 1.0;
+    constants.hall_trigger_zeroing_length = 2;
+    constants.zeroing_move_direction = false;
+    return constants;
+  }
+
+  // Constants, and the simulation using them.
+  const constants::HallEffectZeroingConstants constants_;
+  PositionSensorSimulator sim_;
+};
+
+// Tests that an error is detected when the starting position changes too much.
+TEST_F(HallEffectAndPositionZeroingTest, TestHallEffectZeroing) {
+  HallEffectAndPositionZeroingEstimator estimator(constants_);
+
+  // Should not be zeroed when we stand still.
+  for (int i = 0; i < 300; ++i) {
+    MoveTo(&sim_, &estimator, kStartPosition);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim_, &estimator, 1.9);
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Move to where the hall effect is triggered and make sure it becomes zeroed.
+  MoveTo(&sim_, &estimator, 1.5);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 1.5);
+  ASSERT_TRUE(estimator.zeroed());
+
+  // Check that the offset is calculated correctly.  We should expect to read
+  // 0.5.  Since the encoder is reading -0.5 right now, the offset needs to be
+  // 1.
+  EXPECT_DOUBLE_EQ(1.0, estimator.offset());
+
+  // Make sure triggering errors works.
+  estimator.TriggerError();
+  ASSERT_TRUE(estimator.error());
+
+  // Ensure resetting resets the state of the estimator.
+  estimator.Reset();
+  ASSERT_FALSE(estimator.zeroed());
+  ASSERT_FALSE(estimator.error());
+}
+
+// Tests that we don't zero on a too short pulse.
+TEST_F(HallEffectAndPositionZeroingTest, TestTooShortPulse) {
+  HallEffectAndPositionZeroingEstimator estimator(constants_);
+
+  // Trigger for 1 cycle.
+  MoveTo(&sim_, &estimator, 0.9);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.5);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.9);
+  EXPECT_FALSE(estimator.zeroed());
+}
+
+// Tests that we don't zero when we go the wrong direction.
+TEST_F(HallEffectAndPositionZeroingTest, TestWrongDirectionNoZero) {
+  HallEffectAndPositionZeroingEstimator estimator(constants_);
+
+  // Pass through the sensor, lingering long enough that we should zero.
+  MoveTo(&sim_, &estimator, 0.0);
+  ASSERT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.4);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.6);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.7);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.9);
+  EXPECT_FALSE(estimator.zeroed());
+}
+
+// Make sure we don't zero if we start in the hall effect's range.
+TEST_F(HallEffectAndPositionZeroingTest, TestStartingOnNoZero) {
+  HallEffectAndPositionZeroingEstimator estimator(constants_);
+  MoveTo(&sim_, &estimator, 0.5);
+  estimator.Reset();
+
+  // Stay on the hall effect.  We shouldn't zero.
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.5);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.5);
+  EXPECT_FALSE(estimator.zeroed());
+
+  // Verify moving off the hall still doesn't zero us.
+  MoveTo(&sim_, &estimator, 0.0);
+  EXPECT_FALSE(estimator.zeroed());
+  MoveTo(&sim_, &estimator, 0.0);
+  EXPECT_FALSE(estimator.zeroed());
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pot_and_absolute_encoder.cc b/frc971/zeroing/pot_and_absolute_encoder.cc
new file mode 100644
index 0000000..200f399
--- /dev/null
+++ b/frc971/zeroing/pot_and_absolute_encoder.cc
@@ -0,0 +1,196 @@
+#include "frc971/zeroing/pot_and_absolute_encoder.h"
+
+#include <cmath>
+#include <numeric>
+
+#include "glog/logging.h"
+
+#include "frc971/zeroing/wrap.h"
+
+namespace frc971 {
+namespace zeroing {
+
+PotAndAbsoluteEncoderZeroingEstimator::PotAndAbsoluteEncoderZeroingEstimator(
+    const constants::PotAndAbsoluteEncoderZeroingConstants &constants)
+    : constants_(constants), move_detector_(constants_.moving_buffer_size) {
+  relative_to_absolute_offset_samples_.reserve(constants_.average_filter_size);
+  offset_samples_.reserve(constants_.average_filter_size);
+  Reset();
+}
+
+void PotAndAbsoluteEncoderZeroingEstimator::Reset() {
+  first_offset_ = 0.0;
+  pot_relative_encoder_offset_ = 0.0;
+  offset_ = 0.0;
+  samples_idx_ = 0;
+  filtered_position_ = 0.0;
+  position_ = 0.0;
+  zeroed_ = false;
+  nan_samples_ = 0;
+  relative_to_absolute_offset_samples_.clear();
+  offset_samples_.clear();
+  move_detector_.Reset();
+  error_ = false;
+}
+
+// So, this needs to be a multistep process.  We need to first estimate the
+// offset between the absolute encoder and the relative encoder.  That process
+// should get us an absolute number which is off by integer multiples of the
+// distance/rev.  In parallel, we can estimate the offset between the pot and
+// encoder.  When both estimates have converged, we can then compute the offset
+// in a cycle, and which cycle, which gives us the accurate global offset.
+//
+// It's tricky to compute the offset between the absolute and relative encoder.
+// We need to compute this inside 1 revolution.  The easiest way to do this
+// would be to wrap the encoder, subtract the two of them, and then average the
+// result.  That will struggle when they are off by PI.  Instead, we need to
+// wrap the number to +- PI from the current averaged offset.
+//
+// To guard against the robot moving while updating estimates, buffer a number
+// of samples and check that the buffered samples are not different than the
+// zeroing threshold. At any point that the samples differ too much, do not
+// update estimates based on those samples.
+void PotAndAbsoluteEncoderZeroingEstimator::UpdateEstimate(
+    const PotAndAbsolutePosition &info) {
+  // Check for Abs Encoder NaN value that would mess up the rest of the zeroing
+  // code below. NaN values are given when the Absolute Encoder is disconnected.
+  if (::std::isnan(info.absolute_encoder())) {
+    if (zeroed_) {
+      VLOG(1) << "NAN on absolute encoder.";
+      error_ = true;
+    } else {
+      ++nan_samples_;
+      VLOG(1) << "NAN on absolute encoder while zeroing" << nan_samples_;
+      if (nan_samples_ >= constants_.average_filter_size) {
+        error_ = true;
+        zeroed_ = true;
+      }
+    }
+    // Throw some dummy values in for now.
+    filtered_absolute_encoder_ = info.absolute_encoder();
+    filtered_position_ = pot_relative_encoder_offset_ + info.encoder();
+    position_ = offset_ + info.encoder();
+    return;
+  }
+
+  const bool moving = move_detector_.Update(info, constants_.moving_buffer_size,
+                                            constants_.zeroing_threshold);
+
+  if (!moving) {
+    const PositionStruct &sample = move_detector_.GetSample();
+
+    // Compute the average offset between the absolute encoder and relative
+    // encoder.  If we have 0 samples, assume it is 0.
+    double average_relative_to_absolute_offset =
+        relative_to_absolute_offset_samples_.size() == 0
+            ? 0.0
+            : ::std::accumulate(relative_to_absolute_offset_samples_.begin(),
+                                relative_to_absolute_offset_samples_.end(),
+                                0.0) /
+                  relative_to_absolute_offset_samples_.size();
+
+    const double adjusted_incremental_encoder =
+        sample.encoder + average_relative_to_absolute_offset;
+
+    // Now, compute the nearest absolute encoder value to the offset relative
+    // encoder position.
+    const double adjusted_absolute_encoder =
+        UnWrap(adjusted_incremental_encoder,
+               sample.absolute_encoder - constants_.measured_absolute_position,
+               constants_.one_revolution_distance);
+
+    // We can now compute the offset now that we have unwrapped the absolute
+    // encoder.
+    const double relative_to_absolute_offset =
+        adjusted_absolute_encoder - sample.encoder;
+
+    // Add the sample and update the average with the new reading.
+    const size_t relative_to_absolute_offset_samples_size =
+        relative_to_absolute_offset_samples_.size();
+    if (relative_to_absolute_offset_samples_size <
+        constants_.average_filter_size) {
+      average_relative_to_absolute_offset =
+          (average_relative_to_absolute_offset *
+               relative_to_absolute_offset_samples_size +
+           relative_to_absolute_offset) /
+          (relative_to_absolute_offset_samples_size + 1);
+
+      relative_to_absolute_offset_samples_.push_back(
+          relative_to_absolute_offset);
+    } else {
+      average_relative_to_absolute_offset -=
+          relative_to_absolute_offset_samples_[samples_idx_] /
+          relative_to_absolute_offset_samples_size;
+      relative_to_absolute_offset_samples_[samples_idx_] =
+          relative_to_absolute_offset;
+      average_relative_to_absolute_offset +=
+          relative_to_absolute_offset /
+          relative_to_absolute_offset_samples_size;
+    }
+
+    // Now compute the offset between the pot and relative encoder.
+    if (offset_samples_.size() < constants_.average_filter_size) {
+      offset_samples_.push_back(sample.pot - sample.encoder);
+    } else {
+      offset_samples_[samples_idx_] = sample.pot - sample.encoder;
+    }
+
+    // Drop the oldest sample when we run this function the next time around.
+    samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
+
+    pot_relative_encoder_offset_ =
+        ::std::accumulate(offset_samples_.begin(), offset_samples_.end(), 0.0) /
+        offset_samples_.size();
+
+    offset_ = UnWrap(sample.encoder + pot_relative_encoder_offset_,
+                     average_relative_to_absolute_offset + sample.encoder,
+                     constants_.one_revolution_distance) -
+              sample.encoder;
+
+    // Reverse the math for adjusted_absolute_encoder to compute the absolute
+    // encoder. Do this by taking the adjusted encoder, and then subtracting off
+    // the second argument above, and the value that was added by Wrap.
+    filtered_absolute_encoder_ =
+        ((sample.encoder + average_relative_to_absolute_offset) -
+         (-constants_.measured_absolute_position +
+          (adjusted_absolute_encoder -
+           (sample.absolute_encoder - constants_.measured_absolute_position))));
+
+    if (offset_ready()) {
+      if (!zeroed_) {
+        first_offset_ = offset_;
+      }
+
+      if (::std::abs(first_offset_ - offset_) >
+          constants_.allowable_encoder_error *
+              constants_.one_revolution_distance) {
+        VLOG(1) << "Offset moved too far. Initial: " << first_offset_
+                << ", current " << offset_ << ", allowable change: "
+                << constants_.allowable_encoder_error *
+                       constants_.one_revolution_distance;
+        error_ = true;
+      }
+
+      zeroed_ = true;
+    }
+  }
+
+  // Update the position.
+  filtered_position_ = pot_relative_encoder_offset_ + info.encoder();
+  position_ = offset_ + info.encoder();
+}
+
+flatbuffers::Offset<PotAndAbsoluteEncoderZeroingEstimator::State>
+PotAndAbsoluteEncoderZeroingEstimator::GetEstimatorState(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  State::Builder builder(*fbb);
+  builder.add_error(error_);
+  builder.add_zeroed(zeroed_);
+  builder.add_position(position_);
+  builder.add_pot_position(filtered_position_);
+  builder.add_absolute_position(filtered_absolute_encoder_);
+  return builder.Finish();
+}
+
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pot_and_absolute_encoder.h b/frc971/zeroing/pot_and_absolute_encoder.h
new file mode 100644
index 0000000..133eaf5
--- /dev/null
+++ b/frc971/zeroing/pot_and_absolute_encoder.h
@@ -0,0 +1,100 @@
+#ifndef FRC971_ZEROING_POT_AND_ABSOLUTE_ENCODER_H_
+#define FRC971_ZEROING_POT_AND_ABSOLUTE_ENCODER_H_
+
+#include <vector>
+
+#include "flatbuffers/flatbuffers.h"
+
+#include "frc971/zeroing/zeroing.h"
+
+namespace frc971 {
+namespace zeroing {
+
+// Estimates the position with an absolute encoder which also reports
+// incremental counts, and a potentiometer.
+class PotAndAbsoluteEncoderZeroingEstimator
+    : public ZeroingEstimator<PotAndAbsolutePosition,
+                              constants::PotAndAbsoluteEncoderZeroingConstants,
+                              PotAndAbsoluteEncoderEstimatorState> {
+ public:
+  explicit PotAndAbsoluteEncoderZeroingEstimator(
+      const constants::PotAndAbsoluteEncoderZeroingConstants &constants);
+
+  // Resets the internal logic so it needs to be re-zeroed.
+  void Reset() override;
+
+  // Updates the sensor values for the zeroing logic.
+  void UpdateEstimate(const PotAndAbsolutePosition &info) override;
+
+  void TriggerError() override { error_ = true; }
+
+  bool zeroed() const override { return zeroed_; }
+
+  double offset() const override { return offset_; }
+
+  bool error() const override { return error_; }
+
+  // Returns true if the sample buffer is full.
+  bool offset_ready() const override {
+    return relative_to_absolute_offset_samples_.size() ==
+               constants_.average_filter_size &&
+           offset_samples_.size() == constants_.average_filter_size;
+  }
+
+  // Returns information about our current state.
+  virtual flatbuffers::Offset<State> GetEstimatorState(
+      flatbuffers::FlatBufferBuilder *fbb) const override;
+
+ private:
+  struct PositionStruct {
+    PositionStruct(const PotAndAbsolutePosition &position_buffer)
+        : absolute_encoder(position_buffer.absolute_encoder()),
+          encoder(position_buffer.encoder()),
+          pot(position_buffer.pot()) {}
+    double absolute_encoder;
+    double encoder;
+    double pot;
+  };
+
+  // The zeroing constants used to describe the configuration of the system.
+  const constants::PotAndAbsoluteEncoderZeroingConstants constants_;
+
+  // True if the mechanism is zeroed.
+  bool zeroed_;
+  // Marker to track whether an error has occurred.
+  bool error_;
+  // The first valid offset we recorded. This is only set after zeroed_ first
+  // changes to true.
+  double first_offset_;
+
+  // The filtered absolute encoder.  This is used in the status for calibration.
+  double filtered_absolute_encoder_ = 0.0;
+
+  // Samples of the offset needed to line the relative encoder up with the
+  // absolute encoder.
+  ::std::vector<double> relative_to_absolute_offset_samples_;
+  // Offset between the Pot and Relative encoder position.
+  ::std::vector<double> offset_samples_;
+
+  MoveDetector<PositionStruct, PotAndAbsolutePosition> move_detector_;
+
+  // Estimated offset between the pot and relative encoder.
+  double pot_relative_encoder_offset_ = 0;
+  // Estimated start position of the mechanism
+  double offset_ = 0;
+  // The next position in 'relative_to_absolute_offset_samples_' and
+  // 'encoder_samples_' to be used to store the next sample.
+  int samples_idx_ = 0;
+
+  size_t nan_samples_ = 0;
+
+  // The unzeroed filtered position.
+  double filtered_position_ = 0.0;
+  // The filtered position.
+  double position_ = 0.0;
+};
+
+}  // namespace zeroing
+}  // namespace frc971
+
+#endif  // FRC971_ZEROING_POT_AND_ABSOLUTE_ENCODER_H_
diff --git a/frc971/zeroing/pot_and_absolute_encoder_test.cc b/frc971/zeroing/pot_and_absolute_encoder_test.cc
new file mode 100644
index 0000000..ba89834
--- /dev/null
+++ b/frc971/zeroing/pot_and_absolute_encoder_test.cc
@@ -0,0 +1,143 @@
+#include "frc971/zeroing/pot_and_absolute_encoder.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+using constants::PotAndAbsoluteEncoderZeroingConstants;
+
+class PotAndAbsoluteEncoderZeroingTest : public ZeroingTest {
+ protected:
+  void MoveTo(PositionSensorSimulator *simulator,
+              PotAndAbsoluteEncoderZeroingEstimator *estimator,
+              double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<PotAndAbsolutePosition>(&fbb));
+  }
+};
+
+// Makes sure that using an absolute encoder lets us zero without moving.
+TEST_F(PotAndAbsoluteEncoderZeroingTest,
+       TestPotAndAbsoluteEncoderZeroingWithoutMovement) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double start_pos = 2.1;
+  double measured_absolute_position = 0.3 * index_diff;
+
+  PotAndAbsoluteEncoderZeroingConstants constants{
+      kSampleSize, index_diff,        measured_absolute_position,
+      0.1,         kMovingBufferSize, kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_TRUE(estimator.zeroed());
+  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
+}
+
+// Makes sure that we ignore a NAN if we get it, but will correctly zero
+// afterwards.
+TEST_F(PotAndAbsoluteEncoderZeroingTest,
+       TestPotAndAbsoluteEncoderZeroingIgnoresNAN) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double start_pos = 2.1;
+  double measured_absolute_position = 0.3 * index_diff;
+
+  PotAndAbsoluteEncoderZeroingConstants constants{
+      kSampleSize, index_diff,        measured_absolute_position,
+      0.1,         kMovingBufferSize, kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
+
+  // We tolerate a couple NANs before we start.
+  FBB fbb;
+  fbb.Finish(CreatePotAndAbsolutePosition(
+      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN(), 0.0));
+  for (size_t i = 0; i < kSampleSize - 1; ++i) {
+    estimator.UpdateEstimate(
+        *flatbuffers::GetRoot<PotAndAbsolutePosition>(fbb.GetBufferPointer()));
+  }
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_TRUE(estimator.zeroed());
+  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
+}
+
+// Makes sure that using an absolute encoder doesn't let us zero while moving.
+TEST_F(PotAndAbsoluteEncoderZeroingTest,
+       TestPotAndAbsoluteEncoderZeroingWithMovement) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+
+  const double start_pos = 10 * index_diff;
+  double measured_absolute_position = 0.3 * index_diff;
+
+  PotAndAbsoluteEncoderZeroingConstants constants{
+      kSampleSize, index_diff,        measured_absolute_position,
+      0.1,         kMovingBufferSize, kIndexErrorFraction};
+
+  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
+                 constants.measured_absolute_position);
+
+  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
+
+  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
+    MoveTo(&sim, &estimator, start_pos + i * index_diff);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+  MoveTo(&sim, &estimator, start_pos + 10 * index_diff);
+
+  MoveTo(&sim, &estimator, start_pos);
+  ASSERT_FALSE(estimator.zeroed());
+}
+
+// Makes sure we detect an error if the ZeroingEstimator gets sent a NaN.
+TEST_F(PotAndAbsoluteEncoderZeroingTest,
+       TestPotAndAbsoluteEncoderZeroingWithNaN) {
+  PotAndAbsoluteEncoderZeroingConstants constants{
+      kSampleSize, 1, 0.3, 0.1, kMovingBufferSize, kIndexErrorFraction};
+
+  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
+
+  FBB fbb;
+  fbb.Finish(CreatePotAndAbsolutePosition(
+      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN(), 0.0));
+  const auto sensor_values =
+      flatbuffers::GetRoot<PotAndAbsolutePosition>(fbb.GetBufferPointer());
+  for (size_t i = 0; i < kSampleSize - 1; ++i) {
+    estimator.UpdateEstimate(*sensor_values);
+  }
+  ASSERT_FALSE(estimator.error());
+
+  estimator.UpdateEstimate(*sensor_values);
+  ASSERT_TRUE(estimator.error());
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pot_and_index.cc b/frc971/zeroing/pot_and_index.cc
new file mode 100644
index 0000000..7c713d9
--- /dev/null
+++ b/frc971/zeroing/pot_and_index.cc
@@ -0,0 +1,135 @@
+#include "frc971/zeroing/pot_and_index.h"
+
+#include <cmath>
+
+#include "glog/logging.h"
+
+namespace frc971 {
+namespace zeroing {
+
+PotAndIndexPulseZeroingEstimator::PotAndIndexPulseZeroingEstimator(
+    const constants::PotAndIndexPulseZeroingConstants &constants)
+    : constants_(constants) {
+  start_pos_samples_.reserve(constants_.average_filter_size);
+  Reset();
+}
+
+void PotAndIndexPulseZeroingEstimator::Reset() {
+  samples_idx_ = 0;
+  offset_ = 0;
+  start_pos_samples_.clear();
+  zeroed_ = false;
+  wait_for_index_pulse_ = true;
+  last_used_index_pulse_count_ = 0;
+  error_ = false;
+}
+
+void PotAndIndexPulseZeroingEstimator::TriggerError() {
+  if (!error_) {
+    VLOG(1) << "Manually triggered zeroing error.";
+    error_ = true;
+  }
+}
+
+double PotAndIndexPulseZeroingEstimator::CalculateStartPosition(
+    double start_average, double latched_encoder) const {
+  // We calculate an aproximation of the value of the last index position.
+  // Also account for index pulses not lining up with integer multiples of the
+  // index_diff.
+  double index_pos =
+      start_average + latched_encoder - constants_.measured_index_position;
+  // We round index_pos to the closest valid value of the index.
+  double accurate_index_pos = (round(index_pos / constants_.index_difference)) *
+                              constants_.index_difference;
+  // Now we reverse the first calculation to get the accurate start position.
+  return accurate_index_pos - latched_encoder +
+         constants_.measured_index_position;
+}
+
+void PotAndIndexPulseZeroingEstimator::UpdateEstimate(
+    const PotAndIndexPosition &info) {
+  // We want to make sure that we encounter at least one index pulse while
+  // zeroing. So we take the index pulse count from the first sample after
+  // reset and wait for that count to change before we consider ourselves
+  // zeroed.
+  if (wait_for_index_pulse_) {
+    last_used_index_pulse_count_ = info.index_pulses();
+    wait_for_index_pulse_ = false;
+  }
+
+  if (start_pos_samples_.size() < constants_.average_filter_size) {
+    start_pos_samples_.push_back(info.pot() - info.encoder());
+  } else {
+    start_pos_samples_[samples_idx_] = info.pot() - info.encoder();
+  }
+
+  // Drop the oldest sample when we run this function the next time around.
+  samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
+
+  double sample_sum = 0.0;
+
+  for (size_t i = 0; i < start_pos_samples_.size(); ++i) {
+    sample_sum += start_pos_samples_[i];
+  }
+
+  // Calculates the average of the starting position.
+  double start_average = sample_sum / start_pos_samples_.size();
+
+  // If there are no index pulses to use or we don't have enough samples yet to
+  // have a well-filtered starting position then we use the filtered value as
+  // our best guess.
+  if (!zeroed_ &&
+      (info.index_pulses() == last_used_index_pulse_count_ || !offset_ready())) {
+    offset_ = start_average;
+  } else if (!zeroed_ || last_used_index_pulse_count_ != info.index_pulses()) {
+    // Note the accurate start position and the current index pulse count so
+    // that we only run this logic once per index pulse. That should be more
+    // resilient to corrupted intermediate data.
+    offset_ = CalculateStartPosition(start_average, info.latched_encoder());
+    last_used_index_pulse_count_ = info.index_pulses();
+
+    // TODO(austin): Reject encoder positions which have x% error rather than
+    // rounding to the closest index pulse.
+
+    // Save the first starting position.
+    if (!zeroed_) {
+      first_start_pos_ = offset_;
+      VLOG(2) << "latching start position" << first_start_pos_;
+    }
+
+    // Now that we have an accurate starting position we can consider ourselves
+    // zeroed.
+    zeroed_ = true;
+    // Throw an error if first_start_pos is bigger/smaller than
+    // constants_.allowable_encoder_error * index_diff + start_pos.
+    if (::std::abs(first_start_pos_ - offset_) >
+        constants_.allowable_encoder_error * constants_.index_difference) {
+      if (!error_) {
+        VLOG(1)
+            << "Encoder ticks out of range since last index pulse. first start "
+               "position: "
+            << first_start_pos_ << " recent starting position: " << offset_
+            << ", allowable error: "
+            << constants_.allowable_encoder_error * constants_.index_difference;
+        error_ = true;
+      }
+    }
+  }
+
+  position_ = offset_ + info.encoder();
+  filtered_position_ = start_average + info.encoder();
+}
+
+flatbuffers::Offset<PotAndIndexPulseZeroingEstimator::State>
+PotAndIndexPulseZeroingEstimator::GetEstimatorState(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  State::Builder builder(*fbb);
+  builder.add_error(error_);
+  builder.add_zeroed(zeroed_);
+  builder.add_position(position_);
+  builder.add_pot_position(filtered_position_);
+  return builder.Finish();
+}
+
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pot_and_index.h b/frc971/zeroing/pot_and_index.h
new file mode 100644
index 0000000..473c674
--- /dev/null
+++ b/frc971/zeroing/pot_and_index.h
@@ -0,0 +1,103 @@
+#ifndef FRC971_ZEROING_POT_AND_INDEX_H_
+#define FRC971_ZEROING_POT_AND_INDEX_H_
+
+#include <vector>
+
+#include "flatbuffers/flatbuffers.h"
+
+#include "frc971/zeroing/zeroing.h"
+
+namespace frc971 {
+namespace zeroing {
+
+// Estimates the position with an incremental encoder with an index pulse and a
+// potentiometer.
+class PotAndIndexPulseZeroingEstimator
+    : public ZeroingEstimator<PotAndIndexPosition,
+                              constants::PotAndIndexPulseZeroingConstants,
+                              EstimatorState> {
+ public:
+  explicit PotAndIndexPulseZeroingEstimator(
+      const constants::PotAndIndexPulseZeroingConstants &constants);
+
+  // Update the internal logic with the next sensor values.
+  void UpdateEstimate(const PotAndIndexPosition &info) override;
+
+  // Reset the internal logic so it needs to be re-zeroed.
+  void Reset() override;
+
+  // Manually trigger an internal error. This is used for testing the error
+  // logic.
+  void TriggerError() override;
+
+  bool error() const override { return error_; }
+
+  bool zeroed() const override { return zeroed_; }
+
+  double offset() const override { return offset_; }
+
+  // Returns a number between 0 and 1 that represents the percentage of the
+  // samples being used in the moving average filter. A value of 0.0 means that
+  // no samples are being used. A value of 1.0 means that the filter is using
+  // as many samples as it has room for. For example, after a Reset() this
+  // value returns 0.0. As more samples get added with UpdateEstimate(...) the
+  // return value starts increasing to 1.0.
+  double offset_ratio_ready() const {
+    return start_pos_samples_.size() /
+           static_cast<double>(constants_.average_filter_size);
+  }
+
+  // Returns true if the sample buffer is full.
+  bool offset_ready() const override {
+    return start_pos_samples_.size() == constants_.average_filter_size;
+  }
+
+  // Returns information about our current state.
+  virtual flatbuffers::Offset<State> GetEstimatorState(
+      flatbuffers::FlatBufferBuilder *fbb) const override;
+
+ private:
+  // This function calculates the start position given the internal state and
+  // the provided `latched_encoder' value.
+  double CalculateStartPosition(double start_average,
+                                double latched_encoder) const;
+
+  // The zeroing constants used to describe the configuration of the system.
+  const constants::PotAndIndexPulseZeroingConstants constants_;
+
+  // The estimated position.
+  double position_;
+  // The unzeroed filtered position.
+  double filtered_position_ = 0.0;
+  // The next position in 'start_pos_samples_' to be used to store the next
+  // sample.
+  int samples_idx_;
+  // Last 'max_sample_count_' samples for start positions.
+  std::vector<double> start_pos_samples_;
+  // The estimated starting position of the mechanism. We also call this the
+  // 'offset' in some contexts.
+  double offset_;
+  // Flag for triggering logic that takes note of the current index pulse count
+  // after a reset. See `last_used_index_pulse_count_'.
+  bool wait_for_index_pulse_;
+  // After a reset we keep track of the index pulse count with this. Only after
+  // the index pulse count changes (i.e. increments at least once or wraps
+  // around) will we consider the mechanism zeroed. We also use this to store
+  // the most recent `PotAndIndexPosition::index_pulses' value when the start
+  // position was calculated. It helps us calculate the start position only on
+  // index pulses to reject corrupted intermediate data.
+  uint32_t last_used_index_pulse_count_;
+  // Marker to track whether we're fully zeroed yet or not.
+  bool zeroed_;
+  // Marker to track whether an error has occurred. This gets reset to false
+  // whenever Reset() is called.
+  bool error_;
+  // Stores the position "start_pos" variable the first time the program
+  // is zeroed.
+  double first_start_pos_;
+};
+
+}  // namespace zeroing
+}  // namespace frc971
+
+#endif  // FRC971_ZEROING_POT_AND_INDEX_H_
diff --git a/frc971/zeroing/pot_and_index_test.cc b/frc971/zeroing/pot_and_index_test.cc
new file mode 100644
index 0000000..b2b00ce
--- /dev/null
+++ b/frc971/zeroing/pot_and_index_test.cc
@@ -0,0 +1,283 @@
+#include "frc971/zeroing/pot_and_index.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+using constants::PotAndIndexPulseZeroingConstants;
+
+class PotAndIndexZeroingTest : public ZeroingTest {
+ protected:
+  void MoveTo(PositionSensorSimulator *simulator,
+              PotAndIndexPulseZeroingEstimator *estimator,
+              double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<PotAndIndexPosition>(&fbb));
+  }
+};
+
+TEST_F(PotAndIndexZeroingTest, TestMovingAverageFilter) {
+  const double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.6 * index_diff, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  // The zeroing code is supposed to perform some filtering on the difference
+  // between the potentiometer value and the encoder value. We assume that 300
+  // samples are sufficient to have updated the filter.
+  for (int i = 0; i < 300; i++) {
+    MoveTo(&sim, &estimator, 3.3 * index_diff);
+  }
+  ASSERT_NEAR(3.3 * index_diff, GetEstimatorPosition(&estimator),
+              kAcceptableUnzeroedError * index_diff);
+
+  for (int i = 0; i < 300; i++) {
+    MoveTo(&sim, &estimator, 3.9 * index_diff);
+  }
+  ASSERT_NEAR(3.9 * index_diff, GetEstimatorPosition(&estimator),
+              kAcceptableUnzeroedError * index_diff);
+}
+
+TEST_F(PotAndIndexZeroingTest, NotZeroedBeforeEnoughSamplesCollected) {
+  double index_diff = 0.5;
+  double position = 3.6 * index_diff;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(position, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  // Make sure that the zeroing code does not consider itself zeroed until we
+  // collect a good amount of samples. In this case we're waiting until the
+  // moving average filter is full.
+  for (unsigned int i = 0; i < kSampleSize - 1; i++) {
+    MoveTo(&sim, &estimator, position += index_diff);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  MoveTo(&sim, &estimator, position);
+  ASSERT_TRUE(estimator.zeroed());
+}
+
+TEST_F(PotAndIndexZeroingTest, TestLotsOfMovement) {
+  double index_diff = 1.0;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.6, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  // The zeroing code is supposed to perform some filtering on the difference
+  // between the potentiometer value and the encoder value. We assume that 300
+  // samples are sufficient to have updated the filter.
+  for (int i = 0; i < 300; i++) {
+    MoveTo(&sim, &estimator, 3.6);
+  }
+  ASSERT_NEAR(3.6, GetEstimatorPosition(&estimator),
+              kAcceptableUnzeroedError * index_diff);
+
+  // With a single index pulse the zeroing estimator should be able to lock
+  // onto the true value of the position.
+  MoveTo(&sim, &estimator, 4.01);
+  ASSERT_NEAR(4.01, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 4.99);
+  ASSERT_NEAR(4.99, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 3.99);
+  ASSERT_NEAR(3.99, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 3.01);
+  ASSERT_NEAR(3.01, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 13.55);
+  ASSERT_NEAR(13.55, GetEstimatorPosition(&estimator), 0.001);
+}
+
+TEST_F(PotAndIndexZeroingTest, TestDifferentIndexDiffs) {
+  double index_diff = 0.89;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.5 * index_diff, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  // The zeroing code is supposed to perform some filtering on the difference
+  // between the potentiometer value and the encoder value. We assume that 300
+  // samples are sufficient to have updated the filter.
+  for (int i = 0; i < 300; i++) {
+    MoveTo(&sim, &estimator, 3.5 * index_diff);
+  }
+  ASSERT_NEAR(3.5 * index_diff, GetEstimatorPosition(&estimator),
+              kAcceptableUnzeroedError * index_diff);
+
+  // With a single index pulse the zeroing estimator should be able to lock
+  // onto the true value of the position.
+  MoveTo(&sim, &estimator, 4.01);
+  ASSERT_NEAR(4.01, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 4.99);
+  ASSERT_NEAR(4.99, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 3.99);
+  ASSERT_NEAR(3.99, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 3.01);
+  ASSERT_NEAR(3.01, GetEstimatorPosition(&estimator), 0.001);
+
+  MoveTo(&sim, &estimator, 13.55);
+  ASSERT_NEAR(13.55, GetEstimatorPosition(&estimator), 0.001);
+}
+
+TEST_F(PotAndIndexZeroingTest, TestPercentage) {
+  double index_diff = 0.89;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.5 * index_diff, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  for (unsigned int i = 0; i < kSampleSize / 2; i++) {
+    MoveTo(&sim, &estimator, 3.5 * index_diff);
+  }
+  ASSERT_NEAR(0.5, estimator.offset_ratio_ready(), 0.001);
+  ASSERT_FALSE(estimator.offset_ready());
+
+  for (unsigned int i = 0; i < kSampleSize / 2; i++) {
+    MoveTo(&sim, &estimator, 3.5 * index_diff);
+  }
+  ASSERT_NEAR(1.0, estimator.offset_ratio_ready(), 0.001);
+  ASSERT_TRUE(estimator.offset_ready());
+}
+
+TEST_F(PotAndIndexZeroingTest, TestOffset) {
+  double index_diff = 0.89;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.1 * index_diff, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  MoveTo(&sim, &estimator, 3.1 * index_diff);
+
+  for (unsigned int i = 0; i < kSampleSize; i++) {
+    MoveTo(&sim, &estimator, 5.0 * index_diff);
+  }
+
+  ASSERT_NEAR(3.1 * index_diff, estimator.offset(), 0.001);
+}
+
+TEST_F(PotAndIndexZeroingTest, WaitForIndexPulseAfterReset) {
+  double index_diff = 0.6;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.1 * index_diff, index_diff / 3.0);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+
+  // Make sure to fill up the averaging filter with samples.
+  for (unsigned int i = 0; i < kSampleSize; i++) {
+    MoveTo(&sim, &estimator, 3.1 * index_diff);
+  }
+
+  // Make sure we're not zeroed until we hit an index pulse.
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Trigger an index pulse; we should now be zeroed.
+  MoveTo(&sim, &estimator, 4.5 * index_diff);
+  ASSERT_TRUE(estimator.zeroed());
+
+  // Reset the zeroing logic and supply a bunch of samples within the current
+  // index segment.
+  estimator.Reset();
+  for (unsigned int i = 0; i < kSampleSize; i++) {
+    MoveTo(&sim, &estimator, 4.2 * index_diff);
+  }
+
+  // Make sure we're not zeroed until we hit an index pulse.
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Trigger another index pulse; we should be zeroed again.
+  MoveTo(&sim, &estimator, 3.1 * index_diff);
+  ASSERT_TRUE(estimator.zeroed());
+}
+
+TEST_F(PotAndIndexZeroingTest, TestNonZeroIndexPulseOffsets) {
+  const double index_diff = 0.9;
+  const double known_index_pos = 3.5 * index_diff;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(3.3 * index_diff, index_diff / 3.0, known_index_pos);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, known_index_pos, kIndexErrorFraction});
+
+  // Make sure to fill up the averaging filter with samples.
+  for (unsigned int i = 0; i < kSampleSize; i++) {
+    MoveTo(&sim, &estimator, 3.3 * index_diff);
+  }
+
+  // Make sure we're not zeroed until we hit an index pulse.
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Trigger an index pulse; we should now be zeroed.
+  MoveTo(&sim, &estimator, 3.7 * index_diff);
+  ASSERT_TRUE(estimator.zeroed());
+  ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
+  ASSERT_DOUBLE_EQ(3.7 * index_diff, GetEstimatorPosition(&estimator));
+
+  // Trigger one more index pulse and check the offset.
+  MoveTo(&sim, &estimator, 4.7 * index_diff);
+  ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
+  ASSERT_DOUBLE_EQ(4.7 * index_diff, GetEstimatorPosition(&estimator));
+}
+
+TEST_F(PotAndIndexZeroingTest, BasicErrorAPITest) {
+  const double index_diff = 1.0;
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(1.5 * index_diff, index_diff / 3.0, 0.0);
+
+  // Perform a simple move and make sure that no error occured.
+  MoveTo(&sim, &estimator, 3.5 * index_diff);
+  ASSERT_FALSE(estimator.error());
+
+  // Trigger an error and make sure it's reported.
+  estimator.TriggerError();
+  ASSERT_TRUE(estimator.error());
+
+  // Make sure that it can recover after a reset.
+  estimator.Reset();
+  ASSERT_FALSE(estimator.error());
+  MoveTo(&sim, &estimator, 4.5 * index_diff);
+  MoveTo(&sim, &estimator, 5.5 * index_diff);
+  ASSERT_FALSE(estimator.error());
+}
+
+// Tests that an error is detected when the starting position changes too much.
+TEST_F(PotAndIndexZeroingTest, TestIndexOffsetError) {
+  const double index_diff = 0.8;
+  const double known_index_pos = 2 * index_diff;
+  const size_t sample_size = 30;
+  PositionSensorSimulator sim(index_diff);
+  sim.Initialize(10 * index_diff, index_diff / 3.0, known_index_pos);
+  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
+      sample_size, index_diff, known_index_pos, kIndexErrorFraction});
+
+  for (size_t i = 0; i < sample_size; i++) {
+    MoveTo(&sim, &estimator, 13 * index_diff);
+  }
+  MoveTo(&sim, &estimator, 8 * index_diff);
+
+  ASSERT_TRUE(estimator.zeroed());
+  ASSERT_FALSE(estimator.error());
+  sim.Initialize(9.0 * index_diff + 0.31 * index_diff, index_diff / 3.0,
+                 known_index_pos);
+  MoveTo(&sim, &estimator, 9 * index_diff);
+  ASSERT_TRUE(estimator.zeroed());
+  ASSERT_TRUE(estimator.error());
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pulse_index.cc b/frc971/zeroing/pulse_index.cc
new file mode 100644
index 0000000..c0831e8
--- /dev/null
+++ b/frc971/zeroing/pulse_index.cc
@@ -0,0 +1,116 @@
+#include "frc971/zeroing/pulse_index.h"
+
+#include <cmath>
+#include <limits>
+
+#include "glog/logging.h"
+
+namespace frc971 {
+namespace zeroing {
+
+void PulseIndexZeroingEstimator::Reset() {
+  max_index_position_ = ::std::numeric_limits<double>::lowest();
+  min_index_position_ = ::std::numeric_limits<double>::max();
+  offset_ = 0;
+  last_used_index_pulse_count_ = 0;
+  zeroed_ = false;
+  error_ = false;
+}
+
+void PulseIndexZeroingEstimator::StoreIndexPulseMaxAndMin(
+    const IndexPosition &info) {
+  // If we have a new index pulse.
+  if (last_used_index_pulse_count_ != info.index_pulses()) {
+    // If the latest pulses's position is outside the range we've currently
+    // seen, record it appropriately.
+    if (info.latched_encoder() > max_index_position_) {
+      max_index_position_ = info.latched_encoder();
+    }
+    if (info.latched_encoder() < min_index_position_) {
+      min_index_position_ = info.latched_encoder();
+    }
+    last_used_index_pulse_count_ = info.index_pulses();
+  }
+}
+
+int PulseIndexZeroingEstimator::IndexPulseCount() const {
+  if (min_index_position_ > max_index_position_) {
+    // This condition means we haven't seen a pulse yet.
+    return 0;
+  }
+
+  // Calculate the number of pulses encountered so far.
+  return 1 + static_cast<int>(
+                 ::std::round((max_index_position_ - min_index_position_) /
+                              constants_.index_difference));
+}
+
+void PulseIndexZeroingEstimator::UpdateEstimate(const IndexPosition &info) {
+  StoreIndexPulseMaxAndMin(info);
+  const int index_pulse_count = IndexPulseCount();
+  if (index_pulse_count > constants_.index_pulse_count) {
+    if (!error_) {
+      VLOG(1) << "Got more index pulses than expected. Got "
+              << index_pulse_count << " expected "
+              << constants_.index_pulse_count;
+      error_ = true;
+    }
+  }
+
+  // TODO(austin): Detect if the encoder or index pulse is unplugged.
+  // TODO(austin): Detect missing counts.
+
+  if (index_pulse_count == constants_.index_pulse_count && !zeroed_) {
+    offset_ = constants_.measured_index_position -
+              constants_.known_index_pulse * constants_.index_difference -
+              min_index_position_;
+    zeroed_ = true;
+  } else if (zeroed_ && !error_) {
+    // Detect whether the index pulse is somewhere other than where we expect
+    // it to be. First we compute the position of the most recent index pulse.
+    double index_pulse_distance =
+        info.latched_encoder() + offset_ - constants_.measured_index_position;
+    // Second we compute the position of the index pulse in terms of
+    // the index difference. I.e. if this index pulse is two pulses away from
+    // the index pulse that we know about then this number should be positive
+    // or negative two.
+    double relative_distance =
+        index_pulse_distance / constants_.index_difference;
+    // Now we compute how far away the measured index pulse is from the
+    // expected index pulse.
+    double error = relative_distance - ::std::round(relative_distance);
+    // This lets us check if the index pulse is within an acceptable error
+    // margin of where we expected it to be.
+    if (::std::abs(error) > constants_.allowable_encoder_error) {
+      VLOG(1)
+          << "Encoder ticks out of range since last index pulse. known index "
+             "pulse: "
+          << constants_.measured_index_position << ", expected index pulse: "
+          << round(relative_distance) * constants_.index_difference +
+                 constants_.measured_index_position
+          << ", actual index pulse: " << info.latched_encoder() + offset_
+          << ", "
+             "allowable error: "
+          << constants_.allowable_encoder_error * constants_.index_difference;
+      error_ = true;
+    }
+  }
+
+  position_ = info.encoder() + offset_;
+}
+
+flatbuffers::Offset<PulseIndexZeroingEstimator::State>
+PulseIndexZeroingEstimator::GetEstimatorState(
+    flatbuffers::FlatBufferBuilder *fbb) const {
+  State::Builder builder(*fbb);
+  builder.add_error(error_);
+  builder.add_zeroed(zeroed_);
+  builder.add_position(position_);
+  builder.add_min_index_position(min_index_position_);
+  builder.add_max_index_position(max_index_position_);
+  builder.add_index_pulses_seen(IndexPulseCount());
+  return builder.Finish();
+}
+
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/pulse_index.h b/frc971/zeroing/pulse_index.h
new file mode 100644
index 0000000..4bcf210
--- /dev/null
+++ b/frc971/zeroing/pulse_index.h
@@ -0,0 +1,84 @@
+#ifndef FRC971_ZEROING_PULSE_INDEX_H_
+#define FRC971_ZEROING_PULSE_INDEX_H_
+
+#include "flatbuffers/flatbuffers.h"
+
+#include "frc971/zeroing/zeroing.h"
+
+namespace frc971 {
+namespace zeroing {
+
+// Zeros by seeing all the index pulses in the range of motion of the mechanism
+// and using that to figure out which index pulse is which.
+class PulseIndexZeroingEstimator
+    : public ZeroingEstimator<IndexPosition,
+                              constants::EncoderPlusIndexZeroingConstants,
+                              IndexEstimatorState> {
+ public:
+  explicit PulseIndexZeroingEstimator(const ZeroingConstants &constants)
+      : constants_(constants) {
+    Reset();
+  }
+
+  // Resets the internal logic so it needs to be re-zeroed.
+  void Reset() override;
+
+  bool zeroed() const override { return zeroed_; }
+
+  // It's as ready as it'll ever be...
+  bool offset_ready() const override { return true; }
+
+  double offset() const override { return offset_; }
+
+  bool error() const override { return error_; }
+
+  // Updates the internal logic with the next sensor values.
+  void UpdateEstimate(const IndexPosition &info) override;
+
+  // Returns information about our current state.
+  virtual flatbuffers::Offset<State> GetEstimatorState(
+      flatbuffers::FlatBufferBuilder *fbb) const override;
+
+  void TriggerError() override { error_ = true; }
+
+ private:
+  // Returns the current real position using the relative encoder offset.
+  double CalculateCurrentPosition(const IndexPosition &info);
+
+  // Sets the minimum and maximum index pulse position values.
+  void StoreIndexPulseMaxAndMin(const IndexPosition &info);
+
+  // Returns the number of index pulses we should have seen so far.
+  int IndexPulseCount() const;
+
+  // Contains the physical constants describing the system.
+  const ZeroingConstants constants_;
+
+  // The smallest position of all the index pulses.
+  double min_index_position_;
+  // The largest position of all the index pulses.
+  double max_index_position_;
+
+  // The estimated starting position of the mechanism.
+  double offset_;
+  // After a reset we keep track of the index pulse count with this. Only after
+  // the index pulse count changes (i.e. increments at least once or wraps
+  // around) will we consider the mechanism zeroed. We also use this to store
+  // the most recent `PotAndIndexPosition::index_pulses' value when the start
+  // position was calculated. It helps us calculate the start position only on
+  // index pulses to reject corrupted intermediate data.
+  uint32_t last_used_index_pulse_count_;
+
+  // True if we are fully zeroed.
+  bool zeroed_;
+  // Marker to track whether an error has occurred.
+  bool error_;
+
+  // The estimated position.
+  double position_;
+};
+
+}  // namespace zeroing
+}  // namespace frc971
+
+#endif  // FRC971_ZEROING_PULSE_INDEX_H_
diff --git a/frc971/zeroing/pulse_index_test.cc b/frc971/zeroing/pulse_index_test.cc
new file mode 100644
index 0000000..bd257f7
--- /dev/null
+++ b/frc971/zeroing/pulse_index_test.cc
@@ -0,0 +1,132 @@
+#include "frc971/zeroing/pulse_index.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+using constants::EncoderPlusIndexZeroingConstants;
+
+class PulseIndexZeroingTest : public ZeroingTest {
+ protected:
+  void MoveTo(PositionSensorSimulator *simulator,
+              PulseIndexZeroingEstimator *estimator, double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<IndexPosition>(&fbb));
+  }
+};
+
+// Tests that an error is detected when the starting position changes too much.
+TEST_F(PulseIndexZeroingTest, TestRelativeEncoderZeroing) {
+  EncoderPlusIndexZeroingConstants constants;
+  constants.index_pulse_count = 3;
+  constants.index_difference = 10.0;
+  constants.measured_index_position = 20.0;
+  constants.known_index_pulse = 1;
+  constants.allowable_encoder_error = 0.01;
+
+  PositionSensorSimulator sim(constants.index_difference);
+
+  const double start_pos = 2.5 * constants.index_difference;
+
+  sim.Initialize(start_pos, constants.index_difference / 3.0,
+                 constants.measured_index_position);
+
+  PulseIndexZeroingEstimator estimator(constants);
+
+  // Should not be zeroed when we stand still.
+  for (int i = 0; i < 300; ++i) {
+    MoveTo(&sim, &estimator, start_pos);
+    ASSERT_FALSE(estimator.zeroed());
+  }
+
+  // Move to 1.5 constants.index_difference and we should still not be zeroed.
+  MoveTo(&sim, &estimator, 1.5 * constants.index_difference);
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Move to 0.5 constants.index_difference and we should still not be zeroed.
+  MoveTo(&sim, &estimator, 0.5 * constants.index_difference);
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Move back to 1.5 constants.index_difference and we should still not be
+  // zeroed.
+  MoveTo(&sim, &estimator, 1.5 * constants.index_difference);
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Move back to 2.5 constants.index_difference and we should still not be
+  // zeroed.
+  MoveTo(&sim, &estimator, 2.5 * constants.index_difference);
+  ASSERT_FALSE(estimator.zeroed());
+
+  // Move back to 3.5 constants.index_difference and we should now be zeroed.
+  MoveTo(&sim, &estimator, 3.5 * constants.index_difference);
+  ASSERT_TRUE(estimator.zeroed());
+
+  ASSERT_DOUBLE_EQ(start_pos, estimator.offset());
+  ASSERT_DOUBLE_EQ(3.5 * constants.index_difference,
+                   GetEstimatorPosition(&estimator));
+
+  MoveTo(&sim, &estimator, 0.5 * constants.index_difference);
+  ASSERT_DOUBLE_EQ(0.5 * constants.index_difference,
+                   GetEstimatorPosition(&estimator));
+}
+
+// Tests that we can detect when an index pulse occurs where we didn't expect
+// it to for the PulseIndexZeroingEstimator.
+TEST_F(PulseIndexZeroingTest, TestRelativeEncoderSlipping) {
+  EncoderPlusIndexZeroingConstants constants;
+  constants.index_pulse_count = 3;
+  constants.index_difference = 10.0;
+  constants.measured_index_position = 20.0;
+  constants.known_index_pulse = 1;
+  constants.allowable_encoder_error = 0.05;
+
+  PositionSensorSimulator sim(constants.index_difference);
+
+  const double start_pos =
+      constants.measured_index_position + 0.5 * constants.index_difference;
+
+  for (double direction : {1.0, -1.0}) {
+    sim.Initialize(start_pos, constants.index_difference / 3.0,
+                   constants.measured_index_position);
+
+    PulseIndexZeroingEstimator estimator(constants);
+
+    // Zero the estimator.
+    MoveTo(&sim, &estimator, start_pos - 1 * constants.index_difference);
+    MoveTo(
+        &sim, &estimator,
+        start_pos - constants.index_pulse_count * constants.index_difference);
+    ASSERT_TRUE(estimator.zeroed());
+    ASSERT_FALSE(estimator.error());
+
+    // We have a 5% allowable error so we slip a little bit each time and make
+    // sure that the index pulses are still accepted.
+    for (double error = 0.00;
+         ::std::abs(error) < constants.allowable_encoder_error;
+         error += 0.01 * direction) {
+      sim.Initialize(start_pos, constants.index_difference / 3.0,
+                     constants.measured_index_position +
+                         error * constants.index_difference);
+      MoveTo(&sim, &estimator, start_pos - constants.index_difference);
+      EXPECT_FALSE(estimator.error());
+    }
+
+    // As soon as we hit cross the error margin, we should trigger an error.
+    sim.Initialize(start_pos, constants.index_difference / 3.0,
+                   constants.measured_index_position +
+                       constants.allowable_encoder_error * 1.1 *
+                           constants.index_difference * direction);
+    MoveTo(&sim, &estimator, start_pos - constants.index_difference);
+    ASSERT_TRUE(estimator.error());
+  }
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/relative_encoder_test.cc b/frc971/zeroing/relative_encoder_test.cc
new file mode 100644
index 0000000..fd86fb3
--- /dev/null
+++ b/frc971/zeroing/relative_encoder_test.cc
@@ -0,0 +1,40 @@
+#include "frc971/zeroing/zeroing.h"
+
+#include "gtest/gtest.h"
+
+#include "frc971/zeroing/zeroing_test.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+class RelativeEncoderZeroingTest : public ZeroingTest {
+ protected:
+  void MoveTo(PositionSensorSimulator *simulator,
+              RelativeEncoderZeroingEstimator *estimator, double new_position) {
+    simulator->MoveTo(new_position);
+    FBB fbb;
+    estimator->UpdateEstimate(
+        *simulator->FillSensorValues<RelativePosition>(&fbb));
+  }
+};
+
+TEST_F(RelativeEncoderZeroingTest, TestRelativeEncoderZeroingWithoutMovement) {
+  PositionSensorSimulator sim(1.0);
+  RelativeEncoderZeroingEstimator estimator;
+
+  sim.InitializeRelativeEncoder();
+
+  ASSERT_TRUE(estimator.zeroed());
+  ASSERT_TRUE(estimator.offset_ready());
+  EXPECT_DOUBLE_EQ(estimator.offset(), 0.0);
+  EXPECT_DOUBLE_EQ(GetEstimatorPosition(&estimator), 0.0);
+
+  MoveTo(&sim, &estimator, 0.1);
+
+  EXPECT_DOUBLE_EQ(GetEstimatorPosition(&estimator), 0.1);
+}
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/frc971/zeroing/zeroing.cc b/frc971/zeroing/zeroing.cc
deleted file mode 100644
index f0aab23..0000000
--- a/frc971/zeroing/zeroing.cc
+++ /dev/null
@@ -1,706 +0,0 @@
-#include "frc971/zeroing/zeroing.h"
-
-#include <algorithm>
-#include <cmath>
-#include <limits>
-#include <numeric>
-#include <vector>
-
-#include "frc971/zeroing/wrap.h"
-
-#include "flatbuffers/flatbuffers.h"
-#include "glog/logging.h"
-
-namespace frc971 {
-namespace zeroing {
-
-PotAndIndexPulseZeroingEstimator::PotAndIndexPulseZeroingEstimator(
-    const constants::PotAndIndexPulseZeroingConstants &constants)
-    : constants_(constants) {
-  start_pos_samples_.reserve(constants_.average_filter_size);
-  Reset();
-}
-
-void PotAndIndexPulseZeroingEstimator::Reset() {
-  samples_idx_ = 0;
-  offset_ = 0;
-  start_pos_samples_.clear();
-  zeroed_ = false;
-  wait_for_index_pulse_ = true;
-  last_used_index_pulse_count_ = 0;
-  error_ = false;
-}
-
-void PotAndIndexPulseZeroingEstimator::TriggerError() {
-  if (!error_) {
-    VLOG(1) << "Manually triggered zeroing error.";
-    error_ = true;
-  }
-}
-
-double PotAndIndexPulseZeroingEstimator::CalculateStartPosition(
-    double start_average, double latched_encoder) const {
-  // We calculate an aproximation of the value of the last index position.
-  // Also account for index pulses not lining up with integer multiples of the
-  // index_diff.
-  double index_pos =
-      start_average + latched_encoder - constants_.measured_index_position;
-  // We round index_pos to the closest valid value of the index.
-  double accurate_index_pos = (round(index_pos / constants_.index_difference)) *
-                              constants_.index_difference;
-  // Now we reverse the first calculation to get the accurate start position.
-  return accurate_index_pos - latched_encoder +
-         constants_.measured_index_position;
-}
-
-void PotAndIndexPulseZeroingEstimator::UpdateEstimate(
-    const PotAndIndexPosition &info) {
-  // We want to make sure that we encounter at least one index pulse while
-  // zeroing. So we take the index pulse count from the first sample after
-  // reset and wait for that count to change before we consider ourselves
-  // zeroed.
-  if (wait_for_index_pulse_) {
-    last_used_index_pulse_count_ = info.index_pulses();
-    wait_for_index_pulse_ = false;
-  }
-
-  if (start_pos_samples_.size() < constants_.average_filter_size) {
-    start_pos_samples_.push_back(info.pot() - info.encoder());
-  } else {
-    start_pos_samples_[samples_idx_] = info.pot() - info.encoder();
-  }
-
-  // Drop the oldest sample when we run this function the next time around.
-  samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
-
-  double sample_sum = 0.0;
-
-  for (size_t i = 0; i < start_pos_samples_.size(); ++i) {
-    sample_sum += start_pos_samples_[i];
-  }
-
-  // Calculates the average of the starting position.
-  double start_average = sample_sum / start_pos_samples_.size();
-
-  // If there are no index pulses to use or we don't have enough samples yet to
-  // have a well-filtered starting position then we use the filtered value as
-  // our best guess.
-  if (!zeroed_ &&
-      (info.index_pulses() == last_used_index_pulse_count_ || !offset_ready())) {
-    offset_ = start_average;
-  } else if (!zeroed_ || last_used_index_pulse_count_ != info.index_pulses()) {
-    // Note the accurate start position and the current index pulse count so
-    // that we only run this logic once per index pulse. That should be more
-    // resilient to corrupted intermediate data.
-    offset_ = CalculateStartPosition(start_average, info.latched_encoder());
-    last_used_index_pulse_count_ = info.index_pulses();
-
-    // TODO(austin): Reject encoder positions which have x% error rather than
-    // rounding to the closest index pulse.
-
-    // Save the first starting position.
-    if (!zeroed_) {
-      first_start_pos_ = offset_;
-      VLOG(2) << "latching start position" << first_start_pos_;
-    }
-
-    // Now that we have an accurate starting position we can consider ourselves
-    // zeroed.
-    zeroed_ = true;
-    // Throw an error if first_start_pos is bigger/smaller than
-    // constants_.allowable_encoder_error * index_diff + start_pos.
-    if (::std::abs(first_start_pos_ - offset_) >
-        constants_.allowable_encoder_error * constants_.index_difference) {
-      if (!error_) {
-        VLOG(1)
-            << "Encoder ticks out of range since last index pulse. first start "
-               "position: "
-            << first_start_pos_ << " recent starting position: " << offset_
-            << ", allowable error: "
-            << constants_.allowable_encoder_error * constants_.index_difference;
-        error_ = true;
-      }
-    }
-  }
-
-  position_ = offset_ + info.encoder();
-  filtered_position_ = start_average + info.encoder();
-}
-
-flatbuffers::Offset<PotAndIndexPulseZeroingEstimator::State>
-PotAndIndexPulseZeroingEstimator::GetEstimatorState(
-    flatbuffers::FlatBufferBuilder *fbb) const {
-  State::Builder builder(*fbb);
-  builder.add_error(error_);
-  builder.add_zeroed(zeroed_);
-  builder.add_position(position_);
-  builder.add_pot_position(filtered_position_);
-  return builder.Finish();
-}
-
-HallEffectAndPositionZeroingEstimator::HallEffectAndPositionZeroingEstimator(
-    const ZeroingConstants &constants)
-    : constants_(constants) {
-  Reset();
-}
-
-void HallEffectAndPositionZeroingEstimator::Reset() {
-  offset_ = 0.0;
-  min_low_position_ = ::std::numeric_limits<double>::max();
-  max_low_position_ = ::std::numeric_limits<double>::lowest();
-  zeroed_ = false;
-  initialized_ = false;
-  last_used_posedge_count_ = 0;
-  cycles_high_ = 0;
-  high_long_enough_ = false;
-  first_start_pos_ = 0.0;
-  error_ = false;
-  current_ = 0.0;
-  first_start_pos_ = 0.0;
-}
-
-void HallEffectAndPositionZeroingEstimator::TriggerError() {
-  if (!error_) {
-    VLOG(1) << "Manually triggered zeroing error.\n";
-    error_ = true;
-  }
-}
-
-void HallEffectAndPositionZeroingEstimator::StoreEncoderMaxAndMin(
-    const HallEffectAndPosition &info) {
-  // If we have a new posedge.
-  if (!info.current()) {
-    if (last_hall_) {
-      min_low_position_ = max_low_position_ = info.encoder();
-    } else {
-      min_low_position_ = ::std::min(min_low_position_, info.encoder());
-      max_low_position_ = ::std::max(max_low_position_, info.encoder());
-    }
-  }
-  last_hall_ = info.current();
-}
-
-void HallEffectAndPositionZeroingEstimator::UpdateEstimate(
-    const HallEffectAndPosition &info) {
-  // We want to make sure that we encounter at least one posedge while zeroing.
-  // So we take the posedge count from the first sample after reset and wait for
-  // that count to change and for the hall effect to stay high before we
-  // consider ourselves zeroed.
-  if (!initialized_) {
-    last_used_posedge_count_ = info.posedge_count();
-    initialized_ = true;
-    last_hall_ = info.current();
-  }
-
-  StoreEncoderMaxAndMin(info);
-
-  if (info.current()) {
-    cycles_high_++;
-  } else {
-    cycles_high_ = 0;
-    last_used_posedge_count_ = info.posedge_count();
-  }
-
-  high_long_enough_ = cycles_high_ >= constants_.hall_trigger_zeroing_length;
-
-  bool moving_backward = false;
-  if (constants_.zeroing_move_direction) {
-    moving_backward = info.encoder() > min_low_position_;
-  } else {
-    moving_backward = info.encoder() < max_low_position_;
-  }
-
-  // If there are no posedges to use or we don't have enough samples yet to
-  // have a well-filtered starting position then we use the filtered value as
-  // our best guess.
-  if (last_used_posedge_count_ != info.posedge_count() && high_long_enough_ &&
-      moving_backward) {
-    // Note the offset and the current posedge count so that we only run this
-    // logic once per posedge. That should be more resilient to corrupted
-    // intermediate data.
-    offset_ = -info.posedge_value();
-    if (constants_.zeroing_move_direction) {
-      offset_ += constants_.lower_hall_position;
-    } else {
-      offset_ += constants_.upper_hall_position;
-    }
-    last_used_posedge_count_ = info.posedge_count();
-
-    // Save the first starting position.
-    if (!zeroed_) {
-      first_start_pos_ = offset_;
-      VLOG(2) << "latching start position" << first_start_pos_;
-    }
-
-    // Now that we have an accurate starting position we can consider ourselves
-    // zeroed.
-    zeroed_ = true;
-  }
-
-  position_ = info.encoder() - offset_;
-}
-
-flatbuffers::Offset<HallEffectAndPositionZeroingEstimator::State>
-HallEffectAndPositionZeroingEstimator::GetEstimatorState(
-    flatbuffers::FlatBufferBuilder *fbb) const {
-  State::Builder builder(*fbb);
-  builder.add_error(error_);
-  builder.add_zeroed(zeroed_);
-  builder.add_encoder(position_);
-  builder.add_high_long_enough(high_long_enough_);
-  builder.add_offset(offset_);
-  return builder.Finish();
-}
-
-PotAndAbsoluteEncoderZeroingEstimator::PotAndAbsoluteEncoderZeroingEstimator(
-    const constants::PotAndAbsoluteEncoderZeroingConstants &constants)
-    : constants_(constants), move_detector_(constants_.moving_buffer_size) {
-  relative_to_absolute_offset_samples_.reserve(constants_.average_filter_size);
-  offset_samples_.reserve(constants_.average_filter_size);
-  Reset();
-}
-
-void PotAndAbsoluteEncoderZeroingEstimator::Reset() {
-  first_offset_ = 0.0;
-  pot_relative_encoder_offset_ = 0.0;
-  offset_ = 0.0;
-  samples_idx_ = 0;
-  filtered_position_ = 0.0;
-  position_ = 0.0;
-  zeroed_ = false;
-  nan_samples_ = 0;
-  relative_to_absolute_offset_samples_.clear();
-  offset_samples_.clear();
-  move_detector_.Reset();
-  error_ = false;
-}
-
-// So, this needs to be a multistep process.  We need to first estimate the
-// offset between the absolute encoder and the relative encoder.  That process
-// should get us an absolute number which is off by integer multiples of the
-// distance/rev.  In parallel, we can estimate the offset between the pot and
-// encoder.  When both estimates have converged, we can then compute the offset
-// in a cycle, and which cycle, which gives us the accurate global offset.
-//
-// It's tricky to compute the offset between the absolute and relative encoder.
-// We need to compute this inside 1 revolution.  The easiest way to do this
-// would be to wrap the encoder, subtract the two of them, and then average the
-// result.  That will struggle when they are off by PI.  Instead, we need to
-// wrap the number to +- PI from the current averaged offset.
-//
-// To guard against the robot moving while updating estimates, buffer a number
-// of samples and check that the buffered samples are not different than the
-// zeroing threshold. At any point that the samples differ too much, do not
-// update estimates based on those samples.
-void PotAndAbsoluteEncoderZeroingEstimator::UpdateEstimate(
-    const PotAndAbsolutePosition &info) {
-  // Check for Abs Encoder NaN value that would mess up the rest of the zeroing
-  // code below. NaN values are given when the Absolute Encoder is disconnected.
-  if (::std::isnan(info.absolute_encoder())) {
-    if (zeroed_) {
-      VLOG(1) << "NAN on absolute encoder.";
-      error_ = true;
-    } else {
-      ++nan_samples_;
-      VLOG(1) << "NAN on absolute encoder while zeroing" << nan_samples_;
-      if (nan_samples_ >= constants_.average_filter_size) {
-        error_ = true;
-        zeroed_ = true;
-      }
-    }
-    // Throw some dummy values in for now.
-    filtered_absolute_encoder_ = info.absolute_encoder();
-    filtered_position_ = pot_relative_encoder_offset_ + info.encoder();
-    position_ = offset_ + info.encoder();
-    return;
-  }
-
-  const bool moving = move_detector_.Update(info, constants_.moving_buffer_size,
-                                            constants_.zeroing_threshold);
-
-  if (!moving) {
-    const PositionStruct &sample = move_detector_.GetSample();
-
-    // Compute the average offset between the absolute encoder and relative
-    // encoder.  If we have 0 samples, assume it is 0.
-    double average_relative_to_absolute_offset =
-        relative_to_absolute_offset_samples_.size() == 0
-            ? 0.0
-            : ::std::accumulate(relative_to_absolute_offset_samples_.begin(),
-                                relative_to_absolute_offset_samples_.end(),
-                                0.0) /
-                  relative_to_absolute_offset_samples_.size();
-
-    const double adjusted_incremental_encoder =
-        sample.encoder + average_relative_to_absolute_offset;
-
-    // Now, compute the nearest absolute encoder value to the offset relative
-    // encoder position.
-    const double adjusted_absolute_encoder =
-        UnWrap(adjusted_incremental_encoder,
-               sample.absolute_encoder - constants_.measured_absolute_position,
-               constants_.one_revolution_distance);
-
-    // We can now compute the offset now that we have unwrapped the absolute
-    // encoder.
-    const double relative_to_absolute_offset =
-        adjusted_absolute_encoder - sample.encoder;
-
-    // Add the sample and update the average with the new reading.
-    const size_t relative_to_absolute_offset_samples_size =
-        relative_to_absolute_offset_samples_.size();
-    if (relative_to_absolute_offset_samples_size <
-        constants_.average_filter_size) {
-      average_relative_to_absolute_offset =
-          (average_relative_to_absolute_offset *
-               relative_to_absolute_offset_samples_size +
-           relative_to_absolute_offset) /
-          (relative_to_absolute_offset_samples_size + 1);
-
-      relative_to_absolute_offset_samples_.push_back(
-          relative_to_absolute_offset);
-    } else {
-      average_relative_to_absolute_offset -=
-          relative_to_absolute_offset_samples_[samples_idx_] /
-          relative_to_absolute_offset_samples_size;
-      relative_to_absolute_offset_samples_[samples_idx_] =
-          relative_to_absolute_offset;
-      average_relative_to_absolute_offset +=
-          relative_to_absolute_offset /
-          relative_to_absolute_offset_samples_size;
-    }
-
-    // Now compute the offset between the pot and relative encoder.
-    if (offset_samples_.size() < constants_.average_filter_size) {
-      offset_samples_.push_back(sample.pot - sample.encoder);
-    } else {
-      offset_samples_[samples_idx_] = sample.pot - sample.encoder;
-    }
-
-    // Drop the oldest sample when we run this function the next time around.
-    samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
-
-    pot_relative_encoder_offset_ =
-        ::std::accumulate(offset_samples_.begin(), offset_samples_.end(), 0.0) /
-        offset_samples_.size();
-
-    offset_ = UnWrap(sample.encoder + pot_relative_encoder_offset_,
-                     average_relative_to_absolute_offset + sample.encoder,
-                     constants_.one_revolution_distance) -
-              sample.encoder;
-
-    // Reverse the math for adjusted_absolute_encoder to compute the absolute
-    // encoder. Do this by taking the adjusted encoder, and then subtracting off
-    // the second argument above, and the value that was added by Wrap.
-    filtered_absolute_encoder_ =
-        ((sample.encoder + average_relative_to_absolute_offset) -
-         (-constants_.measured_absolute_position +
-          (adjusted_absolute_encoder -
-           (sample.absolute_encoder - constants_.measured_absolute_position))));
-
-    if (offset_ready()) {
-      if (!zeroed_) {
-        first_offset_ = offset_;
-      }
-
-      if (::std::abs(first_offset_ - offset_) >
-          constants_.allowable_encoder_error *
-              constants_.one_revolution_distance) {
-        VLOG(1) << "Offset moved too far. Initial: " << first_offset_
-                << ", current " << offset_ << ", allowable change: "
-                << constants_.allowable_encoder_error *
-                       constants_.one_revolution_distance;
-        error_ = true;
-      }
-
-      zeroed_ = true;
-    }
-  }
-
-  // Update the position.
-  filtered_position_ = pot_relative_encoder_offset_ + info.encoder();
-  position_ = offset_ + info.encoder();
-}
-
-flatbuffers::Offset<PotAndAbsoluteEncoderZeroingEstimator::State>
-PotAndAbsoluteEncoderZeroingEstimator::GetEstimatorState(
-    flatbuffers::FlatBufferBuilder *fbb) const {
-  State::Builder builder(*fbb);
-  builder.add_error(error_);
-  builder.add_zeroed(zeroed_);
-  builder.add_position(position_);
-  builder.add_pot_position(filtered_position_);
-  builder.add_absolute_position(filtered_absolute_encoder_);
-  return builder.Finish();
-}
-
-void PulseIndexZeroingEstimator::Reset() {
-  max_index_position_ = ::std::numeric_limits<double>::lowest();
-  min_index_position_ = ::std::numeric_limits<double>::max();
-  offset_ = 0;
-  last_used_index_pulse_count_ = 0;
-  zeroed_ = false;
-  error_ = false;
-}
-
-void PulseIndexZeroingEstimator::StoreIndexPulseMaxAndMin(
-    const IndexPosition &info) {
-  // If we have a new index pulse.
-  if (last_used_index_pulse_count_ != info.index_pulses()) {
-    // If the latest pulses's position is outside the range we've currently
-    // seen, record it appropriately.
-    if (info.latched_encoder() > max_index_position_) {
-      max_index_position_ = info.latched_encoder();
-    }
-    if (info.latched_encoder() < min_index_position_) {
-      min_index_position_ = info.latched_encoder();
-    }
-    last_used_index_pulse_count_ = info.index_pulses();
-  }
-}
-
-int PulseIndexZeroingEstimator::IndexPulseCount() const {
-  if (min_index_position_ > max_index_position_) {
-    // This condition means we haven't seen a pulse yet.
-    return 0;
-  }
-
-  // Calculate the number of pulses encountered so far.
-  return 1 + static_cast<int>(
-                 ::std::round((max_index_position_ - min_index_position_) /
-                              constants_.index_difference));
-}
-
-void PulseIndexZeroingEstimator::UpdateEstimate(const IndexPosition &info) {
-  StoreIndexPulseMaxAndMin(info);
-  const int index_pulse_count = IndexPulseCount();
-  if (index_pulse_count > constants_.index_pulse_count) {
-    if (!error_) {
-      VLOG(1) << "Got more index pulses than expected. Got "
-              << index_pulse_count << " expected "
-              << constants_.index_pulse_count;
-      error_ = true;
-    }
-  }
-
-  // TODO(austin): Detect if the encoder or index pulse is unplugged.
-  // TODO(austin): Detect missing counts.
-
-  if (index_pulse_count == constants_.index_pulse_count && !zeroed_) {
-    offset_ = constants_.measured_index_position -
-              constants_.known_index_pulse * constants_.index_difference -
-              min_index_position_;
-    zeroed_ = true;
-  } else if (zeroed_ && !error_) {
-    // Detect whether the index pulse is somewhere other than where we expect
-    // it to be. First we compute the position of the most recent index pulse.
-    double index_pulse_distance =
-        info.latched_encoder() + offset_ - constants_.measured_index_position;
-    // Second we compute the position of the index pulse in terms of
-    // the index difference. I.e. if this index pulse is two pulses away from
-    // the index pulse that we know about then this number should be positive
-    // or negative two.
-    double relative_distance =
-        index_pulse_distance / constants_.index_difference;
-    // Now we compute how far away the measured index pulse is from the
-    // expected index pulse.
-    double error = relative_distance - ::std::round(relative_distance);
-    // This lets us check if the index pulse is within an acceptable error
-    // margin of where we expected it to be.
-    if (::std::abs(error) > constants_.allowable_encoder_error) {
-      VLOG(1)
-          << "Encoder ticks out of range since last index pulse. known index "
-             "pulse: "
-          << constants_.measured_index_position << ", expected index pulse: "
-          << round(relative_distance) * constants_.index_difference +
-                 constants_.measured_index_position
-          << ", actual index pulse: " << info.latched_encoder() + offset_
-          << ", "
-             "allowable error: "
-          << constants_.allowable_encoder_error * constants_.index_difference;
-      error_ = true;
-    }
-  }
-
-  position_ = info.encoder() + offset_;
-}
-
-flatbuffers::Offset<PulseIndexZeroingEstimator::State>
-PulseIndexZeroingEstimator::GetEstimatorState(
-    flatbuffers::FlatBufferBuilder *fbb) const {
-  State::Builder builder(*fbb);
-  builder.add_error(error_);
-  builder.add_zeroed(zeroed_);
-  builder.add_position(position_);
-  builder.add_min_index_position(min_index_position_);
-  builder.add_max_index_position(max_index_position_);
-  builder.add_index_pulses_seen(IndexPulseCount());
-  return builder.Finish();
-}
-
-AbsoluteEncoderZeroingEstimator::AbsoluteEncoderZeroingEstimator(
-    const constants::AbsoluteEncoderZeroingConstants &constants)
-    : constants_(constants), move_detector_(constants_.moving_buffer_size) {
-  relative_to_absolute_offset_samples_.reserve(constants_.average_filter_size);
-  Reset();
-}
-
-void AbsoluteEncoderZeroingEstimator::Reset() {
-  zeroed_ = false;
-  error_ = false;
-  first_offset_ = 0.0;
-  offset_ = 0.0;
-  samples_idx_ = 0;
-  position_ = 0.0;
-  nan_samples_ = 0;
-  relative_to_absolute_offset_samples_.clear();
-  move_detector_.Reset();
-}
-
-
-// The math here is a bit backwards, but I think it'll be less error prone that
-// way and more similar to the version with a pot as well.
-//
-// We start by unwrapping the absolute encoder using the relative encoder.  This
-// puts us in a non-wrapping space and lets us average a bit easier.  From
-// there, we can compute an offset and wrap ourselves back such that we stay
-// close to the middle value.
-//
-// To guard against the robot moving while updating estimates, buffer a number
-// of samples and check that the buffered samples are not different than the
-// zeroing threshold. At any point that the samples differ too much, do not
-// update estimates based on those samples.
-void AbsoluteEncoderZeroingEstimator::UpdateEstimate(
-    const AbsolutePosition &info) {
-  // Check for Abs Encoder NaN value that would mess up the rest of the zeroing
-  // code below. NaN values are given when the Absolute Encoder is disconnected.
-  if (::std::isnan(info.absolute_encoder())) {
-    if (zeroed_) {
-      VLOG(1) << "NAN on absolute encoder.";
-      error_ = true;
-    } else {
-      ++nan_samples_;
-      VLOG(1) << "NAN on absolute encoder while zeroing " << nan_samples_;
-      if (nan_samples_ >= constants_.average_filter_size) {
-        error_ = true;
-        zeroed_ = true;
-      }
-    }
-    // Throw some dummy values in for now.
-    filtered_absolute_encoder_ = info.absolute_encoder();
-    position_ = offset_ + info.encoder();
-    return;
-  }
-
-  const bool moving = move_detector_.Update(info, constants_.moving_buffer_size,
-                                            constants_.zeroing_threshold);
-
-  if (!moving) {
-    const PositionStruct &sample = move_detector_.GetSample();
-
-    // Compute the average offset between the absolute encoder and relative
-    // encoder.  If we have 0 samples, assume it is 0.
-    double average_relative_to_absolute_offset =
-        relative_to_absolute_offset_samples_.size() == 0
-            ? 0.0
-            : ::std::accumulate(relative_to_absolute_offset_samples_.begin(),
-                                relative_to_absolute_offset_samples_.end(),
-                                0.0) /
-                  relative_to_absolute_offset_samples_.size();
-
-    // Now, compute the estimated absolute position using the previously
-    // estimated offset and the incremental encoder.
-    const double adjusted_incremental_encoder =
-        sample.encoder + average_relative_to_absolute_offset;
-
-    // Now, compute the absolute encoder value nearest to the offset relative
-    // encoder position.
-    const double adjusted_absolute_encoder =
-        UnWrap(adjusted_incremental_encoder,
-               sample.absolute_encoder - constants_.measured_absolute_position,
-               constants_.one_revolution_distance);
-
-    // We can now compute the offset now that we have unwrapped the absolute
-    // encoder.
-    const double relative_to_absolute_offset =
-        adjusted_absolute_encoder - sample.encoder;
-
-    // Add the sample and update the average with the new reading.
-    const size_t relative_to_absolute_offset_samples_size =
-        relative_to_absolute_offset_samples_.size();
-    if (relative_to_absolute_offset_samples_size <
-        constants_.average_filter_size) {
-      average_relative_to_absolute_offset =
-          (average_relative_to_absolute_offset *
-               relative_to_absolute_offset_samples_size +
-           relative_to_absolute_offset) /
-          (relative_to_absolute_offset_samples_size + 1);
-
-      relative_to_absolute_offset_samples_.push_back(
-          relative_to_absolute_offset);
-    } else {
-      average_relative_to_absolute_offset -=
-          relative_to_absolute_offset_samples_[samples_idx_] /
-          relative_to_absolute_offset_samples_size;
-      relative_to_absolute_offset_samples_[samples_idx_] =
-          relative_to_absolute_offset;
-      average_relative_to_absolute_offset +=
-          relative_to_absolute_offset /
-          relative_to_absolute_offset_samples_size;
-    }
-
-    // Drop the oldest sample when we run this function the next time around.
-    samples_idx_ = (samples_idx_ + 1) % constants_.average_filter_size;
-
-    // And our offset is the offset that gives us the position within +- ord/2
-    // of the middle position.
-    offset_ = Wrap(constants_.middle_position,
-                   average_relative_to_absolute_offset + sample.encoder,
-                   constants_.one_revolution_distance) -
-              sample.encoder;
-
-    // Reverse the math for adjusted_absolute_encoder to compute the absolute
-    // encoder. Do this by taking the adjusted encoder, and then subtracting off
-    // the second argument above, and the value that was added by Wrap.
-    filtered_absolute_encoder_ =
-        ((sample.encoder + average_relative_to_absolute_offset) -
-         (-constants_.measured_absolute_position +
-          (adjusted_absolute_encoder -
-           (sample.absolute_encoder - constants_.measured_absolute_position))));
-
-    if (offset_ready()) {
-      if (!zeroed_) {
-        first_offset_ = offset_;
-      }
-
-      if (::std::abs(first_offset_ - offset_) >
-          constants_.allowable_encoder_error *
-              constants_.one_revolution_distance) {
-        VLOG(1) << "Offset moved too far. Initial: " << first_offset_
-                << ", current " << offset_ << ", allowable change: "
-                << constants_.allowable_encoder_error *
-                       constants_.one_revolution_distance;
-        error_ = true;
-      }
-
-      zeroed_ = true;
-    }
-  }
-
-  // Update the position.
-  position_ = offset_ + info.encoder();
-}
-
-flatbuffers::Offset<AbsoluteEncoderZeroingEstimator::State>
-AbsoluteEncoderZeroingEstimator::GetEstimatorState(
-    flatbuffers::FlatBufferBuilder *fbb) const {
-  State::Builder builder(*fbb);
-  builder.add_error(error_);
-  builder.add_zeroed(zeroed_);
-  builder.add_position(position_);
-  builder.add_absolute_position(filtered_absolute_encoder_);
-  return builder.Finish();
-}
-
-}  // namespace zeroing
-}  // namespace frc971
diff --git a/frc971/zeroing/zeroing.h b/frc971/zeroing/zeroing.h
index fe1b461..4d0c1cc 100644
--- a/frc971/zeroing/zeroing.h
+++ b/frc971/zeroing/zeroing.h
@@ -58,169 +58,6 @@
       flatbuffers::FlatBufferBuilder *fbb) const = 0;
 };
 
-// Estimates the position with an incremental encoder with an index pulse and a
-// potentiometer.
-class PotAndIndexPulseZeroingEstimator
-    : public ZeroingEstimator<PotAndIndexPosition,
-                              constants::PotAndIndexPulseZeroingConstants,
-                              EstimatorState> {
- public:
-  explicit PotAndIndexPulseZeroingEstimator(
-      const constants::PotAndIndexPulseZeroingConstants &constants);
-
-  // Update the internal logic with the next sensor values.
-  void UpdateEstimate(const PotAndIndexPosition &info) override;
-
-  // Reset the internal logic so it needs to be re-zeroed.
-  void Reset() override;
-
-  // Manually trigger an internal error. This is used for testing the error
-  // logic.
-  void TriggerError() override;
-
-  bool error() const override { return error_; }
-
-  bool zeroed() const override { return zeroed_; }
-
-  double offset() const override { return offset_; }
-
-  // Returns a number between 0 and 1 that represents the percentage of the
-  // samples being used in the moving average filter. A value of 0.0 means that
-  // no samples are being used. A value of 1.0 means that the filter is using
-  // as many samples as it has room for. For example, after a Reset() this
-  // value returns 0.0. As more samples get added with UpdateEstimate(...) the
-  // return value starts increasing to 1.0.
-  double offset_ratio_ready() const {
-    return start_pos_samples_.size() /
-           static_cast<double>(constants_.average_filter_size);
-  }
-
-  // Returns true if the sample buffer is full.
-  bool offset_ready() const override {
-    return start_pos_samples_.size() == constants_.average_filter_size;
-  }
-
-  // Returns information about our current state.
-  virtual flatbuffers::Offset<State> GetEstimatorState(
-      flatbuffers::FlatBufferBuilder *fbb) const override;
-
- private:
-  // This function calculates the start position given the internal state and
-  // the provided `latched_encoder' value.
-  double CalculateStartPosition(double start_average,
-                                double latched_encoder) const;
-
-  // The zeroing constants used to describe the configuration of the system.
-  const constants::PotAndIndexPulseZeroingConstants constants_;
-
-  // The estimated position.
-  double position_;
-  // The unzeroed filtered position.
-  double filtered_position_ = 0.0;
-  // The next position in 'start_pos_samples_' to be used to store the next
-  // sample.
-  int samples_idx_;
-  // Last 'max_sample_count_' samples for start positions.
-  std::vector<double> start_pos_samples_;
-  // The estimated starting position of the mechanism. We also call this the
-  // 'offset' in some contexts.
-  double offset_;
-  // Flag for triggering logic that takes note of the current index pulse count
-  // after a reset. See `last_used_index_pulse_count_'.
-  bool wait_for_index_pulse_;
-  // After a reset we keep track of the index pulse count with this. Only after
-  // the index pulse count changes (i.e. increments at least once or wraps
-  // around) will we consider the mechanism zeroed. We also use this to store
-  // the most recent `PotAndIndexPosition::index_pulses' value when the start
-  // position was calculated. It helps us calculate the start position only on
-  // index pulses to reject corrupted intermediate data.
-  uint32_t last_used_index_pulse_count_;
-  // Marker to track whether we're fully zeroed yet or not.
-  bool zeroed_;
-  // Marker to track whether an error has occurred. This gets reset to false
-  // whenever Reset() is called.
-  bool error_;
-  // Stores the position "start_pos" variable the first time the program
-  // is zeroed.
-  double first_start_pos_;
-};
-
-// Estimates the position with an incremental encoder and a hall effect sensor.
-class HallEffectAndPositionZeroingEstimator
-    : public ZeroingEstimator<HallEffectAndPosition,
-                              constants::HallEffectZeroingConstants,
-                              HallEffectAndPositionEstimatorState> {
- public:
-  explicit HallEffectAndPositionZeroingEstimator(
-      const ZeroingConstants &constants);
-
-  // Update the internal logic with the next sensor values.
-  void UpdateEstimate(const Position &info) override;
-
-  // Reset the internal logic so it needs to be re-zeroed.
-  void Reset() override;
-
-  // Manually trigger an internal error. This is used for testing the error
-  // logic.
-  void TriggerError() override;
-
-  bool error() const override { return error_; }
-
-  bool zeroed() const override { return zeroed_; }
-
-  double offset() const override { return offset_; }
-
-  bool offset_ready() const override { return zeroed_; }
-
-  // Returns information about our current state.
-  virtual flatbuffers::Offset<State> GetEstimatorState(
-      flatbuffers::FlatBufferBuilder *fbb) const override;
-
- private:
-  // Sets the minimum and maximum posedge position values.
-  void StoreEncoderMaxAndMin(const HallEffectAndPosition &info);
-
-  // The zeroing constants used to describe the configuration of the system.
-  const ZeroingConstants constants_;
-
-  // The estimated state of the hall effect.
-  double current_ = 0.0;
-  // The estimated position.
-  double position_ = 0.0;
-  // The smallest and largest positions of the last set of encoder positions
-  // while the hall effect was low.
-  double min_low_position_;
-  double max_low_position_;
-  // If we've seen the hall effect high for enough times without going low, then
-  // we can be sure it isn't a false positive.
-  bool high_long_enough_;
-  size_t cycles_high_;
-
-  bool last_hall_ = false;
-
-  // The estimated starting position of the mechanism. We also call this the
-  // 'offset' in some contexts.
-  double offset_;
-  // Flag for triggering logic that takes note of the current posedge count
-  // after a reset. See `last_used_posedge_count_'.
-  bool initialized_;
-  // After a reset we keep track of the posedge count with this. Only after the
-  // posedge count changes (i.e. increments at least once or wraps around) will
-  // we consider the mechanism zeroed. We also use this to store the most recent
-  // `HallEffectAndPosition::posedge_count' value when the start position
-  // was calculated. It helps us calculate the start position only on posedges
-  // to reject corrupted intermediate data.
-  int32_t last_used_posedge_count_;
-  // Marker to track whether we're fully zeroed yet or not.
-  bool zeroed_;
-  // Marker to track whether an error has occurred. This gets reset to false
-  // whenever Reset() is called.
-  bool error_ = false;
-  // Stores the position "start_pos" variable the first time the program
-  // is zeroed.
-  double first_start_pos_;
-};
-
 // Class to encapsulate the logic to decide when we are moving and which samples
 // are safe to use.
 template <typename Position, typename PositionBuffer>
@@ -287,237 +124,7 @@
   size_t buffered_samples_idx_;
 };
 
-// Estimates the position with an absolute encoder which also reports
-// incremental counts, and a potentiometer.
-class PotAndAbsoluteEncoderZeroingEstimator
-    : public ZeroingEstimator<PotAndAbsolutePosition,
-                              constants::PotAndAbsoluteEncoderZeroingConstants,
-                              PotAndAbsoluteEncoderEstimatorState> {
- public:
-  explicit PotAndAbsoluteEncoderZeroingEstimator(
-      const constants::PotAndAbsoluteEncoderZeroingConstants &constants);
-
-  // Resets the internal logic so it needs to be re-zeroed.
-  void Reset() override;
-
-  // Updates the sensor values for the zeroing logic.
-  void UpdateEstimate(const PotAndAbsolutePosition &info) override;
-
-  void TriggerError() override { error_ = true; }
-
-  bool zeroed() const override { return zeroed_; }
-
-  double offset() const override { return offset_; }
-
-  bool error() const override { return error_; }
-
-  // Returns true if the sample buffer is full.
-  bool offset_ready() const override {
-    return relative_to_absolute_offset_samples_.size() ==
-               constants_.average_filter_size &&
-           offset_samples_.size() == constants_.average_filter_size;
-  }
-
-  // Returns information about our current state.
-  virtual flatbuffers::Offset<State> GetEstimatorState(
-      flatbuffers::FlatBufferBuilder *fbb) const override;
-
- private:
-  struct PositionStruct {
-    PositionStruct(const PotAndAbsolutePosition &position_buffer)
-        : absolute_encoder(position_buffer.absolute_encoder()),
-          encoder(position_buffer.encoder()),
-          pot(position_buffer.pot()) {}
-    double absolute_encoder;
-    double encoder;
-    double pot;
-  };
-
-  // The zeroing constants used to describe the configuration of the system.
-  const constants::PotAndAbsoluteEncoderZeroingConstants constants_;
-
-  // True if the mechanism is zeroed.
-  bool zeroed_;
-  // Marker to track whether an error has occurred.
-  bool error_;
-  // The first valid offset we recorded. This is only set after zeroed_ first
-  // changes to true.
-  double first_offset_;
-
-  // The filtered absolute encoder.  This is used in the status for calibration.
-  double filtered_absolute_encoder_ = 0.0;
-
-  // Samples of the offset needed to line the relative encoder up with the
-  // absolute encoder.
-  ::std::vector<double> relative_to_absolute_offset_samples_;
-  // Offset between the Pot and Relative encoder position.
-  ::std::vector<double> offset_samples_;
-
-  MoveDetector<PositionStruct, PotAndAbsolutePosition> move_detector_;
-
-  // Estimated offset between the pot and relative encoder.
-  double pot_relative_encoder_offset_ = 0;
-  // Estimated start position of the mechanism
-  double offset_ = 0;
-  // The next position in 'relative_to_absolute_offset_samples_' and
-  // 'encoder_samples_' to be used to store the next sample.
-  int samples_idx_ = 0;
-
-  size_t nan_samples_ = 0;
-
-  // The unzeroed filtered position.
-  double filtered_position_ = 0.0;
-  // The filtered position.
-  double position_ = 0.0;
-};
-
-// Zeros by seeing all the index pulses in the range of motion of the mechanism
-// and using that to figure out which index pulse is which.
-class PulseIndexZeroingEstimator
-    : public ZeroingEstimator<IndexPosition,
-                              constants::EncoderPlusIndexZeroingConstants,
-                              IndexEstimatorState> {
- public:
-  explicit PulseIndexZeroingEstimator(const ZeroingConstants &constants)
-      : constants_(constants) {
-    Reset();
-  }
-
-  // Resets the internal logic so it needs to be re-zeroed.
-  void Reset() override;
-
-  bool zeroed() const override { return zeroed_; }
-
-  // It's as ready as it'll ever be...
-  bool offset_ready() const override { return true; }
-
-  double offset() const override { return offset_; }
-
-  bool error() const override { return error_; }
-
-  // Updates the internal logic with the next sensor values.
-  void UpdateEstimate(const IndexPosition &info) override;
-
-  // Returns information about our current state.
-  virtual flatbuffers::Offset<State> GetEstimatorState(
-      flatbuffers::FlatBufferBuilder *fbb) const override;
-
-  void TriggerError() override { error_ = true; }
-
- private:
-  // Returns the current real position using the relative encoder offset.
-  double CalculateCurrentPosition(const IndexPosition &info);
-
-  // Sets the minimum and maximum index pulse position values.
-  void StoreIndexPulseMaxAndMin(const IndexPosition &info);
-
-  // Returns the number of index pulses we should have seen so far.
-  int IndexPulseCount() const;
-
-  // Contains the physical constants describing the system.
-  const ZeroingConstants constants_;
-
-  // The smallest position of all the index pulses.
-  double min_index_position_;
-  // The largest position of all the index pulses.
-  double max_index_position_;
-
-  // The estimated starting position of the mechanism.
-  double offset_;
-  // After a reset we keep track of the index pulse count with this. Only after
-  // the index pulse count changes (i.e. increments at least once or wraps
-  // around) will we consider the mechanism zeroed. We also use this to store
-  // the most recent `PotAndIndexPosition::index_pulses' value when the start
-  // position was calculated. It helps us calculate the start position only on
-  // index pulses to reject corrupted intermediate data.
-  uint32_t last_used_index_pulse_count_;
-
-  // True if we are fully zeroed.
-  bool zeroed_;
-  // Marker to track whether an error has occurred.
-  bool error_;
-
-  // The estimated position.
-  double position_;
-};
-
-// Estimates the position with an absolute encoder which also reports
-// incremental counts.  The absolute encoder can't spin more than one
-// revolution.
-class AbsoluteEncoderZeroingEstimator
-    : public ZeroingEstimator<AbsolutePosition,
-                              constants::AbsoluteEncoderZeroingConstants,
-                              AbsoluteEncoderEstimatorState> {
- public:
-  explicit AbsoluteEncoderZeroingEstimator(
-      const constants::AbsoluteEncoderZeroingConstants &constants);
-
-  // Resets the internal logic so it needs to be re-zeroed.
-  void Reset() override;
-
-  // Updates the sensor values for the zeroing logic.
-  void UpdateEstimate(const AbsolutePosition &info) override;
-
-  void TriggerError() override { error_ = true; }
-
-  bool zeroed() const override { return zeroed_; }
-
-  double offset() const override { return offset_; }
-
-  bool error() const override { return error_; }
-
-  // Returns true if the sample buffer is full.
-  bool offset_ready() const override {
-    return relative_to_absolute_offset_samples_.size() ==
-           constants_.average_filter_size;
-  }
-
-  // Returns information about our current state.
-  virtual flatbuffers::Offset<State> GetEstimatorState(
-      flatbuffers::FlatBufferBuilder *fbb) const override;
-
- private:
-  struct PositionStruct {
-    PositionStruct(const AbsolutePosition &position_buffer)
-        : absolute_encoder(position_buffer.absolute_encoder()),
-          encoder(position_buffer.encoder()) {}
-    double absolute_encoder;
-    double encoder;
-  };
-
-  // The zeroing constants used to describe the configuration of the system.
-  const constants::AbsoluteEncoderZeroingConstants constants_;
-
-  // True if the mechanism is zeroed.
-  bool zeroed_;
-  // Marker to track whether an error has occurred.
-  bool error_;
-  // The first valid offset we recorded. This is only set after zeroed_ first
-  // changes to true.
-  double first_offset_;
-
-  // The filtered absolute encoder.  This is used in the status for calibration.
-  double filtered_absolute_encoder_ = 0.0;
-
-  // Samples of the offset needed to line the relative encoder up with the
-  // absolute encoder.
-  ::std::vector<double> relative_to_absolute_offset_samples_;
-
-  MoveDetector<PositionStruct, AbsolutePosition> move_detector_;
-
-  // Estimated start position of the mechanism
-  double offset_ = 0;
-  // The next position in 'relative_to_absolute_offset_samples_' and
-  // 'encoder_samples_' to be used to store the next sample.
-  int samples_idx_ = 0;
-
-  // Number of NANs we've seen in a row.
-  size_t nan_samples_ = 0;
-
-  // The filtered position.
-  double position_ = 0.0;
-};
-
+// A trivial ZeroingEstimator which just passes the position straight through.
 class RelativeEncoderZeroingEstimator
     : public ZeroingEstimator<RelativePosition, void,
                               RelativeEncoderEstimatorState> {
@@ -562,4 +169,12 @@
 }  // namespace zeroing
 }  // namespace frc971
 
+// TODO(Brian): Actually split these targets apart. Need to convert all the
+// reverse dependencies to #include what they actually need...
+#include "frc971/zeroing/absolute_encoder.h"
+#include "frc971/zeroing/hall_effect_and_position.h"
+#include "frc971/zeroing/pot_and_absolute_encoder.h"
+#include "frc971/zeroing/pot_and_index.h"
+#include "frc971/zeroing/pulse_index.h"
+
 #endif  // FRC971_ZEROING_ZEROING_H_
diff --git a/frc971/zeroing/zeroing_test.cc b/frc971/zeroing/zeroing_test.cc
deleted file mode 100644
index 2b715ea..0000000
--- a/frc971/zeroing/zeroing_test.cc
+++ /dev/null
@@ -1,817 +0,0 @@
-#include "frc971/zeroing/zeroing.h"
-
-#include <unistd.h>
-
-#include <memory>
-#include <random>
-
-#include "aos/die.h"
-#include "frc971/control_loops/control_loops_generated.h"
-#include "frc971/control_loops/position_sensor_sim.h"
-#include "gtest/gtest.h"
-
-namespace frc971 {
-namespace zeroing {
-
-using constants::AbsoluteEncoderZeroingConstants;
-using constants::EncoderPlusIndexZeroingConstants;
-using constants::PotAndAbsoluteEncoderZeroingConstants;
-using constants::PotAndIndexPulseZeroingConstants;
-using control_loops::PositionSensorSimulator;
-using FBB = flatbuffers::FlatBufferBuilder;
-
-static const size_t kSampleSize = 30;
-static const double kAcceptableUnzeroedError = 0.2;
-static const double kIndexErrorFraction = 0.3;
-static const size_t kMovingBufferSize = 3;
-
-class ZeroingTest : public ::testing::Test {
- protected:
-  void SetUp() override {}
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              PotAndIndexPulseZeroingEstimator *estimator,
-              double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<PotAndIndexPosition>(&fbb));
-  }
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              AbsoluteEncoderZeroingEstimator *estimator, double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<AbsolutePosition>(&fbb));
-  }
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              PotAndAbsoluteEncoderZeroingEstimator *estimator,
-              double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<PotAndAbsolutePosition>(&fbb));
-  }
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              PulseIndexZeroingEstimator *estimator, double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<IndexPosition>(&fbb));
-  }
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              HallEffectAndPositionZeroingEstimator *estimator,
-              double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<HallEffectAndPosition>(&fbb));
-  }
-
-  void MoveTo(PositionSensorSimulator *simulator,
-              RelativeEncoderZeroingEstimator *estimator, double new_position) {
-    simulator->MoveTo(new_position);
-    FBB fbb;
-    estimator->UpdateEstimate(
-        *simulator->FillSensorValues<RelativePosition>(&fbb));
-  }
-
-  template <typename T>
-  double GetEstimatorPosition(T *estimator) {
-    FBB fbb;
-    fbb.Finish(estimator->GetEstimatorState(&fbb));
-    return flatbuffers::GetRoot<typename T::State>(fbb.GetBufferPointer())
-        ->position();
-  }
-};
-
-TEST_F(ZeroingTest, TestMovingAverageFilter) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.6 * index_diff, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  // The zeroing code is supposed to perform some filtering on the difference
-  // between the potentiometer value and the encoder value. We assume that 300
-  // samples are sufficient to have updated the filter.
-  for (int i = 0; i < 300; i++) {
-    MoveTo(&sim, &estimator, 3.3 * index_diff);
-  }
-  ASSERT_NEAR(3.3 * index_diff, GetEstimatorPosition(&estimator),
-              kAcceptableUnzeroedError * index_diff);
-
-  for (int i = 0; i < 300; i++) {
-    MoveTo(&sim, &estimator, 3.9 * index_diff);
-  }
-  ASSERT_NEAR(3.9 * index_diff, GetEstimatorPosition(&estimator),
-              kAcceptableUnzeroedError * index_diff);
-}
-
-TEST_F(ZeroingTest, NotZeroedBeforeEnoughSamplesCollected) {
-  double index_diff = 0.5;
-  double position = 3.6 * index_diff;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(position, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  // Make sure that the zeroing code does not consider itself zeroed until we
-  // collect a good amount of samples. In this case we're waiting until the
-  // moving average filter is full.
-  for (unsigned int i = 0; i < kSampleSize - 1; i++) {
-    MoveTo(&sim, &estimator, position += index_diff);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim, &estimator, position);
-  ASSERT_TRUE(estimator.zeroed());
-}
-
-TEST_F(ZeroingTest, TestLotsOfMovement) {
-  double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.6, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  // The zeroing code is supposed to perform some filtering on the difference
-  // between the potentiometer value and the encoder value. We assume that 300
-  // samples are sufficient to have updated the filter.
-  for (int i = 0; i < 300; i++) {
-    MoveTo(&sim, &estimator, 3.6);
-  }
-  ASSERT_NEAR(3.6, GetEstimatorPosition(&estimator),
-              kAcceptableUnzeroedError * index_diff);
-
-  // With a single index pulse the zeroing estimator should be able to lock
-  // onto the true value of the position.
-  MoveTo(&sim, &estimator, 4.01);
-  ASSERT_NEAR(4.01, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 4.99);
-  ASSERT_NEAR(4.99, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 3.99);
-  ASSERT_NEAR(3.99, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 3.01);
-  ASSERT_NEAR(3.01, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 13.55);
-  ASSERT_NEAR(13.55, GetEstimatorPosition(&estimator), 0.001);
-}
-
-TEST_F(ZeroingTest, TestDifferentIndexDiffs) {
-  double index_diff = 0.89;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.5 * index_diff, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  // The zeroing code is supposed to perform some filtering on the difference
-  // between the potentiometer value and the encoder value. We assume that 300
-  // samples are sufficient to have updated the filter.
-  for (int i = 0; i < 300; i++) {
-    MoveTo(&sim, &estimator, 3.5 * index_diff);
-  }
-  ASSERT_NEAR(3.5 * index_diff, GetEstimatorPosition(&estimator),
-              kAcceptableUnzeroedError * index_diff);
-
-  // With a single index pulse the zeroing estimator should be able to lock
-  // onto the true value of the position.
-  MoveTo(&sim, &estimator, 4.01);
-  ASSERT_NEAR(4.01, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 4.99);
-  ASSERT_NEAR(4.99, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 3.99);
-  ASSERT_NEAR(3.99, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 3.01);
-  ASSERT_NEAR(3.01, GetEstimatorPosition(&estimator), 0.001);
-
-  MoveTo(&sim, &estimator, 13.55);
-  ASSERT_NEAR(13.55, GetEstimatorPosition(&estimator), 0.001);
-}
-
-TEST_F(ZeroingTest, TestPercentage) {
-  double index_diff = 0.89;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.5 * index_diff, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  for (unsigned int i = 0; i < kSampleSize / 2; i++) {
-    MoveTo(&sim, &estimator, 3.5 * index_diff);
-  }
-  ASSERT_NEAR(0.5, estimator.offset_ratio_ready(), 0.001);
-  ASSERT_FALSE(estimator.offset_ready());
-
-  for (unsigned int i = 0; i < kSampleSize / 2; i++) {
-    MoveTo(&sim, &estimator, 3.5 * index_diff);
-  }
-  ASSERT_NEAR(1.0, estimator.offset_ratio_ready(), 0.001);
-  ASSERT_TRUE(estimator.offset_ready());
-}
-
-TEST_F(ZeroingTest, TestOffset) {
-  double index_diff = 0.89;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.1 * index_diff, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  MoveTo(&sim, &estimator, 3.1 * index_diff);
-
-  for (unsigned int i = 0; i < kSampleSize; i++) {
-    MoveTo(&sim, &estimator, 5.0 * index_diff);
-  }
-
-  ASSERT_NEAR(3.1 * index_diff, estimator.offset(), 0.001);
-}
-
-TEST_F(ZeroingTest, WaitForIndexPulseAfterReset) {
-  double index_diff = 0.6;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.1 * index_diff, index_diff / 3.0);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-
-  // Make sure to fill up the averaging filter with samples.
-  for (unsigned int i = 0; i < kSampleSize; i++) {
-    MoveTo(&sim, &estimator, 3.1 * index_diff);
-  }
-
-  // Make sure we're not zeroed until we hit an index pulse.
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Trigger an index pulse; we should now be zeroed.
-  MoveTo(&sim, &estimator, 4.5 * index_diff);
-  ASSERT_TRUE(estimator.zeroed());
-
-  // Reset the zeroing logic and supply a bunch of samples within the current
-  // index segment.
-  estimator.Reset();
-  for (unsigned int i = 0; i < kSampleSize; i++) {
-    MoveTo(&sim, &estimator, 4.2 * index_diff);
-  }
-
-  // Make sure we're not zeroed until we hit an index pulse.
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Trigger another index pulse; we should be zeroed again.
-  MoveTo(&sim, &estimator, 3.1 * index_diff);
-  ASSERT_TRUE(estimator.zeroed());
-}
-
-TEST_F(ZeroingTest, TestNonZeroIndexPulseOffsets) {
-  const double index_diff = 0.9;
-  const double known_index_pos = 3.5 * index_diff;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(3.3 * index_diff, index_diff / 3.0, known_index_pos);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, known_index_pos, kIndexErrorFraction});
-
-  // Make sure to fill up the averaging filter with samples.
-  for (unsigned int i = 0; i < kSampleSize; i++) {
-    MoveTo(&sim, &estimator, 3.3 * index_diff);
-  }
-
-  // Make sure we're not zeroed until we hit an index pulse.
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Trigger an index pulse; we should now be zeroed.
-  MoveTo(&sim, &estimator, 3.7 * index_diff);
-  ASSERT_TRUE(estimator.zeroed());
-  ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
-  ASSERT_DOUBLE_EQ(3.7 * index_diff, GetEstimatorPosition(&estimator));
-
-  // Trigger one more index pulse and check the offset.
-  MoveTo(&sim, &estimator, 4.7 * index_diff);
-  ASSERT_DOUBLE_EQ(3.3 * index_diff, estimator.offset());
-  ASSERT_DOUBLE_EQ(4.7 * index_diff, GetEstimatorPosition(&estimator));
-}
-
-TEST_F(ZeroingTest, BasicErrorAPITest) {
-  const double index_diff = 1.0;
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      kSampleSize, index_diff, 0.0, kIndexErrorFraction});
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(1.5 * index_diff, index_diff / 3.0, 0.0);
-
-  // Perform a simple move and make sure that no error occured.
-  MoveTo(&sim, &estimator, 3.5 * index_diff);
-  ASSERT_FALSE(estimator.error());
-
-  // Trigger an error and make sure it's reported.
-  estimator.TriggerError();
-  ASSERT_TRUE(estimator.error());
-
-  // Make sure that it can recover after a reset.
-  estimator.Reset();
-  ASSERT_FALSE(estimator.error());
-  MoveTo(&sim, &estimator, 4.5 * index_diff);
-  MoveTo(&sim, &estimator, 5.5 * index_diff);
-  ASSERT_FALSE(estimator.error());
-}
-
-// Tests that an error is detected when the starting position changes too much.
-TEST_F(ZeroingTest, TestIndexOffsetError) {
-  const double index_diff = 0.8;
-  const double known_index_pos = 2 * index_diff;
-  const size_t sample_size = 30;
-  PositionSensorSimulator sim(index_diff);
-  sim.Initialize(10 * index_diff, index_diff / 3.0, known_index_pos);
-  PotAndIndexPulseZeroingEstimator estimator(PotAndIndexPulseZeroingConstants{
-      sample_size, index_diff, known_index_pos, kIndexErrorFraction});
-
-  for (size_t i = 0; i < sample_size; i++) {
-    MoveTo(&sim, &estimator, 13 * index_diff);
-  }
-  MoveTo(&sim, &estimator, 8 * index_diff);
-
-  ASSERT_TRUE(estimator.zeroed());
-  ASSERT_FALSE(estimator.error());
-  sim.Initialize(9.0 * index_diff + 0.31 * index_diff, index_diff / 3.0,
-                 known_index_pos);
-  MoveTo(&sim, &estimator, 9 * index_diff);
-  ASSERT_TRUE(estimator.zeroed());
-  ASSERT_TRUE(estimator.error());
-}
-
-// Makes sure that using an absolute encoder lets us zero without moving.
-TEST_F(ZeroingTest, TestPotAndAbsoluteEncoderZeroingWithoutMovement) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double start_pos = 2.1;
-  double measured_absolute_position = 0.3 * index_diff;
-
-  PotAndAbsoluteEncoderZeroingConstants constants{
-      kSampleSize, index_diff,        measured_absolute_position,
-      0.1,         kMovingBufferSize, kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_TRUE(estimator.zeroed());
-  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
-}
-
-// Makes sure that we ignore a NAN if we get it, but will correctly zero
-// afterwards.
-TEST_F(ZeroingTest, TestPotAndAbsoluteEncoderZeroingIgnoresNAN) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double start_pos = 2.1;
-  double measured_absolute_position = 0.3 * index_diff;
-
-  PotAndAbsoluteEncoderZeroingConstants constants{
-      kSampleSize, index_diff,        measured_absolute_position,
-      0.1,         kMovingBufferSize, kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
-
-  // We tolerate a couple NANs before we start.
-  FBB fbb;
-  fbb.Finish(CreatePotAndAbsolutePosition(
-      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN(), 0.0));
-  for (size_t i = 0; i < kSampleSize - 1; ++i) {
-    estimator.UpdateEstimate(
-        *flatbuffers::GetRoot<PotAndAbsolutePosition>(fbb.GetBufferPointer()));
-  }
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_TRUE(estimator.zeroed());
-  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
-}
-
-// Makes sure that using an absolute encoder doesn't let us zero while moving.
-TEST_F(ZeroingTest, TestPotAndAbsoluteEncoderZeroingWithMovement) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double start_pos = 10 * index_diff;
-  double measured_absolute_position = 0.3 * index_diff;
-
-  PotAndAbsoluteEncoderZeroingConstants constants{
-      kSampleSize, index_diff,        measured_absolute_position,
-      0.1,         kMovingBufferSize, kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos + i * index_diff);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-  MoveTo(&sim, &estimator, start_pos + 10 * index_diff);
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_FALSE(estimator.zeroed());
-}
-
-// Makes sure we detect an error if the ZeroingEstimator gets sent a NaN.
-TEST_F(ZeroingTest, TestPotAndAbsoluteEncoderZeroingWithNaN) {
-  PotAndAbsoluteEncoderZeroingConstants constants{
-      kSampleSize, 1, 0.3, 0.1, kMovingBufferSize, kIndexErrorFraction};
-
-  PotAndAbsoluteEncoderZeroingEstimator estimator(constants);
-
-  FBB fbb;
-  fbb.Finish(CreatePotAndAbsolutePosition(
-      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN(), 0.0));
-  const auto sensor_values =
-      flatbuffers::GetRoot<PotAndAbsolutePosition>(fbb.GetBufferPointer());
-  for (size_t i = 0; i < kSampleSize - 1; ++i) {
-    estimator.UpdateEstimate(*sensor_values);
-  }
-  ASSERT_FALSE(estimator.error());
-
-  estimator.UpdateEstimate(*sensor_values);
-  ASSERT_TRUE(estimator.error());
-}
-
-// Tests that an error is detected when the starting position changes too much.
-TEST_F(ZeroingTest, TestRelativeEncoderZeroing) {
-  EncoderPlusIndexZeroingConstants constants;
-  constants.index_pulse_count = 3;
-  constants.index_difference = 10.0;
-  constants.measured_index_position = 20.0;
-  constants.known_index_pulse = 1;
-  constants.allowable_encoder_error = 0.01;
-
-  PositionSensorSimulator sim(constants.index_difference);
-
-  const double start_pos = 2.5 * constants.index_difference;
-
-  sim.Initialize(start_pos, constants.index_difference / 3.0,
-                 constants.measured_index_position);
-
-  PulseIndexZeroingEstimator estimator(constants);
-
-  // Should not be zeroed when we stand still.
-  for (int i = 0; i < 300; ++i) {
-    MoveTo(&sim, &estimator, start_pos);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  // Move to 1.5 constants.index_difference and we should still not be zeroed.
-  MoveTo(&sim, &estimator, 1.5 * constants.index_difference);
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Move to 0.5 constants.index_difference and we should still not be zeroed.
-  MoveTo(&sim, &estimator, 0.5 * constants.index_difference);
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Move back to 1.5 constants.index_difference and we should still not be
-  // zeroed.
-  MoveTo(&sim, &estimator, 1.5 * constants.index_difference);
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Move back to 2.5 constants.index_difference and we should still not be
-  // zeroed.
-  MoveTo(&sim, &estimator, 2.5 * constants.index_difference);
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Move back to 3.5 constants.index_difference and we should now be zeroed.
-  MoveTo(&sim, &estimator, 3.5 * constants.index_difference);
-  ASSERT_TRUE(estimator.zeroed());
-
-  ASSERT_DOUBLE_EQ(start_pos, estimator.offset());
-  ASSERT_DOUBLE_EQ(3.5 * constants.index_difference,
-                   GetEstimatorPosition(&estimator));
-
-  MoveTo(&sim, &estimator, 0.5 * constants.index_difference);
-  ASSERT_DOUBLE_EQ(0.5 * constants.index_difference,
-                   GetEstimatorPosition(&estimator));
-}
-
-// Tests that we can detect when an index pulse occurs where we didn't expect
-// it to for the PulseIndexZeroingEstimator.
-TEST_F(ZeroingTest, TestRelativeEncoderSlipping) {
-  EncoderPlusIndexZeroingConstants constants;
-  constants.index_pulse_count = 3;
-  constants.index_difference = 10.0;
-  constants.measured_index_position = 20.0;
-  constants.known_index_pulse = 1;
-  constants.allowable_encoder_error = 0.05;
-
-  PositionSensorSimulator sim(constants.index_difference);
-
-  const double start_pos =
-      constants.measured_index_position + 0.5 * constants.index_difference;
-
-  for (double direction : {1.0, -1.0}) {
-    sim.Initialize(start_pos, constants.index_difference / 3.0,
-                   constants.measured_index_position);
-
-    PulseIndexZeroingEstimator estimator(constants);
-
-    // Zero the estimator.
-    MoveTo(&sim, &estimator, start_pos - 1 * constants.index_difference);
-    MoveTo(
-        &sim, &estimator,
-        start_pos - constants.index_pulse_count * constants.index_difference);
-    ASSERT_TRUE(estimator.zeroed());
-    ASSERT_FALSE(estimator.error());
-
-    // We have a 5% allowable error so we slip a little bit each time and make
-    // sure that the index pulses are still accepted.
-    for (double error = 0.00;
-         ::std::abs(error) < constants.allowable_encoder_error;
-         error += 0.01 * direction) {
-      sim.Initialize(start_pos, constants.index_difference / 3.0,
-                     constants.measured_index_position +
-                         error * constants.index_difference);
-      MoveTo(&sim, &estimator, start_pos - constants.index_difference);
-      EXPECT_FALSE(estimator.error());
-    }
-
-    // As soon as we hit cross the error margin, we should trigger an error.
-    sim.Initialize(start_pos, constants.index_difference / 3.0,
-                   constants.measured_index_position +
-                       constants.allowable_encoder_error * 1.1 *
-                           constants.index_difference * direction);
-    MoveTo(&sim, &estimator, start_pos - constants.index_difference);
-    ASSERT_TRUE(estimator.error());
-  }
-}
-
-// Test fixture for HallEffectAndPositionZeroingEstimator.
-class HallEffectAndPositionZeroingEstimatorTest : public ZeroingTest {
- public:
-  // The starting position of the system.
-  static constexpr double kStartPosition = 2.0;
-
-  // Returns a reasonable set of test constants.
-  static constants::HallEffectZeroingConstants MakeConstants() {
-    constants::HallEffectZeroingConstants constants;
-    constants.lower_hall_position = 0.25;
-    constants.upper_hall_position = 0.75;
-    constants.index_difference = 1.0;
-    constants.hall_trigger_zeroing_length = 2;
-    constants.zeroing_move_direction = false;
-    return constants;
-  }
-
-  HallEffectAndPositionZeroingEstimatorTest()
-      : constants_(MakeConstants()), sim_(constants_.index_difference) {
-    // Start the system out at the starting position.
-    sim_.InitializeHallEffectAndPosition(kStartPosition,
-                                         constants_.lower_hall_position,
-                                         constants_.upper_hall_position);
-  }
-
- protected:
-  // Constants, and the simulation using them.
-  const constants::HallEffectZeroingConstants constants_;
-  PositionSensorSimulator sim_;
-};
-
-// Tests that an error is detected when the starting position changes too much.
-TEST_F(HallEffectAndPositionZeroingEstimatorTest, TestHallEffectZeroing) {
-  HallEffectAndPositionZeroingEstimator estimator(constants_);
-
-  // Should not be zeroed when we stand still.
-  for (int i = 0; i < 300; ++i) {
-    MoveTo(&sim_, &estimator, kStartPosition);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim_, &estimator, 1.9);
-  ASSERT_FALSE(estimator.zeroed());
-
-  // Move to where the hall effect is triggered and make sure it becomes zeroed.
-  MoveTo(&sim_, &estimator, 1.5);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 1.5);
-  ASSERT_TRUE(estimator.zeroed());
-
-  // Check that the offset is calculated correctly.  We should expect to read
-  // 0.5.  Since the encoder is reading -0.5 right now, the offset needs to be
-  // 1.
-  EXPECT_DOUBLE_EQ(1.0, estimator.offset());
-
-  // Make sure triggering errors works.
-  estimator.TriggerError();
-  ASSERT_TRUE(estimator.error());
-
-  // Ensure resetting resets the state of the estimator.
-  estimator.Reset();
-  ASSERT_FALSE(estimator.zeroed());
-  ASSERT_FALSE(estimator.error());
-}
-
-// Tests that we don't zero on a too short pulse.
-TEST_F(HallEffectAndPositionZeroingEstimatorTest, TestTooShortPulse) {
-  HallEffectAndPositionZeroingEstimator estimator(constants_);
-
-  // Trigger for 1 cycle.
-  MoveTo(&sim_, &estimator, 0.9);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.5);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.9);
-  EXPECT_FALSE(estimator.zeroed());
-}
-
-// Tests that we don't zero when we go the wrong direction.
-TEST_F(HallEffectAndPositionZeroingEstimatorTest, TestWrongDirectionNoZero) {
-  HallEffectAndPositionZeroingEstimator estimator(constants_);
-
-  // Pass through the sensor, lingering long enough that we should zero.
-  MoveTo(&sim_, &estimator, 0.0);
-  ASSERT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.4);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.6);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.7);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.9);
-  EXPECT_FALSE(estimator.zeroed());
-}
-
-// Make sure we don't zero if we start in the hall effect's range.
-TEST_F(HallEffectAndPositionZeroingEstimatorTest, TestStartingOnNoZero) {
-  HallEffectAndPositionZeroingEstimator estimator(constants_);
-  MoveTo(&sim_, &estimator, 0.5);
-  estimator.Reset();
-
-  // Stay on the hall effect.  We shouldn't zero.
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.5);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.5);
-  EXPECT_FALSE(estimator.zeroed());
-
-  // Verify moving off the hall still doesn't zero us.
-  MoveTo(&sim_, &estimator, 0.0);
-  EXPECT_FALSE(estimator.zeroed());
-  MoveTo(&sim_, &estimator, 0.0);
-  EXPECT_FALSE(estimator.zeroed());
-}
-
-// Makes sure that using an absolute encoder lets us zero without moving.
-TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingWithoutMovement) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double kMiddlePosition = 2.5;
-  const double start_pos = 2.1;
-  double measured_absolute_position = 0.3 * index_diff;
-
-  AbsoluteEncoderZeroingConstants constants{
-      kSampleSize,        index_diff, measured_absolute_position,
-      kMiddlePosition,    0.1,        kMovingBufferSize,
-      kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  AbsoluteEncoderZeroingEstimator estimator(constants);
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_TRUE(estimator.zeroed());
-  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
-}
-
-// Makes sure that we ignore a NAN if we get it, but will correctly zero
-// afterwards.
-TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingIgnoresNAN) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double start_pos = 2.1;
-  double measured_absolute_position = 0.3 * index_diff;
-  const double kMiddlePosition = 2.5;
-
-  AbsoluteEncoderZeroingConstants constants{
-      kSampleSize,        index_diff, measured_absolute_position,
-      kMiddlePosition,    0.1,        kMovingBufferSize,
-      kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  AbsoluteEncoderZeroingEstimator estimator(constants);
-
-  // We tolerate a couple NANs before we start.
-  FBB fbb;
-  fbb.Finish(CreateAbsolutePosition(
-      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN()));
-  const auto sensor_values =
-      flatbuffers::GetRoot<AbsolutePosition>(fbb.GetBufferPointer());
-  for (size_t i = 0; i < kSampleSize - 1; ++i) {
-    estimator.UpdateEstimate(*sensor_values);
-  }
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_TRUE(estimator.zeroed());
-  EXPECT_DOUBLE_EQ(start_pos, estimator.offset());
-}
-
-// Makes sure that using an absolute encoder doesn't let us zero while moving.
-TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingWithMovement) {
-  const double index_diff = 1.0;
-  PositionSensorSimulator sim(index_diff);
-
-  const double start_pos = 10 * index_diff;
-  double measured_absolute_position = 0.3 * index_diff;
-  const double kMiddlePosition = 2.5;
-
-  AbsoluteEncoderZeroingConstants constants{
-      kSampleSize,        index_diff, measured_absolute_position,
-      kMiddlePosition,    0.1,        kMovingBufferSize,
-      kIndexErrorFraction};
-
-  sim.Initialize(start_pos, index_diff / 3.0, 0.0,
-                 constants.measured_absolute_position);
-
-  AbsoluteEncoderZeroingEstimator estimator(constants);
-
-  for (size_t i = 0; i < kSampleSize + kMovingBufferSize - 1; ++i) {
-    MoveTo(&sim, &estimator, start_pos + i * index_diff);
-    ASSERT_FALSE(estimator.zeroed());
-  }
-  MoveTo(&sim, &estimator, start_pos + 10 * index_diff);
-
-  MoveTo(&sim, &estimator, start_pos);
-  ASSERT_FALSE(estimator.zeroed());
-}
-
-// Makes sure we detect an error if the ZeroingEstimator gets sent a NaN.
-TEST_F(ZeroingTest, TestAbsoluteEncoderZeroingWithNaN) {
-  AbsoluteEncoderZeroingConstants constants{
-      kSampleSize, 1, 0.3, 1.0, 0.1, kMovingBufferSize, kIndexErrorFraction};
-
-  AbsoluteEncoderZeroingEstimator estimator(constants);
-
-  FBB fbb;
-  fbb.Finish(CreateAbsolutePosition(
-      fbb, 0.0, ::std::numeric_limits<double>::quiet_NaN()));
-  const auto sensor_values =
-      flatbuffers::GetRoot<AbsolutePosition>(fbb.GetBufferPointer());
-  for (size_t i = 0; i < kSampleSize - 1; ++i) {
-    estimator.UpdateEstimate(*sensor_values);
-  }
-  ASSERT_FALSE(estimator.error());
-
-  estimator.UpdateEstimate(*sensor_values);
-  ASSERT_TRUE(estimator.error());
-}
-
-TEST_F(ZeroingTest, TestRelativeEncoderZeroingWithoutMovement) {
-  PositionSensorSimulator sim(1.0);
-  RelativeEncoderZeroingEstimator estimator;
-
-  sim.InitializeRelativeEncoder();
-
-  ASSERT_TRUE(estimator.zeroed());
-  ASSERT_TRUE(estimator.offset_ready());
-  EXPECT_DOUBLE_EQ(estimator.offset(), 0.0);
-  EXPECT_DOUBLE_EQ(GetEstimatorPosition(&estimator), 0.0);
-
-  MoveTo(&sim, &estimator, 0.1);
-
-  EXPECT_DOUBLE_EQ(GetEstimatorPosition(&estimator), 0.1);
-}
-
-}  // namespace zeroing
-}  // namespace frc971
diff --git a/frc971/zeroing/zeroing_test.h b/frc971/zeroing/zeroing_test.h
new file mode 100644
index 0000000..9b7ba95
--- /dev/null
+++ b/frc971/zeroing/zeroing_test.h
@@ -0,0 +1,32 @@
+#include "frc971/zeroing/zeroing.h"
+
+#include "frc971/control_loops/control_loops_generated.h"
+#include "frc971/control_loops/position_sensor_sim.h"
+#include "gtest/gtest.h"
+
+namespace frc971 {
+namespace zeroing {
+namespace testing {
+
+using control_loops::PositionSensorSimulator;
+using FBB = flatbuffers::FlatBufferBuilder;
+
+constexpr size_t kSampleSize = 30;
+constexpr double kAcceptableUnzeroedError = 0.2;
+constexpr double kIndexErrorFraction = 0.3;
+constexpr size_t kMovingBufferSize = 3;
+
+class ZeroingTest : public ::testing::Test {
+ protected:
+  template <typename T>
+  double GetEstimatorPosition(T *estimator) {
+    FBB fbb;
+    fbb.Finish(estimator->GetEstimatorState(&fbb));
+    return flatbuffers::GetRoot<typename T::State>(fbb.GetBufferPointer())
+        ->position();
+  }
+};
+
+}  // namespace testing
+}  // namespace zeroing
+}  // namespace frc971
diff --git a/third_party/BUILD b/third_party/BUILD
index fd10d43..bcba978 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -44,6 +44,20 @@
 )
 
 cc_library(
+    name = "gstreamer",
+    restricted_to = [
+        "//tools:k8",
+        "//tools:armhf-debian",
+    ],
+    visibility = ["//visibility:public"],
+    deps = select({
+        "//tools:cpu_k8": ["@gstreamer_k8//:gstreamer"],
+        "//tools:cpu_armhf": ["@gstreamer_armhf//:gstreamer"],
+        "//conditions:default": [],
+    }),
+)
+
+cc_library(
     name = "halide",
     restricted_to = [
         "//tools:k8",
diff --git a/third_party/google-glog/src/logging.cc b/third_party/google-glog/src/logging.cc
index ad4047d..2bfce3d 100644
--- a/third_party/google-glog/src/logging.cc
+++ b/third_party/google-glog/src/logging.cc
@@ -1459,6 +1459,13 @@
   // someone else can use them (as long as they flush afterwards)
   if (data_->severity_ == GLOG_FATAL && exit_on_dfatal) {
     if (data_->first_fatal_) {
+      {
+        // Put this back on SCHED_OTHER by default.
+        struct sched_param param;
+        param.sched_priority = 0;
+        sched_setscheduler(0, SCHED_OTHER, &param);
+      }
+
       // Store crash information so that it is accessible from within signal
       // handlers that may be invoked later.
       RecordCrashReason(&crash_reason);
diff --git a/third_party/google-glog/src/signalhandler.cc b/third_party/google-glog/src/signalhandler.cc
index c3a0f80..049efa5 100644
--- a/third_party/google-glog/src/signalhandler.cc
+++ b/third_party/google-glog/src/signalhandler.cc
@@ -308,6 +308,14 @@
       sleep(1);
     }
   }
+
+  {
+    // Put this back on SCHED_OTHER by default.
+    struct sched_param param;
+    param.sched_priority = 0;
+    sched_setscheduler(0, SCHED_OTHER, &param);
+  }
+
   // This is the first time we enter the signal handler.  We are going to
   // do some interesting stuff from here.
   // TODO(satorux): We might want to set timeout here using alarm(), but
diff --git a/y2019/control_loops/drivetrain/localizer_test.cc b/y2019/control_loops/drivetrain/localizer_test.cc
index 7b25ca5..e3b6398 100644
--- a/y2019/control_loops/drivetrain/localizer_test.cc
+++ b/y2019/control_loops/drivetrain/localizer_test.cc
@@ -599,7 +599,7 @@
             /*noisify=*/true,
             /*disturb=*/false,
             /*estimate_tolerance=*/0.4,
-            /*goal_tolerance=*/0.4,
+            /*goal_tolerance=*/0.8,
         }),
         // Repeats perfect scenario, but add initial estimator error.
         LocalizerTestParams({
@@ -629,7 +629,7 @@
             /*noisify=*/false,
             /*disturb=*/false,
             /*estimate_tolerance=*/1e-2,
-            /*goal_tolerance=*/2e-2,
+            /*goal_tolerance=*/3e-2,
         }),
         // Add disturbances while we are driving:
         LocalizerTestParams({
diff --git a/y2020/BUILD b/y2020/BUILD
index 320a0cf..5b7560d 100644
--- a/y2020/BUILD
+++ b/y2020/BUILD
@@ -24,6 +24,9 @@
 
 robot_downloader(
     name = "pi_download",
+    binaries = [
+        "//y2020/vision:viewer",
+    ],
     data = [
         ":config.json",
     ],
diff --git a/y2020/constants.cc b/y2020/constants.cc
index 463919a..eefc158 100644
--- a/y2020/constants.cc
+++ b/y2020/constants.cc
@@ -76,7 +76,7 @@
 
   // Turret Constants
   turret_params->zeroing_voltage = 4.0;
-  turret_params->operating_voltage = 12.0;
+  turret_params->operating_voltage = 8.0;
   // TODO(austin): Tune these.
   turret_params->zeroing_profile_params = {0.5, 2.0};
   turret_params->default_profile_params = {15.0, 40.0};
diff --git a/y2020/control_loops/python/flywheel.py b/y2020/control_loops/python/flywheel.py
index 451788a..f284450 100755
--- a/y2020/control_loops/python/flywheel.py
+++ b/y2020/control_loops/python/flywheel.py
@@ -295,6 +295,18 @@
     loop_writer.AddConstant(
         control_loop.Constant('kFreeSpeed', '%f',
                               flywheels[0].motor.free_speed))
+    loop_writer.AddConstant(
+        control_loop.Constant(
+            'kBemf',
+            '%f',
+            flywheels[0].motor.Kv * flywheels[0].G,
+            comment="// Radians/sec / volt"))
+    loop_writer.AddConstant(
+        control_loop.Constant(
+            'kResistance',
+            '%f',
+            flywheels[0].motor.resistance,
+            comment="// Ohms"))
     loop_writer.Write(plant_files[0], plant_files[1])
 
     integral_loop_writer = control_loop.ControlLoopWriter(
diff --git a/y2020/control_loops/superstructure/shooter/flywheel_controller.cc b/y2020/control_loops/superstructure/shooter/flywheel_controller.cc
index 07c3d99..a8921d9 100644
--- a/y2020/control_loops/superstructure/shooter/flywheel_controller.cc
+++ b/y2020/control_loops/superstructure/shooter/flywheel_controller.cc
@@ -11,12 +11,56 @@
 namespace superstructure {
 namespace shooter {
 
+// Class to current limit battery current for a flywheel controller.
+class CurrentLimitedStateFeedbackController
+    : public StateFeedbackLoop<3, 1, 1, double,
+                               StateFeedbackHybridPlant<3, 1, 1>,
+                               HybridKalman<3, 1, 1>> {
+ public:
+  // Builds a CurrentLimitedStateFeedbackController given the coefficients, bemf
+  // coefficient (units of radians/sec / volt), and motor resistance in ohms.
+  CurrentLimitedStateFeedbackController(
+      StateFeedbackLoop<3, 1, 1, double, StateFeedbackHybridPlant<3, 1, 1>,
+                        HybridKalman<3, 1, 1>> &&other,
+      double bemf, double resistance)
+      : StateFeedbackLoop(std::move(other)),
+        bemf_(bemf),
+        resistance_(resistance) {}
+
+  void CapU() override {
+    const double bemf_voltage = X_hat(1) / bemf_;
+    // Solve the system of equations:
+    //
+    //   motor_current = (u - bemf_voltage) / resistance
+    //   battery_current = ((u - bemf_voltage) / resistance) * u / 12.0
+    //   0.0 = u * u - u * bemf_voltage - max_current * 12.0 * resistance
+    //
+    // And we have a quadratic!
+    const double a = 1;
+    const double b = -bemf_voltage;
+    const double c = -50.0 * 12.0 * resistance_;
+
+    // Root is always positive.
+    const double root = std::sqrt(b * b - 4.0 * a * c);
+    const double upper_limit = (-b + root) / (2.0 * a);
+    const double lower_limit = (-b - root) / (2.0 * a);
+
+    // Limit to the battery voltage and the current limit voltage.
+    mutable_U(0, 0) = std::clamp(U(0, 0), lower_limit, upper_limit);
+    mutable_U(0, 0) = std::clamp(U(0, 0), -12.0, 12.0);
+  }
+
+ private:
+  double bemf_ = 0.0;
+  double resistance_ = 0.0;
+};
+
 FlywheelController::FlywheelController(
     StateFeedbackLoop<3, 1, 1, double, StateFeedbackHybridPlant<3, 1, 1>,
-                      HybridKalman<3, 1, 1>> &&loop)
-    : loop_(new StateFeedbackLoop<3, 1, 1, double,
-                                  StateFeedbackHybridPlant<3, 1, 1>,
-                                  HybridKalman<3, 1, 1>>(std::move(loop))) {
+                      HybridKalman<3, 1, 1>> &&loop,
+    double bemf, double resistance)
+    : loop_(new CurrentLimitedStateFeedbackController(std::move(loop), bemf,
+                                                      resistance)) {
   history_.fill(std::pair<double, ::aos::monotonic_clock::time_point>(
       0, ::aos::monotonic_clock::epoch()));
   Y_.setZero();
diff --git a/y2020/control_loops/superstructure/shooter/flywheel_controller.h b/y2020/control_loops/superstructure/shooter/flywheel_controller.h
index e130389..d5f7ede 100644
--- a/y2020/control_loops/superstructure/shooter/flywheel_controller.h
+++ b/y2020/control_loops/superstructure/shooter/flywheel_controller.h
@@ -20,7 +20,8 @@
  public:
   FlywheelController(
       StateFeedbackLoop<3, 1, 1, double, StateFeedbackHybridPlant<3, 1, 1>,
-                        HybridKalman<3, 1, 1>> &&loop);
+                        HybridKalman<3, 1, 1>> &&loop,
+      double bemf, double resistance);
 
   // Sets the velocity goal in radians/sec
   void set_goal(double angular_velocity_goal);
diff --git a/y2020/control_loops/superstructure/shooter/shooter.cc b/y2020/control_loops/superstructure/shooter/shooter.cc
index a9f1c4c..6e48a21 100644
--- a/y2020/control_loops/superstructure/shooter/shooter.cc
+++ b/y2020/control_loops/superstructure/shooter/shooter.cc
@@ -16,9 +16,12 @@
 }  // namespace
 
 Shooter::Shooter()
-    : finisher_(finisher::MakeIntegralFinisherLoop()),
-      accelerator_left_(accelerator::MakeIntegralAcceleratorLoop()),
-      accelerator_right_(accelerator::MakeIntegralAcceleratorLoop()) {}
+    : finisher_(finisher::MakeIntegralFinisherLoop(), finisher::kBemf,
+                finisher::kResistance),
+      accelerator_left_(accelerator::MakeIntegralAcceleratorLoop(),
+                        accelerator::kBemf, accelerator::kResistance),
+      accelerator_right_(accelerator::MakeIntegralAcceleratorLoop(),
+                         accelerator::kBemf, accelerator::kResistance) {}
 
 bool Shooter::UpToSpeed(const ShooterGoal *goal) {
   return (
diff --git a/y2020/control_loops/superstructure/superstructure.cc b/y2020/control_loops/superstructure/superstructure.cc
index 8947f85..0d740e9 100644
--- a/y2020/control_loops/superstructure/superstructure.cc
+++ b/y2020/control_loops/superstructure/superstructure.cc
@@ -40,7 +40,8 @@
 
   if (drivetrain_status_fetcher_.Fetch()) {
     aos::Alliance alliance = aos::Alliance::kInvalid;
-    if (joystick_state_fetcher_.Fetch()) {
+    joystick_state_fetcher_.Fetch();
+    if (joystick_state_fetcher_.get() != nullptr) {
       alliance = joystick_state_fetcher_->alliance();
     }
     const turret::Aimer::WrapMode mode =
@@ -120,7 +121,8 @@
 
   if (output != nullptr) {
     // Friction is a pain and putting a really high burden on the integrator.
-    const double turret_velocity_sign = turret_status->velocity() * kTurretFrictionGain;
+    const double turret_velocity_sign =
+        turret_status->velocity() * kTurretFrictionGain;
     output_struct.turret_voltage +=
         std::clamp(turret_velocity_sign, -kTurretFrictionVoltageLimit,
                    kTurretFrictionVoltageLimit);
diff --git a/y2020/control_loops/superstructure/superstructure_lib_test.cc b/y2020/control_loops/superstructure/superstructure_lib_test.cc
index e8f1f8d..1d42d23 100644
--- a/y2020/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2020/control_loops/superstructure/superstructure_lib_test.cc
@@ -43,14 +43,25 @@
 
 class FlywheelPlant : public StateFeedbackPlant<2, 1, 1> {
  public:
-  explicit FlywheelPlant(StateFeedbackPlant<2, 1, 1> &&other)
-      : StateFeedbackPlant<2, 1, 1>(::std::move(other)) {}
+  explicit FlywheelPlant(StateFeedbackPlant<2, 1, 1> &&other, double bemf,
+                         double resistance)
+      : StateFeedbackPlant<2, 1, 1>(::std::move(other)),
+        bemf_(bemf),
+        resistance_(resistance) {}
 
   void CheckU(const Eigen::Matrix<double, 1, 1> &U) override {
     EXPECT_LE(U(0, 0), U_max(0, 0) + 0.00001 + voltage_offset_);
     EXPECT_GE(U(0, 0), U_min(0, 0) - 0.00001 + voltage_offset_);
   }
 
+  double motor_current(const Eigen::Matrix<double, 1, 1> U) const {
+    return (U(0) - X(1) / bemf_) / resistance_;
+  }
+
+  double battery_current(const Eigen::Matrix<double, 1, 1> U) const {
+    return motor_current(U) * U(0) / 12.0;
+  }
+
   double voltage_offset() const { return voltage_offset_; }
   void set_voltage_offset(double voltage_offset) {
     voltage_offset_ = voltage_offset;
@@ -58,6 +69,9 @@
 
  private:
   double voltage_offset_ = 0.0;
+
+  double bemf_;
+  double resistance_;
 };
 
 // Class which simulates the superstructure and sends out queue messages with
@@ -84,10 +98,14 @@
                             .turret.subsystem_params.zeroing_constants
                             .one_revolution_distance),
         accelerator_left_plant_(
-            new FlywheelPlant(accelerator::MakeAcceleratorPlant())),
+            new FlywheelPlant(accelerator::MakeAcceleratorPlant(),
+                              accelerator::kBemf, accelerator::kResistance)),
         accelerator_right_plant_(
-            new FlywheelPlant(accelerator::MakeAcceleratorPlant())),
-        finisher_plant_(new FlywheelPlant(finisher::MakeFinisherPlant())) {
+            new FlywheelPlant(accelerator::MakeAcceleratorPlant(),
+                              accelerator::kBemf, accelerator::kResistance)),
+        finisher_plant_(new FlywheelPlant(finisher::MakeFinisherPlant(),
+                                          finisher::kBemf,
+                                          finisher::kResistance)) {
     InitializeHoodPosition(constants::Values::kHoodRange().upper);
     InitializeIntakePosition(constants::Values::kIntakeRange().upper);
     InitializeTurretPosition(constants::Values::kTurretRange().middle());
@@ -264,15 +282,26 @@
         << superstructure_output_fetcher_->accelerator_left_voltage() +
                accelerator_left_plant_->voltage_offset();
 
+    // Confirm that we aren't drawing too much current.
+    CHECK_NEAR(accelerator_left_plant_->battery_current(accelerator_left_U),
+               0.0, 60.0);
+
     ::Eigen::Matrix<double, 1, 1> accelerator_right_U;
     accelerator_right_U
         << superstructure_output_fetcher_->accelerator_right_voltage() +
                accelerator_right_plant_->voltage_offset();
 
+    // Confirm that we aren't drawing too much current.
+    CHECK_NEAR(accelerator_right_plant_->battery_current(accelerator_right_U),
+               0.0, 60.0);
+
     ::Eigen::Matrix<double, 1, 1> finisher_U;
     finisher_U << superstructure_output_fetcher_->finisher_voltage() +
                       finisher_plant_->voltage_offset();
 
+    // Confirm that we aren't drawing too much current.
+    CHECK_NEAR(finisher_plant_->battery_current(finisher_U), 0.0, 60.0);
+
     hood_plant_->Update(hood_U);
     intake_plant_->Update(intake_U);
     turret_plant_->Update(turret_U);
diff --git a/y2020/control_loops/superstructure/superstructure_status.fbs b/y2020/control_loops/superstructure/superstructure_status.fbs
index 81293d3..dc6116c 100644
--- a/y2020/control_loops/superstructure/superstructure_status.fbs
+++ b/y2020/control_loops/superstructure/superstructure_status.fbs
@@ -36,6 +36,11 @@
   turret_velocity:double;
   // Whether we are currently aiming for the inner port.
   aiming_for_inner_port:bool;
+  // The current distance to the target, in meters.
+  target_distance:double;
+  // The current "shot distance." When shooting on the fly, this may be
+  // different from the static distance to the target.
+  shot_distance:double;
 }
 
 table Status {
diff --git a/y2020/control_loops/superstructure/turret/aiming.cc b/y2020/control_loops/superstructure/turret/aiming.cc
index 360fd1a..0ce4871 100644
--- a/y2020/control_loops/superstructure/turret/aiming.cc
+++ b/y2020/control_loops/superstructure/turret/aiming.cc
@@ -50,8 +50,10 @@
 // outer port entirely.
 constexpr double kMaxInnerPortAngle = 20.0 * M_PI / 180.0;
 
-// Distance (in meters) from the edge of the field to the port.
-constexpr double kEdgeOfFieldToPort = 2.404;
+// Distance (in meters) from the edge of the field to the port, with some
+// compensation to ensure that our definition of where the target is matches
+// that reported by the cameras.
+constexpr double kEdgeOfFieldToPort = 2.404 + .0034;
 
 // The amount (in meters) that the inner port is set back from the outer port.
 constexpr double kInnerPortBackset = 0.743;
@@ -70,7 +72,7 @@
 // Minimum distance that we must be from the inner port in order to attempt the
 // shot--this is to account for the fact that if we are too close to the target,
 // then we won't have a clear shot on the inner port.
-constexpr double kMinimumInnerPortShotDistance = 4.0;
+constexpr double kMinimumInnerPortShotDistance = 3.0;
 
 // Amount of buffer, in radians, to leave to help avoid wrapping. I.e., any time
 // that we are in kAvoidEdges mode, we will keep ourselves at least
@@ -123,7 +125,7 @@
 Pose InnerPortPose(aos::Alliance alliance) {
   const Pose target({kFieldLength / 2 + kInnerPortBackset,
                      -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight},
-                    0.0);
+                    M_PI);
   if (alliance == aos::Alliance::kRed) {
     return ReverseSideOfField(target);
   }
@@ -133,7 +135,7 @@
 Pose OuterPortPose(aos::Alliance alliance) {
   Pose target(
       {kFieldLength / 2, -kFieldWidth / 2.0 + kEdgeOfFieldToPort, kPortHeight},
-      0.0);
+      M_PI);
   if (alliance == aos::Alliance::kRed) {
     return ReverseSideOfField(target);
   }
@@ -164,9 +166,16 @@
 
   const double inner_port_angle = robot_pose_from_inner_port.heading();
   const double inner_port_distance = robot_pose_from_inner_port.xy_norm();
+  // Add a bit of hysteresis so that we don't jump between aiming for the inner
+  // and outer ports.
+  const double max_inner_port_angle =
+      aiming_for_inner_port_ ? 1.2 * kMaxInnerPortAngle : kMaxInnerPortAngle;
+  const double min_inner_port_distance =
+      aiming_for_inner_port_ ? 0.8 * kMinimumInnerPortShotDistance
+                             : kMinimumInnerPortShotDistance;
   aiming_for_inner_port_ =
-      (std::abs(inner_port_angle) < kMaxInnerPortAngle) &&
-      (inner_port_distance > kMinimumInnerPortShotDistance);
+      (std::abs(inner_port_angle) < max_inner_port_angle) &&
+      (inner_port_distance > min_inner_port_distance);
 
   // This code manages compensating the goal turret heading for the robot's
   // current velocity, to allow for shooting on-the-fly.
@@ -177,6 +186,7 @@
   Pose virtual_goal;
   {
     const Pose goal = aiming_for_inner_port_ ? inner_port : outer_port;
+    target_distance_ = goal.Rebase(&robot_pose).xy_norm();
     virtual_goal = goal;
     if (shot_mode == ShotMode::kShootOnTheFly) {
       for (int ii = 0; ii < 3; ++ii) {
@@ -191,7 +201,7 @@
 
   const double heading_to_goal = virtual_goal.heading();
   CHECK(status->has_localizer());
-  distance_ = virtual_goal.xy_norm();
+  shot_distance_ = virtual_goal.xy_norm();
 
   // The following code all works to calculate what the rate of turn of the
   // turret should be. The code only accounts for the rate of turn if we are
@@ -201,6 +211,12 @@
   const double rel_x = virtual_goal.rel_pos().x();
   const double rel_y = virtual_goal.rel_pos().y();
   const double squared_norm = rel_x * rel_x + rel_y * rel_y;
+  // rel_xdot and rel_ydot are the derivatives (with respect to time) of rel_x
+  // and rel_y. Since these are in the robot's coordinate frame, and since we
+  // are ignoring lateral velocity for this exercise, rel_ydot is zero, and
+  // rel_xdot is just the inverse of the robot's velocity.
+  const double rel_xdot = -linear_angular(0);
+  const double rel_ydot = 0.0;
 
   // If squared_norm gets to be too close to zero, just zero out the relevant
   // term to prevent NaNs. Note that this doesn't address the chattering that
@@ -208,11 +224,12 @@
   // Note that x and y terms are swapped relative to what you would normally see
   // in the derivative of atan because xdot and ydot are the derivatives of
   // robot_pos and we are working with the atan of (target_pos - robot_pos).
-  const double atan_diff = (squared_norm < 1e-3)
-                               ? 0.0
-                               : (rel_y * xdot - rel_x * ydot) / squared_norm;
+  const double atan_diff =
+      (squared_norm < 1e-3) ? 0.0 : (rel_x * rel_ydot - rel_y * rel_xdot) /
+                                        squared_norm;
   // heading = atan2(relative_y, relative_x) - robot_theta
-  // dheading / dt = (rel_x * rel_y' - rel_y * rel_x') / (rel_x^2 + rel_y^2) - dtheta / dt
+  // dheading / dt =
+  //     (rel_x * rel_y' - rel_y * rel_x') / (rel_x^2 + rel_y^2) - dtheta / dt
   const double dheading_dt = atan_diff - linear_angular(1);
 
   double range = kTurretRange;
@@ -234,7 +251,8 @@
   }
 
   goal_.mutable_message()->mutate_unsafe_goal(turret_heading);
-  goal_.mutable_message()->mutate_goal_velocity(dheading_dt);
+  goal_.mutable_message()->mutate_goal_velocity(
+      std::clamp(dheading_dt, -2.0, 2.0));
 }
 
 flatbuffers::Offset<AimerStatus> Aimer::PopulateStatus(
@@ -243,6 +261,8 @@
   builder.add_turret_position(goal_.message().unsafe_goal());
   builder.add_turret_velocity(goal_.message().goal_velocity());
   builder.add_aiming_for_inner_port(aiming_for_inner_port_);
+  builder.add_target_distance(target_distance_);
+  builder.add_shot_distance(DistanceToGoal());
   return builder.Finish();
 }
 
diff --git a/y2020/control_loops/superstructure/turret/aiming.h b/y2020/control_loops/superstructure/turret/aiming.h
index 3b3071e..854518c 100644
--- a/y2020/control_loops/superstructure/turret/aiming.h
+++ b/y2020/control_loops/superstructure/turret/aiming.h
@@ -56,7 +56,7 @@
   const Goal *TurretGoal() const { return &goal_.message(); }
 
   // Returns the distance to the goal, in meters.
-  double DistanceToGoal() const { return distance_; }
+  double DistanceToGoal() const { return shot_distance_; }
 
   flatbuffers::Offset<AimerStatus> PopulateStatus(
       flatbuffers::FlatBufferBuilder *fbb) const;
@@ -64,7 +64,11 @@
  private:
   aos::FlatbufferDetachedBuffer<Goal> goal_;
   bool aiming_for_inner_port_ = false;
-  double distance_ = 0.0;
+  // Distance of the shot to the virtual target, used for calculating hood
+  // position and shooter speed.
+  double shot_distance_ = 0.0;    // meters
+  // Real-world distance to the target.
+  double target_distance_ = 0.0;  // meters
 };
 
 }  // namespace turret
diff --git a/y2020/control_loops/superstructure/turret/aiming_test.cc b/y2020/control_loops/superstructure/turret/aiming_test.cc
index cb95b56..ab600fa 100644
--- a/y2020/control_loops/superstructure/turret/aiming_test.cc
+++ b/y2020/control_loops/superstructure/turret/aiming_test.cc
@@ -178,7 +178,7 @@
 // angle on it.
 TEST_F(AimerTest, InnerPort) {
   const Pose target = InnerPortPose(aos::Alliance::kRed);
-  const Goal *goal = Update({.x = target.abs_pos().x() + 1.0,
+  const Goal *goal = Update({.x = target.abs_pos().x() + 10.0,
                              .y = target.abs_pos().y() + 0.0,
                              .theta = 0.0,
                              .linear = 0.0,
@@ -186,6 +186,7 @@
                             aos::Alliance::kRed);
   EXPECT_EQ(0.0, goal->unsafe_goal());
   EXPECT_EQ(0.0, goal->goal_velocity());
+  EXPECT_LT(1.0, aimer_.DistanceToGoal());
 }
 
 // Confirms that when we move the turret heading so that it would be entirely
diff --git a/y2020/vision/BUILD b/y2020/vision/BUILD
index 3baa61b..86088e6 100644
--- a/y2020/vision/BUILD
+++ b/y2020/vision/BUILD
@@ -78,6 +78,7 @@
         "//aos:init",
         "//aos/events:shm_event_loop",
         "//third_party:opencv",
+        "//y2020/vision/sift:sift_fbs",
     ],
 )
 
diff --git a/y2020/vision/camera_reader.cc b/y2020/vision/camera_reader.cc
index f38a40e..d0479d2 100644
--- a/y2020/vision/camera_reader.cc
+++ b/y2020/vision/camera_reader.cc
@@ -521,14 +521,15 @@
   std::vector<std::vector<sift::Match>> per_image_matches(
       number_training_images());
   for (const std::vector<cv::DMatch> &image_matches : matches) {
-    for (const cv::DMatch &image_match : image_matches) {
-      CHECK_LT(image_match.imgIdx, number_training_images());
-      per_image_matches[image_match.imgIdx].emplace_back();
-      sift::Match *const match = &per_image_matches[image_match.imgIdx].back();
-      match->mutate_query_feature(image_match.queryIdx);
-      match->mutate_train_feature(image_match.trainIdx);
-      match->mutate_distance(image_match.distance);
-    }
+    CHECK_GT(image_matches.size(), 0u);
+    // We're only using the first of the two matches
+    const cv::DMatch &image_match = image_matches[0];
+    CHECK_LT(image_match.imgIdx, number_training_images());
+    per_image_matches[image_match.imgIdx].emplace_back();
+    sift::Match *const match = &per_image_matches[image_match.imgIdx].back();
+    match->mutate_query_feature(image_match.queryIdx);
+    match->mutate_train_feature(image_match.trainIdx);
+    match->mutate_distance(image_match.distance);
   }
 
   // Then, we need to build up each ImageMatch table.
diff --git a/y2020/vision/tools/python_code/BUILD b/y2020/vision/tools/python_code/BUILD
index a932886..53464ac 100644
--- a/y2020/vision/tools/python_code/BUILD
+++ b/y2020/vision/tools/python_code/BUILD
@@ -12,13 +12,9 @@
     args = [
         "sift_training_data.h",
     ],
-    data = [
-        ":test_images/train_loading_bay_blue.png",
-        ":test_images/train_loading_bay_red.png",
-        ":test_images/train_power_port_blue.png",
-        ":test_images/train_power_port_red.png",
-        ":test_images/train_power_port_red_webcam.png",
-    ],
+    data = glob(["calib_files/*.json"]) + glob([
+        "test_images/*.png",
+    ]),
     default_python_version = "PY3",
     srcs_version = "PY2AND3",
     deps = [
@@ -64,13 +60,14 @@
         "sift_training_data_test.h",
         "test",
     ],
-    data = [
-        ":test_images/train_power_port_red.png",
-    ],
+    data = glob(["calib_files/*.json"]) + glob([
+        "test_images/*.png",
+    ]),
     default_python_version = "PY3",
     main = "load_sift_training.py",
     srcs_version = "PY2AND3",
     deps = [
+        ":load_sift_training",
         "//external:python-glog",
         "//y2020/vision/sift:sift_fbs_python",
         "@bazel_tools//tools/python/runfiles",
diff --git a/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-7971-3_Feb-13-2020-00-00-00.json b/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-7971-3_Feb-13-2020-00-00-00.json
new file mode 100644
index 0000000..8c1efbb
--- /dev/null
+++ b/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-7971-3_Feb-13-2020-00-00-00.json
@@ -0,0 +1 @@
+{"hostname": "pi-7971-3", "node_name": "pi3", "team_number": 7971, "timestamp": "Feb-13-2020-00-00-00", "camera_matrix": [[388.79947319784713, 0.0, 345.0976031055917], [0.0, 388.539148344188, 265.2780372766764], [0.0, 0.0, 1.0]], "dist_coeffs": [[0.13939139612079282, -0.24345067782097646, -0.0004228219772016648, -0.0004552350162154737, 0.08966339831250879]]}
diff --git a/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-971-1_2020-03-07-15-34-00.json b/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-971-1_2020-03-07-15-34-00.json
new file mode 100644
index 0000000..cf45ef0
--- /dev/null
+++ b/y2020/vision/tools/python_code/calib_files/cam-calib-int_pi-971-1_2020-03-07-15-34-00.json
@@ -0,0 +1 @@
+{"hostname": "pi-971-1", "node_name": "pi1", "team_number": 971, "timestamp": "2020-03-07-15-34-00", "camera_matrix": [[387.95046316, 0.0, 341.13297242], [0.0, 387.85366427, 245.69219733], [0.0, 0.0, 1.0]], "dist_coeffs": [[ 0.13594152, -0.23946991, -0.00088608,  0.00038653,  0.08745377]]}
diff --git a/y2020/vision/tools/python_code/calibrate_intrinsics.py b/y2020/vision/tools/python_code/calibrate_intrinsics.py
index df1b328..fb95599 100644
--- a/y2020/vision/tools/python_code/calibrate_intrinsics.py
+++ b/y2020/vision/tools/python_code/calibrate_intrinsics.py
@@ -1,23 +1,62 @@
-import time
 import cv2
-import cv2.aruco as A
+import cv2.aruco
+import datetime
+import json
+from json import JSONEncoder
 import numpy as np
+import os
+import time
 
-dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
-board = cv2.aruco.CharucoBoard_create(11, 8, .015, .011, dictionary)
-img = board.draw((200 * 11, 200 * 8))
+
+# From: https://pynative.com/python-serialize-numpy-ndarray-into-json/
+class NumpyArrayEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, np.integer):
+            return int(obj)
+        elif isinstance(obj, np.floating):
+            return float(obj)
+        elif isinstance(obj, np.ndarray):
+            return obj.tolist()
+        else:
+            return super(NumpyArrayEncoder, self).default(obj)
+
+
+def get_robot_info(hostname):
+    hostname_split = hostname.split("-")
+    if hostname_split[0] != "pi":
+        print(
+            "ERROR: expected hostname to start with pi!  Got '%s'" % hostname)
+        quit()
+
+    team_number = int(hostname_split[1])
+    node_name = hostname_split[0] + hostname_split[2]
+    return node_name, team_number
+
+
+USE_LARGE_BOARD = True
+
+if USE_LARGE_BOARD:
+    dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_5X5_100)
+    board = cv2.aruco.CharucoBoard_create(12, 9, .06, .045, dictionary)
+    img = board.draw((200 * 12, 200 * 9))
+else:
+    dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
+    board = cv2.aruco.CharucoBoard_create(11, 8, .015, .011, dictionary)
+    img = board.draw((200 * 11, 200 * 8))
 
 #Dump the calibration board to a file
-cv2.imwrite('charuco.png', img)
+#cv2.imwrite('charuco.png', img)
 
 #Start capturing images for calibration
-CAMERA_INDEX = 2
+CAMERA_INDEX = 0  # Capture from /dev/videoX, where X=CAMERA_INDEX
 cap = cv2.VideoCapture(CAMERA_INDEX)
 
 allCorners = []
 allIds = []
 capture_count = 0
-while (capture_count < 50):
+MIN_IMAGES = 50
+
+while (capture_count < MIN_IMAGES):
 
     ret, frame = cap.read()
     assert ret, "Unable to get image from the camera at /dev/video%d" % CAMERA_INDEX
@@ -25,45 +64,69 @@
     res = cv2.aruco.detectMarkers(gray, dictionary)
     aruco_detect_image = frame.copy()
 
-    if len(res[0]) > 0:
+    if len(res[0]) > 0 and len(res[1]) > 0:
         cv2.aruco.drawDetectedMarkers(aruco_detect_image, res[0], res[1])
 
+    # Display every image to let user trigger capture
     cv2.imshow('frame', aruco_detect_image)
     keystroke = cv2.waitKey(1)
+
     if keystroke & 0xFF == ord('q'):
         break
     elif keystroke & 0xFF == ord('c'):
-        print("Res:", len(res[0]), res[1].shape)
+        print("Asked to capture image")
+        if len(res[0]) == 0 or len(res[1]) == 0:
+            # Can't use this image
+            continue
+
         res2 = cv2.aruco.interpolateCornersCharuco(res[0], res[1], gray, board)
         if res2[1] is not None and res2[2] is not None and len(res2[1]) > 3:
             capture_count += 1
             charuco_detect_image = frame.copy()
             allCorners.append(res2[1])
             allIds.append(res2[2])
-            print("Res2: ", res2[1].shape, res2[2].shape)
+            print("Capturing image #%d" % capture_count)
             cv2.aruco.drawDetectedCornersCharuco(charuco_detect_image, res2[1],
                                                  res2[2])
+
             cv2.imshow('frame', charuco_detect_image)
             cv2.waitKey(1000)
-            # TODO: Should log image to disk
-            print("Captured image #", capture_count)
-
-imsize = gray.shape
+            # TODO<Jim>: Should log image to disk
 
 #Calibration fails for lots of reasons. Release the video if we do
 try:
+    imsize = gray.shape
     cal = cv2.aruco.calibrateCameraCharuco(allCorners, allIds, board, imsize,
                                            None, None)
-    #print("Calibration is:\n", cal)
+    print("Calibration is:\n", cal)
     print("Reproduction error:", cal[0])
     if (cal[0] > 1.0):
         print("REPRODUCTION ERROR NOT GOOD")
-    # TODO<jim>: Need to save these out in format that can be used elsewhere
     print("Calibration matrix:\n", cal[1])
     print("Distortion Coefficients:\n", cal[2])
 except:
     print("Calibration failed")
     cap.release()
+    quit()
+
+hostname = os.uname()[1]
+date_str = datetime.datetime.today().strftime("%Y-%m-%d-%H-%M-%S")
+node_name, team_number = get_robot_info(hostname)
+numpyData = {
+    "hostname": hostname,
+    "node_name": node_name,
+    "team_number": team_number,
+    "timestamp": date_str,
+    "camera_matrix": cal[1],
+    "dist_coeffs": cal[2]
+}
+encodedNumpyData = json.dumps(
+    numpyData, cls=NumpyArrayEncoder)  # use dump() to write array into file
+
+# Write out the data
+calib_file = open("cam_calib_%s_%s.json" % (hostname, date_str), "w")
+calib_file.write(encodedNumpyData)
+calib_file.close()
 
 cap.release()
 cv2.destroyAllWindows()
diff --git a/y2020/vision/tools/python_code/camera_definition.py b/y2020/vision/tools/python_code/camera_definition.py
index 0667b16..b8c71ff 100644
--- a/y2020/vision/tools/python_code/camera_definition.py
+++ b/y2020/vision/tools/python_code/camera_definition.py
@@ -1,6 +1,13 @@
 import copy
+import glog
+import json
 import math
 import numpy as np
+import os
+
+import define_training_data as dtd
+
+glog.setLevel("WARN")
 
 
 class CameraIntrinsics:
@@ -21,41 +28,91 @@
     def __init__(self):
         self.camera_int = CameraIntrinsics()
         self.camera_ext = CameraExtrinsics()
+        self.turret_ext = None
         self.node_name = ""
         self.team_number = -1
 
 
-### CAMERA DEFINITIONS
+def load_camera_definitions():
+    ### CAMERA DEFINITIONS
 
-# Robot camera has:
-# FOV_H = 93.*math.pi()/180.
-# FOV_V = 70.*math.pi()/180.
+    # Robot camera has:
+    # FOV_H = 93.*math.pi()/180.
+    # FOV_V = 70.*math.pi()/180.
 
-# Create fake camera (based on USB webcam params)
-fx = 810.
-fy = 810.
-cx = 320.
-cy = 240.
+    # Create fake camera (based on USB webcam params)
+    fx = 810.
+    fy = 810.
+    cx = 320.
+    cy = 240.
 
-# Define a web_cam
-web_cam_int = CameraIntrinsics()
-web_cam_int.camera_matrix = np.asarray([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
-web_cam_int.dist_coeffs = np.zeros((5, 1))
+    # Define a web_cam
+    web_cam_int = CameraIntrinsics()
+    web_cam_int.camera_matrix = np.asarray([[fx, 0, cx], [0, fy, cy],
+                                            [0, 0, 1]])
+    web_cam_int.dist_coeffs = np.zeros((5, 1))
 
-web_cam_ext = CameraExtrinsics()
-# Camera rotation from robot x,y,z to opencv (z, -x, -y)
-web_cam_ext.R = np.array([[0., 0., 1.], [-1, 0, 0], [0, -1., 0]])
-web_cam_ext.T = np.array([0., 0., 0.])
+    web_cam_ext = CameraExtrinsics()
+    # Camera rotation from robot x,y,z to opencv (z, -x, -y)
+    # This is extrinsics for the turret camera
+    # camera pose relative to center, base of the turret
+    # TODO<Jim>: Need to implement per-camera calibration, like with intrinsics
+    camera_pitch = 34.0 * np.pi / 180.0
+    camera_pitch_matrix = np.matrix(
+        [[np.cos(camera_pitch), 0.0, -np.sin(camera_pitch)], [0.0, 1.0, 0.0],
+         [np.sin(camera_pitch), 0.0,
+          np.cos(camera_pitch)]])
+    web_cam_ext.R = np.array(
+        camera_pitch_matrix *
+        np.matrix([[0., 0., 1.], [-1, 0, 0], [0, -1., 0]]))
+    #web_cam_ext.T = np.array([0., 0., 0.])
+    web_cam_ext.T = np.array([2.0 * 0.0254, -6.0 * 0.0254, 41.0 * 0.0254])
+    fixed_ext = CameraExtrinsics()
+    fixed_ext.R = np.array([[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0],
+                            [0.0, 0.0, 1.0]])
+    fixed_ext.T = np.array([0.0, 0.0, 0.0])
 
-web_cam_params = CameraParameters()
-web_cam_params.camera_int = web_cam_int
-web_cam_params.camera_ext = web_cam_ext
+    web_cam_params = CameraParameters()
+    web_cam_params.camera_int = web_cam_int
+    # Fixed extrinsics are for the turret, which is centered on the robot and
+    # pointed straight backwards.
+    web_cam_params.camera_ext = fixed_ext
+    web_cam_params.turret_ext = web_cam_ext
 
-camera_list = []
+    camera_list = []
 
-for team_number in (971, 7971, 8971, 9971):
-    for node_name in ("pi0", "pi1", "pi2", "pi3", "pi4"):
-        camera_base = copy.deepcopy(web_cam_params)
-        camera_base.node_name = node_name
-        camera_base.team_number = team_number
-        camera_list.append(camera_base)
+    # TODO<Jim>: Should probably make this a dict to make replacing easier
+    for team_number in (971, 7971, 8971, 9971):
+        for node_name in ("pi0", "pi1", "pi2", "pi3", "pi4", "pi5"):
+            camera_base = copy.deepcopy(web_cam_params)
+            camera_base.node_name = node_name
+            camera_base.team_number = team_number
+            camera_list.append(camera_base)
+
+    dir_name = dtd.bazel_name_fix('calib_files')
+    for filename in os.listdir(dir_name):
+        if "cam-calib-int" in filename and filename.endswith(".json"):
+            # Extract intrinsics from file
+            fn_split = filename.split("_")
+            hostname_split = fn_split[1].split("-")
+            if hostname_split[0] == "pi":
+                team_number = int(hostname_split[1])
+                node_name = hostname_split[0] + hostname_split[2]
+
+            calib_file = open(dir_name + "/" + filename, 'r')
+            calib_dict = json.loads(calib_file.read())
+            hostname = np.asarray(calib_dict["hostname"])
+            camera_matrix = np.asarray(calib_dict["camera_matrix"])
+            dist_coeffs = np.asarray(calib_dict["dist_coeffs"])
+
+            # Look for match, and replace camera_intrinsics
+            for camera_calib in camera_list:
+                if camera_calib.node_name == node_name and camera_calib.team_number == team_number:
+                    glog.info("Found calib for %s, team #%d" %
+                              (node_name, team_number))
+                    camera_calib.camera_int.camera_matrix = copy.copy(
+                        camera_matrix)
+                    camera_calib.camera_int.dist_coeffs = copy.copy(
+                        dist_coeffs)
+
+    return camera_list
diff --git a/y2020/vision/tools/python_code/camera_definition_test.py b/y2020/vision/tools/python_code/camera_definition_test.py
index 732dbb8..f157ba3 100644
--- a/y2020/vision/tools/python_code/camera_definition_test.py
+++ b/y2020/vision/tools/python_code/camera_definition_test.py
@@ -2,26 +2,9 @@
 import math
 import numpy as np
 
-
-class CameraIntrinsics:
-    def __init__(self):
-        self.camera_matrix = []
-        self.dist_coeffs = []
-
-
-class CameraExtrinsics:
-    def __init__(self):
-        self.R = []
-        self.T = []
-
-
-class CameraParameters:
-    def __init__(self):
-        self.camera_int = CameraIntrinsics()
-        self.camera_ext = CameraExtrinsics()
-        self.node_name = ""
-        self.team_number = -1
-
+from y2020.vision.tools.python_code.camera_definition import (CameraIntrinsics,
+                                                              CameraExtrinsics,
+                                                              CameraParameters)
 
 ### CAMERA DEFINITIONS
 
@@ -56,6 +39,6 @@
         camera_base = copy.deepcopy(web_cam_params)
         camera_base.node_name = node_name
         camera_base.team_number = team_number
-        camera_base.camera_ext.T = np.asarray(
-            np.float32([i + 1, i + 1, i + 1]))
+        camera_base.camera_ext.T = np.asarray(np.float32([i + 1, i + 1,
+                                                          i + 1]))
         camera_list.append(camera_base)
diff --git a/y2020/vision/tools/python_code/define_training_data.py b/y2020/vision/tools/python_code/define_training_data.py
index 8116fb7..76e73da 100644
--- a/y2020/vision/tools/python_code/define_training_data.py
+++ b/y2020/vision/tools/python_code/define_training_data.py
@@ -244,6 +244,18 @@
     return img
 
 
+def bazel_name_fix(filename):
+    ret_name = filename
+    try:
+        from bazel_tools.tools.python.runfiles import runfiles
+        r = runfiles.Create()
+        ret_name = r.Rlocation('org_frc971/y2020/vision/tools/python_code/' + filename)
+    except:
+        pass
+
+    return ret_name
+
+
 def sample_define_polygon_usage():
     image = cv2.imread("test_images/train_power_port_red.png")
 
diff --git a/y2020/vision/tools/python_code/image_match_test.py b/y2020/vision/tools/python_code/image_match_test.py
index 455bc30..8d16104 100644
--- a/y2020/vision/tools/python_code/image_match_test.py
+++ b/y2020/vision/tools/python_code/image_match_test.py
@@ -10,8 +10,9 @@
 
 ### DEFINITIONS
 target_definition.USE_BAZEL = False
+camera_definition.USE_BAZEL = False
 target_list = target_definition.compute_target_definition()
-camera_list = camera_definition.camera_list
+camera_list = camera_definition.load_camera_definitions()
 
 # For now, just use the first one
 camera_params = camera_list[0]
@@ -29,12 +30,14 @@
     'test_images/test_raspi3_sample.jpg',  #7
     'test_images/test_VR_sample1.png',  #8
     'test_images/train_loading_bay_blue.png',  #9
-    'test_images/train_loading_bay_red.png'  #10
+    'test_images/train_loading_bay_red.png',  #10
+    'test_images/pi-7971-3_test_image.png',  #11
+    'sample_images/capture-2020-02-13-16-40-07.png',
 ]
 
 training_image_index = 0
 # TODO: Should add argParser here to select this
-query_image_index = 0  # Use -1 to use camera capture; otherwise index above list
+query_image_index = 12  # Use -1 to use camera capture; otherwise index above list
 
 ##### Let's get to work!
 
@@ -162,8 +165,6 @@
         for m in good_matches:
             src_pts_3d.append(target_list[i].keypoint_list_3d[m.trainIdx])
             pt = query_keypoint_lists[0][m.queryIdx].pt
-            print("Color at ", pt, " is ", query_images[0][int(pt[1])][int(
-                pt[0])])
             query_images[0] = cv2.circle(
                 query_images[0], (int(pt[0]), int(pt[1])), 5, (0, 255, 0), 3)
 
diff --git a/y2020/vision/tools/python_code/image_stream.py b/y2020/vision/tools/python_code/image_stream.py
new file mode 100644
index 0000000..2cd5ac7
--- /dev/null
+++ b/y2020/vision/tools/python_code/image_stream.py
@@ -0,0 +1,34 @@
+import cv2
+import datetime
+# Open the device at the ID X for /dev/videoX
+CAMERA_INDEX = 0
+cap = cv2.VideoCapture(CAMERA_INDEX)
+
+#Check whether user selected camera is opened successfully.
+if not (cap.isOpened()):
+    print("Could not open video device /dev/video%d" % CAMERA_INDEX)
+    quit()
+
+while True:
+    # Capture frame-by-frame
+    ret, frame = cap.read()
+
+    exp = cap.get(cv2.CAP_PROP_EXPOSURE)
+    #print("Exposure:", exp)
+    # Display the resulting frame
+    cv2.imshow('preview', frame)
+
+    #Waits for a user input to capture image or quit the application
+    keystroke = cv2.waitKey(1)
+
+    if keystroke & 0xFF == ord('q'):
+        break
+    elif keystroke & 0xFF == ord('c'):
+        image_name = datetime.datetime.today().strftime(
+            "capture-%b-%d-%Y-%H-%M-%S.png")
+        print("Capturing image as %s" % image_name)
+        cv2.imwrite(image_name, frame)
+
+# When everything's done, release the capture
+cap.release()
+cv2.destroyAllWindows()
diff --git a/y2020/vision/tools/python_code/load_sift_training.py b/y2020/vision/tools/python_code/load_sift_training.py
index 0ce70b7..a21a06f 100644
--- a/y2020/vision/tools/python_code/load_sift_training.py
+++ b/y2020/vision/tools/python_code/load_sift_training.py
@@ -27,6 +27,19 @@
     return output_list
 
 
+def list_to_transformation_matrix(values, fbb):
+    """Puts a list of values into an FBB TransformationMatrix."""
+
+    TransformationMatrix.TransformationMatrixStartDataVector(fbb, len(values))
+    for n in reversed(values):
+        fbb.PrependFloat32(n)
+    list_offset = fbb.EndVector(len(values))
+
+    TransformationMatrix.TransformationMatrixStart(fbb)
+    TransformationMatrix.TransformationMatrixAddData(fbb, list_offset)
+    return TransformationMatrix.TransformationMatrixEnd(fbb)
+
+
 def main():
 
     target_data_list = None
@@ -50,7 +63,7 @@
         import camera_definition
         import target_definition
         target_data_list = target_definition.compute_target_definition()
-        camera_calib_list = camera_definition.camera_list
+        camera_calib_list = camera_definition.load_camera_definitions()
 
     glog.info("Writing file to ", output_path)
 
@@ -138,18 +151,15 @@
     for camera_calib in camera_calib_list:
         fixed_extrinsics_list = rot_and_trans_to_list(
             camera_calib.camera_ext.R, camera_calib.camera_ext.T)
-        TransformationMatrix.TransformationMatrixStartDataVector(
-            fbb, len(fixed_extrinsics_list))
-        for n in reversed(fixed_extrinsics_list):
-            fbb.PrependFloat32(n)
-        fixed_extrinsics_data_offset = fbb.EndVector(
-            len(fixed_extrinsics_list))
+        fixed_extrinsics_vector = list_to_transformation_matrix(
+            fixed_extrinsics_list, fbb)
 
-        TransformationMatrix.TransformationMatrixStart(fbb)
-        TransformationMatrix.TransformationMatrixAddData(
-            fbb, fixed_extrinsics_data_offset)
-        fixed_extrinsics_vector = TransformationMatrix.TransformationMatrixEnd(
-            fbb)
+        turret_extrinsics_vector = None
+        if camera_calib.turret_ext is not None:
+            turret_extrinsics_list = rot_and_trans_to_list(
+                camera_calib.turret_ext.R, camera_calib.turret_ext.T)
+            turret_extrinsics_vector = list_to_transformation_matrix(
+                turret_extrinsics_list, fbb)
 
         # TODO: Need to add in distortion coefficients here
         # For now, just send camera paramter matrix (fx, fy, cx, cy)
@@ -179,6 +189,9 @@
             fbb, dist_coeffs_vector)
         CameraCalibration.CameraCalibrationAddFixedExtrinsics(
             fbb, fixed_extrinsics_vector)
+        if turret_extrinsics_vector is not None:
+            CameraCalibration.CameraCalibrationAddTurretExtrinsics(
+                fbb, turret_extrinsics_vector)
         camera_calibration_vector.append(
             CameraCalibration.CameraCalibrationEnd(fbb))
 
diff --git a/y2020/vision/tools/python_code/target_definition.py b/y2020/vision/tools/python_code/target_definition.py
index fb2f03a..6080ac2 100644
--- a/y2020/vision/tools/python_code/target_definition.py
+++ b/y2020/vision/tools/python_code/target_definition.py
@@ -12,31 +12,16 @@
 # TODO<Jim>: Allow command-line setting of logging level
 glog.setLevel("WARN")
 global VISUALIZE_KEYPOINTS
-global USE_BAZEL
-USE_BAZEL = True
 VISUALIZE_KEYPOINTS = False
 
 # For now, just have a 32 pixel radius, based on original training image
 target_radius_default = 32.
 
 
-def bazel_name_fix(filename):
-    ret_name = filename
-    if USE_BAZEL:
-        ret_name = 'org_frc971/y2020/vision/tools/python_code/' + filename
-
-    return ret_name
-
-
 class TargetData:
     def __init__(self, filename):
-        self.image_filename = filename
+        self.image_filename = dtd.bazel_name_fix(filename)
         # Load an image (will come in as a 1-element list)
-        if USE_BAZEL:
-            from bazel_tools.tools.python.runfiles import runfiles
-            r = runfiles.Create()
-            self.image_filename = r.Rlocation(
-                bazel_name_fix(self.image_filename))
         self.image = tam.load_images([self.image_filename])[0]
         self.polygon_list = []
         self.polygon_list_3d = []
@@ -90,7 +75,7 @@
         return point_list_3d
 
 
-def compute_target_definition():
+def load_training_data():
     ############################################################
     # TARGET DEFINITIONS
     ############################################################
@@ -121,12 +106,82 @@
     power_port_target_height = (
         power_port_total_height + power_port_bottom_wing_height) / 2.
 
+    ### Cafeteria target definition
+    inch_to_meter = 0.0254
+    c_power_port_total_height = (79.5 + 39.5) * inch_to_meter
+    c_power_port_edge_y = 1.089
+    c_power_port_width = 4.0 * 12 * inch_to_meter
+    c_power_port_bottom_wing_height = 79.5 * inch_to_meter
+    c_power_port_wing_width = 47.5 * inch_to_meter
+    c_power_port_white_marker_z = (79.5 - 19.5) * inch_to_meter
+
+    # Pick the target center location at halfway between top and bottom of the top panel
+    c_power_port_target_height = (
+        power_port_total_height + power_port_bottom_wing_height) / 2.
+
+    ###
+    ### Cafe power port
+    ###
+
+    # Create the reference "ideal" image
+    ideal_power_port_cafe = TargetData(
+        'test_images/train_cafeteria-2020-02-13-16-27-25.png')
+
+    # Start at lower left corner, and work around clockwise
+    # These are taken by manually finding the points in gimp for this image
+    power_port_cafe_main_panel_polygon_points_2d = [(271, 456), (278, 394),
+                                                    (135, 382), (286, 294),
+                                                    (389, 311), (397,
+                                                                 403), (401,
+                                                                        458)]
+
+    # These are "virtual" 3D points based on the expected geometry
+    power_port_cafe_main_panel_polygon_points_3d = [
+        (field_length / 2., -c_power_port_edge_y,
+         c_power_port_white_marker_z), (field_length / 2.,
+                                        -c_power_port_edge_y,
+                                        c_power_port_bottom_wing_height),
+        (field_length / 2., -c_power_port_edge_y + c_power_port_wing_width,
+         c_power_port_bottom_wing_height), (field_length / 2.,
+                                            -c_power_port_edge_y,
+                                            c_power_port_total_height),
+        (field_length / 2., -c_power_port_edge_y - c_power_port_width,
+         c_power_port_total_height),
+        (field_length / 2., -c_power_port_edge_y - c_power_port_width,
+         c_power_port_bottom_wing_height),
+        (field_length / 2., -c_power_port_edge_y - c_power_port_width,
+         c_power_port_white_marker_z)
+    ]
+
+    # Populate the cafe power port
+    ideal_power_port_cafe.polygon_list.append(
+        power_port_cafe_main_panel_polygon_points_2d)
+    ideal_power_port_cafe.polygon_list_3d.append(
+        power_port_cafe_main_panel_polygon_points_3d)
+
+    # Location of target.  Rotation is pointing in -x direction
+    ideal_power_port_cafe.target_rotation = np.identity(3, np.double)
+    ideal_power_port_cafe.target_position = np.array([
+        field_length / 2., -c_power_port_edge_y - c_power_port_width / 2.,
+        c_power_port_target_height
+    ])
+    ideal_power_port_cafe.target_point_2d = np.float32([[340, 350]]).reshape(
+        -1, 1, 2)  # train_cafeteria-2020-02-13-16-27-25.png
+
+    ideal_target_list.append(ideal_power_port_cafe)
+    training_target_power_port_cafe = TargetData(
+        'test_images/train_cafeteria-2020-02-13-16-27-25.png')
+    training_target_power_port_cafe.target_rotation = ideal_power_port_cafe.target_rotation
+    training_target_power_port_cafe.target_position = ideal_power_port_cafe.target_position
+    training_target_power_port_cafe.target_radius = target_radius_default
+    training_target_list.append(training_target_power_port_cafe)
+
     ###
     ### Red Power Port
     ###
 
     # Create the reference "ideal" image
-    ideal_power_port_red = TargetData('test_images/train_power_port_red.png')
+    ideal_power_port_red = TargetData('test_images/ideal_power_port_red.png')
 
     # Start at lower left corner, and work around clockwise
     # These are taken by manually finding the points in gimp for this image
@@ -188,7 +243,7 @@
     # and entering the pixel values from the target center for each image.
     # These are currently only used for visualization of the target
     ideal_power_port_red.target_point_2d = np.float32([[570, 192]]).reshape(
-        -1, 1, 2)  # train_power_port_red.png
+        -1, 1, 2)  # ideal_power_port_red.png
     # np.float32([[305, 97]]).reshape(-1, 1, 2),  #train_power_port_red_webcam.png
 
     # Add the ideal 3D target to our list
@@ -196,6 +251,7 @@
     # And add the training image we'll actually use to the training list
     training_target_power_port_red = TargetData(
         'test_images/train_power_port_red_webcam.png')
+    #'test_images/train_power_port_red_pi-7971-3.png')
     training_target_power_port_red.target_rotation = ideal_power_port_red.target_rotation
     training_target_power_port_red.target_position = ideal_power_port_red.target_position
     training_target_power_port_red.target_radius = target_radius_default
@@ -206,7 +262,7 @@
     ### Red Loading Bay
     ###
 
-    ideal_loading_bay_red = TargetData('test_images/train_loading_bay_red.png')
+    ideal_loading_bay_red = TargetData('test_images/ideal_loading_bay_red.png')
 
     # Start at lower left corner, and work around clockwise
     # These are taken by manually finding the points in gimp for this image
@@ -233,7 +289,7 @@
         loading_bay_height / 2.
     ])
     ideal_loading_bay_red.target_point_2d = np.float32([[366, 236]]).reshape(
-        -1, 1, 2)  # train_loading_bay_red.png
+        -1, 1, 2)  # ideal_loading_bay_red.png
 
     ideal_target_list.append(ideal_loading_bay_red)
     training_target_loading_bay_red = TargetData(
@@ -247,7 +303,7 @@
     ### Blue Power Port
     ###
 
-    ideal_power_port_blue = TargetData('test_images/train_power_port_blue.png')
+    ideal_power_port_blue = TargetData('test_images/ideal_power_port_blue.png')
 
     # Start at lower left corner, and work around clockwise
     # These are taken by manually finding the points in gimp for this image
@@ -299,22 +355,24 @@
         power_port_target_height
     ])
     ideal_power_port_blue.target_point_2d = np.float32([[567, 180]]).reshape(
-        -1, 1, 2)  # train_power_port_blue.png
+        -1, 1, 2)  # ideal_power_port_blue.png
 
-    ideal_target_list.append(ideal_power_port_blue)
+    #### TEMPORARILY DISABLING the BLUE POWER PORT target
+    #ideal_target_list.append(ideal_power_port_blue)
     training_target_power_port_blue = TargetData(
         'test_images/train_power_port_blue.png')
     training_target_power_port_blue.target_rotation = ideal_power_port_blue.target_rotation
     training_target_power_port_blue.target_position = ideal_power_port_blue.target_position
     training_target_power_port_blue.target_radius = target_radius_default
-    training_target_list.append(training_target_power_port_blue)
+    #### TEMPORARILY DISABLING the BLUE POWER PORT target
+    #training_target_list.append(training_target_power_port_blue)
 
     ###
     ### Blue Loading Bay
     ###
 
     ideal_loading_bay_blue = TargetData(
-        'test_images/train_loading_bay_blue.png')
+        'test_images/ideal_loading_bay_blue.png')
 
     # Start at lower left corner, and work around clockwise
     # These are taken by manually finding the points in gimp for this image
@@ -343,7 +401,7 @@
         loading_bay_height / 2.
     ])
     ideal_loading_bay_blue.target_point_2d = np.float32([[366, 236]]).reshape(
-        -1, 1, 2)  # train_loading_bay_blue.png
+        -1, 1, 2)  # ideal_loading_bay_blue.png
 
     ideal_target_list.append(ideal_loading_bay_blue)
     training_target_loading_bay_blue = TargetData(
@@ -353,11 +411,17 @@
     training_target_loading_bay_blue.target_radius = target_radius_default
     training_target_list.append(training_target_loading_bay_blue)
 
+    return ideal_target_list, training_target_list
+
+
+def compute_target_definition():
+    ideal_target_list, training_target_list = load_training_data()
+
     # Create feature extractor
     feature_extractor = tam.load_feature_extractor()
 
     # Use webcam parameters for now
-    camera_params = camera_definition.web_cam_params
+    camera_params = camera_definition.load_camera_definitions()[0]
 
     for ideal_target in ideal_target_list:
         glog.info(
@@ -523,20 +587,10 @@
         help="Whether to visualize the results",
         default=False,
         action='store_true')
-    ap.add_argument(
-        "-n",
-        "--no_bazel",
-        help="Don't run using Bazel",
-        default=True,
-        action='store_false')
     args = vars(ap.parse_args())
 
     VISUALIZE_KEYPOINTS = args["visualize"]
     if args["visualize"]:
         glog.info("Visualizing results")
 
-    USE_BAZEL = args["no_bazel"]
-    if args["no_bazel"]:
-        glog.info("Running on command line (no Bazel)")
-
     compute_target_definition()
diff --git a/y2020/vision/tools/python_code/test_images/ideal_loading_bay_blue.png b/y2020/vision/tools/python_code/test_images/ideal_loading_bay_blue.png
new file mode 100644
index 0000000..c3c0aea
--- /dev/null
+++ b/y2020/vision/tools/python_code/test_images/ideal_loading_bay_blue.png
Binary files differ
diff --git a/y2020/vision/tools/python_code/test_images/ideal_loading_bay_red.png b/y2020/vision/tools/python_code/test_images/ideal_loading_bay_red.png
new file mode 100644
index 0000000..42091a6
--- /dev/null
+++ b/y2020/vision/tools/python_code/test_images/ideal_loading_bay_red.png
Binary files differ
diff --git a/y2020/vision/tools/python_code/test_images/ideal_power_port_blue.png b/y2020/vision/tools/python_code/test_images/ideal_power_port_blue.png
new file mode 100644
index 0000000..a3a7597
--- /dev/null
+++ b/y2020/vision/tools/python_code/test_images/ideal_power_port_blue.png
Binary files differ
diff --git a/y2020/vision/tools/python_code/test_images/ideal_power_port_red.png b/y2020/vision/tools/python_code/test_images/ideal_power_port_red.png
new file mode 100644
index 0000000..9d2f0bf
--- /dev/null
+++ b/y2020/vision/tools/python_code/test_images/ideal_power_port_red.png
Binary files differ
diff --git a/y2020/vision/tools/python_code/test_images/train_cafeteria-2020-02-13-16-27-25.png b/y2020/vision/tools/python_code/test_images/train_cafeteria-2020-02-13-16-27-25.png
new file mode 100644
index 0000000..be67176
--- /dev/null
+++ b/y2020/vision/tools/python_code/test_images/train_cafeteria-2020-02-13-16-27-25.png
Binary files differ
diff --git a/y2020/vision/viewer.cc b/y2020/vision/viewer.cc
index be9b980..08409b2 100644
--- a/y2020/vision/viewer.cc
+++ b/y2020/vision/viewer.cc
@@ -1,3 +1,4 @@
+#include <map>
 #include <opencv2/calib3d.hpp>
 #include <opencv2/features2d.hpp>
 #include <opencv2/highgui/highgui.hpp>
@@ -5,6 +6,8 @@
 
 #include "aos/events/shm_event_loop.h"
 #include "aos/init.h"
+#include "aos/time/time.h"
+#include "y2020/vision/sift/sift_generated.h"
 #include "y2020/vision/vision_generated.h"
 
 DEFINE_string(config, "config.json", "Path to the config file to use.");
@@ -14,23 +17,66 @@
 namespace {
 
 void ViewerMain() {
+  struct TargetData {
+    float x;
+    float y;
+    float radius;
+  };
+
+  std::map<int64_t, TargetData> target_data_map;
+
   aos::FlatbufferDetachedBuffer<aos::Configuration> config =
       aos::configuration::ReadConfig(FLAGS_config);
 
   aos::ShmEventLoop event_loop(&config.message());
 
-  event_loop.MakeWatcher("/camera", [](const CameraImage &image) {
-    cv::Mat image_mat(image.rows(), image.cols(), CV_8U);
-    CHECK(image_mat.isContinuous());
-    const int number_pixels = image.rows() * image.cols();
-    for (int i = 0; i < number_pixels; ++i) {
-      reinterpret_cast<uint8_t *>(image_mat.data)[i] =
-          image.data()->data()[i * 2];
-    }
+  event_loop.MakeWatcher(
+      "/camera", [&target_data_map](const CameraImage &image) {
+        // Create color image:
+        cv::Mat image_color_mat(cv::Size(image.cols(), image.rows()), CV_8UC2,
+                                (void *)image.data()->data());
+        cv::Mat rgb_image(cv::Size(image.cols(), image.rows()), CV_8UC3);
+        cv::cvtColor(image_color_mat, rgb_image, CV_YUV2BGR_YUYV);
 
-    cv::imshow("Display", image_mat);
-    cv::waitKey(1);
-  });
+        unsigned long timestamp = image.monotonic_timestamp_ns();
+        auto target_it = target_data_map.find(timestamp);
+        if (target_it != target_data_map.end()) {
+          float x = target_it->second.x;
+          float y = target_it->second.y;
+          float radius = target_it->second.radius;
+          cv::circle(rgb_image, cv::Point2f(x, y), radius,
+                     cv::Scalar(0, 255, 0), 5);
+        }
+
+        cv::imshow("Display", rgb_image);
+        int keystroke = cv::waitKey(1);
+        if ((keystroke & 0xFF) == static_cast<int>('c')) {
+          // Convert again, to get clean image
+          cv::cvtColor(image_color_mat, rgb_image, CV_YUV2BGR_YUYV);
+          std::stringstream name;
+          name << "capture-" << aos::realtime_clock::now() << ".png";
+          cv::imwrite(name.str(), rgb_image);
+          LOG(INFO) << "Saved image file: " << name.str();
+        } else if ((keystroke & 0xFF) == static_cast<int>('q')) {
+          exit(0);
+        }
+      });
+
+  event_loop.MakeWatcher(
+      "/camera", [&target_data_map](const sift::ImageMatchResult &match) {
+        int64_t timestamp = match.image_monotonic_timestamp_ns();
+        if (match.camera_poses() != NULL && match.camera_poses()->size() > 0) {
+          LOG(INFO) << "Got match!\n";
+          TargetData target_data = {
+              match.camera_poses()->Get(0)->query_target_point_x(),
+              match.camera_poses()->Get(0)->query_target_point_y(),
+              match.camera_poses()->Get(0)->query_target_point_radius()};
+          target_data_map[timestamp] = target_data;
+          while (target_data_map.size() > 10u) {
+            target_data_map.erase(target_data_map.begin());
+          }
+        }
+      });
 
   event_loop.Run();
 }
diff --git a/y2020/vision/viewer_replay.cc b/y2020/vision/viewer_replay.cc
index c81796a..82ab11a 100644
--- a/y2020/vision/viewer_replay.cc
+++ b/y2020/vision/viewer_replay.cc
@@ -11,6 +11,8 @@
 DEFINE_string(config, "y2020/config.json", "Path to the config file to use.");
 DEFINE_string(logfile, "", "Path to the log file to use.");
 DEFINE_string(node, "pi1", "Node name to replay.");
+DEFINE_string(image_save_prefix, "/tmp/img",
+              "Prefix to use for saving images from the logfile.");
 
 namespace frc971 {
 namespace vision {
@@ -31,7 +33,8 @@
   std::unique_ptr<aos::EventLoop> event_loop =
       reader.event_loop_factory()->MakeEventLoop("player", node);
 
-  event_loop->MakeWatcher("/camera", [](const CameraImage &image) {
+  int image_count = 0;
+  event_loop->MakeWatcher("/camera", [&image_count](const CameraImage &image) {
     cv::Mat image_mat(image.rows(), image.cols(), CV_8U);
     CHECK(image_mat.isContinuous());
     const int number_pixels = image.rows() * image.cols();
@@ -41,6 +44,10 @@
     }
 
     cv::imshow("Display", image_mat);
+    if (!FLAGS_image_save_prefix.empty()) {
+      cv::imwrite("/tmp/img" + std::to_string(image_count++) + ".png",
+                  image_mat);
+    }
     cv::waitKey(1);
   });
 
diff --git a/y2020/www/BUILD b/y2020/www/BUILD
index 6b27c66..a8b0c67 100644
--- a/y2020/www/BUILD
+++ b/y2020/www/BUILD
@@ -26,6 +26,7 @@
     deps = [
         "//aos/network/www:proxy",
         "//y2020/vision/sift:sift_ts_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_status_ts_fbs",
     ],
 )
 
diff --git a/y2020/www/field_handler.ts b/y2020/www/field_handler.ts
index 0d2ea34..6ec1afb 100644
--- a/y2020/www/field_handler.ts
+++ b/y2020/www/field_handler.ts
@@ -1,9 +1,11 @@
-import {Configuration, Channel} from 'aos/configuration_generated';
-import {Connection} from 'aos/network/www/proxy';
+import {Channel, Configuration} from 'aos/configuration_generated';
 import {Connect} from 'aos/network/connect_generated';
-import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
+import {Connection} from 'aos/network/www/proxy';
+import {Status as DrivetrainStatus} from 'frc971/control_loops/drivetrain/drivetrain_status_generated';
 import {ImageMatchResult} from 'y2020/vision/sift/sift_generated'
 
+import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
+
 // (0,0) is field center, +X is toward red DS
 const FIELD_SIDE_Y = FIELD_WIDTH / 2;
 const FIELD_EDGE_X = FIELD_LENGTH / 2;
@@ -45,6 +47,9 @@
 const TARGET_ZONE_WIDTH = 48 * IN_TO_M;
 const LOADING_ZONE_WIDTH = 60 * IN_TO_M;
 
+const ROBOT_WIDTH = 28 * IN_TO_M;
+const ROBOT_LENGTH = 30 * IN_TO_M;
+
 /**
  * All the messages that are required to display camera information on the field.
  * Messages not readable on the server node are ignored.
@@ -70,11 +75,16 @@
     name: '/pi5/camera',
     type: 'frc971.vision.sift.ImageMatchResult',
   },
+  {
+    name: '/drivetrain',
+    type: 'frc971.control_loops.drivetrain.Status',
+  },
 ];
 
 export class FieldHandler {
   private canvas = document.createElement('canvas');
-  private imageMatchResult :ImageMatchResult|null = null
+  private imageMatchResult: ImageMatchResult|null = null;
+  private drivetrainStatus: DrivetrianStatus|null = null;
 
   constructor(private readonly connection: Connection) {
     document.body.appendChild(this.canvas);
@@ -85,6 +95,9 @@
     this.connection.addHandler(ImageMatchResult.getFullyQualifiedName(), (res) => {
       this.handleImageMatchResult(res);
     });
+    this.connection.addHandler(DrivetrainStatus.getFullyQualifiedName(), (data) => {
+      this.handleDrivetrainStatus(data);
+    });
   }
 
   private handleImageMatchResult(data: Uint8Array): void {
@@ -92,6 +105,11 @@
     this.imageMatchResult = ImageMatchResult.getRootAsImageMatchResult(fbBuffer);
   }
 
+  private handleDrivetrainStatus(data: Uint8Array): void {
+    const fbBuffer = new flatbuffers.ByteBuffer(data);
+    this.drivetrainStatus = DrivetrainStatus.getRootAsStatus(fbBuffer);
+  }
+
   private sendConnect(): void {
     const builder = new flatbuffers.Builder(512);
     const channels: flatbuffers.Offset[] = [];
@@ -199,6 +217,20 @@
     ctx.restore();
   }
 
+  drawRobot(x: number, y: number, theta: number): void {
+    const ctx = this.canvas.getContext('2d');
+    ctx.save();
+    ctx.translate(x, y);
+    ctx.rotate(theta);
+    ctx.rect(-ROBOT_LENGTH / 2, -ROBOT_WIDTH / 2, ROBOT_LENGTH, ROBOT_WIDTH);
+    ctx.stroke();
+    ctx.beginPath();
+    ctx.moveTo(0, 0);
+    ctx.lineTo(ROBOT_LENGTH / 2, 0);
+    ctx.stroke();
+    ctx.restore();
+  }
+
   draw(): void  {
     this.reset();
     this.drawField();
@@ -209,11 +241,19 @@
         const mat = pose.fieldToCamera();
         const x = mat.data(3);
         const y = mat.data(7);
-        this.drawCamera(x, y, 0);
-        console.log(x, y);
+        const theta = Math.atan2(
+            -mat.data(8),
+            Math.sqrt(Math.pow(mat.data(9), 2) + Math.pow(mat.data(10), 2)));
+        this.drawCamera(x, y, theta);
       }
     }
 
+    if (this.drivetrainStatus) {
+      this.drawRobot(
+          this.drivetrainStatus.x(), this.drivetrainStatus.y(),
+          this.drivetrainStatus.theta());
+    }
+
     window.requestAnimationFrame(() => this.draw());
   }
 
diff --git a/y2020/y2020.json b/y2020/y2020.json
index b40ac87..285a23d 100644
--- a/y2020/y2020.json
+++ b/y2020/y2020.json
@@ -46,7 +46,7 @@
       "name": "/aos/roborio",
       "type": "aos.logging.LogMessageFbs",
       "source_node": "roborio",
-      "frequency": 200,
+      "frequency": 400,
       "num_senders": 20
     },
     {
@@ -293,7 +293,24 @@
       "source_node": "roborio",
       "frequency": 200,
       "max_size": 2000,
-      "num_senders": 2
+      "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",
@@ -325,6 +342,8 @@
       "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": [
@@ -361,6 +380,8 @@
       "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": [
@@ -397,6 +418,8 @@
       "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": [