Merge "Create database functions for rankings"
diff --git a/.bazelrc b/.bazelrc
index 3a09925..d231812 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -77,7 +77,8 @@
 # Show paths to a few more than just 1 target.
 build --show_result 5
 # Dump the output of the failing test to stdout.
-test --test_output=errors
+# Keep the default test timeouts except make 'eternal'=4500 secs
+test --test_output=errors --test_timeout=-1,-1,-1,4500
 
 build --sandbox_base=/dev/shm/
 build --experimental_multi_threaded_digest
diff --git a/WORKSPACE b/WORKSPACE
index 4a00be7..31bc03c 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -76,6 +76,10 @@
     gstreamer_armhf_debs = "files",
 )
 load(
+    "//debian:gstreamer_arm64.bzl",
+    gstreamer_arm64_debs = "files",
+)
+load(
     "//debian:m4.bzl",
     m4_debs = "files",
 )
@@ -129,10 +133,18 @@
 
 generate_repositories_for_debs(opencv_amd64_debs)
 
-generate_repositories_for_debs(gstreamer_amd64_debs)
+generate_repositories_for_debs(
+    gstreamer_amd64_debs,
+    base_url = "https://www.frc971.org/Build-Dependencies/gstreamer_bullseye_amd64_deps",
+)
 
 generate_repositories_for_debs(gstreamer_armhf_debs)
 
+generate_repositories_for_debs(
+    gstreamer_arm64_debs,
+    base_url = "https://www.frc971.org/Build-Dependencies/gstreamer_bullseye_arm64_deps",
+)
+
 generate_repositories_for_debs(m4_debs)
 
 generate_repositories_for_debs(lzma_amd64_debs)
@@ -975,8 +987,8 @@
 http_archive(
     name = "gstreamer_k8",
     build_file = "@//debian:gstreamer.BUILD",
-    sha256 = "4d74d4a82f7a73dc9fe9463d5fae409b17845eef7cd64ef9c4c4553816c53589",
-    url = "https://www.frc971.org/Build-Dependencies/gstreamer_amd64.tar.gz",
+    sha256 = "d4994261a432c188716f0bdf30fc3f0dff6727319d9c58e7156e2b3ed5105248",
+    url = "https://www.frc971.org/Build-Dependencies/gstreamer_1.20.1-1~bpo11+1_amd64.tar.gz",
 )
 
 http_archive(
@@ -986,6 +998,13 @@
     url = "https://www.frc971.org/Build-Dependencies/gstreamer_armhf.tar.gz",
 )
 
+http_archive(
+    name = "gstreamer_arm64",
+    build_file = "@//debian:gstreamer.BUILD",
+    sha256 = "42b414c565ffdbae3d2d7796a66da9de42a650de757fa6554fd624f0cc3aaa9b",
+    url = "https://www.frc971.org/Build-Dependencies/gstreamer_1.20.1-1~bpo11+1_arm64.tar.gz",
+)
+
 # Downloaded from:
 # https://files.pythonhosted.org/packages/64/a7/45e11eebf2f15bf987c3bc11d37dcc838d9dc81250e67e4c5968f6008b6c/Jinja2-2.11.2.tar.gz
 http_archive(
diff --git a/aos/configuration.fbs b/aos/configuration.fbs
index c0d67b0..7595167 100644
--- a/aos/configuration.fbs
+++ b/aos/configuration.fbs
@@ -57,6 +57,9 @@
   // Type name of the flatbuffer.
   type:string (id: 1);
   // Max frequency in messages/sec of the data published on this channel.
+  // The maximum number of messages that can be sent
+  // in a channel_storage_duration is
+  // frequency * channel_storage_duration (in seconds).
   frequency:int = 100 (id: 2);
   // Max size of the data being published.  (This will hopefully be
   // automatically computed in the future.)
diff --git a/aos/events/event_loop.cc b/aos/events/event_loop.cc
index 67c4472..08b0064 100644
--- a/aos/events/event_loop.cc
+++ b/aos/events/event_loop.cc
@@ -26,6 +26,8 @@
       return "RawSender::Error::kOk";
     case RawSender::Error::kMessagesSentTooFast:
       return "RawSender::Error::kMessagesSentTooFast";
+    case RawSender::Error::kInvalidRedzone:
+      return "RawSender::Error::kInvalidRedzone";
   }
   LOG(FATAL) << "Unknown error given with code " << static_cast<int>(err);
 }
diff --git a/aos/events/event_loop.h b/aos/events/event_loop.h
index e810e3f..c2498b3 100644
--- a/aos/events/event_loop.h
+++ b/aos/events/event_loop.h
@@ -139,13 +139,16 @@
  public:
   using SharedSpan = std::shared_ptr<const absl::Span<const uint8_t>>;
 
-  enum class [[nodiscard]] Error{
+  enum class [[nodiscard]] Error {
       // Represents success and no error
       kOk,
 
       // Error for messages on channels being sent faster than their
       // frequency and channel storage duration allow
-      kMessagesSentTooFast};
+      kMessagesSentTooFast,
+      // Access to Redzone was attempted in Sender Queue
+      kInvalidRedzone
+  };
 
   RawSender(EventLoop *event_loop, const Channel *channel);
   RawSender(const RawSender &) = delete;
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index fca25d4..18f5f96 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -4,6 +4,7 @@
 #include <unordered_map>
 #include <unordered_set>
 
+#include "aos/events/test_message_generated.h"
 #include "aos/flatbuffer_merge.h"
 #include "aos/logging/log_message_generated.h"
 #include "aos/logging/logging.h"
@@ -2567,5 +2568,148 @@
                "May only send the buffer detached from this Sender");
 }
 
+int TestChannelFrequency(EventLoop *event_loop) {
+  return event_loop->GetChannel<TestMessage>("/test")->frequency();
+}
+
+int TestChannelQueueSize(EventLoop *event_loop) {
+  const int frequency = TestChannelFrequency(event_loop);
+  const auto channel_storage_duration = std::chrono::nanoseconds(
+      event_loop->configuration()->channel_storage_duration());
+  const int queue_size =
+      frequency * std::chrono::duration_cast<std::chrono::duration<double>>(
+                      channel_storage_duration)
+                      .count();
+
+  return queue_size;
+}
+
+RawSender::Error SendTestMessage(aos::Sender<TestMessage> &sender) {
+  aos::Sender<TestMessage>::Builder builder = sender.MakeBuilder();
+  TestMessage::Builder test_message_builder =
+      builder.MakeBuilder<TestMessage>();
+  test_message_builder.add_value(0);
+  return builder.Send(test_message_builder.Finish());
+}
+
+// Test that sending messages too fast returns
+// RawSender::Error::kMessagesSentTooFast.
+TEST_P(AbstractEventLoopTest, SendingMessagesTooFast) {
+  auto event_loop = MakePrimary();
+
+  auto sender = event_loop->MakeSender<TestMessage>("/test");
+
+  // Send one message in the beginning, then wait until the
+  // channel_storage_duration is almost done and start sending messages rapidly,
+  // having some come in the next chanel_storage_duration. The queue_size is
+  // 1600, so the 1601st message will be the last valid one (the initial message
+  // having being sent more than a channel_storage_duration ago), and trying to
+  // send the 1602nd message should return
+  // RawSender::Error::kMessagesSentTooFast.
+  EXPECT_EQ(SendTestMessage(sender), RawSender::Error::kOk);
+  int msgs_sent = 1;
+  const int queue_size = TestChannelQueueSize(event_loop.get());
+
+  const auto timer = event_loop->AddTimer([&]() {
+    const bool done = (msgs_sent == queue_size + 1);
+    ASSERT_EQ(
+        SendTestMessage(sender),
+        done ? RawSender::Error::kMessagesSentTooFast : RawSender::Error::kOk);
+    msgs_sent++;
+    if (done) {
+      Exit();
+    }
+  });
+
+  const auto kRepeatOffset = std::chrono::milliseconds(1);
+  const auto base_offset =
+      std::chrono::nanoseconds(
+          event_loop->configuration()->channel_storage_duration()) -
+      (kRepeatOffset * (queue_size / 2));
+  event_loop->OnRun([&event_loop, &timer, &base_offset, &kRepeatOffset]() {
+    timer->Setup(event_loop->monotonic_now() + base_offset, kRepeatOffset);
+  });
+
+  Run();
+}
+
+// Tests that we are able to send messages successfully after sending messages
+// too fast and waiting while continuously attempting to send messages.
+// Also tests that SendFailureCounter is working correctly in this
+// situation
+TEST_P(AbstractEventLoopTest, SendingAfterSendingTooFast) {
+  auto event_loop = MakePrimary();
+
+  auto sender = event_loop->MakeSender<TestMessage>("/test");
+
+  // We are sending messages at 1 kHz, so we will be sending too fast after
+  // queue_size (1600) ms. After this, keep sending messages, and exactly a
+  // channel storage duration (2s) after we send the first message we should
+  // be able to successfully send a message.
+
+  const monotonic_clock::duration kInterval = std::chrono::milliseconds(1);
+  const monotonic_clock::duration channel_storage_duration =
+      std::chrono::nanoseconds(
+          event_loop->configuration()->channel_storage_duration());
+  const int queue_size = TestChannelQueueSize(event_loop.get());
+
+  int msgs_sent = 0;
+  SendFailureCounter counter;
+  auto start = monotonic_clock::min_time;
+
+  event_loop->AddPhasedLoop(
+      [&](int) {
+        const auto actual_err = SendTestMessage(sender);
+        const bool done_waiting = (start != monotonic_clock::min_time &&
+                                   sender.monotonic_sent_time() >=
+                                       (start + channel_storage_duration));
+        const auto expected_err =
+            (msgs_sent < queue_size || done_waiting
+                 ? RawSender::Error::kOk
+                 : RawSender::Error::kMessagesSentTooFast);
+
+        if (start == monotonic_clock::min_time) {
+          start = sender.monotonic_sent_time();
+        }
+
+        ASSERT_EQ(actual_err, expected_err);
+        counter.Count(actual_err);
+        msgs_sent++;
+
+        EXPECT_EQ(counter.failures(),
+                  msgs_sent <= queue_size
+                      ? 0
+                      : (msgs_sent - queue_size) -
+                            (actual_err == RawSender::Error::kOk ? 1 : 0));
+        EXPECT_EQ(counter.just_failed(), actual_err != RawSender::Error::kOk);
+
+        if (done_waiting) {
+          Exit();
+        }
+      },
+      kInterval);
+  Run();
+}
+
+// Tests that RawSender::Error::kMessagesSentTooFast is returned
+// when messages are sent too fast from senders in different loops
+TEST_P(AbstractEventLoopTest, SendingTooFastWithMultipleLoops) {
+  auto loop1 = MakePrimary();
+  auto loop2 = Make();
+
+  auto sender1 = loop1->MakeSender<TestMessage>("/test");
+  auto sender2 = loop2->MakeSender<TestMessage>("/test");
+
+  // Send queue_size messages split between the senders.
+  const int queue_size = TestChannelQueueSize(loop1.get());
+  for (int i = 0; i < queue_size / 2; i++) {
+    ASSERT_EQ(SendTestMessage(sender1), RawSender::Error::kOk);
+    ASSERT_EQ(SendTestMessage(sender2), RawSender::Error::kOk);
+  }
+
+  // Since queue_size messages have been sent, this should return an error
+  EXPECT_EQ(SendTestMessage(sender2), RawSender::Error::kMessagesSentTooFast);
+}
+
 }  // namespace testing
 }  // namespace aos
diff --git a/aos/events/event_loop_param_test.h b/aos/events/event_loop_param_test.h
index a9d280e..fad8dea 100644
--- a/aos/events/event_loop_param_test.h
+++ b/aos/events/event_loop_param_test.h
@@ -359,6 +359,12 @@
       std::vector<std::reference_wrapper<const Fetcher<TestMessage>>> fetchers,
       std::vector<std::reference_wrapper<const Sender<TestMessage>>> senders);
 
+  // Helper function for testing the sent too fast check using a PhasedLoop with
+  // an interval that sends exactly at the frequency of the channel
+  void TestSentTooFastCheckEdgeCase(
+      const std::function<RawSender::Error(int, int)> expected_err,
+      const bool send_twice_at_end);
+
  private:
   const ::std::unique_ptr<EventLoopTestFactory> factory_;
 
@@ -367,6 +373,13 @@
 
 using AbstractEventLoopDeathTest = AbstractEventLoopTest;
 
+// Returns the frequency of the /test TestMessage channel
+int TestChannelFrequency(EventLoop *event_loop);
+// Returns the queue size of the /test TestMessage channel
+int TestChannelQueueSize(EventLoop *event_loop);
+// Sends a test message with value 0 with the given sender
+RawSender::Error SendTestMessage(aos::Sender<TestMessage> &sender);
+
 }  // namespace testing
 }  // namespace aos
 
diff --git a/aos/events/event_scheduler.cc b/aos/events/event_scheduler.cc
index 97e0946..cb0c629 100644
--- a/aos/events/event_scheduler.cc
+++ b/aos/events/event_scheduler.cc
@@ -54,7 +54,7 @@
   auto iter = events_list_.begin();
   const logger::BootTimestamp t =
       FromDistributedClock(scheduler_scheduler_->distributed_now());
-  VLOG(1) << "Got time back " << t;
+  VLOG(2) << "Got time back " << t;
   CHECK_EQ(t.boot, boot_count_);
   CHECK_EQ(t.time, iter->first) << ": Time is wrong on node " << node_index_;
 
@@ -274,7 +274,7 @@
   }
 
   if (min_scheduler) {
-    VLOG(1) << "Oldest event " << min_event_time << " on scheduler "
+    VLOG(2) << "Oldest event " << min_event_time << " on scheduler "
             << min_scheduler->node_index_;
   }
   return std::make_tuple(min_event_time, min_scheduler);
diff --git a/aos/events/logging/boot_timestamp.h b/aos/events/logging/boot_timestamp.h
index dac6533..7eead2e 100644
--- a/aos/events/logging/boot_timestamp.h
+++ b/aos/events/logging/boot_timestamp.h
@@ -22,6 +22,16 @@
   bool operator==(const BootDuration &m2) const {
     return boot == m2.boot && duration == m2.duration;
   }
+  bool operator!=(const BootDuration &m2) const {
+    return boot != m2.boot || duration != m2.duration;
+  }
+
+  static constexpr BootDuration max_time() {
+    return BootDuration{
+        .boot = std::numeric_limits<size_t>::max(),
+        .duration = monotonic_clock::duration(
+            ::std::numeric_limits<monotonic_clock::duration::rep>::max())};
+  }
 };
 
 // Simple class representing which boot and what monotonic time in that boot.
diff --git a/aos/events/logging/log_cat.cc b/aos/events/logging/log_cat.cc
index 60ad0e2..2b6fe3e 100644
--- a/aos/events/logging/log_cat.cc
+++ b/aos/events/logging/log_cat.cc
@@ -46,6 +46,12 @@
             "If true, only print out the results of logfile sorting.");
 DEFINE_bool(channels, false,
             "If true, print out all the configured channels for this log.");
+DEFINE_double(monotonic_start_time, 0.0,
+              "If set, only print messages sent at or after this many seconds "
+              "after epoch.");
+DEFINE_double(monotonic_end_time, 0.0,
+              "If set, only print messages sent at or before this many seconds "
+              "after epoch.");
 
 using aos::monotonic_clock;
 namespace chrono = std::chrono;
@@ -272,6 +278,21 @@
     const flatbuffers::Vector<flatbuffers::Offset<aos::Channel>> *channels =
         event_loop_->configuration()->channels();
 
+    const monotonic_clock::time_point start_time =
+        (FLAGS_monotonic_start_time == 0.0
+             ? monotonic_clock::min_time
+             : monotonic_clock::time_point(
+                   std::chrono::duration_cast<monotonic_clock::duration>(
+                       std::chrono::duration<double>(
+                           FLAGS_monotonic_start_time))));
+    const monotonic_clock::time_point end_time =
+        (FLAGS_monotonic_end_time == 0.0
+             ? monotonic_clock::max_time
+             : monotonic_clock::time_point(
+                   std::chrono::duration_cast<monotonic_clock::duration>(
+                       std::chrono::duration<double>(
+                           FLAGS_monotonic_end_time))));
+
     for (flatbuffers::uoffset_t i = 0; i < channels->size(); i++) {
       const aos::Channel *channel = channels->Get(i);
       const flatbuffers::string_view name = channel->name()->string_view();
@@ -286,8 +307,9 @@
 
         CHECK_NOTNULL(channel->schema());
         event_loop_->MakeRawWatcher(
-            channel, [this, channel](const aos::Context &context,
-                                     const void * /*message*/) {
+            channel,
+            [this, channel, start_time, end_time](const aos::Context &context,
+                                                  const void * /*message*/) {
               if (!FLAGS_print) {
                 return;
               }
@@ -296,6 +318,11 @@
                 return;
               }
 
+              if (context.monotonic_event_time < start_time ||
+                  context.monotonic_event_time > end_time) {
+                return;
+              }
+
               PrintMessage(node_name_, channel, context, builder_);
               ++(*message_print_counter_);
               if (FLAGS_count > 0 && *message_print_counter_ >= FLAGS_count) {
diff --git a/aos/events/logging/log_reader.cc b/aos/events/logging/log_reader.cc
index 4e89a0e..1c3a349 100644
--- a/aos/events/logging/log_reader.cc
+++ b/aos/events/logging/log_reader.cc
@@ -276,6 +276,9 @@
 
           // Otherwise collect this one up as a node to look for a combined
           // channel from.  It is more efficient to compare nodes than channels.
+          LOG(WARNING) << "Failed to find channel "
+                       << finder.SplitChannelName(channel, connection)
+                       << " on node " << aos::FlatbufferToJson(node);
           remote_nodes.insert(connection->name()->string_view());
         }
       }
diff --git a/aos/events/logging/logger_test.cc b/aos/events/logging/logger_test.cc
index e9f44b2..c346854 100644
--- a/aos/events/logging/logger_test.cc
+++ b/aos/events/logging/logger_test.cc
@@ -4067,6 +4067,76 @@
   ConfirmReadable(filenames);
 }
 
+// Tests that we properly handle only one direction ever existing after a
+// reboot.
+TEST(MissingDirectionTest, OneDirectionAfterReboot) {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(ArtifactPath(
+          "aos/events/logging/multinode_pingpong_split4_config.json"));
+  message_bridge::TestingTimeConverter time_converter(
+      configuration::NodesCount(&config.message()));
+  SimulatedEventLoopFactory event_loop_factory(&config.message());
+  event_loop_factory.SetTimeConverter(&time_converter);
+
+  NodeEventLoopFactory *const pi1 =
+      event_loop_factory.GetNodeEventLoopFactory("pi1");
+  const size_t pi1_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi1->node());
+  NodeEventLoopFactory *const pi2 =
+      event_loop_factory.GetNodeEventLoopFactory("pi2");
+  const size_t pi2_index = configuration::GetNodeIndex(
+      event_loop_factory.configuration(), pi2->node());
+  std::vector<std::string> filenames;
+
+  {
+    CHECK_EQ(pi1_index, 0u);
+    CHECK_EQ(pi2_index, 1u);
+
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch(),
+        {BootTimestamp::epoch(), BootTimestamp::epoch()});
+
+    const chrono::nanoseconds reboot_time = chrono::milliseconds(5000);
+    time_converter.AddNextTimestamp(
+        distributed_clock::epoch() + reboot_time,
+        {BootTimestamp{.boot = 1,
+                       .time = monotonic_clock::epoch()},
+         BootTimestamp::epoch() + reboot_time});
+  }
+
+  const std::string kLogfile2_1 =
+      aos::testing::TestTmpDir() + "/multi_logfile2.1/";
+  util::UnlinkRecursive(kLogfile2_1);
+
+
+  pi1->AlwaysStart<Ping>("ping");
+
+  // Pi1 sends to pi2.  Reboot pi1, but don't let pi2 connect to pi1.  This
+  // makes it such that we will only get timestamps from pi1 -> pi2 on the
+  // second boot.
+  {
+    LoggerState pi2_logger = LoggerState::MakeLogger(
+        pi2, &event_loop_factory, SupportedCompressionAlgorithms()[0]);
+
+    event_loop_factory.RunFor(chrono::milliseconds(95));
+
+    pi2_logger.StartLogger(kLogfile2_1);
+
+    event_loop_factory.RunFor(chrono::milliseconds(4000));
+
+    pi2->Disconnect(pi1->node());
+
+    event_loop_factory.RunFor(chrono::milliseconds(1000));
+    pi1->AlwaysStart<Ping>("ping");
+
+    event_loop_factory.RunFor(chrono::milliseconds(5000));
+    pi2_logger.AppendAllFilenames(&filenames);
+  }
+
+  const std::vector<LogFile> sorted_parts = SortParts(filenames);
+  ConfirmReadable(filenames);
+}
+
 }  // namespace testing
 }  // namespace logger
 }  // namespace aos
diff --git a/aos/events/shm_event_loop.cc b/aos/events/shm_event_loop.cc
index ed3d099..9159553 100644
--- a/aos/events/shm_event_loop.cc
+++ b/aos/events/shm_event_loop.cc
@@ -522,7 +522,10 @@
             chrono::ceil<chrono::seconds>(chrono::nanoseconds(
                 event_loop->configuration()->channel_storage_duration()))),
         lockless_queue_sender_(VerifySender(
-            ipc_lib::LocklessQueueSender::Make(lockless_queue_memory_.queue()),
+            ipc_lib::LocklessQueueSender::Make(
+                lockless_queue_memory_.queue(),
+                std::chrono::nanoseconds(
+                    event_loop->configuration()->channel_storage_duration())),
             channel)),
         wake_upper_(lockless_queue_memory_.queue()) {}
 
@@ -557,17 +560,17 @@
     CHECK_LE(length, static_cast<size_t>(channel()->max_size()))
         << ": Sent too big a message on "
         << configuration::CleanedChannelToString(channel());
-    CHECK(lockless_queue_sender_.Send(length, monotonic_remote_time,
-                                      realtime_remote_time, remote_queue_index,
-                                      source_boot_uuid, &monotonic_sent_time_,
-                                      &realtime_sent_time_, &sent_queue_index_))
+    const auto result = lockless_queue_sender_.Send(
+        length, monotonic_remote_time, realtime_remote_time, remote_queue_index,
+        source_boot_uuid, &monotonic_sent_time_, &realtime_sent_time_,
+        &sent_queue_index_);
+    CHECK_NE(result, ipc_lib::LocklessQueueSender::Result::INVALID_REDZONE)
         << ": Somebody wrote outside the buffer of their message on channel "
         << configuration::CleanedChannelToString(channel());
 
     wake_upper_.Wakeup(event_loop()->is_running() ? event_loop()->priority()
                                                   : 0);
-    // TODO(Milind): check for messages sent too fast
-    return Error::kOk;
+    return CheckLocklessQueueResult(result);
   }
 
   Error DoSend(const void *msg, size_t length,
@@ -579,16 +582,19 @@
     CHECK_LE(length, static_cast<size_t>(channel()->max_size()))
         << ": Sent too big a message on "
         << configuration::CleanedChannelToString(channel());
-    CHECK(lockless_queue_sender_.Send(
+    const auto result = lockless_queue_sender_.Send(
         reinterpret_cast<const char *>(msg), length, monotonic_remote_time,
         realtime_remote_time, remote_queue_index, source_boot_uuid,
-        &monotonic_sent_time_, &realtime_sent_time_, &sent_queue_index_))
-        << ": Somebody wrote outside the buffer of their message on channel "
+        &monotonic_sent_time_, &realtime_sent_time_, &sent_queue_index_);
+
+    CHECK_NE(result, ipc_lib::LocklessQueueSender::Result::INVALID_REDZONE)
+        << ": Somebody wrote outside the buffer of their message on "
+           "channel "
         << configuration::CleanedChannelToString(channel());
     wake_upper_.Wakeup(event_loop()->is_running() ? event_loop()->priority()
                                                   : 0);
-    // TODO(austin): Return an error if we send too fast.
-    return RawSender::Error::kOk;
+
+    return CheckLocklessQueueResult(result);
   }
 
   absl::Span<char> GetSharedMemory() const {
@@ -605,6 +611,20 @@
     return static_cast<const ShmEventLoop *>(event_loop());
   }
 
+  RawSender::Error CheckLocklessQueueResult(
+      const ipc_lib::LocklessQueueSender::Result &result) {
+    switch (result) {
+      case ipc_lib::LocklessQueueSender::Result::GOOD:
+        return Error::kOk;
+      case ipc_lib::LocklessQueueSender::Result::MESSAGES_SENT_TOO_FAST:
+        return Error::kMessagesSentTooFast;
+      case ipc_lib::LocklessQueueSender::Result::INVALID_REDZONE:
+        return Error::kInvalidRedzone;
+    }
+    LOG(FATAL) << "Unknown lockless queue sender result"
+               << static_cast<int>(result);
+  }
+
   MMappedQueue lockless_queue_memory_;
   ipc_lib::LocklessQueueSender lockless_queue_sender_;
   ipc_lib::LocklessQueueWakeUpper wake_upper_;
diff --git a/aos/events/shm_event_loop_test.cc b/aos/events/shm_event_loop_test.cc
index 01ca92b..f4107b0 100644
--- a/aos/events/shm_event_loop_test.cc
+++ b/aos/events/shm_event_loop_test.cc
@@ -116,6 +116,59 @@
 
   ShmEventLoopTestFactory *factory() { return &factory_; }
 
+  // Helper functions for testing when a fetcher cannot fetch the next message
+  // because it was overwritten
+  void TestNextMessageNotAvailable(const bool skip_timing_report) {
+    auto loop1 = factory()->MakePrimary("loop1");
+    if (skip_timing_report) {
+      loop1->SkipTimingReport();
+    }
+    auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
+    auto loop2 = factory()->Make("loop2");
+    auto sender = loop2->MakeSender<TestMessage>("/test");
+    bool ran = false;
+    loop1->AddPhasedLoop(
+        [&sender](int) {
+          auto builder = sender.MakeBuilder();
+          TestMessage::Builder test_builder(*builder.fbb());
+          test_builder.add_value(0);
+          builder.CheckOk(builder.Send(test_builder.Finish()));
+        },
+        std::chrono::milliseconds(2));
+    loop1
+        ->AddTimer([this, &fetcher, &ran]() {
+          EXPECT_DEATH(fetcher.FetchNext(),
+                       "The next message is no longer "
+                       "available.*\"/test\".*\"aos\\.TestMessage\"");
+          factory()->Exit();
+          ran = true;
+        })
+        ->Setup(loop1->monotonic_now() + std::chrono::seconds(4));
+    factory()->Run();
+    EXPECT_TRUE(ran);
+  }
+  void TestNextMessageNotAvailableNoRun(const bool skip_timing_report) {
+    auto loop1 = factory()->MakePrimary("loop1");
+    if (skip_timing_report) {
+      loop1->SkipTimingReport();
+    }
+    auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
+    auto loop2 = factory()->Make("loop2");
+    auto sender = loop2->MakeSender<TestMessage>("/test");
+    time::PhasedLoop phased_loop(std::chrono::milliseconds(2),
+                                 loop2->monotonic_now());
+    for (int i = 0; i < 2000; ++i) {
+      auto builder = sender.MakeBuilder();
+      TestMessage::Builder test_builder(*builder.fbb());
+      test_builder.add_value(0);
+      builder.CheckOk(builder.Send(test_builder.Finish()));
+      phased_loop.SleepUntilNext();
+    }
+    EXPECT_DEATH(fetcher.FetchNext(),
+                 "The next message is no longer "
+                 "available.*\"/test\".*\"aos\\.TestMessage\"");
+  }
+
  private:
   ShmEventLoopTestFactory factory_;
 };
@@ -351,89 +404,25 @@
 // Tests that the next message not being available prints a helpful error in the
 // normal case.
 TEST_P(ShmEventLoopDeathTest, NextMessageNotAvailable) {
-  auto loop1 = factory()->MakePrimary("loop1");
-  auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
-  auto loop2 = factory()->Make("loop2");
-  auto sender = loop2->MakeSender<TestMessage>("/test");
-  bool ran = false;
-  loop1->OnRun([this, &sender, &fetcher, &ran]() {
-    for (int i = 0; i < 2000; ++i) {
-      auto builder = sender.MakeBuilder();
-      TestMessage::Builder test_builder(*builder.fbb());
-      test_builder.add_value(0);
-      builder.CheckOk(builder.Send(test_builder.Finish()));
-    }
-    EXPECT_DEATH(fetcher.FetchNext(),
-                 "The next message is no longer "
-                 "available.*\"/test\".*\"aos\\.TestMessage\"");
-    factory()->Exit();
-    ran = true;
-  });
-  factory()->Run();
-  EXPECT_TRUE(ran);
+  TestNextMessageNotAvailable(false);
 }
 
 // Tests that the next message not being available prints a helpful error with
 // timing reports disabled.
 TEST_P(ShmEventLoopDeathTest, NextMessageNotAvailableNoTimingReports) {
-  auto loop1 = factory()->MakePrimary("loop1");
-  loop1->SkipTimingReport();
-  auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
-  auto loop2 = factory()->Make("loop2");
-  auto sender = loop2->MakeSender<TestMessage>("/test");
-  bool ran = false;
-  loop1->OnRun([this, &sender, &fetcher, &ran]() {
-    for (int i = 0; i < 2000; ++i) {
-      auto builder = sender.MakeBuilder();
-      TestMessage::Builder test_builder(*builder.fbb());
-      test_builder.add_value(0);
-      builder.CheckOk(builder.Send(test_builder.Finish()));
-    }
-    EXPECT_DEATH(fetcher.FetchNext(),
-                 "The next message is no longer "
-                 "available.*\"/test\".*\"aos\\.TestMessage\"");
-    factory()->Exit();
-    ran = true;
-  });
-  factory()->Run();
-  EXPECT_TRUE(ran);
+  TestNextMessageNotAvailable(true);
 }
 
 // Tests that the next message not being available prints a helpful error even
 // when Run is never called.
 TEST_P(ShmEventLoopDeathTest, NextMessageNotAvailableNoRun) {
-  auto loop1 = factory()->MakePrimary("loop1");
-  auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
-  auto loop2 = factory()->Make("loop2");
-  auto sender = loop2->MakeSender<TestMessage>("/test");
-  for (int i = 0; i < 2000; ++i) {
-    auto builder = sender.MakeBuilder();
-    TestMessage::Builder test_builder(*builder.fbb());
-    test_builder.add_value(0);
-    builder.CheckOk(builder.Send(test_builder.Finish()));
-  }
-  EXPECT_DEATH(fetcher.FetchNext(),
-               "The next message is no longer "
-               "available.*\"/test\".*\"aos\\.TestMessage\"");
+  TestNextMessageNotAvailableNoRun(false);
 }
 
 // Tests that the next message not being available prints a helpful error even
 // when Run is never called without timing reports.
 TEST_P(ShmEventLoopDeathTest, NextMessageNotAvailableNoRunNoTimingReports) {
-  auto loop1 = factory()->MakePrimary("loop1");
-  loop1->SkipTimingReport();
-  auto fetcher = loop1->MakeFetcher<TestMessage>("/test");
-  auto loop2 = factory()->Make("loop2");
-  auto sender = loop2->MakeSender<TestMessage>("/test");
-  for (int i = 0; i < 2000; ++i) {
-    auto builder = sender.MakeBuilder();
-    TestMessage::Builder test_builder(*builder.fbb());
-    test_builder.add_value(0);
-    builder.CheckOk(builder.Send(test_builder.Finish()));
-  }
-  EXPECT_DEATH(fetcher.FetchNext(),
-               "The next message is no longer "
-               "available.*\"/test\".*\"aos\\.TestMessage\"");
+  TestNextMessageNotAvailableNoRun(true);
 }
 
 // TODO(austin): Test that missing a deadline with a timer recovers as expected.
diff --git a/aos/events/simulated_event_loop.cc b/aos/events/simulated_event_loop.cc
index a1ef95d..69e6638 100644
--- a/aos/events/simulated_event_loop.cc
+++ b/aos/events/simulated_event_loop.cc
@@ -151,10 +151,12 @@
 class SimulatedChannel {
  public:
   explicit SimulatedChannel(const Channel *channel,
-                            std::chrono::nanoseconds channel_storage_duration)
+                            std::chrono::nanoseconds channel_storage_duration,
+                            const EventScheduler *scheduler)
       : channel_(channel),
         channel_storage_duration_(channel_storage_duration),
-        next_queue_index_(ipc_lib::QueueIndex::Zero(number_buffers())) {
+        next_queue_index_(ipc_lib::QueueIndex::Zero(number_buffers())),
+        scheduler_(scheduler) {
     available_buffer_indices_.resize(number_buffers());
     for (int i = 0; i < number_buffers(); ++i) {
       available_buffer_indices_[i] = i;
@@ -296,6 +298,11 @@
   int sender_count_ = 0;
 
   std::vector<uint16_t> available_buffer_indices_;
+
+  const EventScheduler *scheduler_;
+
+  // Queue of all the message send times in the last channel_storage_duration_
+  std::queue<monotonic_clock::time_point> last_times_;
 };
 
 namespace {
@@ -782,14 +789,14 @@
     const Channel *channel) {
   auto it = channels_->find(SimpleChannel(channel));
   if (it == channels_->end()) {
-    it =
-        channels_
-            ->emplace(
-                SimpleChannel(channel),
-                std::unique_ptr<SimulatedChannel>(new SimulatedChannel(
-                    channel, std::chrono::nanoseconds(
-                                 configuration()->channel_storage_duration()))))
-            .first;
+    it = channels_
+             ->emplace(SimpleChannel(channel),
+                       std::unique_ptr<SimulatedChannel>(new SimulatedChannel(
+                           channel,
+                           std::chrono::nanoseconds(
+                               configuration()->channel_storage_duration()),
+                           scheduler_)))
+             .first;
   }
   return it->second.get();
 }
@@ -930,7 +937,21 @@
 
 std::optional<uint32_t> SimulatedChannel::Send(
     std::shared_ptr<SimulatedMessage> message) {
-  std::optional<uint32_t> queue_index = {next_queue_index_.index()};
+  const auto now = scheduler_->monotonic_now();
+  // Remove times that are greater than or equal to a channel_storage_duration_
+  // ago
+  while (!last_times_.empty() &&
+         (now - last_times_.front() >= channel_storage_duration_)) {
+    last_times_.pop();
+  }
+
+  // Check that we are not sending messages too fast
+  if (static_cast<int>(last_times_.size()) >= queue_size()) {
+    return std::nullopt;
+  }
+
+  const std::optional<uint32_t> queue_index = {next_queue_index_.index()};
+  last_times_.push(now);
 
   message->context.queue_index = *queue_index;
   // Points to the actual data depending on the size set in context. Data may
diff --git a/aos/events/simulated_event_loop_test.cc b/aos/events/simulated_event_loop_test.cc
index 09202ff..a83243d 100644
--- a/aos/events/simulated_event_loop_test.cc
+++ b/aos/events/simulated_event_loop_test.cc
@@ -1,6 +1,7 @@
 #include "aos/events/simulated_event_loop.h"
 
 #include <chrono>
+#include <functional>
 #include <string_view>
 
 #include "aos/events/event_loop_param_test.h"
@@ -139,13 +140,13 @@
 };
 
 class FunctionEvent : public EventScheduler::Event {
-  public:
-   FunctionEvent(std::function<void()> fn) : fn_(fn) {}
+ public:
+  FunctionEvent(std::function<void()> fn) : fn_(fn) {}
 
-   void Handle() noexcept override { fn_(); }
+  void Handle() noexcept override { fn_(); }
 
-  private:
-   std::function<void()> fn_;
+ private:
+  std::function<void()> fn_;
 };
 
 // Test that creating an event and running the scheduler runs the event.
@@ -204,14 +205,6 @@
   EXPECT_EQ(counter, 1);
 }
 
-void SendTestMessage(aos::Sender<TestMessage> *sender, int value) {
-  aos::Sender<TestMessage>::Builder builder = sender->MakeBuilder();
-  TestMessage::Builder test_message_builder =
-      builder.MakeBuilder<TestMessage>();
-  test_message_builder.add_value(value);
-  ASSERT_EQ(builder.Send(test_message_builder.Finish()), RawSender::Error::kOk);
-}
-
 // Test that sending a message after running gets properly notified.
 TEST(SimulatedEventLoopTest, SendAfterRunFor) {
   SimulatedEventLoopTestFactory factory;
@@ -223,7 +216,7 @@
       simulated_event_loop_factory.MakeEventLoop("ping");
   aos::Sender<TestMessage> test_message_sender =
       ping_event_loop->MakeSender<TestMessage>("/test");
-  SendTestMessage(&test_message_sender, 1);
+  ASSERT_EQ(SendTestMessage(test_message_sender), RawSender::Error::kOk);
 
   std::unique_ptr<EventLoop> pong1_event_loop =
       simulated_event_loop_factory.MakeEventLoop("pong");
@@ -243,7 +236,7 @@
 
   // Pauses in the middle don't count though, so this should be counted.
   // But, the fresh watcher shouldn't pick it up yet.
-  SendTestMessage(&test_message_sender, 2);
+  ASSERT_EQ(SendTestMessage(test_message_sender), RawSender::Error::kOk);
 
   EXPECT_EQ(test_message_counter1.count(), 0u);
   EXPECT_EQ(test_message_counter2.count(), 0u);
@@ -253,6 +246,63 @@
   EXPECT_EQ(test_message_counter2.count(), 0u);
 }
 
+void TestSentTooFastCheckEdgeCase(
+    const std::function<RawSender::Error(int, int)> expected_err,
+    const bool send_twice_at_end) {
+  SimulatedEventLoopTestFactory factory;
+
+  auto event_loop = factory.MakePrimary("primary");
+
+  auto sender = event_loop->MakeSender<TestMessage>("/test");
+
+  const int queue_size = TestChannelQueueSize(event_loop.get());
+  int msgs_sent = 0;
+  event_loop->AddPhasedLoop(
+      [&](int) {
+        EXPECT_EQ(SendTestMessage(sender), expected_err(msgs_sent, queue_size));
+        msgs_sent++;
+
+        // If send_twice_at_end, send the last two messages (message
+        // queue_size and queue_size + 1) in the same iteration, meaning that
+        // we would be sending very slightly too fast. Otherwise, we will send
+        // message queue_size + 1 in the next iteration and we will continue
+        // to be sending exactly at the channel frequency.
+        if (send_twice_at_end && (msgs_sent == queue_size)) {
+          EXPECT_EQ(SendTestMessage(sender),
+                    expected_err(msgs_sent, queue_size));
+          msgs_sent++;
+        }
+
+        if (msgs_sent > queue_size) {
+          factory.Exit();
+        }
+      },
+      std::chrono::duration_cast<std::chrono::nanoseconds>(
+          std::chrono::duration<double>(
+              1.0 / TestChannelFrequency(event_loop.get()))));
+
+  factory.Run();
+}
+
+// Tests that RawSender::Error::kMessagesSentTooFast is not returned
+// when messages are sent at the exact frequency of the channel.
+TEST(SimulatedEventLoopTest, SendingAtExactlyChannelFrequency) {
+  TestSentTooFastCheckEdgeCase([](int, int) { return RawSender::Error::kOk; },
+                               false);
+}
+
+// Tests that RawSender::Error::kMessagesSentTooFast is returned
+// when sending exactly one more message than allowed in a channel storage
+// duration.
+TEST(SimulatedEventLoopTest, SendingSlightlyTooFast) {
+  TestSentTooFastCheckEdgeCase(
+      [](const int msgs_sent, const int queue_size) {
+        return (msgs_sent == queue_size ? RawSender::Error::kMessagesSentTooFast
+                                        : RawSender::Error::kOk);
+      },
+      true);
+}
+
 // Test that creating an event loop while running dies.
 TEST(SimulatedEventLoopDeathTest, MakeEventLoopWhileRunning) {
   SimulatedEventLoopTestFactory factory;
@@ -1338,7 +1388,6 @@
   EXPECT_EQ(ConnectedCount(pi3_client_statistics_fetcher.get(), "pi1"), 2u)
       << " : " << aos::FlatbufferToJson(pi3_client_statistics_fetcher.get());
 
-
   EXPECT_EQ(pi1_pong_counter.count(), 601u);
   EXPECT_EQ(pi2_pong_counter.count(), 601u);
 
@@ -1707,18 +1756,17 @@
       });
 
   // Confirm that reboot changes the UUID.
-  pi2->OnShutdown(
-      [&expected_boot_uuid, &boot_number, &expected_connection_time, pi1, pi2,
-       pi2_boot1]() {
-        expected_boot_uuid = pi2_boot1;
-        ++boot_number;
-        LOG(INFO) << "OnShutdown triggered for pi2";
-        pi2->OnStartup(
-            [&expected_boot_uuid, &expected_connection_time, pi1, pi2]() {
-              EXPECT_EQ(expected_boot_uuid, pi2->boot_uuid());
-              expected_connection_time = pi1->monotonic_now();
-            });
-      });
+  pi2->OnShutdown([&expected_boot_uuid, &boot_number, &expected_connection_time,
+                   pi1, pi2, pi2_boot1]() {
+    expected_boot_uuid = pi2_boot1;
+    ++boot_number;
+    LOG(INFO) << "OnShutdown triggered for pi2";
+    pi2->OnStartup(
+        [&expected_boot_uuid, &expected_connection_time, pi1, pi2]() {
+          EXPECT_EQ(expected_boot_uuid, pi2->boot_uuid());
+          expected_connection_time = pi1->monotonic_now();
+        });
+  });
 
   // Let a couple of ServerStatistics messages show up before rebooting.
   factory.RunFor(chrono::milliseconds(2002));
diff --git a/aos/ipc_lib/BUILD b/aos/ipc_lib/BUILD
index c124fe0..ef6c4f3 100644
--- a/aos/ipc_lib/BUILD
+++ b/aos/ipc_lib/BUILD
@@ -216,6 +216,7 @@
         "//aos/events:epoll",
         "//aos/testing:googletest",
         "//aos/testing:prevent_exit",
+        "//aos/util:phased_loop",
     ],
 )
 
diff --git a/aos/ipc_lib/lockless_queue.cc b/aos/ipc_lib/lockless_queue.cc
index fe86f6c..5f12423 100644
--- a/aos/ipc_lib/lockless_queue.cc
+++ b/aos/ipc_lib/lockless_queue.cc
@@ -4,6 +4,7 @@
 #include <sys/types.h>
 #include <syscall.h>
 #include <unistd.h>
+
 #include <algorithm>
 #include <iomanip>
 #include <iostream>
@@ -503,7 +504,7 @@
 
   bool bad = false;
 
-  for (size_t i = 0; i < redzone.size(); ++i) {
+  for (size_t i = 0; i < redzone.size() && !bad; ++i) {
     if (memcmp(&redzone[i], &redzone_value, 1)) {
       bad = true;
     }
@@ -603,6 +604,7 @@
       Message *const message =
           memory->GetMessage(Index(QueueIndex::Zero(memory->queue_size()), i));
       message->header.queue_index.Invalidate();
+      message->header.monotonic_sent_time = monotonic_clock::min_time;
       FillRedzone(memory, message->PreRedzone(memory->message_data_size()));
       FillRedzone(memory, message->PostRedzone(memory->message_data_size(),
                                                memory->message_size()));
@@ -831,8 +833,16 @@
   return count;
 }
 
-LocklessQueueSender::LocklessQueueSender(LocklessQueueMemory *memory)
-    : memory_(memory) {
+std::ostream &operator<<(std::ostream &os,
+                         const LocklessQueueSender::Result r) {
+  os << static_cast<int>(r);
+  return os;
+}
+
+LocklessQueueSender::LocklessQueueSender(
+    LocklessQueueMemory *memory,
+    monotonic_clock::duration channel_storage_duration)
+    : memory_(memory), channel_storage_duration_(channel_storage_duration) {
   GrabQueueSetupLockOrDie grab_queue_setup_lock(memory_);
 
   // Since we already have the lock, go ahead and try cleaning up.
@@ -877,9 +887,9 @@
 }
 
 std::optional<LocklessQueueSender> LocklessQueueSender::Make(
-    LocklessQueue queue) {
+    LocklessQueue queue, monotonic_clock::duration channel_storage_duration) {
   queue.Initialize();
-  LocklessQueueSender result(queue.memory());
+  LocklessQueueSender result(queue.memory(), channel_storage_duration);
   if (result.sender_index_ != -1) {
     return std::move(result);
   } else {
@@ -904,7 +914,7 @@
   return message->data(memory_->message_data_size());
 }
 
-bool LocklessQueueSender::Send(
+LocklessQueueSender::Result LocklessQueueSender::Send(
     const char *data, size_t length,
     monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
@@ -921,7 +931,7 @@
               realtime_sent_time, queue_index);
 }
 
-bool LocklessQueueSender::Send(
+LocklessQueueSender::Result LocklessQueueSender::Send(
     size_t length, monotonic_clock::time_point monotonic_remote_time,
     realtime_clock::time_point realtime_remote_time,
     uint32_t remote_queue_index, const UUID &source_boot_uuid,
@@ -936,7 +946,7 @@
   const Index scratch_index = sender->scratch_index.RelaxedLoad();
   Message *const message = memory_->GetMessage(scratch_index);
   if (CheckBothRedzones(memory_, message)) {
-    return false;
+    return Result::INVALID_REDZONE;
   }
 
   // We should have invalidated this when we first got the buffer. Verify that
@@ -995,11 +1005,14 @@
     // This is just a best-effort check to skip reading the clocks if possible.
     // If this fails, then the compare-exchange below definitely would, so we
     // can bail out now.
+    const Message *message_to_replace = memory_->GetMessage(to_replace);
+    bool is_previous_index_valid = false;
     {
       const QueueIndex previous_index =
-          memory_->GetMessage(to_replace)
-              ->header.queue_index.RelaxedLoad(queue_size);
-      if (previous_index != decremented_queue_index && previous_index.valid()) {
+          message_to_replace->header.queue_index.RelaxedLoad(queue_size);
+      is_previous_index_valid = previous_index.valid();
+      if (previous_index != decremented_queue_index &&
+          is_previous_index_valid) {
         // Retry.
         VLOG(3) << "Something fishy happened, queue index doesn't match.  "
                    "Retrying.  Previous index was "
@@ -1011,6 +1024,7 @@
 
     message->header.monotonic_sent_time = ::aos::monotonic_clock::now();
     message->header.realtime_sent_time = ::aos::realtime_clock::now();
+
     if (monotonic_sent_time != nullptr) {
       *monotonic_sent_time = message->header.monotonic_sent_time;
     }
@@ -1021,8 +1035,48 @@
       *queue_index = next_queue_index.index();
     }
 
+    const auto to_replace_monotonic_sent_time =
+        message_to_replace->header.monotonic_sent_time;
+
+    // If we are overwriting a message sent in the last
+    // channel_storage_duration_, that means that we would be sending more than
+    // queue_size messages and would therefore be sending too fast. If the
+    // previous index is not valid then the message hasn't been filled out yet
+    // so we aren't sending too fast. And, if it is not less than the sent time
+    // of the message that we are going to write, someone else beat us and the
+    // compare and exchange below will fail.
+    if (is_previous_index_valid &&
+        (to_replace_monotonic_sent_time <
+         message->header.monotonic_sent_time) &&
+        (message->header.monotonic_sent_time - to_replace_monotonic_sent_time <
+         channel_storage_duration_)) {
+      // There is a possibility that another context beat us to writing out the
+      // message in the queue, but we beat that context to acquiring the sent
+      // time. In this case our sent time is *greater than* the other context's
+      // sent time. Therefore, we can check if we got beat filling out this
+      // message *after* doing the above check to determine if we hit this edge
+      // case. Otherwise, messages are being sent too fast.
+      const QueueIndex previous_index =
+          message_to_replace->header.queue_index.Load(queue_size);
+      if (previous_index != decremented_queue_index && previous_index.valid()) {
+        VLOG(3) << "Got beat during check for messages being sent too fast"
+                   "Retrying.";
+        continue;
+      } else {
+        VLOG(3) << "Messages sent too fast. Returning. Attempted index: "
+                << decremented_queue_index.index()
+                << " message sent time: " << message->header.monotonic_sent_time
+                << "  message to replace sent time: "
+                << to_replace_monotonic_sent_time;
+        // Since we are not using the message obtained from scratch_index
+        // and we are not retrying, we need to invalidate its queue_index.
+        message->header.queue_index.Invalidate();
+        return Result::MESSAGES_SENT_TOO_FAST;
+      }
+    }
+
     // Before we are fully done filling out the message, update the Sender state
-    // with the new index to write.  This re-uses the barrier for the
+    // with the new index to write. This re-uses the barrier for the
     // queue_index store.
     const Index index_to_write(next_queue_index, scratch_index.message_index());
 
@@ -1085,7 +1139,7 @@
   // If anybody is looking at this message (they shouldn't be), then try telling
   // them about it (best-effort).
   memory_->GetMessage(new_scratch)->header.queue_index.RelaxedInvalidate();
-  return true;
+  return Result::GOOD;
 }
 
 int LocklessQueueSender::buffer_index() const {
diff --git a/aos/ipc_lib/lockless_queue.h b/aos/ipc_lib/lockless_queue.h
index d6fb72f..f7a85c3 100644
--- a/aos/ipc_lib/lockless_queue.h
+++ b/aos/ipc_lib/lockless_queue.h
@@ -282,10 +282,19 @@
 // scoped to this object's lifetime.
 class LocklessQueueSender {
  public:
+  // Enum of possible sending errors
+  // Send returns GOOD if the messages was sent successfully, INVALID_REDZONE if
+  // one of a message's redzones has invalid data, or MESSAGES_SENT_TOO_FAST if
+  // more than queue_size messages were going to be sent in a
+  // channel_storage_duration_.
+  enum class Result { GOOD, INVALID_REDZONE, MESSAGES_SENT_TOO_FAST };
+
   LocklessQueueSender(const LocklessQueueSender &) = delete;
   LocklessQueueSender &operator=(const LocklessQueueSender &) = delete;
   LocklessQueueSender(LocklessQueueSender &&other)
-      : memory_(other.memory_), sender_index_(other.sender_index_) {
+      : memory_(other.memory_),
+        sender_index_(other.sender_index_),
+        channel_storage_duration_(other.channel_storage_duration_) {
     other.memory_ = nullptr;
     other.sender_index_ = -1;
   }
@@ -299,7 +308,8 @@
 
   // Creates a sender.  If we couldn't allocate a sender, returns nullopt.
   // TODO(austin): Change the API if we find ourselves with more errors.
-  static std::optional<LocklessQueueSender> Make(LocklessQueue queue);
+  static std::optional<LocklessQueueSender> Make(
+      LocklessQueue queue, monotonic_clock::duration channel_storage_duration);
 
   // Sends a message without copying the data.
   // Copy at most size() bytes of data into the memory pointed to by Data(),
@@ -307,34 +317,43 @@
   // Note: calls to Data() are expensive enough that you should cache it.
   size_t size() const;
   void *Data();
-  bool Send(size_t length, monotonic_clock::time_point monotonic_remote_time,
-            realtime_clock::time_point realtime_remote_time,
-            uint32_t remote_queue_index, const UUID &source_boot_uuid,
-            monotonic_clock::time_point *monotonic_sent_time = nullptr,
-            realtime_clock::time_point *realtime_sent_time = nullptr,
-            uint32_t *queue_index = nullptr);
+  LocklessQueueSender::Result Send(
+      size_t length, monotonic_clock::time_point monotonic_remote_time,
+      realtime_clock::time_point realtime_remote_time,
+      uint32_t remote_queue_index, const UUID &source_boot_uuid,
+      monotonic_clock::time_point *monotonic_sent_time = nullptr,
+      realtime_clock::time_point *realtime_sent_time = nullptr,
+      uint32_t *queue_index = nullptr);
 
   // Sends up to length data.  Does not wakeup the target.
-  bool Send(const char *data, size_t length,
-            monotonic_clock::time_point monotonic_remote_time,
-            realtime_clock::time_point realtime_remote_time,
-            uint32_t remote_queue_index, const UUID &source_boot_uuid,
-            monotonic_clock::time_point *monotonic_sent_time = nullptr,
-            realtime_clock::time_point *realtime_sent_time = nullptr,
-            uint32_t *queue_index = nullptr);
+  LocklessQueueSender::Result Send(
+      const char *data, size_t length,
+      monotonic_clock::time_point monotonic_remote_time,
+      realtime_clock::time_point realtime_remote_time,
+      uint32_t remote_queue_index, const UUID &source_boot_uuid,
+      monotonic_clock::time_point *monotonic_sent_time = nullptr,
+      realtime_clock::time_point *realtime_sent_time = nullptr,
+      uint32_t *queue_index = nullptr);
 
   int buffer_index() const;
 
  private:
-  LocklessQueueSender(LocklessQueueMemory *memory);
+  LocklessQueueSender(LocklessQueueMemory *memory,
+                      monotonic_clock::duration channel_storage_duration);
 
   // Pointer to the backing memory.
   LocklessQueueMemory *memory_ = nullptr;
 
   // Index into the sender list.
   int sender_index_ = -1;
+
+  // Storage duration of the channel used to check if messages were sent too
+  // fast
+  const monotonic_clock::duration channel_storage_duration_;
 };
 
+std::ostream &operator<<(std::ostream &os, const LocklessQueueSender::Result r);
+
 // Pinner for blocks of data.  The resources associated with a pinner are
 // scoped to this object's lifetime.
 class LocklessQueuePinner {
diff --git a/aos/ipc_lib/lockless_queue_death_test.cc b/aos/ipc_lib/lockless_queue_death_test.cc
index 2217811..c5edb9e 100644
--- a/aos/ipc_lib/lockless_queue_death_test.cc
+++ b/aos/ipc_lib/lockless_queue_death_test.cc
@@ -558,6 +558,9 @@
 
 static int kPinnedMessageIndex = 0;
 
+constexpr monotonic_clock::duration kChannelStorageDuration =
+    std::chrono::milliseconds(500);
+
 }  // namespace
 
 // Tests that death during sends is recovered from correctly.
@@ -575,7 +578,7 @@
   config.num_watchers = 2;
   config.num_senders = 2;
   config.num_pinners = 1;
-  config.queue_size = 2;
+  config.queue_size = 10;
   config.message_data_size = 32;
 
   TestShmRobustness(
@@ -596,14 +599,16 @@
             config);
         // Now try to write some messages.  We will get killed a bunch as this
         // tries to happen.
-        LocklessQueueSender sender = LocklessQueueSender::Make(queue).value();
+        LocklessQueueSender sender =
+            LocklessQueueSender::Make(queue, kChannelStorageDuration).value();
         LocklessQueuePinner pinner = LocklessQueuePinner::Make(queue).value();
         for (int i = 0; i < 5; ++i) {
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", i + 1);
-          sender.Send(data, s + 1, monotonic_clock::min_time,
-                      realtime_clock::min_time, 0xffffffffl, UUID::Zero(),
-                      nullptr, nullptr, nullptr);
+          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
+                                realtime_clock::min_time, 0xffffffffl,
+                                UUID::Zero(), nullptr, nullptr, nullptr),
+                    LocklessQueueSender::Result::GOOD);
           // Pin a message, so when we keep writing we will exercise the pinning
           // logic.
           if (i == 1) {
@@ -613,7 +618,8 @@
       },
       [config, tid](void *raw_memory) {
         ::aos::ipc_lib::LocklessQueueMemory *const memory =
-            reinterpret_cast<::aos::ipc_lib::LocklessQueueMemory *>(raw_memory);
+            reinterpret_cast< ::aos::ipc_lib::LocklessQueueMemory *>(
+                raw_memory);
         // Confirm that we can create 2 senders (the number in the queue), and
         // send a message.  And that all the messages in the queue are valid.
         LocklessQueue queue(memory, memory, config);
@@ -639,7 +645,7 @@
         }
 
         // Building and destroying a sender will clean up the queue.
-        LocklessQueueSender::Make(queue).value();
+        LocklessQueueSender::Make(queue, kChannelStorageDuration).value();
 
         if (print) {
           LOG(INFO) << "Cleaned up version:";
@@ -665,19 +671,21 @@
         }
 
         {
-          LocklessQueueSender sender = LocklessQueueSender::Make(queue).value();
+          LocklessQueueSender sender =
+              LocklessQueueSender::Make(queue, kChannelStorageDuration).value();
           {
             // Make a second sender to confirm that the slot was freed.
             // If the sender doesn't get cleaned up, this will fail.
-            LocklessQueueSender::Make(queue).value();
+            LocklessQueueSender::Make(queue, kChannelStorageDuration).value();
           }
 
           // Send a message to make sure that the queue still works.
           char data[100];
           size_t s = snprintf(data, sizeof(data), "foobar%d", 971);
-          sender.Send(data, s + 1, monotonic_clock::min_time,
-                      realtime_clock::min_time, 0xffffffffl, UUID::Zero(),
-                      nullptr, nullptr, nullptr);
+          ASSERT_EQ(sender.Send(data, s + 1, monotonic_clock::min_time,
+                                realtime_clock::min_time, 0xffffffffl,
+                                UUID::Zero(), nullptr, nullptr, nullptr),
+                    LocklessQueueSender::Result::GOOD);
         }
 
         // Now loop through the queue and make sure the number in the snprintf
diff --git a/aos/ipc_lib/lockless_queue_test.cc b/aos/ipc_lib/lockless_queue_test.cc
index 2b9f49c..57dd94b 100644
--- a/aos/ipc_lib/lockless_queue_test.cc
+++ b/aos/ipc_lib/lockless_queue_test.cc
@@ -16,6 +16,7 @@
 #include "aos/ipc_lib/queue_racer.h"
 #include "aos/ipc_lib/signalfd.h"
 #include "aos/realtime.h"
+#include "aos/util/phased_loop.h"
 #include "gflags/gflags.h"
 #include "gtest/gtest.h"
 
@@ -42,6 +43,9 @@
 
 class LocklessQueueTest : public ::testing::Test {
  public:
+  static constexpr monotonic_clock::duration kChannelStorageDuration =
+      std::chrono::milliseconds(500);
+
   LocklessQueueTest() {
     config_.num_watchers = 10;
     config_.num_senders = 100;
@@ -99,8 +103,6 @@
   LocklessQueueConfiguration config_;
 };
 
-typedef LocklessQueueTest LocklessQueueDeathTest;
-
 // Tests that wakeup doesn't do anything if nothing was registered.
 TEST_F(LocklessQueueTest, NoWatcherWakeup) {
   LocklessQueueWakeUpper wake_upper(queue());
@@ -190,9 +192,10 @@
 TEST_F(LocklessQueueTest, TooManySenders) {
   ::std::vector<LocklessQueueSender> senders;
   for (size_t i = 0; i < config_.num_senders; ++i) {
-    senders.emplace_back(LocklessQueueSender::Make(queue()).value());
+    senders.emplace_back(
+        LocklessQueueSender::Make(queue(), kChannelStorageDuration).value());
   }
-  EXPECT_FALSE(LocklessQueueSender::Make(queue()));
+  EXPECT_FALSE(LocklessQueueSender::Make(queue(), kChannelStorageDuration));
 }
 
 // Now, start 2 threads and have them receive the signals.
@@ -226,9 +229,11 @@
 
 // Do a simple send test.
 TEST_F(LocklessQueueTest, Send) {
-  LocklessQueueSender sender = LocklessQueueSender::Make(queue()).value();
+  LocklessQueueSender sender =
+      LocklessQueueSender::Make(queue(), kChannelStorageDuration).value();
   LocklessQueueReader reader(queue());
 
+  time::PhasedLoop loop(std::chrono::microseconds(1), monotonic_clock::now());
   // Send enough messages to wrap.
   for (int i = 0; i < 20000; ++i) {
     // Confirm that the queue index makes sense given the number of sends.
@@ -238,8 +243,10 @@
     // Send a trivial piece of data.
     char data[100];
     size_t s = snprintf(data, sizeof(data), "foobar%d", i);
-    sender.Send(data, s, monotonic_clock::min_time, realtime_clock::min_time,
-                0xffffffffu, UUID::Zero(), nullptr, nullptr, nullptr);
+    EXPECT_EQ(sender.Send(data, s, monotonic_clock::min_time,
+                          realtime_clock::min_time, 0xffffffffu, UUID::Zero(),
+                          nullptr, nullptr, nullptr),
+              LocklessQueueSender::Result::GOOD);
 
     // Confirm that the queue index still makes sense.  This is easier since the
     // empty case has been handled.
@@ -271,6 +278,8 @@
     if (read_result != LocklessQueueReader::Result::GOOD) {
       EXPECT_EQ(read_result, LocklessQueueReader::Result::TOO_OLD);
     }
+
+    loop.SleepUntilNext();
   }
 }
 
@@ -342,7 +351,42 @@
 
 }  // namespace
 
-// Send enough messages to wrap the 32 bit send counter.
+class LocklessQueueTestTooFast : public LocklessQueueTest {
+ public:
+  LocklessQueueTestTooFast() {
+    // Force a scenario where senders get rate limited
+    config_.num_watchers = 1000;
+    config_.num_senders = 100;
+    config_.num_pinners = 5;
+    config_.queue_size = 100;
+    // Exercise the alignment code.  This would throw off alignment.
+    config_.message_data_size = 101;
+
+    // Since our backing store is an array of uint64_t for alignment purposes,
+    // normalize by the size.
+    memory_.resize(LocklessQueueMemorySize(config_) / sizeof(uint64_t));
+
+    Reset();
+  }
+};
+
+// Ensure we always return OK or MESSAGES_SENT_TOO_FAST under an extreme load
+// on the Sender Queue.
+TEST_F(LocklessQueueTestTooFast, MessagesSentTooFast) {
+  PinForTest pin_cpu;
+  uint64_t kNumMessages = 1000000;
+  QueueRacer racer(queue(),
+                   {FLAGS_thread_count,
+                    kNumMessages,
+                    {LocklessQueueSender::Result::GOOD,
+                     LocklessQueueSender::Result::MESSAGES_SENT_TOO_FAST},
+                    std::chrono::milliseconds(500),
+                    false});
+
+  EXPECT_NO_FATAL_FAILURE(racer.RunIteration(false, 0));
+}
+
+// // Send enough messages to wrap the 32 bit send counter.
 TEST_F(LocklessQueueTest, WrappedSend) {
   PinForTest pin_cpu;
   uint64_t kNumMessages = 0x100010000ul;
diff --git a/aos/ipc_lib/queue_racer.cc b/aos/ipc_lib/queue_racer.cc
index b738805..414b7fb 100644
--- a/aos/ipc_lib/queue_racer.cc
+++ b/aos/ipc_lib/queue_racer.cc
@@ -26,7 +26,23 @@
 
 QueueRacer::QueueRacer(LocklessQueue queue, int num_threads,
                        uint64_t num_messages)
-    : queue_(queue), num_threads_(num_threads), num_messages_(num_messages) {
+    : queue_(queue),
+      num_threads_(num_threads),
+      num_messages_(num_messages),
+      channel_storage_duration_(std::chrono::nanoseconds(1)),
+      expected_send_results_({LocklessQueueSender::Result::GOOD}),
+      check_writes_and_reads_(true) {
+  Reset();
+}
+
+QueueRacer::QueueRacer(LocklessQueue queue,
+                       const QueueRacerConfiguration &config)
+    : queue_(queue),
+      num_threads_(config.num_threads),
+      num_messages_(config.num_messages),
+      channel_storage_duration_(config.channel_storage_duration),
+      expected_send_results_(config.expected_send_results),
+      check_writes_and_reads_(config.check_writes_and_reads) {
   Reset();
 }
 
@@ -117,7 +133,9 @@
           EXPECT_NE(latest_queue_index_queue_index, QueueIndex::Invalid());
           // latest_queue_index is an index, not a count.  So it always reads 1
           // low.
-          EXPECT_GE(latest_queue_index + 1, finished_writes);
+          if (check_writes_and_reads_) {
+            EXPECT_GE(latest_queue_index + 1, finished_writes);
+          }
         }
       }
     }
@@ -133,8 +151,8 @@
     }
     t.thread = ::std::thread([this, &t, thread_index, &run,
                               write_wrap_count]() {
-      // Build up a sender.
-      LocklessQueueSender sender = LocklessQueueSender::Make(queue_).value();
+      LocklessQueueSender sender =
+          LocklessQueueSender::Make(queue_, channel_storage_duration_).value();
       CHECK_GE(sender.size(), sizeof(ThreadPlusCount));
 
       // Signal that we are ready to start sending.
@@ -176,9 +194,16 @@
         }
 
         ++started_writes_;
-        sender.Send(sizeof(ThreadPlusCount), aos::monotonic_clock::min_time,
-                    aos::realtime_clock::min_time, 0xffffffff, UUID::Zero(),
-                    nullptr, nullptr, nullptr);
+        auto result =
+            sender.Send(sizeof(ThreadPlusCount), aos::monotonic_clock::min_time,
+                        aos::realtime_clock::min_time, 0xffffffff, UUID::Zero(),
+                        nullptr, nullptr, nullptr);
+
+        CHECK(std::find(expected_send_results_.begin(),
+                        expected_send_results_.end(),
+                        result) != expected_send_results_.end())
+            << "Unexpected send result: " << result;
+
         // Blank out the new scratch buffer, to catch other people using it.
         {
           char *const new_data = static_cast<char *>(sender.Data()) +
@@ -210,7 +235,9 @@
     queue_index_racer.join();
   }
 
-  CheckReads(race_reads, write_wrap_count, &threads);
+  if (check_writes_and_reads_) {
+    CheckReads(race_reads, write_wrap_count, &threads);
+  }
 
   // Reap all the threads.
   if (race_reads) {
@@ -221,26 +248,28 @@
     queue_index_racer.join();
   }
 
-  // Confirm that the number of writes matches the expected number of writes.
-  ASSERT_EQ(num_threads_ * num_messages_ * (1 + write_wrap_count),
-            started_writes_);
-  ASSERT_EQ(num_threads_ * num_messages_ * (1 + write_wrap_count),
-            finished_writes_);
+  if (check_writes_and_reads_) {
+    // Confirm that the number of writes matches the expected number of writes.
+    ASSERT_EQ(num_threads_ * num_messages_ * (1 + write_wrap_count),
+              started_writes_);
+    ASSERT_EQ(num_threads_ * num_messages_ * (1 + write_wrap_count),
+              finished_writes_);
 
-  // And that every thread sent the right number of messages.
-  for (ThreadState &t : threads) {
-    if (will_wrap) {
-      if (!race_reads) {
-        // If we are wrapping, there is a possibility that a thread writes
-        // everything *before* we can read any of it, and it all gets
-        // overwritten.
-        ASSERT_TRUE(t.event_count == ::std::numeric_limits<uint64_t>::max() ||
-                    t.event_count == (1 + write_wrap_count) * num_messages_)
-            << ": Got " << t.event_count << " events, expected "
-            << (1 + write_wrap_count) * num_messages_;
+    // And that every thread sent the right number of messages.
+    for (ThreadState &t : threads) {
+      if (will_wrap) {
+        if (!race_reads) {
+          // If we are wrapping, there is a possibility that a thread writes
+          // everything *before* we can read any of it, and it all gets
+          // overwritten.
+          ASSERT_TRUE(t.event_count == ::std::numeric_limits<uint64_t>::max() ||
+                      t.event_count == (1 + write_wrap_count) * num_messages_)
+              << ": Got " << t.event_count << " events, expected "
+              << (1 + write_wrap_count) * num_messages_;
+        }
+      } else {
+        ASSERT_EQ(t.event_count, num_messages_);
       }
-    } else {
-      ASSERT_EQ(t.event_count, num_messages_);
     }
   }
 }
diff --git a/aos/ipc_lib/queue_racer.h b/aos/ipc_lib/queue_racer.h
index ea0238e..3e5ca94 100644
--- a/aos/ipc_lib/queue_racer.h
+++ b/aos/ipc_lib/queue_racer.h
@@ -10,11 +10,28 @@
 
 struct ThreadState;
 
+struct QueueRacerConfiguration {
+  // Number of threads that send messages
+  const int num_threads;
+  // Number of messages sent by each thread
+  const uint64_t num_messages;
+  // Allows QueueRacer to check for multiple returns from calling Send()
+  const std::vector<LocklessQueueSender::Result> expected_send_results = {
+      LocklessQueueSender::Result::GOOD};
+  // Channel Storage Duration for queue used by QueueRacer
+  const monotonic_clock::duration channel_storage_duration =
+      std::chrono::nanoseconds(1);
+  // Set to true if all writes and reads are expected to be successful
+  // This allows QueueRacer to be used for checking failure scenarios
+  const bool check_writes_and_reads;
+};
+
 // Class to test the queue by spinning up a bunch of writing threads and racing
 // them together to all write at once.
 class QueueRacer {
  public:
   QueueRacer(LocklessQueue queue, int num_threads, uint64_t num_messages);
+  QueueRacer(LocklessQueue queue, const QueueRacerConfiguration &config);
 
   // Runs an iteration of the race.
   //
@@ -52,7 +69,10 @@
   LocklessQueue queue_;
   const uint64_t num_threads_;
   const uint64_t num_messages_;
-
+  const monotonic_clock::duration channel_storage_duration_;
+  // Allows QueueRacer to check for multiple returns from calling Send()
+  const std::vector<LocklessQueueSender::Result> expected_send_results_;
+  const bool check_writes_and_reads_;
   // The overall number of writes executed will always be between the two of
   // these.  We can't atomically count writes, so we have to bound them.
   //
diff --git a/aos/network/message_bridge_test_combined_timestamps_common.json b/aos/network/message_bridge_test_combined_timestamps_common.json
index 74c932d..be79014 100644
--- a/aos/network/message_bridge_test_combined_timestamps_common.json
+++ b/aos/network/message_bridge_test_combined_timestamps_common.json
@@ -20,7 +20,7 @@
       "name": "/pi1/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi1",
-      "frequency": 10,
+      "frequency": 15,
       "max_size": 200,
       "destination_nodes": [
         {
@@ -36,7 +36,7 @@
       "name": "/pi2/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi2",
-      "frequency": 10,
+      "frequency": 15,
       "max_size": 200,
       "destination_nodes": [
         {
@@ -64,25 +64,25 @@
       "name": "/pi1/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "pi1",
-      "frequency": 2
+      "frequency": 15
     },
     {
       "name": "/pi2/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "pi2",
-      "frequency": 2
+      "frequency": 15
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi1",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi2/aos/remote_timestamps/pi1",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi2",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi1/aos",
diff --git a/aos/network/message_bridge_test_common.json b/aos/network/message_bridge_test_common.json
index 99c80a9..4623edb 100644
--- a/aos/network/message_bridge_test_common.json
+++ b/aos/network/message_bridge_test_common.json
@@ -20,7 +20,7 @@
       "name": "/pi1/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi1",
-      "frequency": 10,
+      "frequency": 15,
       "max_size": 200,
       "destination_nodes": [
         {
@@ -36,7 +36,7 @@
       "name": "/pi2/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi2",
-      "frequency": 10,
+      "frequency": 15,
       "max_size": 200,
       "destination_nodes": [
         {
@@ -64,43 +64,43 @@
       "name": "/pi1/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "pi1",
-      "frequency": 2
+      "frequency": 15
     },
     {
       "name": "/pi2/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "pi2",
-      "frequency": 2
+      "frequency": 15
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2/pi1/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi1",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi2/aos/remote_timestamps/pi1/pi2/aos/aos-message_bridge-Timestamp",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi2",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2/test/aos-examples-Ping",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi1",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi2/aos/remote_timestamps/pi1/test/aos-examples-Pong",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi2",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi1/aos/remote_timestamps/pi2/unreliable/aos-examples-Ping",
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "pi1",
-      "frequency": 10
+      "frequency": 15
     },
     {
       "name": "/pi1/aos",
diff --git a/aos/network/multinode_timestamp_filter.cc b/aos/network/multinode_timestamp_filter.cc
index 0619b5e..262cb5c 100644
--- a/aos/network/multinode_timestamp_filter.cc
+++ b/aos/network/multinode_timestamp_filter.cc
@@ -43,6 +43,65 @@
 // ms/s.  Figure out how to define it.  Do this last.  This lets us handle
 // constraints going away, and constraints close in time.
 
+bool TimestampProblem::HasObservations(size_t node_a) const {
+  // Note, this function is probably over conservative.  It is requiring all the
+  // pairs for a node to have data in at least one direction rather than enough
+  // pairs to have data to observe the graph.  We can break that when someone
+  // finds it is overly restrictive.
+
+  if (clock_offset_filter_for_node_[node_a].empty()) {
+    // Look for a filter going the other way who's node_b is our node.
+    bool found_filter = false;
+    for (size_t node_b = 0u; node_b < clock_offset_filter_for_node_.size();
+         ++node_b) {
+      for (const struct FilterPair &filter :
+           clock_offset_filter_for_node_[node_b]) {
+        if (filter.b_index == node_a) {
+          if (filter.filter->timestamps_size(base_clock_[node_b].boot,
+                                             base_clock_[node_a].boot) == 0u) {
+            // Found one without data, explode.
+            return false;
+          }
+          found_filter = true;
+        }
+      }
+    }
+    return found_filter;
+  }
+
+  for (const struct FilterPair &filter :
+       clock_offset_filter_for_node_[node_a]) {
+    // There's something in this direction, so we don't need to check the
+    // opposite direction to confirm we have observations.
+    if (filter.filter->timestamps_size(
+            base_clock_[node_a].boot, base_clock_[filter.b_index].boot) != 0u) {
+      continue;
+    }
+
+    // For a boot to exist, we need to have some observations between it and
+    // another boot.  We wouldn't bother to build a problem to solve for
+    // this node otherwise.  Confirm that is true so we at least get
+    // notified if that assumption falls apart.
+    bool valid = false;
+    for (const struct FilterPair &other_filter :
+         clock_offset_filter_for_node_[filter.b_index]) {
+      if (other_filter.b_index == node_a) {
+        // Found our match.  Confirm it has timestamps.
+        if (other_filter.filter->timestamps_size(
+                base_clock_[filter.b_index].boot, base_clock_[node_a].boot) !=
+            0u) {
+          valid = true;
+        }
+        break;
+      }
+    }
+    if (!valid) {
+      return false;
+    }
+  }
+  return true;
+}
+
 bool TimestampProblem::ValidateSolution(std::vector<BootTimestamp> solution) {
   bool success = true;
   for (size_t i = 0u; i < clock_offset_filter_for_node_.size(); ++i) {
@@ -1355,16 +1414,15 @@
 
 std::tuple<std::vector<MultiNodeNoncausalOffsetEstimator::CandidateTimes>, bool>
 MultiNodeNoncausalOffsetEstimator::MakeCandidateTimes() const {
-  bool boots_all_match = true;
   std::vector<CandidateTimes> candidate_times;
   candidate_times.resize(last_monotonics_.size());
 
   size_t node_a_index = 0;
-  size_t last_boot = std::numeric_limits<size_t>::max();
   for (const auto &filters : filters_per_node_) {
     VLOG(2) << "Investigating filter for node " << node_a_index;
     BootTimestamp next_node_time = BootTimestamp::max_time();
-    BootDuration next_node_duration;
+    BootDuration next_node_duration = BootDuration::max_time();
+    size_t b_index = std::numeric_limits<size_t>::max();
     NoncausalTimestampFilter *next_node_filter = nullptr;
     // Find the oldest time for each node in each filter, and solve for that
     // time.  That gives us the next timestamp for this node.
@@ -1379,23 +1437,13 @@
         if (std::get<0>(*candidate) < next_node_time) {
           next_node_time = std::get<0>(*candidate);
           next_node_duration = std::get<1>(*candidate);
+          b_index = filter.b_index;
           next_node_filter = filter.filter;
         }
       }
       ++filter_index;
     }
 
-    // Found no active filters.  Either this node is off, or disconnected, or
-    // we are before the log file starts or after the log file ends.
-    if (next_node_time == BootTimestamp::max_time()) {
-      candidate_times[node_a_index] =
-          CandidateTimes{.next_node_time = next_node_time,
-                         .next_node_duration = next_node_duration,
-                         .next_node_filter = next_node_filter};
-      ++node_a_index;
-      continue;
-    }
-
     // We want to make sure we solve explicitly for the start time for each
     // log.  This is useless (though not all that expensive) if it is in the
     // middle of a set of data since we are just adding an extra point in the
@@ -1419,6 +1467,8 @@
                 << " is the next startup time, " << next_start_time;
         next_node_time = next_start_time;
         next_node_filter = nullptr;
+        b_index = std::numeric_limits<size_t>::max();
+        next_node_duration = BootDuration::max_time();
       }
 
       // We need to make sure we have solutions as well for any local messages
@@ -1436,19 +1486,56 @@
                 << " not applying yet";
         next_node_time = next_oldest_time;
         next_node_filter = nullptr;
+        b_index = std::numeric_limits<size_t>::max();
+        next_node_duration = BootDuration::max_time();
       }
     }
-    if (last_boot != std::numeric_limits<size_t>::max()) {
-      boots_all_match &= (next_node_time.boot == last_boot);
-    }
-    last_boot = next_node_time.boot;
     candidate_times[node_a_index] =
         CandidateTimes{.next_node_time = next_node_time,
                        .next_node_duration = next_node_duration,
+                       .b_index = b_index,
                        .next_node_filter = next_node_filter};
     ++node_a_index;
   }
 
+  // Now that we have all the candidates, confirm everything matches.
+  bool boots_all_match = true;
+  for (size_t i = 0; i < candidate_times.size(); ++i) {
+    const CandidateTimes &candidate = candidate_times[i];
+    if (candidate.next_node_time == logger::BootTimestamp::max_time()) {
+      continue;
+    }
+
+    // First step, if the last solution's boot doesn't match the next solution,
+    // we've got a reboot incoming and can't sort well.  Fall back to the more
+    // basic exhaustive search.
+    if (candidate.next_node_time.boot != last_monotonics_[i].boot) {
+      boots_all_match = false;
+      break;
+    }
+
+    // And then check that the other node's time also hasn't rebooted.  We might
+    // not have both directions of timestamps, so this is our only clue.
+    if (candidate.next_node_duration == BootDuration::max_time()) {
+      continue;
+    }
+
+    DCHECK_LT(candidate.b_index, candidate_times.size());
+    if (candidate_times[candidate.b_index].next_node_time.boot !=
+        candidate.next_node_duration.boot) {
+      boots_all_match = false;
+      break;
+    }
+  }
+  if (VLOG_IS_ON(1)) {
+    LOG(INFO) << "Boots all match: " << boots_all_match;
+    for (size_t i = 0; i < candidate_times.size(); ++i) {
+      LOG(INFO) << "Candidate " << candidate_times[i].next_node_time
+                << " duration " << candidate_times[i].next_node_duration
+                << " (node " << candidate_times[i].b_index << ")";
+    }
+  }
+
   return std::make_tuple(candidate_times, boots_all_match);
 }
 
@@ -1572,6 +1659,7 @@
         candidate_times[node_a_index].next_node_duration;
     NoncausalTimestampFilter *next_node_filter =
         candidate_times[node_a_index].next_node_filter;
+    size_t b_index = candidate_times[node_a_index].b_index;
     if (next_node_time == BootTimestamp::max_time()) {
       continue;
     }
@@ -1586,8 +1674,16 @@
     // TODO(austin): If we start supporting only having 1 direction of
     // timestamps, we might need to change our assumptions around
     // BootTimestamp and BootDuration.
+    bool boots_match = next_node_time.boot == base_times[node_a_index].boot;
 
-    if (next_node_time.boot == base_times[node_a_index].boot) {
+    // Make sure the paired time also has a matching boot.
+    if (next_node_duration != BootDuration::max_time()) {
+      if (next_node_duration.boot != base_times[b_index].boot) {
+        boots_match = false;
+      }
+    }
+
+    if (boots_match) {
       // Optimize, and save the time into times if earlier than time.
       for (size_t node_index = 0; node_index < base_times.size();
            ++node_index) {
@@ -1609,6 +1705,16 @@
       // And we know our solution node will have the wrong boot, so replace
       // it entirely.
       problem->set_base_clock(node_a_index, next_node_time);
+
+      // And update the paired boot for the paired node.
+      if (next_node_duration != BootDuration::max_time()) {
+        if (next_node_duration.boot != base_times[b_index].boot) {
+          problem->set_base_clock(
+              b_index, BootTimestamp{.boot = next_node_duration.boot,
+                                     .time = next_node_time.time +
+                                             next_node_duration.duration});
+        }
+      }
     }
 
     std::vector<BootTimestamp> points(problem->size(),
@@ -1617,6 +1723,14 @@
       problem->Debug();
     }
     points[node_a_index] = next_node_time;
+
+    if (!problem->HasObservations(node_a_index)) {
+      VLOG(1) << "No observations, checking if there's a filter";
+      CHECK(next_node_filter == nullptr)
+          << ": No observations, but this isn't a start time.";
+      continue;
+    }
+
     std::tuple<std::vector<BootTimestamp>, size_t> solution =
         problem->SolveNewton(points);
 
diff --git a/aos/network/multinode_timestamp_filter.h b/aos/network/multinode_timestamp_filter.h
index 7259fc8..f959d7b 100644
--- a/aos/network/multinode_timestamp_filter.h
+++ b/aos/network/multinode_timestamp_filter.h
@@ -61,6 +61,10 @@
   // Validates the solution, returning true if it meets all the constraints, and
   // false otherwise.
   bool ValidateSolution(std::vector<logger::BootTimestamp> solution);
+  // Returns true if the provide node has observations to solve for the
+  // provided boots.  This may happen when we are trying to solve for a reboot
+  // to see if it is next, and haven't queued far enough.
+  bool HasObservations(size_t node_a) const;
 
   // LOGs a representation of the problem.
   void Debug();
@@ -331,7 +335,8 @@
  private:
   struct CandidateTimes {
     logger::BootTimestamp next_node_time = logger::BootTimestamp::max_time();
-    logger::BootDuration next_node_duration;
+    logger::BootDuration next_node_duration = logger::BootDuration::max_time();
+    size_t b_index = std::numeric_limits<size_t>::max();
     NoncausalTimestampFilter *next_node_filter = nullptr;
   };
 
diff --git a/aos/network/timestamp_channel.cc b/aos/network/timestamp_channel.cc
index ab61051..fdaa031 100644
--- a/aos/network/timestamp_channel.cc
+++ b/aos/network/timestamp_channel.cc
@@ -2,6 +2,10 @@
 
 #include "absl/strings/str_cat.h"
 
+DEFINE_bool(combined_timestamp_channel_fallback, true,
+            "If true, fall back to using the combined timestamp channel if the "
+            "single timestamp channel doesn't exist for a timestamp.");
+
 namespace aos {
 namespace message_bridge {
 
@@ -12,7 +16,8 @@
 
 std::string ChannelTimestampFinder::SplitChannelName(
     const Channel *channel, const Connection *connection) {
-  return SplitChannelName(channel->name()->string_view(), channel->type()->str(), connection);
+  return SplitChannelName(channel->name()->string_view(),
+                          channel->type()->str(), connection);
 }
 
 std::string ChannelTimestampFinder::SplitChannelName(
@@ -47,6 +52,15 @@
     return split_timestamp_channel;
   }
 
+  if (!FLAGS_combined_timestamp_channel_fallback) {
+    LOG(FATAL) << "Failed to find new timestamp channel {\"name\": \""
+               << split_timestamp_channel_name << "\", \"type\": \""
+               << RemoteMessage::GetFullyQualifiedName() << "\"} for "
+               << configuration::CleanedChannelToString(channel)
+               << " connection " << aos::FlatbufferToJson(connection)
+               << " and --nocombined_timestamp_channel_fallback is set";
+  }
+
   const std::string shared_timestamp_channel_name =
       CombinedChannelName(connection->name()->string_view());
   const Channel *shared_timestamp_channel = configuration::GetChannel(
diff --git a/aos/network/timestamp_filter.h b/aos/network/timestamp_filter.h
index 6486a1b..b51fe2f 100644
--- a/aos/network/timestamp_filter.h
+++ b/aos/network/timestamp_filter.h
@@ -696,7 +696,9 @@
     auto it =
         std::lower_bound(filters_.begin(), filters_.end(),
                          std::make_pair(boota, bootb), FilterLessThanLower);
-    CHECK(it != filters_.end());
+    if (it == filters_.end()) {
+      return nullptr;
+    }
     if (it->boot == std::make_pair(boota, bootb)) {
       return &it->filter;
     } else {
diff --git a/aos/network/web_proxy.cc b/aos/network/web_proxy.cc
index 6d4f23f..30707b2 100644
--- a/aos/network/web_proxy.cc
+++ b/aos/network/web_proxy.cc
@@ -377,6 +377,7 @@
     return;
   }
   channel->current_queue_index = message_buffer_.back().index;
+  channel->reported_queue_index = message_buffer_.back().index;
   channel->next_packet_number = 0;
 }
 
diff --git a/aos/network/www/aos_plotter.ts b/aos/network/www/aos_plotter.ts
index b7d8771..cd2e131 100644
--- a/aos/network/www/aos_plotter.ts
+++ b/aos/network/www/aos_plotter.ts
@@ -28,7 +28,7 @@
 import {Connection} from 'org_frc971/aos/network/www/proxy';
 import {SubscriberRequest, ChannelRequest, TransferMethod} from 'org_frc971/aos/network/web_proxy_generated';
 import {Parser, Table} from 'org_frc971/aos/network/www/reflection'
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 import {ByteBuffer} from 'flatbuffers';
 
 export class TimestampedMessage {
diff --git a/aos/network/www/proxy.ts b/aos/network/www/proxy.ts
index cf8e972..0bda8b9 100644
--- a/aos/network/www/proxy.ts
+++ b/aos/network/www/proxy.ts
@@ -1,7 +1,7 @@
 import {Builder, ByteBuffer, Offset} from 'flatbuffers';
 import {Channel as ChannelFb, Configuration} from 'org_frc971/aos/configuration_generated';
 import {ChannelRequest as ChannelRequestFb, ChannelState, MessageHeader, Payload, SdpType, SubscriberRequest, TransferMethod, WebSocketIce, WebSocketMessage, WebSocketSdp} from 'org_frc971/aos/network/web_proxy_generated';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 
 // There is one handler for each DataChannel, it maintains the state of
 // multi-part messages and delegates to a callback when the message is fully
diff --git a/aos/starter/BUILD b/aos/starter/BUILD
index 2186421..5bf3811 100644
--- a/aos/starter/BUILD
+++ b/aos/starter/BUILD
@@ -33,6 +33,7 @@
         "//aos/events:event_loop",
         "//aos/events:shm_event_loop",
         "//aos/util:scoped_pipe",
+        "//aos/util:top",
         "@com_github_google_glog//:glog",
     ],
 )
@@ -99,6 +100,8 @@
         "//aos/events:pingpong_config",
         "//aos/events:pong",
     ],
+    # TODO(james): Fix tihs.
+    flaky = True,
     linkopts = ["-lstdc++fs"],
     shard_count = 4,
     # The roborio compiler doesn't support <filesystem>.
@@ -163,6 +166,7 @@
     name = "starter_fbs",
     srcs = ["starter.fbs"],
     gen_reflections = True,
+    includes = ["//aos/util:process_info_fbs_includes"],
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
 )
diff --git a/aos/starter/starter.fbs b/aos/starter/starter.fbs
index 4b66833..7285281 100644
--- a/aos/starter/starter.fbs
+++ b/aos/starter/starter.fbs
@@ -1,3 +1,5 @@
+include "aos/util/process_info.fbs";
+
 namespace aos.starter;
 
 enum State : short {
@@ -73,6 +75,13 @@
   // Indicates the reason the application is not running. Only valid if
   // application is STOPPED.
   last_stop_reason: LastStopReason (id: 6);
+
+  // Debug information providing the approximate CPU usage and memory footprint of the process.
+  // Populated whenever the process is running (i.e., state != STOPPED). While STOPPING could
+  // refer to another process if another process has somehow claimed the application's PID between
+  // actually stopping and the parent process receiving the signal indicating that the application
+  // finished stopping.
+  process_info: util.ProcessInfo (id: 7);
 }
 
 root_type Status;
diff --git a/aos/starter/starter_rpc_lib.cc b/aos/starter/starter_rpc_lib.cc
index e373e6b..884367d 100644
--- a/aos/starter/starter_rpc_lib.cc
+++ b/aos/starter/starter_rpc_lib.cc
@@ -41,10 +41,15 @@
   return *search;
 }
 
-std::string_view FindApplication(const std::string_view &name,
+std::string_view FindApplication(const std::string_view name,
                                  const aos::Configuration *config) {
   std::string_view app_name = name;
   for (const auto app : *config->applications()) {
+    if (app->name()->string_view() == name) {
+      return name;
+    }
+  }
+  for (const auto app : *config->applications()) {
     if (app->has_executable_name() &&
         app->executable_name()->string_view() == name) {
       app_name = app->name()->string_view();
diff --git a/aos/starter/starter_rpc_lib.h b/aos/starter/starter_rpc_lib.h
index 3097b5f..eadf5c6 100644
--- a/aos/starter/starter_rpc_lib.h
+++ b/aos/starter/starter_rpc_lib.h
@@ -75,7 +75,7 @@
 
 // Checks if the name is an executable name and if it is, it returns that
 // application's name, otherwise returns name as given
-std::string_view FindApplication(const std::string_view &name,
+std::string_view FindApplication(const std::string_view name,
                                  const aos::Configuration *config);
 
 // Sends the given command to the application with the name name. Creates a
diff --git a/aos/starter/starter_test.cc b/aos/starter/starter_test.cc
index 120fe38..d489f8f 100644
--- a/aos/starter/starter_test.cc
+++ b/aos/starter/starter_test.cc
@@ -287,27 +287,32 @@
       })
       ->Setup(watcher_loop.monotonic_now() + std::chrono::seconds(7));
 
-  watcher_loop.MakeWatcher(
-      "/aos", [&watcher_loop](const aos::starter::Status &status) {
-        const aos::starter::ApplicationStatus *ping_app_status =
-            FindApplicationStatus(status, "ping");
-        const aos::starter::ApplicationStatus *pong_app_status =
-            FindApplicationStatus(status, "pong");
-        if (ping_app_status == nullptr || pong_app_status == nullptr) {
-          return;
-        }
+  watcher_loop.MakeWatcher("/aos", [&watcher_loop](
+                                       const aos::starter::Status &status) {
+    const aos::starter::ApplicationStatus *ping_app_status =
+        FindApplicationStatus(status, "ping");
+    const aos::starter::ApplicationStatus *pong_app_status =
+        FindApplicationStatus(status, "pong");
+    if (ping_app_status == nullptr || pong_app_status == nullptr) {
+      return;
+    }
 
-        if (ping_app_status->has_state() &&
-            ping_app_status->state() != aos::starter::State::STOPPED) {
-          watcher_loop.Exit();
-          FAIL();
-        }
-        if (pong_app_status->has_state() &&
-            pong_app_status->state() == aos::starter::State::RUNNING) {
-          watcher_loop.Exit();
-          SUCCEED();
-        }
-      });
+    if (ping_app_status->has_state() &&
+        ping_app_status->state() != aos::starter::State::STOPPED) {
+      watcher_loop.Exit();
+      FAIL();
+    }
+    if (pong_app_status->has_state() &&
+        pong_app_status->state() == aos::starter::State::RUNNING) {
+      ASSERT_TRUE(pong_app_status->has_process_info());
+      ASSERT_EQ("pong", pong_app_status->process_info()->name()->string_view())
+          << aos::FlatbufferToJson(&status);
+      ASSERT_EQ(pong_app_status->pid(), pong_app_status->process_info()->pid());
+      ASSERT_LT(0.0, pong_app_status->process_info()->cpu_usage());
+      watcher_loop.Exit();
+      SUCCEED();
+    }
+  });
 
   std::thread starterd_thread([&starter] { starter.Run(); });
   watcher_loop.Run();
diff --git a/aos/starter/starterd_lib.cc b/aos/starter/starterd_lib.cc
index 008c46f..84e4d00 100644
--- a/aos/starter/starterd_lib.cc
+++ b/aos/starter/starterd_lib.cc
@@ -33,7 +33,8 @@
           event_loop_.GetChannel<aos::starter::Status>("/aos")->frequency() -
           1),
       listener_(&event_loop_,
-                [this](signalfd_siginfo signal) { OnSignal(signal); }) {
+                [this](signalfd_siginfo signal) { OnSignal(signal); }),
+      top_(&event_loop_) {
   event_loop_.SkipAosLog();
 
   event_loop_.OnRun([this] {
@@ -117,7 +118,16 @@
   }
 }
 
-void Starter::MaybeSendStatus() {
+void Starter::HandleStateChange() {
+  std::set<pid_t> all_pids;
+  for (const auto &pair : applications_) {
+    if (pair.second.get_pid() > 0 &&
+        pair.second.status() != aos::starter::State::STOPPED) {
+      all_pids.insert(pair.second.get_pid());
+    }
+  }
+  top_.set_track_pids(all_pids);
+
   if (status_count_ < max_status_count_) {
     SendStatus();
     ++status_count_;
@@ -165,9 +175,9 @@
 }
 
 Application *Starter::AddApplication(const aos::Application *application) {
-  auto [iter, success] =
-      applications_.try_emplace(application->name()->str(), application,
-                                &event_loop_, [this]() { MaybeSendStatus(); });
+  auto [iter, success] = applications_.try_emplace(
+      application->name()->str(), application, &event_loop_,
+      [this]() { HandleStateChange(); });
   if (success) {
     // We should be catching and handling SIGCHLD correctly in the starter, so
     // don't leave in the crutch for polling for the child process status (this
@@ -200,7 +210,7 @@
   std::vector<flatbuffers::Offset<aos::starter::ApplicationStatus>> statuses;
 
   for (auto &application : applications_) {
-    statuses.push_back(application.second.PopulateStatus(builder.fbb()));
+    statuses.push_back(application.second.PopulateStatus(builder.fbb(), &top_));
   }
 
   auto statuses_fbs = builder.fbb()->CreateVector(statuses);
diff --git a/aos/starter/starterd_lib.h b/aos/starter/starterd_lib.h
index 834e191..e7ded59 100644
--- a/aos/starter/starterd_lib.h
+++ b/aos/starter/starterd_lib.h
@@ -17,6 +17,7 @@
 #include "aos/starter/starter_generated.h"
 #include "aos/starter/starter_rpc_generated.h"
 #include "aos/starter/subprocess.h"
+#include "aos/util/top.h"
 
 namespace aos {
 namespace starter {
@@ -49,8 +50,10 @@
   void OnSignal(signalfd_siginfo signal);
   void HandleStarterRpc(const StarterRpc &command);
 
-  // Sends the Status message if it wouldn't exceed the rate limit.
-  void MaybeSendStatus();
+  // Handles any potential state change in the child applications.
+  // In particular, sends the Status message if it wouldn't exceed the rate
+  // limit.
+  void HandleStateChange();
 
   void SendStatus();
 
@@ -73,6 +76,8 @@
 
   SignalListener listener_;
 
+  util::Top top_;
+
   DISALLOW_COPY_AND_ASSIGN(Starter);
 };
 
diff --git a/aos/starter/subprocess.cc b/aos/starter/subprocess.cc
index c1eb618..f0c8f85 100644
--- a/aos/starter/subprocess.cc
+++ b/aos/starter/subprocess.cc
@@ -346,10 +346,16 @@
 }
 
 flatbuffers::Offset<aos::starter::ApplicationStatus>
-Application::PopulateStatus(flatbuffers::FlatBufferBuilder *builder) {
+Application::PopulateStatus(flatbuffers::FlatBufferBuilder *builder,
+                            util::Top *top) {
   CHECK_NOTNULL(builder);
   auto name_fbs = builder->CreateString(name_);
 
+  const bool valid_pid = pid_ > 0 && status_ != aos::starter::State::STOPPED;
+  const flatbuffers::Offset<util::ProcessInfo> process_info =
+      valid_pid ? top->InfoForProcess(builder, pid_)
+                : flatbuffers::Offset<util::ProcessInfo>();
+
   aos::starter::ApplicationStatus::Builder status_builder(*builder);
   status_builder.add_name(name_fbs);
   status_builder.add_state(status_);
@@ -361,6 +367,8 @@
     status_builder.add_pid(pid_);
     status_builder.add_id(id_);
   }
+  // Note that even if process_info is null, calling add_process_info is fine.
+  status_builder.add_process_info(process_info);
   status_builder.add_last_start_time(start_time_.time_since_epoch().count());
   return status_builder.Finish();
 }
diff --git a/aos/starter/subprocess.h b/aos/starter/subprocess.h
index 9ee9e31..a4d7cbb 100644
--- a/aos/starter/subprocess.h
+++ b/aos/starter/subprocess.h
@@ -11,6 +11,7 @@
 #include "aos/starter/starter_generated.h"
 #include "aos/starter/starter_rpc_generated.h"
 #include "aos/util/scoped_pipe.h"
+#include "aos/util/top.h"
 
 namespace aos::starter {
 
@@ -45,7 +46,7 @@
               aos::EventLoop *event_loop, std::function<void()> on_change);
 
   flatbuffers::Offset<aos::starter::ApplicationStatus> PopulateStatus(
-      flatbuffers::FlatBufferBuilder *builder);
+      flatbuffers::FlatBufferBuilder *builder, util::Top *top);
   aos::starter::State status() const { return status_; };
 
   // Returns the last pid of this process. -1 if not started yet.
diff --git a/aos/util/BUILD b/aos/util/BUILD
index 3b96cfd..8d21c47 100644
--- a/aos/util/BUILD
+++ b/aos/util/BUILD
@@ -1,3 +1,5 @@
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library")
+
 package(default_visibility = ["//visibility:public"])
 
 cc_library(
@@ -272,6 +274,45 @@
     ],
 )
 
+flatbuffer_cc_library(
+    name = "process_info_fbs",
+    srcs = ["process_info.fbs"],
+    gen_reflections = True,
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "top",
+    srcs = ["top.cc"],
+    hdrs = ["top.h"],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":process_info_fbs",
+        "//aos/containers:ring_buffer",
+        "//aos/events:event_loop",
+        "@com_github_google_glog//:glog",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_test(
+    name = "top_test",
+    srcs = ["top_test.cc"],
+    data = [
+        "//aos/events:pingpong_config",
+    ],
+    flaky = True,
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        ":top",
+        "//aos/events:shm_event_loop",
+        "//aos/testing:googletest",
+        "//aos/testing:path",
+        "//aos/testing:tmpdir",
+    ],
+)
+
 cc_library(
     name = "scoped_pipe",
     srcs = ["scoped_pipe.cc"],
diff --git a/aos/util/process_info.fbs b/aos/util/process_info.fbs
new file mode 100644
index 0000000..aafdba3
--- /dev/null
+++ b/aos/util/process_info.fbs
@@ -0,0 +1,23 @@
+namespace aos.util;
+
+// ProcessInfo captures state information associated with a given process.
+table ProcessInfo {
+  // Process ID of the process in question.
+  pid: uint (id: 0);
+  // Name of the running executable.
+  name: string (id: 1);
+  // Time that the process spent executing over the past ~1 second, divided by
+  // the amount of wall-clock time that elapsed in that period. I.e., if a process is
+  // consuming all of one CPU core then this would be 1.0. Multi-threaded processes
+  // can exceed 1.0.
+  cpu_usage: float (id: 2);
+  // Amount of physical RAM taken by this process, in bytes. Will be a multiple of the
+  // system's page size.
+  physical_memory: uint64 (id: 3);
+}
+
+table TopProcessesFbs {
+  // List of processes consuming the most CPU in the last sample period, in order from
+  // most CPU to least.
+  processes: [ProcessInfo] (id: 0);
+}
diff --git a/aos/util/top.cc b/aos/util/top.cc
new file mode 100644
index 0000000..4882af7
--- /dev/null
+++ b/aos/util/top.cc
@@ -0,0 +1,254 @@
+#include "aos/util/top.h"
+
+#include <dirent.h>
+#include <unistd.h>
+
+#include <queue>
+#include <string>
+
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_split.h"
+
+namespace aos::util {
+namespace {
+std::optional<std::string> ReadShortFile(std::string_view file_name) {
+  // Open as input and seek to end immediately.
+  std::ifstream file(std::string(file_name), std::ios_base::in);
+  if (!file.good()) {
+    VLOG(1) << "Can't read " << file_name;
+    return std::nullopt;
+  }
+  const size_t kMaxLineLength = 4096;
+  char buffer[kMaxLineLength];
+  file.read(buffer, kMaxLineLength);
+  if (!file.eof()) {
+    return std::nullopt;
+  }
+  return std::string(buffer, file.gcount());
+}
+}  // namespace
+
+std::optional<ProcStat> ReadProcStat(pid_t pid) {
+  std::optional<std::string> contents =
+      ReadShortFile(absl::StrFormat("/proc/%d/stat", pid));
+  if (!contents.has_value()) {
+    return std::nullopt;
+  }
+  const size_t start_name = contents->find_first_of('(');
+  const size_t end_name = contents->find_last_of(')');
+  if (start_name == std::string::npos || end_name == std::string::npos ||
+      end_name < start_name) {
+    VLOG(1) << "No name found in stat line " << contents.value();
+    return std::nullopt;
+  }
+  std::string_view name(contents->c_str() + start_name + 1,
+                        end_name - start_name - 1);
+
+  std::vector<std::string_view> fields =
+      absl::StrSplit(std::string_view(contents->c_str() + end_name + 1,
+                                      contents->size() - end_name - 1),
+                     ' ', absl::SkipWhitespace());
+  constexpr int kNumFieldsAfterName = 50;
+  if (fields.size() != kNumFieldsAfterName) {
+    VLOG(1) << "Incorrect number of fields " << fields.size();
+    return std::nullopt;
+  }
+  // The first field is a character for the current process state; every single
+  // field after that should be an integer.
+  if (fields[0].size() != 1) {
+    VLOG(1) << "State field is too long: " << fields[0];
+    return std::nullopt;
+  }
+  std::array<absl::int128, kNumFieldsAfterName - 1> numbers;
+  for (int ii = 1; ii < kNumFieldsAfterName; ++ii) {
+    if (!absl::SimpleAtoi(fields[ii], &numbers[ii - 1])) {
+      VLOG(1) << "Failed to parse field " << ii << " as number: " << fields[ii];
+      return std::nullopt;
+    }
+  }
+  return ProcStat{
+      .pid = pid,
+      .name = std::string(name),
+      .state = fields.at(0).at(0),
+      .parent_pid = static_cast<int64_t>(numbers.at(0)),
+      .group_id = static_cast<int64_t>(numbers.at(1)),
+      .session_id = static_cast<int64_t>(numbers.at(2)),
+      .tty = static_cast<int64_t>(numbers.at(3)),
+      .tpgid = static_cast<int64_t>(numbers.at(4)),
+      .kernel_flags = static_cast<uint64_t>(numbers.at(5)),
+      .minor_faults = static_cast<uint64_t>(numbers.at(6)),
+      .children_minor_faults = static_cast<uint64_t>(numbers.at(7)),
+      .major_faults = static_cast<uint64_t>(numbers.at(8)),
+      .children_major_faults = static_cast<uint64_t>(numbers.at(9)),
+      .user_mode_ticks = static_cast<uint64_t>(numbers.at(10)),
+      .kernel_mode_ticks = static_cast<uint64_t>(numbers.at(11)),
+      .children_user_mode_ticks = static_cast<int64_t>(numbers.at(12)),
+      .children_kernel_mode_ticks = static_cast<int64_t>(numbers.at(13)),
+      .priority = static_cast<int64_t>(numbers.at(14)),
+      .nice = static_cast<int64_t>(numbers.at(15)),
+      .num_threads = static_cast<int64_t>(numbers.at(16)),
+      .itrealvalue = static_cast<int64_t>(numbers.at(17)),
+      .start_time_ticks = static_cast<uint64_t>(numbers.at(18)),
+      .virtual_memory_size = static_cast<uint64_t>(numbers.at(19)),
+      .resident_set_size = static_cast<int64_t>(numbers.at(20)),
+      .rss_soft_limit = static_cast<uint64_t>(numbers.at(21)),
+      .start_code_address = static_cast<uint64_t>(numbers.at(22)),
+      .end_code_address = static_cast<uint64_t>(numbers.at(23)),
+      .start_stack_address = static_cast<uint64_t>(numbers.at(24)),
+      .stack_pointer = static_cast<uint64_t>(numbers.at(25)),
+      .instruction_pointer = static_cast<uint64_t>(numbers.at(26)),
+      .signal_bitmask = static_cast<uint64_t>(numbers.at(27)),
+      .blocked_signals = static_cast<uint64_t>(numbers.at(28)),
+      .ignored_signals = static_cast<uint64_t>(numbers.at(29)),
+      .caught_signals = static_cast<uint64_t>(numbers.at(30)),
+      .wchan = static_cast<uint64_t>(numbers.at(31)),
+      .swap_pages = static_cast<uint64_t>(numbers.at(32)),
+      .children_swap_pages = static_cast<uint64_t>(numbers.at(33)),
+      .exit_signal = static_cast<int64_t>(numbers.at(34)),
+      .processor = static_cast<int64_t>(numbers.at(35)),
+      .rt_priority = static_cast<uint64_t>(numbers.at(36)),
+      .scheduling_policy = static_cast<uint64_t>(numbers.at(37)),
+      .block_io_delay_ticks = static_cast<uint64_t>(numbers.at(38)),
+      .guest_ticks = static_cast<uint64_t>(numbers.at(39)),
+      .children_guest_ticks = static_cast<uint64_t>(numbers.at(40)),
+      .start_data_address = static_cast<uint64_t>(numbers.at(41)),
+      .end_data_address = static_cast<uint64_t>(numbers.at(42)),
+      .start_brk_address = static_cast<uint64_t>(numbers.at(43)),
+      .start_arg_address = static_cast<uint64_t>(numbers.at(44)),
+      .end_arg_address = static_cast<uint64_t>(numbers.at(45)),
+      .start_env_address = static_cast<uint64_t>(numbers.at(46)),
+      .end_env_address = static_cast<uint64_t>(numbers.at(47)),
+      .exit_code = static_cast<int64_t>(numbers.at(48))};
+}
+
+Top::Top(aos::EventLoop *event_loop)
+    : event_loop_(event_loop),
+      clock_tick_(std::chrono::nanoseconds(1000000000 / sysconf(_SC_CLK_TCK))),
+      page_size_(sysconf(_SC_PAGESIZE)) {
+  TimerHandler *timer = event_loop_->AddTimer([this]() { UpdateReadings(); });
+  event_loop_->OnRun([timer, this]() {
+    timer->Setup(event_loop_->monotonic_now(), kSamplePeriod);
+  });
+}
+
+std::chrono::nanoseconds Top::TotalProcessTime(const ProcStat &proc_stat) {
+  return (proc_stat.user_mode_ticks + proc_stat.kernel_mode_ticks) *
+         clock_tick_;
+}
+
+aos::monotonic_clock::time_point Top::ProcessStartTime(
+    const ProcStat &proc_stat) {
+  return aos::monotonic_clock::time_point(proc_stat.start_time_ticks *
+                                          clock_tick_);
+}
+
+uint64_t Top::RealMemoryUsage(const ProcStat &proc_stat) {
+  return proc_stat.resident_set_size * page_size_;
+}
+
+void Top::UpdateReadings() {
+  aos::monotonic_clock::time_point now = event_loop_->monotonic_now();
+  // Get all the processes that we *might* care about.
+  std::set<pid_t> pids = pids_to_track_;
+  if (track_all_) {
+    DIR *const dir = opendir("/proc");
+    if (dir == nullptr) {
+      PLOG(FATAL) << "Failed to open /proc";
+    }
+    while (true) {
+      struct dirent *const dir_entry = readdir(dir);
+      if (dir_entry == nullptr) {
+        break;
+      }
+      pid_t pid;
+      if (dir_entry->d_type == DT_DIR &&
+          absl::SimpleAtoi(dir_entry->d_name, &pid)) {
+        pids.insert(pid);
+      }
+    }
+  }
+
+  for (const pid_t pid : pids) {
+    std::optional<ProcStat> proc_stat = ReadProcStat(pid);
+    // Stop tracking processes that have died.
+    if (!proc_stat.has_value()) {
+      readings_.erase(pid);
+      continue;
+    }
+    const aos::monotonic_clock::time_point start_time =
+        ProcessStartTime(*proc_stat);
+    auto reading_iter = readings_.find(pid);
+    if (reading_iter == readings_.end()) {
+      reading_iter = readings_
+                         .insert(std::make_pair(
+                             pid, ProcessReadings{.name = proc_stat->name,
+                                                  .start_time = start_time,
+                                                  .cpu_percent = 0.0,
+                                                  .readings = {}}))
+                         .first;
+    }
+    ProcessReadings &process = reading_iter->second;
+    // The process associated with the PID has changed; reset the state.
+    if (process.start_time != start_time) {
+      process.name = proc_stat->name;
+      process.start_time = start_time;
+      process.readings.Reset();
+    }
+
+    process.readings.Push(Reading{now, TotalProcessTime(*proc_stat),
+                                  RealMemoryUsage(*proc_stat)});
+    if (process.readings.size() == 2) {
+      process.cpu_percent =
+          aos::time::DurationInSeconds(process.readings[1].total_run_time -
+                                       process.readings[0].total_run_time) /
+          aos::time::DurationInSeconds(process.readings[1].reading_time -
+                                       process.readings[0].reading_time);
+    } else {
+      process.cpu_percent = 0.0;
+    }
+  }
+}
+
+flatbuffers::Offset<ProcessInfo> Top::InfoForProcess(
+    flatbuffers::FlatBufferBuilder *fbb, pid_t pid) {
+  auto reading_iter = readings_.find(pid);
+  if (reading_iter == readings_.end()) {
+    return {};
+  }
+  const ProcessReadings &reading = reading_iter->second;
+  const flatbuffers::Offset<flatbuffers::String> name =
+      fbb->CreateString(reading.name);
+  ProcessInfo::Builder builder(*fbb);
+  builder.add_pid(pid);
+  builder.add_name(name);
+  builder.add_cpu_usage(reading.cpu_percent);
+  builder.add_physical_memory(
+      reading.readings[reading.readings.size() - 1].memory_usage);
+  return builder.Finish();
+}
+
+flatbuffers::Offset<TopProcessesFbs> Top::TopProcesses(
+    flatbuffers::FlatBufferBuilder *fbb, int n) {
+  // Pair is {cpu_usage, pid}.
+  std::priority_queue<std::pair<double, pid_t>> cpu_usages;
+  for (const auto &pair : readings_) {
+    // Deliberately include 0.0 percent CPU things in the usage list so that if
+    // the user asks for an arbitrarily large number of processes they'll get
+    // everything.
+    cpu_usages.push(std::make_pair(pair.second.cpu_percent, pair.first));
+  }
+  std::vector<flatbuffers::Offset<ProcessInfo>> offsets;
+  for (int ii = 0; ii < n && !cpu_usages.empty(); ++ii) {
+    offsets.push_back(InfoForProcess(fbb, cpu_usages.top().second));
+    cpu_usages.pop();
+  }
+  const flatbuffers::Offset<
+      flatbuffers::Vector<flatbuffers::Offset<ProcessInfo>>>
+      vector_offset = fbb->CreateVector(offsets);
+  TopProcessesFbs::Builder builder(*fbb);
+  builder.add_processes(vector_offset);
+  return builder.Finish();
+}
+
+}  // namespace aos::util
diff --git a/aos/util/top.h b/aos/util/top.h
new file mode 100644
index 0000000..32ff65d
--- /dev/null
+++ b/aos/util/top.h
@@ -0,0 +1,157 @@
+#ifndef AOS_UTIL_TOP_H_
+#define AOS_UTIL_TOP_H_
+
+#include <map>
+#include <string>
+
+#include "aos/containers/ring_buffer.h"
+#include "aos/events/event_loop.h"
+#include "aos/util/process_info_generated.h"
+
+namespace aos::util {
+
+// ProcStat is a struct to hold all the fields available in /proc/[pid]/stat.
+// Currently we only use a small subset of the feilds. See man 5 proc for
+// details on what the fields are--these are in the same order as they appear in
+// the stat file.
+//
+// Things are signed or unsigned based on whether they are listed
+// as signed/unsigned in man 5 proc. We just make everything 64 bits wide
+// because otherwise we have to write out way too many casts everywhere.
+struct ProcStat {
+  int pid;
+  std::string name;
+  char state;
+  int64_t parent_pid;
+  int64_t group_id;
+  int64_t session_id;
+  int64_t tty;
+  int64_t tpgid;
+  uint64_t kernel_flags;
+  uint64_t minor_faults;
+  uint64_t children_minor_faults;
+  uint64_t major_faults;
+  uint64_t children_major_faults;
+  uint64_t user_mode_ticks;
+  uint64_t kernel_mode_ticks;
+  int64_t children_user_mode_ticks;
+  int64_t children_kernel_mode_ticks;
+  int64_t priority;
+  int64_t nice;
+  int64_t num_threads;
+  int64_t itrealvalue;  // always zero.
+  uint64_t start_time_ticks;
+  uint64_t virtual_memory_size;
+  // Number of pages in real memory.
+  int64_t resident_set_size;
+  uint64_t rss_soft_limit;
+  uint64_t start_code_address;
+  uint64_t end_code_address;
+  uint64_t start_stack_address;
+  uint64_t stack_pointer;
+  uint64_t instruction_pointer;
+  uint64_t signal_bitmask;
+  uint64_t blocked_signals;
+  uint64_t ignored_signals;
+  uint64_t caught_signals;
+  uint64_t wchan;
+  // swap_pages fields are not maintained.
+  uint64_t swap_pages;
+  uint64_t children_swap_pages;
+  int64_t exit_signal;
+  // CPU number last exitted on.
+  int64_t processor;
+  // Zero for non-realtime processes.
+  uint64_t rt_priority;
+  uint64_t scheduling_policy;
+  // Aggregated block I/O delay.
+  uint64_t block_io_delay_ticks;
+  uint64_t guest_ticks;
+  uint64_t children_guest_ticks;
+  uint64_t start_data_address;
+  uint64_t end_data_address;
+  uint64_t start_brk_address;
+  uint64_t start_arg_address;
+  uint64_t end_arg_address;
+  uint64_t start_env_address;
+  uint64_t end_env_address;
+  int64_t exit_code;
+};
+
+// Retrieves the stats for a particular process (note that there also exists a
+// /proc/[pid]/task/[tid]/stat with the same format for per-thread information;
+// we currently do not read that).
+// Returns nullopt if unable to read/parse the file.
+std::optional<ProcStat> ReadProcStat(int pid);
+
+// This class provides a basic utility for retrieving general performance
+// information on running processes (named after the top utility). It can either
+// be used to directly get information on individual processes (via
+// set_track_pids()) or used to track a list of the top N processes with the
+// highest CPU usage.
+// Note that this currently relies on sampling processes in /proc every second
+// and using the differences between the two readings to calculate CPU usage.
+// For crash-looping processees or other situations with highly variable or
+// extremely short-lived loads, this may do a poor job of capturing information.
+class Top {
+ public:
+  Top(aos::EventLoop *event_loop);
+
+  // Set whether to track all the top processes (this will result in us having
+  // to track every single process on the system, so that we can sort them).
+  void set_track_top_processes(bool track_all) { track_all_ = track_all; }
+
+  // Specify a set of individual processes to track statistics for.
+  // This can be changed at run-time, although it may take up to kSamplePeriod
+  // to have full statistics on all the relevant processes, since we need at
+  // least two samples to estimate CPU usage.
+  void set_track_pids(const std::set<pid_t> &pids) { pids_to_track_ = pids; }
+
+  // Retrieve statistics for the specified process. Will return the null offset
+  // of no such pid is being tracked.
+  flatbuffers::Offset<ProcessInfo> InfoForProcess(
+      flatbuffers::FlatBufferBuilder *fbb, pid_t pid);
+
+  // Returns information on up to n processes, sorted by CPU usage.
+  flatbuffers::Offset<TopProcessesFbs> TopProcesses(
+      flatbuffers::FlatBufferBuilder *fbb, int n);
+
+ private:
+  // Rate at which to sample /proc/[pid]/stat.
+  static constexpr std::chrono::seconds kSamplePeriod{1};
+
+  struct Reading {
+    aos::monotonic_clock::time_point reading_time;
+    std::chrono::nanoseconds total_run_time;
+    uint64_t memory_usage;
+  };
+
+  struct ProcessReadings {
+    std::string name;
+    aos::monotonic_clock::time_point start_time;
+    // CPU usage is based on the past two readings.
+    double cpu_percent;
+    aos::RingBuffer<Reading, 2> readings;
+  };
+
+  std::chrono::nanoseconds TotalProcessTime(const ProcStat &proc_stat);
+  aos::monotonic_clock::time_point ProcessStartTime(const ProcStat &proc_stat);
+  uint64_t RealMemoryUsage(const ProcStat &proc_stat);
+  void UpdateReadings();
+
+  aos::EventLoop *event_loop_;
+
+  // Length of a clock tick (used to convert from raw numbers in /proc to actual
+  // times).
+  const std::chrono::nanoseconds clock_tick_;
+  // Page size, in bytes, on the current system.
+  const long page_size_;
+
+  std::set<pid_t> pids_to_track_;
+  bool track_all_ = false;
+
+  std::map<pid_t, ProcessReadings> readings_;
+};
+
+}  // namespace aos::util
+#endif  // AOS_UTIL_TOP_H_
diff --git a/aos/util/top_test.cc b/aos/util/top_test.cc
new file mode 100644
index 0000000..cf7e03e
--- /dev/null
+++ b/aos/util/top_test.cc
@@ -0,0 +1,173 @@
+#include "aos/util/top.h"
+
+#include <unistd.h>
+
+#include <array>
+#include <string>
+#include <thread>
+
+#include "aos/events/shm_event_loop.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/testing/path.h"
+#include "aos/testing/tmpdir.h"
+#include "gtest/gtest.h"
+
+namespace aos::util::testing {
+
+class TopTest : public ::testing::Test {
+ protected:
+  TopTest()
+      : shm_dir_(aos::testing::TestTmpDir() + "/aos"),
+        cpu_consumer_([this]() {
+          while (!stop_flag_.load()) {
+          }
+        }),
+        config_file_(
+            aos::testing::ArtifactPath("aos/events/pingpong_config.json")),
+        config_(aos::configuration::ReadConfig(config_file_)),
+        event_loop_(&config_.message()) {
+    FLAGS_shm_base = shm_dir_;
+
+    // Nuke the shm dir, to ensure we aren't being affected by any preexisting tests.
+    aos::util::UnlinkRecursive(shm_dir_);
+  }
+  ~TopTest() {
+    stop_flag_ = true;
+    cpu_consumer_.join();
+  }
+
+  gflags::FlagSaver flag_saver_;
+  std::string shm_dir_;
+
+  std::thread cpu_consumer_;
+  std::atomic<bool> stop_flag_{false};
+  const std::string config_file_;
+  const aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::ShmEventLoop event_loop_;
+};
+
+TEST_F(TopTest, TestSelfStat) {
+  const pid_t pid = getpid();
+  std::optional<ProcStat> proc_stat = ReadProcStat(pid);
+  ASSERT_TRUE(proc_stat.has_value());
+  ASSERT_EQ(pid, proc_stat->pid);
+  ASSERT_EQ("top_test", proc_stat->name);
+  ASSERT_EQ('R', proc_stat->state);
+  ASSERT_LT(1, proc_stat->num_threads);
+}
+
+TEST_F(TopTest, QuerySingleProcess) {
+  const pid_t pid = getpid();
+  Top top(&event_loop_);
+  top.set_track_pids({pid});
+  event_loop_.AddTimer([this]() { event_loop_.Exit(); })
+      ->Setup(event_loop_.monotonic_now() + std::chrono::seconds(2));
+  event_loop_.Run();
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+  fbb.Finish(top.InfoForProcess(&fbb, pid));
+  aos::FlatbufferDetachedBuffer<ProcessInfo> info = fbb.Release();
+  ASSERT_EQ(pid, info.message().pid());
+  ASSERT_TRUE(info.message().has_name());
+  ASSERT_EQ("top_test", info.message().name()->string_view());
+  // Check that we did indeed consume ~1 CPU core (because we're multi-threaded,
+  // we could've consumed a bit more; and on systems where we are competing with
+  // other processes for CPU time, we may not get a full 100% load).
+  ASSERT_LT(0.5, info.message().cpu_usage());
+  ASSERT_GT(1.1, info.message().cpu_usage());
+  // Sanity check memory usage.
+  ASSERT_LT(1000000, info.message().physical_memory());
+  ASSERT_GT(1000000000, info.message().physical_memory());
+}
+
+TEST_F(TopTest, TopProcesses) {
+  // Make some dummy processes that will just spin and get killed off at the
+  // end, so that we actually have things to query.
+  constexpr int kNProcesses = 2;
+  std::vector<pid_t> children;
+  // This will create kNProcesses children + ourself, which means we have enough
+  // processes to test that we correctly exclude extras when requesting fewer
+  // processes than exist.
+  for (int ii = 0; ii < kNProcesses; ++ii) {
+    const pid_t pid = fork();
+    PCHECK(pid >= 0);
+    if (pid == 0) {
+      while (true) {
+      }
+    } else {
+      children.push_back(pid);
+    }
+  }
+
+  Top top(&event_loop_);
+  top.set_track_top_processes(true);
+  event_loop_.AddTimer([this]() { event_loop_.Exit(); })
+      ->Setup(event_loop_.monotonic_now() + std::chrono::seconds(2));
+  event_loop_.SkipTimingReport();
+  event_loop_.SkipAosLog();
+  event_loop_.Run();
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+  fbb.Finish(top.TopProcesses(&fbb, kNProcesses));
+  aos::FlatbufferDetachedBuffer<TopProcessesFbs> info = fbb.Release();
+  ASSERT_EQ(kNProcesses, info.message().processes()->size());
+  double last_cpu = std::numeric_limits<double>::infinity();
+  std::set<pid_t> observed_pids;
+  int process_index = 0;
+  for (const ProcessInfo *info : *info.message().processes()) {
+    SCOPED_TRACE(aos::FlatbufferToJson(info));
+    ASSERT_EQ(0, observed_pids.count(info->pid()));
+    observed_pids.insert(info->pid());
+    ASSERT_TRUE(info->has_name());
+    // Confirm that the top process has non-zero CPU usage, but allow the
+    // lower-down processes to have not been scheduled in the last measurement
+    // cycle.
+    if (process_index < 1) {
+      ASSERT_LT(0.0, info->cpu_usage());
+    } else {
+      ASSERT_LE(0.0, info->cpu_usage());
+    }
+    ++process_index;
+    ASSERT_GE(last_cpu, info->cpu_usage());
+    last_cpu = info->cpu_usage();
+    ASSERT_LT(0, info->physical_memory());
+  }
+
+  for (const pid_t child : children) {
+    kill(child, SIGINT);
+  }
+}
+
+// Test thgat if we request arbitrarily many processes that we only get back as
+// many processes as actually exist and that nothing breaks.
+TEST_F(TopTest, AllTopProcesses) {
+  constexpr int kNProcesses = 1000000;
+
+  Top top(&event_loop_);
+  top.set_track_top_processes(true);
+  event_loop_.AddTimer([this]() { event_loop_.Exit(); })
+      ->Setup(event_loop_.monotonic_now() + std::chrono::seconds(2));
+  event_loop_.Run();
+  flatbuffers::FlatBufferBuilder fbb;
+  fbb.ForceDefaults(true);
+  // There should only be at most 2-3 processes visible inside the bazel
+  // sandbox.
+  fbb.Finish(top.TopProcesses(&fbb, kNProcesses));
+  aos::FlatbufferDetachedBuffer<TopProcessesFbs> info = fbb.Release();
+  ASSERT_GT(kNProcesses, info.message().processes()->size());
+  double last_cpu = std::numeric_limits<double>::infinity();
+  std::set<pid_t> observed_pids;
+  for (const ProcessInfo *info : *info.message().processes()) {
+    SCOPED_TRACE(aos::FlatbufferToJson(info));
+    LOG(INFO) << aos::FlatbufferToJson(info);
+    ASSERT_EQ(0, observed_pids.count(info->pid()));
+    observed_pids.insert(info->pid());
+    ASSERT_TRUE(info->has_name());
+    ASSERT_LE(0.0, info->cpu_usage());
+    ASSERT_GE(last_cpu, info->cpu_usage());
+    last_cpu = info->cpu_usage();
+    ASSERT_LE(0, info->physical_memory());
+  }
+}
+
+}  // namespace aos::util::testing
diff --git a/debian/BUILD b/debian/BUILD
index 3088cf4..c920457 100644
--- a/debian/BUILD
+++ b/debian/BUILD
@@ -71,6 +71,10 @@
     gstreamer_armhf_debs = "files",
 )
 load(
+    ":gstreamer_arm64.bzl",
+    gstreamer_arm64_debs = "files",
+)
+load(
     ":lzma_amd64.bzl",
     lzma_amd64_debs = "files",
 )
@@ -460,6 +464,12 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
+generate_deb_tarball(
+    name = "gstreamer_arm64",
+    files = gstreamer_arm64_debs,
+    target_compatible_with = ["@platforms//os:linux"],
+)
+
 download_packages(
     name = "download_lzma",
     packages = [
diff --git a/debian/gstreamer.BUILD b/debian/gstreamer.BUILD
index b203772..8b58634 100644
--- a/debian/gstreamer.BUILD
+++ b/debian/gstreamer.BUILD
@@ -1,85 +1,233 @@
 load("@//tools/build_rules:select.bzl", "cpu_select")
 
+_common_srcs_list = [
+    "lib/%s/libcap.so.2",
+    "lib/%s/libdbus-1.so.3",
+    "lib/%s/libexpat.so.1",
+    "lib/%s/libgpg-error.so.0",
+    "lib/%s/libpcre.so.3",
+    "usr/lib/%s/blas/libblas.so.3",
+    "usr/lib/%s/gstreamer-1.0/libgstapp.so",
+    "usr/lib/%s/gstreamer-1.0/libgstcoreelements.so",
+    "usr/lib/%s/gstreamer-1.0/libgstdtls.so",
+    "usr/lib/%s/gstreamer-1.0/libgstnice.so",
+    "usr/lib/%s/gstreamer-1.0/libgstrtp.so",
+    "usr/lib/%s/gstreamer-1.0/libgstrtpmanager.so",
+    "usr/lib/%s/gstreamer-1.0/libgstsrtp.so",
+    "usr/lib/%s/gstreamer-1.0/libgstv4l2codecs.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideo4linux2.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideoconvert.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideoparsersbad.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideorate.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideoscale.so",
+    "usr/lib/%s/gstreamer-1.0/libgstvideotestsrc.so",
+    "usr/lib/%s/gstreamer-1.0/libgstwebrtc.so",
+    "usr/lib/%s/gstreamer-1.0/libgstx264.so",
+    "usr/lib/%s/lapack/liblapack.so.3",
+    "usr/lib/%s/libEGL.so.1",
+    "usr/lib/%s/libGL.so.1",
+    "usr/lib/%s/libGLX.so.0",
+    "usr/lib/%s/libGLdispatch.so.0",
+    "usr/lib/%s/libHalf-2_5.so.25",
+    "usr/lib/%s/libIex-2_5.so.25",
+    "usr/lib/%s/libIlmImf-2_5.so.25",
+    "usr/lib/%s/libIlmThread-2_5.so.25",
+    "usr/lib/%s/libImath-2_5.so.25",
+    "usr/lib/%s/libX11-xcb.so.1",
+    "usr/lib/%s/libX11.so.6",
+    "usr/lib/%s/libXau.so.6",
+    "usr/lib/%s/libXcomposite.so.1",
+    "usr/lib/%s/libXcursor.so.1",
+    "usr/lib/%s/libXdamage.so.1",
+    "usr/lib/%s/libXdmcp.so.6",
+    "usr/lib/%s/libXext.so.6",
+    "usr/lib/%s/libXfixes.so.3",
+    "usr/lib/%s/libXi.so.6",
+    "usr/lib/%s/libXinerama.so.1",
+    "usr/lib/%s/libXrandr.so.2",
+    "usr/lib/%s/libXrender.so.1",
+    "usr/lib/%s/libaec.so.0",
+    "usr/lib/%s/libaom.so.0",
+    "usr/lib/%s/libarpack.so.2",
+    "usr/lib/%s/libatk-1.0.so.0",
+    "usr/lib/%s/libatk-bridge-2.0.so.0",
+    "usr/lib/%s/libatspi.so.0",
+    "usr/lib/%s/libblkid.so.1",
+    "usr/lib/%s/libbrotlicommon.so.1",
+    "usr/lib/%s/libbrotlidec.so.1",
+    "usr/lib/%s/libbsd.so.0",
+    "usr/lib/%s/libcairo-gobject.so.2",
+    "usr/lib/%s/libcairo.so.2",
+    "usr/lib/%s/libcfitsio.so.9",
+    "usr/lib/%s/libcharls.so.2",
+    "usr/lib/%s/libcurl-gnutls.so.4",
+    "usr/lib/%s/libcurl.so.4",
+    "usr/lib/%s/libdap.so.27",
+    "usr/lib/%s/libdapclient.so.6",
+    "usr/lib/%s/libdatrie.so.1",
+    "usr/lib/%s/libdav1d.so.4",
+    "usr/lib/%s/libde265.so.0",
+    "usr/lib/%s/libdeflate.so.0",
+    "usr/lib/%s/libdrm.so.2",
+    "usr/lib/%s/libdw.so.1",
+    "usr/lib/%s/libelf.so.1",
+    "usr/lib/%s/libepoxy.so.0",
+    "usr/lib/%s/libepsilon.so.1",
+    "usr/lib/%s/libffi.so.7",
+    "usr/lib/%s/libfontconfig.so.1",
+    "usr/lib/%s/libfreetype.so.6",
+    "usr/lib/%s/libfreexl.so.1",
+    "usr/lib/%s/libfribidi.so.0",
+    "usr/lib/%s/libfyba.so.0",
+    "usr/lib/%s/libfygm.so.0",
+    "usr/lib/%s/libfyut.so.0",
+    "usr/lib/%s/libgbm.so.1",
+    "usr/lib/%s/libgcrypt.so.20",
+    "usr/lib/%s/libgdcmCommon.so.3.0",
+    "usr/lib/%s/libgdcmDICT.so.3.0",
+    "usr/lib/%s/libgdcmDSED.so.3.0",
+    "usr/lib/%s/libgdcmIOD.so.3.0",
+    "usr/lib/%s/libgdcmMSFF.so.3.0",
+    "usr/lib/%s/libgdcmjpeg12.so.3.0",
+    "usr/lib/%s/libgdcmjpeg16.so.3.0",
+    "usr/lib/%s/libgdcmjpeg8.so.3.0",
+    "usr/lib/%s/libgdk-3.so.0",
+    "usr/lib/%s/libgdk_pixbuf-2.0.so.0",
+    "usr/lib/%s/libgeos-3.9.0.so",
+    "usr/lib/%s/libgeos_c.so.1",
+    "usr/lib/%s/libgeotiff.so.5",
+    "usr/lib/%s/libgfortran.so.5",
+    "usr/lib/%s/libgif.so.7",
+    "usr/lib/%s/libgio-2.0.so.0",
+    "usr/lib/%s/libglib-2.0.so.0",
+    "usr/lib/%s/libgmodule-2.0.so.0",
+    "usr/lib/%s/libgmp.so.10",
+    "usr/lib/%s/libgnutls.so.30",
+    "usr/lib/%s/libgobject-2.0.so.0",
+    "usr/lib/%s/libgraphite2.so.3",
+    "usr/lib/%s/libgssdp-1.2.so.0",
+    "usr/lib/%s/libgstallocators-1.0.so.0",
+    "usr/lib/%s/libgstapp-1.0.so.0",
+    "usr/lib/%s/libgstaudio-1.0.so.0",
+    "usr/lib/%s/libgstbase-1.0.so.0",
+    "usr/lib/%s/libgstcodecparsers-1.0.so.0",
+    "usr/lib/%s/libgstcodecs-1.0.so.0",
+    "usr/lib/%s/libgstgl-1.0.so.0",
+    "usr/lib/%s/libgstnet-1.0.so.0",
+    "usr/lib/%s/libgstpbutils-1.0.so.0",
+    "usr/lib/%s/libgstreamer-1.0.so.0",
+    "usr/lib/%s/libgstrtp-1.0.so.0",
+    "usr/lib/%s/libgstsctp-1.0.so.0",
+    "usr/lib/%s/libgstsdp-1.0.so.0",
+    "usr/lib/%s/libgsttag-1.0.so.0",
+    "usr/lib/%s/libgstvideo-1.0.so.0",
+    "usr/lib/%s/libgstwebrtc-1.0.so.0",
+    "usr/lib/%s/libgthread-2.0.so.0",
+    "usr/lib/%s/libgtk-3.so.0",
+    "usr/lib/%s/libgudev-1.0.so.0",
+    "usr/lib/%s/libgupnp-1.2.so.0",
+    "usr/lib/%s/libgupnp-igd-1.0.so.4",
+    "usr/lib/%s/libharfbuzz.so.0",
+    "usr/lib/%s/libhdf5_serial.so.103",
+    "usr/lib/%s/libhdf5_serial_hl.so.100",
+    "usr/lib/%s/libheif.so.1",
+    "usr/lib/%s/libhogweed.so.6",
+    "usr/lib/%s/libicudata.so.67",
+    "usr/lib/%s/libicui18n.so.67",
+    "usr/lib/%s/libicuuc.so.67",
+    "usr/lib/%s/libidn2.so.0",
+    "usr/lib/%s/libjbig.so.0",
+    "usr/lib/%s/libjpeg.so.62",
+    "usr/lib/%s/libjson-c.so.5",
+    "usr/lib/%s/libjson-glib-1.0.so.0",
+    "usr/lib/%s/libkmlbase.so.1",
+    "usr/lib/%s/libkmldom.so.1",
+    "usr/lib/%s/libkmlengine.so.1",
+    "usr/lib/%s/liblber-2.4.so.2",
+    "usr/lib/%s/liblcms2.so.2",
+    "usr/lib/%s/libldap_r-2.4.so.2",
+    "usr/lib/%s/libltdl.so.7",
+    "usr/lib/%s/liblz4.so.1",
+    "usr/lib/%s/libmariadb.so.3",
+    "usr/lib/%s/libmd.so.0",
+    "usr/lib/%s/libminizip.so.1",
+    "usr/lib/%s/libmount.so.1",
+    "usr/lib/%s/libnetcdf.so.18",
+    "usr/lib/%s/libnettle.so.8",
+    "usr/lib/%s/libnghttp2.so.14",
+    "usr/lib/%s/libnice.so.10",
+    "usr/lib/%s/libnspr4.so",
+    "usr/lib/%s/libnss3.so",
+    "usr/lib/%s/libnssutil3.so",
+    "usr/lib/%s/libodbc.so.2",
+    "usr/lib/%s/libodbcinst.so.2",
+    "usr/lib/%s/libopencv_core.so.4.5",
+    "usr/lib/%s/libopencv_highgui.so.4.5",
+    "usr/lib/%s/libopencv_imgcodecs.so.4.5",
+    "usr/lib/%s/libopencv_imgproc.so.4.5",
+    "usr/lib/%s/libopenjp2.so.7",
+    "usr/lib/%s/liborc-0.4.so.0",
+    "usr/lib/%s/libp11-kit.so.0",
+    "usr/lib/%s/libpango-1.0.so.0",
+    "usr/lib/%s/libpangocairo-1.0.so.0",
+    "usr/lib/%s/libpangoft2-1.0.so.0",
+    "usr/lib/%s/libpixman-1.so.0",
+    "usr/lib/%s/libplc4.so",
+    "usr/lib/%s/libplds4.so",
+    "usr/lib/%s/libpng16.so.16",
+    "usr/lib/%s/libpoppler.so.102",
+    "usr/lib/%s/libpq.so.5",
+    "usr/lib/%s/libproj.so.19",
+    "usr/lib/%s/libpsl.so.5",
+    "usr/lib/%s/libqhull.so.8.0",
+    "usr/lib/%s/librtmp.so.1",
+    "usr/lib/%s/librttopo.so.1",
+    "usr/lib/%s/libsasl2.so.2",
+    "usr/lib/%s/libsmime3.so",
+    "usr/lib/%s/libsoup-2.4.so.1",
+    "usr/lib/%s/libspatialite.so.7",
+    "usr/lib/%s/libsqlite3.so.0",
+    "usr/lib/%s/libsrtp2.so.1",
+    "usr/lib/%s/libssh2.so.1",
+    "usr/lib/%s/libsuperlu.so.5",
+    "usr/lib/%s/libsystemd.so.0",
+    "usr/lib/%s/libsz.so.2",
+    "usr/lib/%s/libtasn1.so.6",
+    "usr/lib/%s/libtbb.so.2",
+    "usr/lib/%s/libthai.so.0",
+    "usr/lib/%s/libtiff.so.5",
+    "usr/lib/%s/libudev.so.1",
+    "usr/lib/%s/libunistring.so.2",
+    "usr/lib/%s/liburiparser.so.1",
+    "usr/lib/%s/libuuid.so.1",
+    "usr/lib/%s/libv4l2.so.0",
+    "usr/lib/%s/libv4lconvert.so.0",
+    "usr/lib/%s/libvpx.so.6",
+    "usr/lib/%s/libwayland-client.so.0",
+    "usr/lib/%s/libwayland-cursor.so.0",
+    "usr/lib/%s/libwayland-egl.so.1",
+    "usr/lib/%s/libwayland-server.so.0",
+    "usr/lib/%s/libwebp.so.6",
+    "usr/lib/%s/libx264.so.160",
+    "usr/lib/%s/libx265.so.192",
+    "usr/lib/%s/libxcb-render.so.0",
+    "usr/lib/%s/libxcb-shm.so.0",
+    "usr/lib/%s/libxcb.so.1",
+    "usr/lib/%s/libxerces-c-3.2.so",
+    "usr/lib/%s/libxkbcommon.so.0",
+    "usr/lib/%s/libxml2.so.2",
+    "usr/lib/%s/libzstd.so.1",
+    "usr/lib/libarmadillo.so.10",
+    "usr/lib/libdfalt.so.0",
+    "usr/lib/libgdal.so.28",
+    "usr/lib/libmfhdfalt.so.0",
+    "usr/lib/libogdi.so.4.1",
+]
+
 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",
-        ],
+        "amd64": [s % "x86_64-linux-gnu" if "%" in s else s for s in _common_srcs_list],
         "roborio": [
         ],
         "armhf": [
@@ -159,34 +307,42 @@
             "usr/lib/arm-linux-gnueabihf/libxcb.so.1",
             "usr/lib/arm-linux-gnueabihf/libxml2.so.2",
         ],
-        "arm64": [],
+        "arm64": [s % "aarch64-linux-gnu" if "%" in s else s for s in _common_srcs_list],
         "cortex-m": [],
         "cortex-m0plus": [],
     }),
     hdrs = glob([
+        "usr/lib/aarch64-linux-gnu/glib-2.0/include/**/*.h",
+        "usr/lib/arm-linux-gnueabihf/glib-2.0/include/**/*.h",
         "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",
+        "usr/include/libsoup-2.4/**/*.h",
+        "usr/include/json-glib-1.0/**/*.h",
+        "usr/include/opencv4/**",
+        "usr/include/X11/**",
     ]),
     includes = cpu_select({
         "amd64": [
             "usr/lib/x86_64-linux-gnu/glib-2.0/include",
-            "usr/include",
-            "usr/include/glib-2.0",
-            "usr/include/gstreamer-1.0",
         ],
         "armhf": [
             "usr/lib/arm-linux-gnueabihf/glib-2.0/include",
-            "usr/include",
-            "usr/include/glib-2.0",
-            "usr/include/gstreamer-1.0",
         ],
-        "arm64": [],
+        "arm64": [
+            "usr/lib/aarch64-linux-gnu/glib-2.0/include",
+        ],
         "roborio": [],
         "cortex-m": [],
         "cortex-m0plus": [],
-    }),
+    }) + [
+        "usr/include",
+        "usr/include/glib-2.0",
+        "usr/include/gstreamer-1.0",
+        "usr/include/libsoup-2.4",
+        "usr/include/json-glib-1.0",
+        "usr/include/opencv4",
+    ],
     linkopts = [
         "-ldl",
         "-lresolv",
diff --git a/debian/gstreamer_amd64.bzl b/debian/gstreamer_amd64.bzl
index 2d1558b..59f3d59 100644
--- a/debian/gstreamer_amd64.bzl
+++ b/debian/gstreamer_amd64.bzl
@@ -1,511 +1,524 @@
 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",
+    "adwaita-icon-theme_3.38.0-1_all.deb": "2046876c82fc1c342b38ace9aa0661bcb3e167837c984b4bdc89702bc78df5ac",
+    "coreutils_8.32-4+b1_amd64.deb": "3558a412ab51eee4b60641327cb145bb91415f127769823b68f9335585b308d4",
+    "dconf-gsettings-backend_0.38.0-2_amd64.deb": "194991ed5f4ab1ca25413858cb99c910391cfd6d3b1b6a3d3e56a4b3a706a37d",
+    "dconf-service_0.38.0-2_amd64.deb": "639125f7a44d11f96661c61a07abbb58da0e5636ed406ac186adcef8651775c2",
+    "fluidr3mono-gm-soundfont_2.315-7_all.deb": "4098301bf29f4253c2f5799a844f42dd4aa733d91a210071ad16d7757dea51d6",
+    "fontconfig-config_2.13.1-4.2_all.deb": "48afb6ad7d15e6104a343b789f73697301ad8bff77b69927bc998f5a409d8e90",
+    "fontconfig_2.13.1-4.2_amd64.deb": "c594a100759ef7c94149359cf4d2da5fb59ef30474c7a2dde1e049d32b9c478a",
+    "fonts-croscore_20201225-1_all.deb": "64904820b729ff40038f85683004e3b94b328d969bc0fbba263c58d635452923",
+    "fonts-dejavu-core_2.37-2_all.deb": "1f67421437b6eb18669d2868e3e02cb88668683d635198142f48aacc5b397118",
+    "fonts-freefont-otf_20120503-10_all.deb": "0b63996c80c6c660424af6d3832818e647960d6f65a51de010bb57dd0762faa7",
+    "fonts-freefont-ttf_20120503-10_all.deb": "4ca1c21ebc479198a3a5879d236c8317d6f7b2f1c403f7890e24c02eead05615",
+    "fonts-liberation2_2.1.3-1_all.deb": "e0805f0085132f5e6dd30f88c0d7260caf1e5450832fe2e3988a20fa9fa2150e",
+    "fonts-liberation_1.07.4-11_all.deb": "efd381517f958b01969343634ffcbdd60056be7779af84c6f53a005090430204",
+    "fonts-texgyre_20180621-3.1_all.deb": "cb7e9a4b2471cfdd57194c16364f9102f0639816a2662fed4b30d2a158747076",
+    "fonts-urw-base35_20200910-1_all.deb": "f95a139adb7f1b60626e76d4d45d1b35aad1bc2c2597394c291ef5f84b5dcb43",
+    "gdal-data_3.2.2+dfsg-2+deb11u1_all.deb": "3ae44cc2f51dccc023f9c3cfbea3411508e24f1335651fa0e6cba74b7b9b87aa",
+    "gir1.2-glib-2.0_1.66.1-1+b1_amd64.deb": "1163a4e7eb095e37752739c0065bad50fa2177c13a87e7c1b0d44ed517fe8c91",
+    "gir1.2-gst-plugins-bad-1.0_1.20.1-1~bpo11+1_amd64.deb": "83575d0b8a14c535f8193e8c37885d6523434a8408c128821f9f7cb95962cc57",
+    "gir1.2-gst-plugins-base-1.0_1.20.1-1~bpo11+1_amd64.deb": "79fdb139b1d6a2ae6ce899d776bbb11b0f693018b6eec562dfe973ec7df6e306",
+    "gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_amd64.deb": "312820f8d42f1a052de1db82483e1cfed0fd36b229f9dfd9397537a7ee808798",
+    "gir1.2-gudev-1.0_234-1_amd64.deb": "c9560915329dafdfb32998eb3e90cbb0f37f53134c69c1d5acef070754e36e01",
+    "gir1.2-json-1.0_1.6.2-1_amd64.deb": "c44783e0b94d6a6792ffd837d1e97ebb8ca6ccb8beaf7abe295e367cd92a1b7d",
+    "gir1.2-soup-2.4_2.72.0-2_amd64.deb": "064523a0f3121f9014f9dabd3add9e8441f5e24ac181345a5673b4e0105a4286",
+    "glib-networking-common_2.66.0-2_all.deb": "a07370151ce5169e48ee7799b9bd9a7a035467a21f5cf3373b2aff090968609c",
+    "glib-networking-services_2.66.0-2_amd64.deb": "19131c7c31bc3fae604df30d2f73c3e8338ffeb2988fe167bb8b2b1c8913c9ab",
+    "glib-networking_2.66.0-2_amd64.deb": "b2cd50a8c3b30c16fd1a19c5244f681c6c0d1f426c385d44900477b052f70024",
+    "gsettings-desktop-schemas_3.38.0-2_all.deb": "3758968491a770e50cd85122c00d141944ffb43cb7a5c886a37290fef848cee3",
+    "gstreamer1.0-nice_0.1.18-2~bpo11+1_amd64.deb": "6f8d8bd4119359c73f0978db064b1ffd3f9cfc3f3f9e3f198f04457655808259",
+    "gstreamer1.0-plugins-bad_1.20.1-1~bpo11+1_amd64.deb": "6e948e0c065e5a1662d1a307e29c9afaaa91a1e0d1cbb2caddeff581efe8a5c1",
+    "gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_amd64.deb": "c7910b0b45f844b72852f861bf9470a002b138c926accf3adc6cb86484dd981d",
+    "gstreamer1.0-plugins-good_1.20.1-1~bpo11+1_amd64.deb": "cc40a58c68afce8fe7ffde9f1a5d5be50cbb33e1dde435eab5d03c6e032cb695",
+    "gstreamer1.0-plugins-ugly_1.20.1-1~bpo11+1_amd64.deb": "04605a67ca9fccbea5d5c312f66fb6606200f222573a03ee39709786e02f2fc5",
+    "gtk-update-icon-cache_3.24.24-4+deb11u2_amd64.deb": "b877617f382240663be1010510511a5f9fe10853a3f97088cc01be277ff184d6",
     "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",
+    "icu-devtools_67.1-7_amd64.deb": "0a89d6f360d9c686c08d0156a0c8244715c9aaeffca079cf1716f12cffece82e",
+    "iso-codes_4.6.0-1_all.deb": "4e044d72a9f810aabf2c8addf126327fa845eaf8e983b51eb6356b9ed5108348",
+    "liba52-0.7.4_0.7.4-20_amd64.deb": "da214eaeeeca241ae0bf22e7ad180d8e47603c227583a3e136471df40218bff3",
+    "libaa1_1.4p5-48_amd64.deb": "5fb458e3354308f88161e9bb22c114001a1cc241ebe4915fe6cc11b4ffe533ac",
+    "libaec0_1.0.4-1_amd64.deb": "2d784ab4a922112cc1c3aab0164486e7829fc705913c9ba8bc62a0642d18b8bd",
+    "libaom0_1.0.0.errata1-3_amd64.deb": "900f94cd878e6ba2acf87a2a324838736d5085b436f9bf615b2a3ed0345f8a0d",
+    "libarchive13_3.4.3-2+deb11u1_amd64.deb": "42530f8ae7e5787bd6d269cda49b651c6920db0b18844d71fc960b12cc271259",
+    "libarmadillo10_10.1.2+dfsg-6+b1_amd64.deb": "54daff607308fadf5662836e422442bef3e4435ad56ca669f3d3de4ff41ba2ab",
+    "libarpack2_3.8.0-1_amd64.deb": "c51364e5681e1268f201a57969a4a029d71d3239be993934fad59428b96f588e",
+    "libasound2-data_1.2.4-1.1_all.deb": "76211f5f201ad1069b95d047863e0c1b51d8400c874b59e00f24f31f972b4036",
+    "libasound2_1.2.4-1.1_amd64.deb": "d8c9b5182768db2a7c5c73f1eed0b1be1431ae4f41084d502b325d06d5b0f648",
+    "libass9_0.15.0-2_amd64.deb": "96d52690cfb7c6ff20bea4dc5b931c2da432fc264a9e87388ce3ce2f83f2fca8",
+    "libasyncns0_0.8-6+b2_amd64.deb": "bef9c0ef5c8eeded91a1545b5c66c16aa60775b975bc9747d4b0dccf597d2a4d",
+    "libatk-bridge2.0-0_2.38.0-1_amd64.deb": "65b063b4b45c5fd60d91e374d01bb73eacdb30c545a6ef0873d07d6da97765d1",
+    "libatk1.0-0_2.36.0-2_amd64.deb": "572cd62f92ec25c75b98617321373d46a6717cbcc93d2025ebd6d550f1abf901",
+    "libatk1.0-data_2.36.0-2_all.deb": "86c1acae473977f8a78b905090847df654306996324493f9a39d9f27807778b2",
+    "libatspi2.0-0_2.38.0-4_amd64.deb": "53435278eb8815aafbb41db29a691a43a9de16fa58d9bc7908a1f6f2a07f0b67",
+    "libattr1_2.4.48-6_amd64.deb": "af3c3562eb2802481a2b9558df1b389f3c6d9b1bf3b4219e000e05131372ebaf",
+    "libavahi-client3_0.8-5_amd64.deb": "697dff4185adc2912ee2b27c91bfb4fece4376dde2158dc7249a69498e4c0db0",
+    "libavahi-common-data_0.8-5_amd64.deb": "37595c0c6876ac914f66b081063a8522fb255afadb76e5613343a1d653beca0d",
+    "libavahi-common3_0.8-5_amd64.deb": "1300d89d5fb920753aee4c2b47b1ab1ef60533abe9875ba203096738f4cfb692",
     "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",
+    "libavcodec-dev_4.3.3-0+deb11u1_amd64.deb": "53b824799126078fc41c2d764f897bff04d1be6c083d58d88086e645f06995e0",
+    "libavcodec58_4.3.3-0+deb11u1_amd64.deb": "083c47e88d9d1ed2e40e67b5c71e6c24d13527ad504506a8e99d07c7ce502b26",
+    "libavformat-dev_4.3.3-0+deb11u1_amd64.deb": "6c44ad5535658d379e32e5556b6d69389ed65d096ecf6ae0a1486c94232b14e5",
+    "libavformat58_4.3.3-0+deb11u1_amd64.deb": "f23a5d77570bbdb365f0e33900c92733fa469af58862a3e36f9dab76c86815bd",
+    "libavutil-dev_4.3.3-0+deb11u1_amd64.deb": "c9cac2aa74a9ef8c0841a99fc0e17ab5a5e781e0725cc6d65313c9a97e4b8d5b",
+    "libavutil56_4.3.3-0+deb11u1_amd64.deb": "060320302554ed32634deb1708622c9ea8c2919d7f77316e82e97e68fbf0cbbd",
+    "libblas3_3.9.0-3_amd64.deb": "489238f1d2f65dad98d134e5d7fec2a857422d7d2c8af029fc277cff0eec92d7",
+    "libblkid-dev_2.36.1-8+deb11u1_amd64.deb": "3f224b3dc4d094367b45b31c4bc367dd9528f45eba22af77229a7f9be7e6005d",
+    "libblkid1_2.36.1-8+deb11u1_amd64.deb": "9026ddd9f211008531ce6024d5ce042c723e237ecadfbf1f9343cb44aff492b9",
+    "libbluray2_1.2.1-4+deb11u1_amd64.deb": "da902db9e3dcfb7ac7baf723460054012677aefa5a08bcc3fd3f9b1c7a3b58a9",
+    "libbrotli-dev_1.0.9-2+b2_amd64.deb": "520ef8f3af1a190ac2ce5954c0e42c8e6b80a593124f97e813be33e9e068ffc3",
+    "libbrotli1_1.0.9-2+b2_amd64.deb": "65ca7d8b03e9dac09c5d544a89dd52d1aeb74f6a19583d32e4ff5f0c77624c24",
+    "libbs2b0_3.1.0+dfsg-2.2+b1_amd64.deb": "a33848ffed7d42fe0eaa80ebfc3633c4b89f4673971e407ab27ca30638990d63",
+    "libbsd0_0.11.3-1_amd64.deb": "284a7b8dcfcad74770f57360721365317448b38ab773db542bf630e94e60c13e",
+    "libcaca0_0.99.beta19-2.2_amd64.deb": "6d6279c3fcef45cf09c686dff3f9d15d3ef6164ca614b2d019cfc79a10acc9d6",
+    "libcairo-gobject2_1.16.0-5_amd64.deb": "a046d3ca805d4151029941fae736bfdf1c6f3dbcf1bd581102bd5ad844ea013e",
+    "libcairo2_1.16.0-5_amd64.deb": "b27210c0cf7757120e871abeba7de12a5cf94727a2360ecca5eb8e50ca809d12",
+    "libcap2-bin_2.44-1_amd64.deb": "a5b9717d8455cf8517c4c5f29aa04a4dec973430f0d3c1232f652abb9a4d93cc",
+    "libcap2_2.44-1_amd64.deb": "7a3ae3e97d0d403a4c54663c0bb48e9341d98822420a4ab808c6dc8e8474558f",
+    "libcdio19_2.1.0-2_amd64.deb": "1cfcaabed47e58f40742e491493432e815de0fe54e0294e3a2ea2bcbb131ac14",
+    "libcdparanoia0_3.10.2+debian-13.1_amd64.deb": "42afad3ddc60edb9b5f5501ef4870eceaa68e5f8ea3e27edbefdff51c68d6ecf",
+    "libcfitsio9_3.490-3_amd64.deb": "2b3043e9da483e5b06f74c2c2b9d0990f42bed0750e0adb03d95749a8d32212d",
+    "libcharls2_2.2.0+dfsg-2_amd64.deb": "f0a132f0a54e55eeb6db2d74ff669716b633d3b913ccb406766c7d6f7763512e",
+    "libchromaprint1_1.5.0-2_amd64.deb": "adfc5b664f6ec6f60d76bd41546141914bb5d7c797e5291565305d7c293c0827",
+    "libcodec2-0.9_0.9.2-4_amd64.deb": "27b8f890e7b614ea203826b8ecdff65d742993f1bccb15b22e7ae2683e76ec1e",
+    "libcolord2_1.4.5-3_amd64.deb": "b7f0b90535a04f25f4fe8a838b548eed87447b3225414bd4f30755ee917698dd",
+    "libcups2_2.3.3op2-3+deb11u1_amd64.deb": "b9545555975d3560612a44b23c362a03be517a75ddfa7a63bf828e03c57be37c",
+    "libcurl3-gnutls_7.74.0-1.3+deb11u1_amd64.deb": "86ee8f307582708132806c687e0127375d4a42e23d2bf2c96054a547cadddd80",
+    "libcurl4_7.74.0-1.3+deb11u1_amd64.deb": "6f9c494eecc920899bb2c72d1a507a34b3703105778b0b9b9ae9aebdbdffcaab",
+    "libdap27_3.20.7-6_amd64.deb": "fd6436efe71e465fac68acc00911d34e0eed8c5c0627c3f2d0de3bcc9edcb24b",
+    "libdapclient6v5_3.20.7-6_amd64.deb": "1f30f21e55069d28c49dbcff8cab9af71f785565486ad840d86a95674b0ba587",
+    "libdatrie1_0.2.13-1_amd64.deb": "3544f2cf26039fade9c7e7297dde1458b8386442c3b0fc26fdf10127433341c1",
+    "libdav1d4_0.7.1-3_amd64.deb": "f351142dc854dfada4d24ca173f62382e3e24901fe560be6c3fe4515c36858e5",
+    "libdb5.3_5.3.28+dfsg1-0.8_amd64.deb": "00b9e63e287f45300d4a4f59b6b88e25918443c932ae3e5845d5761ae193c530",
+    "libdbus-1-3_1.12.20-2_amd64.deb": "7256dfeda88461e6fccbf98372d3ec29487b3b2d0ae5d145a3332ab35274f0da",
+    "libdc1394-25_2.2.6-3_amd64.deb": "bfc3b2c124393b1045e709ceb96827ad1a45cfce4ebc111bd75457ea5bd43964",
+    "libdc1394-dev_2.2.6-3_amd64.deb": "f435440dd8408eebe554a47f1388396b514c83f40b3f74d9098ec3b65efcb814",
+    "libdca0_0.0.7-2_amd64.deb": "3d3258fc5e8d94c4dbf078b0e4fc4194cf9c7cb069a3a4bb237e894cd6c0afd8",
+    "libdconf1_0.38.0-2_amd64.deb": "ff3b1d05466782acd6e335b001460b7af4ea76f49bbbbd5447535d2b702fa97e",
+    "libde265-0_1.0.8-1_amd64.deb": "82b2f1e53b7e23ead6e375fc12c979fee63d2cdce9a3054fe1177403da86a7fd",
+    "libdeflate-dev_1.7-1_amd64.deb": "fc95c11795f2efd14b59db87214929879c5019bb1af3e77925019c39a57cffa4",
+    "libdeflate0_1.7-1_amd64.deb": "dadaf0d28360f6eb21ad389b2e0f12f8709c9de539b28de9c11d7ec7043dec95",
+    "libdouble-conversion3_3.1.5-6.1_amd64.deb": "0a22f1cca233c2347320d56c65312caafb99e379c1aa48af6164a8905ce34672",
+    "libdpkg-perl_1.20.9_all.deb": "134bd00e60fa30d39d5f676d306d6f1d61c7f6ec6086c1785dbc355ce6190f29",
+    "libdrm-amdgpu1_2.4.104-1_amd64.deb": "0005f21e342925bd26a25185289ae035aa931ced8f6fd9e3d4deade36d272ecd",
+    "libdrm-common_2.4.104-1_all.deb": "60c69026fb8e4cfdf8d80a4a86ee30516c611dcc4de4aa1c8ccbf06dff563e2b",
+    "libdrm-dev_2.4.104-1_amd64.deb": "426033d284eb582ed7c87b7bf9083ccbb6b239a8983e1f930f33b097b96f7745",
+    "libdrm-intel1_2.4.104-1_amd64.deb": "7d376adc7b5d4d83ec8414ff67dbc18765c6d420de9a6e1045fead7f1f82331d",
+    "libdrm-nouveau2_2.4.104-1_amd64.deb": "dbf4a3be55c609b1a2ea89d6782ae5c9a5b991844917dcd42c01666b73a96ceb",
+    "libdrm-radeon1_2.4.104-1_amd64.deb": "c33cd14e8ed7e2dfc02696ed51d4795c5797b0821666667e0a889bba705862b0",
+    "libdrm2_2.4.104-1_amd64.deb": "113396b3a33000f7f3347cd711ad9bcfe9945927331cc6cee63c751a889a967b",
+    "libdv4_1.0.0-13_amd64.deb": "fa10de9781070b1fc16a9b3686175b9458a828300a3ef6c5b8b28f0045cca17b",
+    "libdvdnav4_6.1.0-1+b1_amd64.deb": "ad5b916c9a373db577827d556827f14e4c17df18b0a56a86654bd4caef3bf37a",
+    "libdvdread8_6.1.1-2_amd64.deb": "5b60fa255ab26171b6faddbc97079653cc04e38b58f07cf14069cfff6d5753b6",
+    "libdw-dev_0.183-1_amd64.deb": "f18363d346023f7560c4d588dbddbf18415b8e9f5cd8f0c3f786c84992b7cf61",
+    "libdw1_0.183-1_amd64.deb": "0ee89e2143356239975e6808ea005de396f8e8e0d771e1376c8bc93e29f07ec8",
+    "libedit2_3.1-20191231-2+b1_amd64.deb": "ac545f6ad10ba791aca24b09255ad1d6d943e6bc7c5511d5998e104aee51c943",
+    "libegl-dev_1.3.2-1_amd64.deb": "2847662b23487d5b1e467bca8cc8753baa880f794744a9b492c978bd5514b286",
+    "libegl-mesa0_20.3.5-1_amd64.deb": "a0c36a3665af89cbc96f865bd1b64c6c07b93096e91ba5b470d375d02dfa6d82",
+    "libegl1_1.3.2-1_amd64.deb": "3a5583ebd7a9d8ad102484db9637c409561428d21345037b310c4ef2ca8e8837",
+    "libelf-dev_0.183-1_amd64.deb": "9b711a3a40b65a3ab435509bc0608d3ac3526744ad9ccbc28cad38e8bce794db",
+    "libelf1_0.183-1_amd64.deb": "e1ad132d502b255023c222d0cae1d02ca941f6b68fd0e9b908c6004cc326592c",
+    "libepoxy0_1.5.5-1_amd64.deb": "3d050c9b138872c83b5b3521c97ab89f8a885b1391fdd0477cf8168ae54728a3",
+    "libepsilon1_0.9.2+dfsg-5_amd64.deb": "18f3c005d4f0d71bd8a448a2fe26867301f9cfd87197c455f30b879376e89703",
+    "libexif-dev_0.6.22-3_amd64.deb": "7c89f9dfad3c52c0b1e8e6944b606ebe443f606a1074ce10fa4b49b58a3d304f",
+    "libexif12_0.6.22-3_amd64.deb": "8ea5b829490f9afeeeb8ffa3191e4c8075250465420435f53a007a0a7cbf4f33",
+    "libexpat1_2.2.10-2+deb11u3_amd64.deb": "e3069628af14657a2fb2cc597c35982bf71d392ccf4e70207c92b156a0e341f5",
+    "libfaad2_2.10.0-1_amd64.deb": "b6e9c9d97e4683a0c0173e43a32b682b8454b4b8e07282516c258664a33253e3",
+    "libffi-dev_3.3-6_amd64.deb": "ca2c71d9c68b1944b689606f12acf8023bad1b5083e8413894fd41ad0b977d20",
+    "libffi7_3.3-6_amd64.deb": "30ca89bfddae5fa6e0a2a044f22b6e50cd17c4bc6bc850c579819aeab7101f0f",
+    "libflac8_1.3.3-2+deb11u1_amd64.deb": "7672e1cbd80dfed5bfa572221f061e61b6d84479ba12dc5825145216fbc01de0",
+    "libflite1_2.2-2_amd64.deb": "39e57480254f4de8a53c46b7d143b727d73404ea76e1670727e95d2ce902eb60",
+    "libfluidsynth2_2.1.7-1.1_amd64.deb": "72a881f542ca6125c4ca2313fd2d3c340471b7d6bfbaf6702138bc9f73278ffc",
+    "libfontconfig1_2.13.1-4.2_amd64.deb": "b92861827627a76e74d6f447a5577d039ef2f95da18af1f29aa98fb96baea4c1",
+    "libfreeaptx0_0.1.1-1_amd64.deb": "4f0c5c9198358cffbe40139fc36211b5b6d83804be9985558cce07c61d9f9983",
+    "libfreetype6_2.10.4+dfsg-1_amd64.deb": "e95396fc3cc806b2b95d9a00b4226eb464bc3ef4817c798749a0dd582546e5bc",
+    "libfreexl1_1.0.6-1_amd64.deb": "36a962b167b1971f89568feccabe4b027f177fcab4cbf9a1ed66a03d25a161ed",
+    "libfribidi0_1.0.8-2_amd64.deb": "fa4c6ea0d4d4709b2414a9d9567a3f9d35cd8a270c8dcc8bd79d046fc200b914",
+    "libfyba0_4.1.1-7_amd64.deb": "4ea6e4170eca7ed2a1319c1d1a35f661421f4879de01eafaaff15d10b036b753",
+    "libgbm-dev_20.3.5-1_amd64.deb": "47d53d5959ef7a6c326c8487db7b33911718616e7d9832f2793691ed4458fb03",
+    "libgbm1_20.3.5-1_amd64.deb": "2d9b07282e46e3c9398613b6d4fe86c3259e4326b158be7e1f4f58cab541156c",
+    "libgcc-s1_10.2.1-6_amd64.deb": "e478f2709d8474165bb664de42e16950c391f30eaa55bc9b3573281d83a29daf",
+    "libgcrypt20_1.8.7-6_amd64.deb": "7a2e0eef8e0c37f03f3a5fcf7102a2e3dc70ba987f696ab71949f9abf36f35ef",
+    "libgd3_2.3.0-2_amd64.deb": "fadaa01272200dcaa476c6b8908e1faa93d6840610beca909099647829f3fdc1",
+    "libgdal28_3.2.2+dfsg-2+deb11u1_amd64.deb": "60466068ca138408812696a64a4d6936ab9d84a991125289db50d8e0697d6cd9",
+    "libgdcm-dev_3.0.8-2_amd64.deb": "4c3b97566041cecf408218c76a7b89350bcbcd81a58f6cfcaff45dda4ad37eba",
+    "libgdcm3.0_3.0.8-2_amd64.deb": "6c67f5e4f1bcfbdbfa7a76f45d0d846d6e53a2f39f91c2941614ff0a3d54e86c",
+    "libgdk-pixbuf-2.0-0_2.42.2+dfsg-1_amd64.deb": "2dd0745a0dde7f6afb97a8ea0a30ce266c34d4f11b023e096437a8cd862f4595",
+    "libgdk-pixbuf2.0-common_2.42.2+dfsg-1_all.deb": "61ff764860dafbd7e3fe2050b9c17db3ae109dea15ac748212eff56fdb3111e1",
+    "libgeos-3.9.0_3.9.0-1_amd64.deb": "c6190966a2410f01f14ce5265e362ba77fdf8c25f3b08b5af71ee05f8d70b09e",
+    "libgeos-c1v5_3.9.0-1_amd64.deb": "88072c56bf83ab01f97096fac8b8acdfdad7122c01dcb9fd9825ef4ac525d3fe",
+    "libgeotiff5_1.6.0-1_amd64.deb": "db80978d150545a90db7b18ab4b7681337dcc69b05ce4b7f3d719db2421cf8cd",
+    "libgfortran5_10.2.1-6_amd64.deb": "6fe41d04ea9ef8c5c684b14585caa7a4a7e04ad6805d59cdd29016960b737123",
+    "libgif7_5.1.9-2_amd64.deb": "d06bd6cb48aa985c0a62948579364fb74c9f9fc85eaf1948faa27035bde0078d",
+    "libgirepository-1.0-1_1.66.1-1+b1_amd64.deb": "787e913bf56f19bc54720c3463ab8afe1cc9442536fde31e2a36afc3939f28c9",
+    "libgl-dev_1.3.2-1_amd64.deb": "a6487873f2706bbabf9346cdb190f47f23a1464f31cecf92c363bac37c342f2f",
+    "libgl1-mesa-dri_20.3.5-1_amd64.deb": "08e8bc20077e188da7061f77d23a336782d8463c0cc112fabbfa9c8b45923fd2",
+    "libgl1_1.3.2-1_amd64.deb": "f300f9610b5f05f1ce566c4095f1bf2170e512ac5d201c40d895b8fce29dec98",
+    "libgl2ps1.4_1.4.2+dfsg1-1_amd64.deb": "c2ed4834c4406d26a0381272f60d04ec36bfe0a770b55f104785c04489106432",
+    "libglapi-mesa_20.3.5-1_amd64.deb": "aa8f8eaf13224cbb8729416be79350460f7f2230193b2da5d5e24f3dc7e9985f",
+    "libgles-dev_1.3.2-1_amd64.deb": "969e9197d8b8a36780f9b5d86f7c3066cdfef9dd7cdc3aee59a1870415c53578",
+    "libgles1_1.3.2-1_amd64.deb": "18425a2558be1de779c7c71ce780b133381f0db594a901839c6ae3d8e3f3c966",
+    "libgles2_1.3.2-1_amd64.deb": "367116f5e3b3a003a80203848b5ce1401451a67c2b2b9d6a383efc91badb0724",
+    "libglew2.1_2.1.0-4+b1_amd64.deb": "5be1139eb2f3156f64788d4beee7569e15741b9478c842165df540ecb578bbef",
+    "libglib2.0-0_2.66.8-1_amd64.deb": "995469490dcc8f667df8051a39dd5abd7149d849456c28af4e58cbfd6d6dc4f8",
+    "libglib2.0-bin_2.66.8-1_amd64.deb": "5adf4c916832ad4203fed68faacd4552361cbccc22f66f4504a7ad6fc955bddd",
+    "libglib2.0-data_2.66.8-1_all.deb": "be41a674336cefd00e2a468fe19c8bbf9f3fac86f39379e1b7acbad41f6af644",
+    "libglib2.0-dev-bin_2.66.8-1_amd64.deb": "2dbca7691d2b43545d7a89bafb4cc92a5e42c68085fa4d8989e74b1f5250f9c6",
+    "libglib2.0-dev_2.66.8-1_amd64.deb": "782fcfd549266048309b8da556377c16445bafe9f0aec31d9f246ac9b736d2aa",
+    "libglvnd0_1.3.2-1_amd64.deb": "52a4464d181949f5ed8f7e55cca67ba2739f019e93fcfa9d14e8d65efe98fffc",
+    "libglx-dev_1.3.2-1_amd64.deb": "5a50549948bc4363eab32b1083dad2165402c3628f2ee85e9a32563228cc61c1",
+    "libglx-mesa0_20.3.5-1_amd64.deb": "2d19e2addfbea965220e62f512318351f12bdfe7e180f265f00d0f2834a77833",
+    "libglx0_1.3.2-1_amd64.deb": "cb642200f7e28e6dbb4075110a0b441880eeec35c8a00a2198c59c53309e5e17",
+    "libgme0_0.6.3-2_amd64.deb": "b1885f1cbe610638da6405941abac10f1ec464ff4851c422c4a7ace30cdd259e",
+    "libgmp10_6.2.1+dfsg-1+deb11u1_amd64.deb": "fc117ccb084a98d25021f7e01e4dfedd414fa2118fdd1e27d2d801d7248aebbc",
+    "libgnutls30_3.7.1-5_amd64.deb": "20b0189b72ad4c791cf5b280c111d41ce071a04dab0e9a9d7daa9504a7a7b543",
+    "libgomp1_10.2.1-6_amd64.deb": "4530c95aefa48e33fd8cf4acbe5c4b559dbe7bdf4c56469986c83a203982cef1",
+    "libgpg-error0_1.38-2_amd64.deb": "16a507fb20cc58b5a524a0dc254a9cb1df02e1ce758a2d8abde0bc4a3c9b7c26",
+    "libgphoto2-6_2.5.27-1_amd64.deb": "af72ea52bd64c83ff9723347818c4532490961c4d48d11d3b5da8b77011e815c",
+    "libgphoto2-dev_2.5.27-1_amd64.deb": "745358ca7b96c6897e852f8213743b1aa04c24b799a319671db11c8a56a876ae",
+    "libgphoto2-port12_2.5.27-1_amd64.deb": "8b91bf19afe0523d5856b2bee76e7b9f02481d617c4729bb7adda27233d77146",
+    "libgpm2_1.20.7-8_amd64.deb": "8c6f58b2f0592fdc9d29abc979d3ff47f2c449e20c4f1b326f84165a86115c7b",
+    "libgraphite2-3_1.3.14-1_amd64.deb": "31113b9e20c89d3b923da0540d6f30535b8d14f32e5904de89e34537fa87d59a",
     "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",
+    "libgssdp-1.2-0_1.2.3-2_amd64.deb": "4ba9103e78206e8af81acd1f2ee7df72befca2080b1106ac410efbdbc5899b4d",
+    "libgstreamer-gl1.0-0_1.20.1-1~bpo11+1_amd64.deb": "5f5a53e3d69b3427d53c0375dd29c651ea37483324053d655e474c516f7fa28d",
+    "libgstreamer-opencv1.0-0_1.20.1-1~bpo11+1_amd64.deb": "98f850c9769edfb3e7258e6bd7b95cba9765ca145447862b4c148f272dcda37f",
+    "libgstreamer-plugins-bad1.0-0_1.20.1-1~bpo11+1_amd64.deb": "2de084e17216c85b9bdf63cd5c3b6f41cdb09612fa641529c217d3f519feae86",
+    "libgstreamer-plugins-bad1.0-dev_1.20.1-1~bpo11+1_amd64.deb": "ee929a14bb78e653d67eeb3156f13d09e52d22f2c25e0cdee29516a4e079fd12",
+    "libgstreamer-plugins-base1.0-0_1.20.1-1~bpo11+1_amd64.deb": "9d2cce1ceacb579a86f74fe21af570c6120b9bbe6b4a2bc242c0e05059b22512",
+    "libgstreamer-plugins-base1.0-dev_1.20.1-1~bpo11+1_amd64.deb": "abee0ed15570032446f36c5c5cd2ff92bf88a5b239d345924a8f4a5212b68f4b",
+    "libgstreamer1.0-0_1.20.1-1~bpo11+1_amd64.deb": "e3b58e3432a7987ec17b748357bb7f50318d17ef69a71d73620c77721007c1f3",
+    "libgstreamer1.0-dev_1.20.1-1~bpo11+1_amd64.deb": "e125c4f47a825385dacff179d823a3b1a478d4ad81b4cd9505539db06501b377",
+    "libgtk-3-0_3.24.24-4+deb11u2_amd64.deb": "f58fcba87f2b7cb03a0f9f174817cc2ef18cd5dcfe41129b618ec3b7d5e0f8a0",
+    "libgtk-3-common_3.24.24-4+deb11u2_all.deb": "172d01f359af8f13cee93dba183e282ea5f059f2a418dfe66d35abf9dd60ddd7",
+    "libgudev-1.0-0_234-1_amd64.deb": "9ec44c4018ed498e871eed85150e5fe557a7fae21f2b5b3d014a0c27be1eaaee",
+    "libgudev-1.0-dev_234-1_amd64.deb": "6c3638cc06781eb697a70d249e6c2b961fc2c2069c7d2d86215c4efe823b0198",
+    "libgupnp-1.2-0_1.2.4-1_amd64.deb": "04737f47c939bc47dbb2ba3ae6914c8528f2319d8275f6ff59c7565db26d4293",
+    "libgupnp-igd-1.0-4_1.2.0-1_amd64.deb": "eae9f92c0591512db92b7bf8b6b13c1084c117a11cd0ade7accce14db4b40db0",
+    "libharfbuzz0b_2.7.4-1_amd64.deb": "c76825341b5877240ff2511a376844a50ffda19d9d019ae65a5b3a97f9a1a183",
+    "libhdf4-0-alt_4.2.15-3_amd64.deb": "43d6a68b0eda21cc1493bf34147317248a35646f97d0192f8e3613287a229e32",
+    "libhdf5-103-1_1.10.6+repack-4+deb11u1_amd64.deb": "ddf76cacf5410bc1e0abe69ed73a56b3be2049f61850a419a0fd4b3479795bd0",
+    "libhdf5-hl-100_1.10.6+repack-4+deb11u1_amd64.deb": "91f0ca7be710a43e9e7a82b8fe4e28387bee103ff64412c7e690f6b27152f922",
+    "libheif1_1.11.0-1_amd64.deb": "79405f9eca217388fd97c4a64557d395f365ad40b412174cae96eb0e5d6738c8",
+    "libhogweed6_3.7.3-1_amd64.deb": "6aab2e892cdb2dfba45707601bc6c3b19aa228f70ae5841017f14c3b0ca3d22f",
+    "libicu-dev_67.1-7_amd64.deb": "7932a6acfbfd76e1dbedcf171dafda9e549b8dc179a666043dbb3d5b733c4a29",
+    "libicu67_67.1-7_amd64.deb": "2bf5c46254f527865bfd6368e1120908755fa57d83634bd7d316c9b3cfd57303",
+    "libidn2-0_2.3.0-5_amd64.deb": "cb80cd769171537bafbb4a16c12ec427065795946b3415781bc9792e92d60b59",
+    "libiec61883-0_1.2.0-4_amd64.deb": "969542c1780f350578b8ed30aaeb770eaa1c714ebdf0fef865691e24ce064ee5",
+    "libilmbase-dev_2.5.4-1_amd64.deb": "a3062a15de35e0ea661a1ccf8bdd85609e17959c495ae9d14967a026f91fa7e7",
+    "libilmbase25_2.5.4-1_amd64.deb": "54b96a6eec874273fedfcd11e2af695a91830a18b3587ac0d7a68e115a82dc2a",
+    "libinstpatch-1.0-2_1.1.6-1_amd64.deb": "47741d5f3efd902515dcf2adfb006c74698d14a91691e16527632299e810208a",
+    "libjack-jackd2-0_1.9.17~dfsg-1_amd64.deb": "2526227a109b4b078cb9483dcab20574d946f9f4bed8b4249d93f8c04d4be22c",
     "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": "c0467781913f8a59e60b63efcbf334f17058128076c1b265803d98e9e93815cd",
-    "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",
+    "libjpeg-dev_2.0.6-4_amd64.deb": "147a736e2eed59e0a2592436b28c410fd59eb18da5912925160496a8e65560e7",
+    "libjpeg62-turbo-dev_2.0.6-4_amd64.deb": "a3e7ccd1a02c147867e5bf29dd35e16246ad4def19421e73e46fee51fe487baf",
+    "libjpeg62-turbo_2.0.6-4_amd64.deb": "28de780a1605cf501c3a4ebf3e588f5110e814b208548748ab064100c32202ea",
+    "libjson-c5_0.15-2_amd64.deb": "911629a85e4f4bfd426a48e10ad8bca33511cedf1a6c96892fc8a51e04099844",
+    "libjson-glib-1.0-0_1.6.2-1_amd64.deb": "c2db69dda6ceda43065d694c5ebd515900dd38d7231a74016f10a2d2a870f01d",
+    "libjson-glib-1.0-common_1.6.2-1_all.deb": "a938ec35a20dca2e5878a8750fb44683b67a5f7c2d23d383963803a9fcfac1a3",
+    "libjson-glib-dev_1.6.2-1_amd64.deb": "a5fceefa2d7c3e3603cc3f85f5716aeb6b61f5e1d9fc9bd9530d065f2e441d40",
+    "libjsoncpp24_1.9.4-4_amd64.deb": "4e43501e9f43f9c8b28ae1480dec83b6f8ffb565acfdab2016e649caf6b9fe5a",
+    "libkate1_0.4.1-11_amd64.deb": "b23460940c4a88cd4a7430eee889a3f512b5ec89ae253361438e6f2c3569d90c",
+    "libkmlbase1_1.3.0-9_amd64.deb": "1d2a98660a142761aeb64b191bd86b5eff51ff15e05f596874c4abe00a07e9ee",
+    "libkmldom1_1.3.0-9_amd64.deb": "5ecaed0211a0fd7793abdd3963941ed25faf74ab86273a57ff90a75f9aeae7e3",
+    "libkmlengine1_1.3.0-9_amd64.deb": "ab0c0a5a4037f266e691a5faba20fa04aa7f5f82763d8dd61ae9957ef4fbc232",
+    "liblapack3_3.9.0-3_amd64.deb": "7fc4cd55ca777dbe0745bd167abebed0b5d64b5cdff8900fec2ae579859fbade",
+    "liblcms2-2_2.12~rc1-2_amd64.deb": "0608ecb6ed258814e390b52b3fb50f2a6d3239b5ecb1086292ae08be00a67b0f",
+    "libldacbt-enc2_2.0.2.3+git20200429+ed310a0-4_amd64.deb": "142f9848fc3fe8f5ace0878b4619d8b5154ed38ff630bab4ecaf9214cf9652f4",
+    "libldap-2.4-2_2.4.57+dfsg-3_amd64.deb": "4186d0d3f086202d391da49d1bb5ced6dde5eafba1dbcffef9a8e1238a7ef7c3",
+    "liblept5_1.79.0-1.1_amd64.deb": "5fb926add78b22c0290d969cec728741a88ae8e28ba18cc82e7ac0db54b25b48",
+    "liblilv-0-0_0.24.12-2_amd64.deb": "1f33d0d543971296a806e264d14b84f662c35739f6525cf607fe363498520369",
+    "libllvm11_11.0.1-2_amd64.deb": "eaff3c8dd6039af90b8b6bdbf33433e35d8c808a7aa195d0e3800ef5e61affff",
+    "libltc11_1.3.1-1_amd64.deb": "aee63cda707bfd3178a8446f8e2a46b30577df74dd2bc2642f1e9f52515a045b",
+    "libltdl7_2.4.6-15_amd64.deb": "52a0a21e06bb89038a3ab6949020228fbf9dd7897e027233cf0a8c2d111d6c10",
+    "liblz4-1_1.9.3-2_amd64.deb": "79ac6e9ca19c483f2e8effcc3401d723dd9dbb3a4ae324714de802adb21a8117",
+    "liblzma-dev_5.2.5-2_amd64.deb": "dd031326f1dfd774ac94e36bb7afdd06f6ce9b5ce3ee4e25b490ab26898fc2dd",
+    "libmariadb3_10.5.15-0+deb11u1_amd64.deb": "81e470c15a8c1fe476cc92f107a80890566af4aa7d27059cc498e4250e98c00f",
+    "libmd0_1.0.3-3_amd64.deb": "9e425b3c128b69126d95e61998e1b5ef74e862dd1fc953d91eebcc315aea62ea",
+    "libmfx1_21.1.0-1_amd64.deb": "0303163e7fbdfb8e32b6322c6a7c32c01c2d4ad2035e4201b9cf375eeb0e5ef9",
     "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",
+    "libmjpegutils-2.1-0_2.1.0+debian-6_amd64.deb": "bfc822b7393db9c94a862bc896e08678e51465d61c239e57b27c3cd7e39f75c6",
+    "libmodplug1_0.8.9.0-3_amd64.deb": "d91bf3bd1c8fa00ad5f38b4095ff500437de40ce53af8c808c69e931b814c3f2",
+    "libmount-dev_2.36.1-8+deb11u1_amd64.deb": "e2ab59f02398ff5f50d58ba5702a3dc27d47b6b028fccab03d0e8060e317f328",
+    "libmount1_2.36.1-8+deb11u1_amd64.deb": "a3d8673804f32e9716e33111714e250b6f1092770a52e21fab99d0ab4b48c5d9",
+    "libmp3lame0_3.100-3_amd64.deb": "0931247b484e5e3444a3273c96d1e8b719325950610e6a018442843a0cdf56bc",
+    "libmpcdec6_0.1~r495-2_amd64.deb": "21a2e6f591a38df89b148c1584d2ce77dd50303f5b54a9fce0182d6df3510269",
+    "libmpeg2-4_0.5.1-9_amd64.deb": "a8e18dd007804321557611d5e73d55c1502c45c63b63df81d1f64346004f9e16",
+    "libmpeg2encpp-2.1-0_2.1.0+debian-6_amd64.deb": "936d1660078fccf5af2ba6f4be790fe927909ef1d29c0fc79d3416fd27c29c71",
+    "libmpg123-0_1.26.4-1_amd64.deb": "c024421b06a7aa4ef0f817f4360ee36aab0a80546de13cbd71df7233ea14751e",
+    "libmplex2-2.1-0_2.1.0+debian-6_amd64.deb": "ae0de179ccbc07dbba895c9eb335eda7112832d9192ba96dd167202f3deae1b5",
+    "libncurses6_6.2+20201114-2_amd64.deb": "dfe45cb6ab048d1182175df55b007a4a188515c6d764a4dd5a44a0b47b6286a1",
+    "libncursesw6_6.2+20201114-2_amd64.deb": "ee3cd315dfa18865cf888ba6813a552077a4f3d1439dd225e4a0d0fee53aadc2",
+    "libnetcdf18_4.7.4-1_amd64.deb": "b25476561e7380308d6c4c7bb9e8f19dd51ee62705c2615c6aab29ecc882c8d8",
+    "libnettle8_3.7.3-1_amd64.deb": "e4f8ec31ed14518b241eb7b423ad5ed3f4a4e8ac50aae72c9fd475c569582764",
+    "libnghttp2-14_1.43.0-1_amd64.deb": "a1a8aae24ced43025c94a9cb0c0eabfb3fc070785de9ee51c9a3a4fe86f0d11e",
+    "libnice10_0.1.18-2~bpo11+1_amd64.deb": "429745caf0b8590da661bf89c45c586ee59fff4b7d84732b91f516c0449a4063",
+    "libnorm1_1.5.9+dfsg-2_amd64.deb": "a6388c8d460e86c7b76b08e82ecb09894d1d9c53b488850fb39702af85b6d159",
+    "libnspr4_4.29-1_amd64.deb": "adc6d0c181279be9f9e422d54fed41f7134eda4a352e98d028a67c2413e62e3d",
+    "libnss3_3.61-1+deb11u2_amd64.deb": "41c6ceecdbe0067fd6ff99f7f96f52599542f712a1f94173980283b3cacbe30d",
+    "libnuma1_2.0.12-1+b1_amd64.deb": "5a0d21a96ec7a5d50e0c2352ac086dde7dd9cd6018f80f2a74ec6fd4dd47b4bf",
+    "libodbc1_2.3.6-0.1+b1_amd64.deb": "a19d4e2aa8f7d692e0e37f09bd9bd098443468b76b7dbbcc7e1aee0b9eda960e",
+    "libogdi4.1_4.1.0+ds-5_amd64.deb": "315cb60027d9db71d9c15b860263cb33242af2480af6ef90c261372f5c6a5c04",
+    "libogg0_1.3.4-0.1_amd64.deb": "008a385ccb755d85893bda7d3820408c1f92439ea112130d579025cadc0f58b1",
+    "libopenal-data_1.19.1-2_all.deb": "695a650803f885459994bb921132d956c2b693759572005351a5b13773c754cd",
+    "libopenal1_1.19.1-2_amd64.deb": "b027d29d37786e4dacb21a7268e911566efb09f5a20facc66c57dd22a7c31e83",
+    "libopencore-amrnb0_0.1.5-1_amd64.deb": "798c77fb6cc44ff35d88b0113c37a01f40790a739b1b12dbd65a3c67f9b00422",
+    "libopencore-amrwb0_0.1.5-1_amd64.deb": "7a13750aed7953d7c9230236d17b8bd560260b55a63583de099062952ccd8b1a",
+    "libopencv-calib3d-dev_4.5.1+dfsg-5_amd64.deb": "9ed0faf68e3c91e640139ecb7729b8b186bd31f80dcf64612dd769a1cee79957",
+    "libopencv-calib3d4.5_4.5.1+dfsg-5_amd64.deb": "7631eb7412ac953c1a4f31fb2254ef6111bf07a55f8d59d11794d8e31164afac",
+    "libopencv-contrib-dev_4.5.1+dfsg-5_amd64.deb": "66dd52c8fd36fa91afc2bdaa4cfa4403788dd7a40536abe44ed50d120063f3c0",
+    "libopencv-contrib4.5_4.5.1+dfsg-5_amd64.deb": "3148a23d69669b63ef6340cd705eb0caf943541f8687ce8cdb1984ec53f980bc",
+    "libopencv-core-dev_4.5.1+dfsg-5_amd64.deb": "cfefbd5ab57bdd5481bcca3e74a9e8ef699a8feb14e7d1087c8163cdaeb3fe1e",
+    "libopencv-core4.5_4.5.1+dfsg-5_amd64.deb": "2fc53f145d40c0e209f31608f0bca7d358f372be80289e0a2b7e777474e2f714",
+    "libopencv-dev_4.5.1+dfsg-5_amd64.deb": "9b9912b39cea7fc43bfd4a6dbe50769a6d2b6d8378e0ac8a48b90f756436b647",
+    "libopencv-dnn-dev_4.5.1+dfsg-5_amd64.deb": "3861eb39bf7a8cb186a9cd99889a160787d6f342e165c45bcab08f7f5101444f",
+    "libopencv-dnn4.5_4.5.1+dfsg-5_amd64.deb": "741fc61ab0858c50362c0f93f700c3facc350c06362a95780bb772a435f862b0",
+    "libopencv-features2d-dev_4.5.1+dfsg-5_amd64.deb": "7bd49185d9edc346b5331e6651fa4a12eb7066d4d632e28a93b26d63aa263560",
+    "libopencv-features2d4.5_4.5.1+dfsg-5_amd64.deb": "9f34ce569249905b596fc0a6cefca51453abd483ff52b8756170b4e31f7cd129",
+    "libopencv-flann-dev_4.5.1+dfsg-5_amd64.deb": "1a1611a9564472d92aefcdad3f4b1bd6cbe1853fe6206a8c39fecbb95defe02b",
+    "libopencv-flann4.5_4.5.1+dfsg-5_amd64.deb": "46a2ac744124fdee8d7e749fa465ea780ccd155bfb04e6c590bf09e2e6396728",
+    "libopencv-highgui-dev_4.5.1+dfsg-5_amd64.deb": "ec710ccae408b9087ae88591ea525c032bfb51af61c47d065a3b2ad1d11e6ee8",
+    "libopencv-highgui4.5_4.5.1+dfsg-5_amd64.deb": "2f0b59baa96ecc9368ca729016d876d5b7d377b511caa021fdb27e00df47b29c",
+    "libopencv-imgcodecs-dev_4.5.1+dfsg-5_amd64.deb": "a276492fb4e20eaab343bf84c70bd1e5bfbefc7e47fcdedcdbe6718892715dac",
+    "libopencv-imgcodecs4.5_4.5.1+dfsg-5_amd64.deb": "b5bde49837bdb1faca8764cf679e45807eed4126e65d92a5391c2d376359c6f3",
+    "libopencv-imgproc-dev_4.5.1+dfsg-5_amd64.deb": "9e2f780fa3d9b6fef898a19e82fbfdafe462286d2e6f698cbd66c7545d5ede87",
+    "libopencv-imgproc4.5_4.5.1+dfsg-5_amd64.deb": "58c93a50b180f8b8935d128438434a0e9407228e6181c4e7312402a486f0ba0a",
+    "libopencv-ml-dev_4.5.1+dfsg-5_amd64.deb": "413b744bfd48e7b945c36494e5aebdd81dcf2b1bea1ef94d918b8a308acb2c40",
+    "libopencv-ml4.5_4.5.1+dfsg-5_amd64.deb": "752861a15059f11a6c9c5d999e4ca00044e66c25bedb4ae7705af12700d885cc",
+    "libopencv-objdetect-dev_4.5.1+dfsg-5_amd64.deb": "a04d554cf783f54ef7cd86651db1411d1fc7c301b9e34b574393d4067b7656f9",
+    "libopencv-objdetect4.5_4.5.1+dfsg-5_amd64.deb": "cef019b21b06240137ef3c529b88c8c2bf0c0e2d4bd18afde1dfd622c8f60135",
+    "libopencv-photo-dev_4.5.1+dfsg-5_amd64.deb": "b565882f3b302d3183cf2d4e4282af963fd08dc9a4538f030b11cbfd23602e3e",
+    "libopencv-photo4.5_4.5.1+dfsg-5_amd64.deb": "b1f9ceed287fc233cac0bd5b219158c699d629a166a1153af25836ec2ed6d7c0",
+    "libopencv-shape-dev_4.5.1+dfsg-5_amd64.deb": "6c1d629d032dfb67d93da236433c1b9979aaf4ecada31d9a2ca38b78e2bf1de9",
+    "libopencv-shape4.5_4.5.1+dfsg-5_amd64.deb": "09ecbf7eaede648828313fb91d21621da2500a45ca9d4a1123be0e9137a317cf",
+    "libopencv-stitching-dev_4.5.1+dfsg-5_amd64.deb": "8524640ba6c94451b0ae20ff566272696c7da5ee150b779ea1002195d7fa77e7",
+    "libopencv-stitching4.5_4.5.1+dfsg-5_amd64.deb": "14134ea9c6edeb648f4aeca3b7920bfa0915b698554bbf227298ed2ee114c704",
+    "libopencv-superres-dev_4.5.1+dfsg-5_amd64.deb": "f7c830ad372c4df8ea546240a9dcb699e41e6e8555f97abbd705e8e46c01a255",
+    "libopencv-superres4.5_4.5.1+dfsg-5_amd64.deb": "cbe4d6331ee43427af5b3c6e9a9d34277cafae7f83759a7b073c88c0728ec2b3",
+    "libopencv-video-dev_4.5.1+dfsg-5_amd64.deb": "f0ec790005a4fa5bcb973b1640f37db373b39e07da2c041fa8a5953ce9175101",
+    "libopencv-video4.5_4.5.1+dfsg-5_amd64.deb": "dc83934e861d0feb4ee3b0d8a0ba058460eb56a80cc6ecb8a99e4bb2eb90cfb1",
+    "libopencv-videoio-dev_4.5.1+dfsg-5_amd64.deb": "4bd9fed0835f2c39f237242ceb48947339310bf78ef148df7330dee01d1fb6af",
+    "libopencv-videoio4.5_4.5.1+dfsg-5_amd64.deb": "2b0124d162ad10202dcbe7d9b714aeb7b9d47b04c434b14b38de44000d86f2f3",
+    "libopencv-videostab-dev_4.5.1+dfsg-5_amd64.deb": "90bd803824091f6d4843288fd057976e66c6d4faffc4ce96f3862995bf882822",
+    "libopencv-videostab4.5_4.5.1+dfsg-5_amd64.deb": "0e81fcbe8e786f17d242803b38f0e23c88ef20ab54b9e376661a86c71c557097",
+    "libopencv-viz-dev_4.5.1+dfsg-5_amd64.deb": "065a1b6646bd330a0eb3b3e58be5be205f1e046bc1f24c846d053c5e889e3beb",
+    "libopencv-viz4.5_4.5.1+dfsg-5_amd64.deb": "acc4812feb90115babee514ff417b9eca0258672163035c3b76e8c51b06ebed2",
+    "libopenexr-dev_2.5.4-2_amd64.deb": "bd730c004fb4c8433f88868e18fb2993e2bf359e410d3baff06de013ca725163",
+    "libopenexr25_2.5.4-2_amd64.deb": "ec97b36c66a060b987d75cb26f09e23cb5e4135e9d1be188f8f09a5d60b87902",
+    "libopengl0_1.3.2-1_amd64.deb": "4327a9f20b88e7bcb07af3b196121096877331b61eeed64467854eb0b525fc43",
+    "libopenh264-6_2.2.0+dfsg-2~bpo11+1_amd64.deb": "1bc69644a6cf305f1b9b447068983fec8200ba4bded28ed323c22edbb7fd0849",
+    "libopenjp2-7_2.4.0-3_amd64.deb": "f99e76456459aa19ac5f610096c7054994130597931abf660b82436c477ff03e",
+    "libopenmpt0_0.4.11-1_amd64.deb": "caa0c618ace54c14980b14d9dc8b5600313ba985e465a64acc59fd1ff5236901",
+    "libopenni2-0_2.2.0.33+dfsg-15_amd64.deb": "2875424fd7510c8cb5eae8dd5f4435b2dcdaef631fbd03973f71fa56fccba959",
+    "libopus0_1.3.1-0.1_amd64.deb": "f8249b9e88e01a2f8945ff3082d488b0b9470b25a1976ab409ed9aff118f9b6c",
+    "liborc-0.4-0_0.4.32-1_amd64.deb": "b1c7560723b12e498958a2af81d6df7f06c7b20d46ac191c2c4330cb6ce5483f",
+    "liborc-0.4-dev-bin_0.4.32-1_amd64.deb": "2069fdc8eb5b3e856695ef40f7d3cd402f4e00841735127f4390e9a3f58dd4d4",
+    "liborc-0.4-dev_0.4.32-1_amd64.deb": "75051ceec695ca956e2f6feecdf5d0df22db9f9390edce118516fbf8e077f612",
+    "libp11-kit0_0.23.22-1_amd64.deb": "bfef5f31ee1c730e56e16bb62cc5ff8372185106c75bf1ed1756c96703019457",
+    "libpango-1.0-0_1.46.2-3_amd64.deb": "cfb3079a7397cc7d50eabe28ea70ce15ba371c84efafd8f8529ee047e667f523",
+    "libpangocairo-1.0-0_1.46.2-3_amd64.deb": "f0489372e4bcb153d750934eb3cddd9104bc3a46d564aa10bef320ba89681d37",
+    "libpangoft2-1.0-0_1.46.2-3_amd64.deb": "78067d7222459902e22da6b4c1ab8ee84940752d25a5f3dea1a43f846a8562e3",
+    "libpciaccess0_0.16-1_amd64.deb": "f581ced157bd475477337860e7e7fcabeeb091444bc5a189c5c97adc8fcabda5",
+    "libpcre16-3_8.39-13_amd64.deb": "04ef146b0119a8a5ab1df09d990bd61a45bf99d2989aa248ebc7f72dbb99544e",
+    "libpcre2-16-0_10.36-2_amd64.deb": "720aa56730b7916680ce2859dbdaa722aa519859b0697d78b34e5c57ee6293c2",
+    "libpcre2-32-0_10.36-2_amd64.deb": "89558554df9e374de506d8372341e1a45a0d6ea8413dc2e49d5d357e571555ee",
+    "libpcre2-dev_10.36-2_amd64.deb": "75de539e873d7c58805ab38a4e17a7fb434abde8beadbe6fe4b8e477e84d68e5",
+    "libpcre2-posix2_10.36-2_amd64.deb": "179664cb063e1761fc8ebe04f8a02f17be22b79b1bdcf66404c3ee35b3884d09",
+    "libpcre3-dev_8.39-13_amd64.deb": "e588a2bd07e2770ad2fa9e3b02e359d3ff3c6f0c17a809365d3e97da7b0e64e0",
+    "libpcre32-3_8.39-13_amd64.deb": "961135f3ff2d00c2e46640b9730d9ddef80ae9d9037e2ec882ee8f6ce5dd48c9",
+    "libpcre3_8.39-13_amd64.deb": "48efcf2348967c211cd9408539edf7ec3fa9d800b33041f6511ccaecc1ffa9d0",
+    "libpcrecpp0v5_8.39-13_amd64.deb": "79e15b8d31f8561ad1c19f8c280d0a9fe280f7872701ef53c9bdfce6b3015a18",
+    "libpgm-5.3-0_5.3.128~dfsg-2_amd64.deb": "3f124acd98fb6d9d78dff583061736bcb738d102f3bd1e0afca4c0f0435534af",
+    "libpixman-1-0_0.40.0-1_amd64.deb": "55236a7d4b9db107eb480ac56b3aa786572ea577ba34323baf46aceb7ba6d012",
+    "libpng-dev_1.6.37-3_amd64.deb": "7be93d99bdab4fd3e230f67ad17739fdfa2bb1fb94ddd84f670e442ffbcabf39",
+    "libpng16-16_1.6.37-3_amd64.deb": "7d5336af395d1f658d0e66d74d0e1f4c632028750e7e04314d1a650e0317f3d6",
+    "libpoppler102_20.09.0-3.1_amd64.deb": "23918f0727b651b1b9346951f2e703a6c6ee69277def309bf0a9f0fb30c5ec1e",
+    "libpq5_13.5-0+deb11u1_amd64.deb": "0bfa1dc24e1275963961efdcc6d2ff4d2eec390d7acd5a6aee3162569ae1886c",
+    "libproj19_7.2.1-1_amd64.deb": "34b3b285f42d89e94e6315ae572ee9bdcb23278538d73b5c5f13526a8da77eae",
+    "libprotobuf23_3.12.4-1_amd64.deb": "c0eddff6bdee79086a2ffa74ed5949e22ff383757520433e70cadb7fcf34e5a5",
+    "libproxy1v5_0.4.17-1_amd64.deb": "b21c1524b972dd72387ecb8b12c0a860738ce0832ed18fe7ffb9da6adc9b9e41",
+    "libpsl-dev_0.21.0-1.2_amd64.deb": "5244ef677e55f85b6ef84ad152f3d2acb29899a98639005aa227290e973db4d0",
+    "libpsl5_0.21.0-1.2_amd64.deb": "d716f5b4346ec85bb728f4530abeb1da4a79f696c72d7f774c59ba127c202fa7",
     "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",
+    "libpulse0_14.2-2_amd64.deb": "f2d2ed93c03cff7cf081d401d855d67e1f73b65dec02687d309e0d72e7d1159d",
+    "libqhull8.0_2020.2-3_amd64.deb": "d30aa8231afdf7997f57a7c28be25868f1f60ea01c7bdb1990e030514a74b9a5",
+    "libqrencode4_4.1.1-1_amd64.deb": "80b901703c5347798e2d87f1275aed1e5375ebc3618ec7fe43b34f7d9490ac64",
+    "libquadmath0_10.2.1-6_amd64.deb": "a9a5e1f53b7e27a3f2b8388929bb622d3c6c35a4e42ac166697444e5ed662fd5",
+    "librabbitmq4_0.10.0-1_amd64.deb": "2c91f91ead5534cda268350a816a64e656fd6fb9d2b658cb3c23ae0424cffa2f",
+    "libraw1394-11_2.1.2-2_amd64.deb": "d8cb92f085d3b32ca23e31b2bf45f66d678f585fef8f8b85510bd41b8ff966ee",
+    "libraw1394-dev_2.1.2-2_amd64.deb": "6cbcde30fa362e8a9e98749e8231c60d3ec561f21e7670d13989073e2e628c98",
+    "libreadline8_8.1-1_amd64.deb": "162ba9fdcde81b5502953ed4d84b24e8ad4e380bbd02990ab1a0e3edffca3c22",
+    "librest-0.7-0_0.8.1-1.1_amd64.deb": "5cd57a96145a362bf60428315ab3fc6c2f528ab38a06a905da2568575c23bdc8",
+    "librsvg2-2_2.50.3+dfsg-1_amd64.deb": "c5f6cdb66683d9b8cd23f0e02e6adb29d43bdca301872842fa98d44e23fa1091",
+    "librtmp1_2.4+20151223.gitfa8646d.1-2+b2_amd64.deb": "e1f69020dc2c466e421ec6a58406b643be8b5c382abf0f8989011c1d3df91c87",
+    "librttopo1_1.1.0-2_amd64.deb": "ce14f3a8a4451398302b2df9fe2fa77df8c4f8df8bee125a52cecccbfbd48960",
+    "libsamplerate0_0.2.1+ds0-1_amd64.deb": "be57259ee0a160557a9492ca001c6cfbd816af661409673ef431465a728bb746",
+    "libsasl2-2_2.1.27+dfsg-2.1+deb11u1_amd64.deb": "2e86ab7a3329aad4b7350a9b067fe8f80b680302f2f82d94f73f9bf075404460",
+    "libsasl2-modules-db_2.1.27+dfsg-2.1+deb11u1_amd64.deb": "122bf3de4ca0ec873bc35bdde1f21ec9d91ace4f5245c3b1240e077f866e1ae9",
+    "libsbc1_1.5-3_amd64.deb": "3ec64d259335cf5582c30fc6e00b492433184d7b4d46b1840578acebf5e005d3",
+    "libsdl2-2.0-0_2.0.14+dfsg2-3_amd64.deb": "42ad145ae733550489913fd68ccafbce85ca68571d7b36e562923c2d37b89cb0",
+    "libselinux1-dev_3.1-3_amd64.deb": "16b14d7e8ed88b9b07d1b52d84d04ab2fcdfcdc4b8cecc9dd34df06f3ce7d3fb",
+    "libsensors-config_3.6.0-7_all.deb": "4265811140a591d27c99d026b63707d8235d98c73d7543c66ab9ec73c28523fc",
+    "libsensors5_3.6.0-7_amd64.deb": "b9cb9a081ea3c9b68ef047d7e51f3b84bccde1a2467d5657df4c5d54775b187e",
+    "libsepol1-dev_3.1-1_amd64.deb": "1bec586de489db87c8746a6eeed27982915fc578c91e9e78ef39773ab824e023",
+    "libsepol1_3.1-1_amd64.deb": "b6057dc6806a6dfaef74b09d84d1f18716d7a6d2f1da30520cef555210c6af62",
+    "libserd-0-0_0.30.10-2_amd64.deb": "fb48a540070b8b044f78a787d8c65d8b85540cf8f165223e08d8554a8b9aba79",
     "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",
+    "libshout3_2.4.5-1+b1_amd64.deb": "c0e76f97bbd4a4145cbc77f2cea7de044ee8b8aa176c8a9d838cf6cbfec4d092",
+    "libsidplay1v5_1.36.60-1_amd64.deb": "e34cbbde2ee6eb6db75a58250f3ad4df2da14386357d47b95f14e6276327525a",
+    "libslang2_2.3.2-5_amd64.deb": "107ad70aba3dc4dab2bc0fe11c3dd7c2afe9549dd45f4da3f4cf9d360e171eba",
+    "libsnappy1v5_1.1.8-1_amd64.deb": "7e34c4e1e3b85c51ed302af50d30d6ef88889d87e310d50dde2ad31dbae3f606",
+    "libsndfile1_1.0.31-2_amd64.deb": "2d703696ec4673051f0f566417871482787749720773e3ac2d909b4a1b937e02",
+    "libsndio7.0_1.5.0-3_amd64.deb": "af3b9425fca1abad25be51774fec175e75f1b92f7a1909e9c27bbbf644c23550",
+    "libsocket++1_1.12.13-11_amd64.deb": "0acd83b0ceacca57aa15e8f95df9f4e591071a8408cdac77a69fddb8e956df60",
+    "libsodium23_1.0.18-1_amd64.deb": "f72e5b1e3a716154c284d98969bb698701daa30b02d755a78d10d460c582d48b",
+    "libsord-0-0_0.16.8-2_amd64.deb": "424f5ac84de7e071e85fd49b9b87c6672acd6eaa778ce26e4f3bf086d6a40dd6",
+    "libsoundtouch1_2.2+ds1-2_amd64.deb": "4387b20aa2a3b514d0e2a0d7465fb4599b1c2455b1637ab5beec17528e152906",
+    "libsoup-gnome2.4-1_2.72.0-2_amd64.deb": "7fdc774b567e3a5e0881aa01fcfcac637fdeeb8ea6233b710571e1f5b3a994b6",
+    "libsoup2.4-1_2.72.0-2_amd64.deb": "32dad5305be0faa619df36688a20d187ba915f02e9e184cc5c3c6e3d98259e9c",
+    "libsoup2.4-dev_2.72.0-2_amd64.deb": "6d091b91beb2290c1d9a5272aaf006c96e08bee1b29dd96392da8d37fd4af32b",
+    "libsoxr0_0.1.3-4_amd64.deb": "73a9f1a290da86562333430517c3f44282f17fcbeefcc2ba7f8abbc2e686f6f3",
+    "libspandsp2_0.0.6+dfsg-2_amd64.deb": "67a140af59ca7f3d75d2a687602bb9e9446cd482ff6b8de9dfcedbe50b1aa119",
+    "libspatialite7_5.0.1-2_amd64.deb": "d7ad246c34ead53e167d6c6eb831b6f4ffe3764c5c43bc629d3390d59f924f03",
+    "libspeex1_1.2~rc1.2-1.1_amd64.deb": "1e5eafc996486d89891897ac3bf0b947dacb5c211514222439b74bcaab6b95ea",
+    "libsqlite3-0_3.34.1-3_amd64.deb": "a0b8d3acf4a0483048637637d269be93af48d5c16f6f139f53edd13384ad4686",
+    "libsqlite3-dev_3.34.1-3_amd64.deb": "1880f26535eb1f7325017fb16bba3f1b9e54d74f3980fca0ec2ddda15acb1915",
+    "libsratom-0-0_0.6.8-1_amd64.deb": "1b4bf67b2c0f2f0df4e1c63f10fe480e069c099ee9479036a4e10bca87911ea2",
+    "libsrt1.4-gnutls_1.4.2-1.3_amd64.deb": "e146115bfe15d58ff00f376b2a8252f2fff8d6dcad060b08fb6346a2653ad800",
+    "libsrtp2-1_2.3.0-5_amd64.deb": "edeb4792a2030b810c116a338aa3b1127b07626b26ca6b1fcd3326273aee7f1e",
+    "libssh-gcrypt-4_0.9.5-1+deb11u1_amd64.deb": "47f4011e5220f319cf5c0fde69d7b466afac1be7c8030dc10fad9b147af6973f",
+    "libssh2-1_1.9.0-2_amd64.deb": "f730fe45716a206003597819ececeeffe0fff754bdbbd0105425a177aa20a2de",
+    "libstdc++6_10.2.1-6_amd64.deb": "5c155c58935870bf3b4bfe769116841c0d286a74f59eccfd5645693ac23f06b1",
+    "libsuperlu5_5.2.2+dfsg1-2_amd64.deb": "9f91a68dc8221cd67b7af765b44fb52401a0dc7609f5c4b926afb6362e475366",
+    "libswresample-dev_4.3.3-0+deb11u1_amd64.deb": "089954a478d194917bf648fc685c5ece35846f8c85eea14ead99a82bc1f538da",
+    "libswresample3_4.3.3-0+deb11u1_amd64.deb": "d687ba1d94fb4eb89e52f92761785d4bfe165e1a9e2d62b891e0644df3b8d73c",
+    "libswscale-dev_4.3.3-0+deb11u1_amd64.deb": "584607defe497fe6f6874419f430028d54392cd23a1ec68853dabb9d7b98e734",
+    "libswscale5_4.3.3-0+deb11u1_amd64.deb": "47abd5f1dfb0e633f2d6f343ad4dfc881f20c9548041e9878f701437a6349156",
+    "libsystemd0_247.3-7_amd64.deb": "5d7d8ee6a4e2f1d48fab1e63a8c6b515a9189936ea162c7a4b0bd5cd09cfd157",
+    "libsz2_1.0.4-1_amd64.deb": "4e833c185bf02d75d013f888677d65afa25c5b84769736860d53f9dbb0f349a2",
+    "libtag1v5-vanilla_1.11.1+dfsg.1-3_amd64.deb": "b53b12cc59f86e4b3ae4bdce2b1620858ce9c450769b7d303df6ef9c5d971570",
+    "libtag1v5_1.11.1+dfsg.1-3_amd64.deb": "4abe55761b4fce387ae27b9b3ebcb43474f85c85f438b08a953b1b84799edd28",
+    "libtasn1-6_4.16.0-2_amd64.deb": "fd7a200100298c2556e67bdc1a5faf5cf21c3136fa47f381d7e9769233ee88a1",
+    "libtbb-dev_2020.3-1_amd64.deb": "8c3f7e2406807c93c197f19d18f5ea92d1a11a7f24c523ebfd5a0a6f3273440f",
+    "libtbb2_2020.3-1_amd64.deb": "87ac7778820cbd7a2c7485f7fccd2ac4437b39cbb390e49b3aeec38a826f5af9",
+    "libtcl8.6_8.6.11+dfsg-1_amd64.deb": "785df3d81010a67ded4a2c216c7b99657c6ab3d1ba7369119894abc851e5bb0c",
+    "libtesseract4_4.1.1-2.1_amd64.deb": "7f2b8ccac9446e4cb2bff265e1b36e513a0978b62fb7687de0e61da14135b6e4",
+    "libthai-data_0.1.28-3_all.deb": "64750cb822e54627a25b5a00cde06e233b5dea28571690215f672af97937f01b",
+    "libthai0_0.1.28-3_amd64.deb": "446e2b6e8e8a0f5f6c0de0a40c2aa4e1c2cf806efc450c37f5358c7ff1092d6a",
     "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",
+    "libtiff-dev_4.2.0-1+deb11u1_amd64.deb": "9936e0503ad418bab17e59dcf67a39c752a3d489b2f3d228b59e07fdcd4860e1",
+    "libtiff5_4.2.0-1+deb11u1_amd64.deb": "b22d25e14421a36c4c3b721c04c6312d79ccd91c9a0e2291f58e36b8d4a07fbb",
+    "libtiffxx5_4.2.0-1+deb11u1_amd64.deb": "bf54e53c47a81c51068b03fee8a53c6bd53a6c115218c34904fe69c4f725c9e0",
+    "libtinfo6_6.2+20201114-2_amd64.deb": "aeaf942c71ecc0ed081efdead1a1de304dcd513a9fc06791f26992e76986597b",
+    "libtk8.6_8.6.11-2_amd64.deb": "20d70721a5d539266a8736800378398d088419b986b5313ca811203284690f12",
+    "libtwolame0_0.4.0-2_amd64.deb": "279802de79d682a671cdb11295abb5cd2d21da5bee6ceefa44324c70a29365b1",
+    "libudev1_247.3-7_amd64.deb": "379ba45ef8b884f686ac0af435fb307137a86e39ee816ae2516f14ff60332488",
+    "libudfread0_1.1.1-1_amd64.deb": "0bd73adf50441a403de1301ce8b2335a315c5dbc6b84893e5d8a79f71ffc49a1",
+    "libunistring2_0.9.10-4_amd64.deb": "654433ad02d3a8b05c1683c6c29a224500bf343039c34dcec4e5e9515345e3d4",
+    "libunwind-dev_1.3.2-2_amd64.deb": "44cdc2b873368b465905cdb06aecf00d5f6d4c4d363718535171f2232110d364",
+    "libunwind8_1.3.2-2_amd64.deb": "a8cc1181a479375aeb603cfe748cc19dc3a700a47ffdcb09fa025fe02b0c73bf",
+    "liburiparser1_0.9.4+dfsg-1+deb11u1_amd64.deb": "668403c9537b40280902de700099cc57938edd75a531ed2ac0d59d3c11325622",
+    "libusb-1.0-0_1.0.24-3_amd64.deb": "946bf6ecad3cec1275fb5a5bedd5cb50676e55d5f5cfb6e28d756442d4601c41",
+    "libuuid1_2.36.1-8+deb11u1_amd64.deb": "31250af4dd3b7d1519326a9a6764d1466a93d8f498cf6545058761ebc38b2823",
+    "libv4l-0_1.20.0-2_amd64.deb": "58c600f7b22c46e3e845127a9654fd74a47f11e67e53513c018dfedd07995c19",
+    "libv4lconvert0_1.20.0-2_amd64.deb": "691106607bf5e52ac8cb54b74cb52e39a4f19a1a909c866bb661cd01f74cdff3",
+    "libva-drm2_2.10.0-1_amd64.deb": "b705c4186947d6c90682d5fcf777ba267d93058b1b50b541c29f8421f14782f5",
+    "libva-x11-2_2.10.0-1_amd64.deb": "a51c8da767b49d78cd037b9da72eb26aff1f5a9cc2bc91b5eca4931d8d5649b0",
+    "libva2_2.10.0-1_amd64.deb": "d0ffc213203186c84facb57463fe120a12d75c5a2fc360a469bd42c8c349f53a",
+    "libvdpau1_1.4-3_amd64.deb": "056cf72eb36a22f462ce99caa7a7a97e1b5e54af23478192d25d4b007e8056c9",
+    "libvisual-0.4-0_0.4.0-17_amd64.deb": "083be3fb1dae77f7b464b0c6d742e090b82e50dc3fac73cdf241506dc85f5d75",
+    "libvo-aacenc0_0.1.3-2_amd64.deb": "58677862f52f5246ed2f77c0329552b5a2ee714989044a1e1790cb373d0fa90b",
+    "libvo-amrwbenc0_0.1.3-2_amd64.deb": "815f2faad7f72ae6e405a7a33974a545babd194633263a7e63e02bbadff162ad",
+    "libvorbis0a_1.3.7-1_amd64.deb": "01c14f9d1109650077a4c5c337c285476a6932d4d448f5a190c2b845724dbf21",
+    "libvorbisenc2_1.3.7-1_amd64.deb": "75dd4d6f904c7db82f5112e60d8efea9e81dfedf94e970c5da7af2b3d81643c0",
+    "libvorbisfile3_1.3.7-1_amd64.deb": "68f84034ae1c9fa0ab6a45f5b6c406a50faae4421f0d78874260aefa2df68efc",
+    "libvpx6_1.9.0-1_amd64.deb": "c2466114b2eb1a6db926f0f530f335886ad824c6add69d01fb79373c4cdd4c08",
+    "libvtk9_9.0.1+dfsg1-8_amd64.deb": "4ad702dead9093501df3efa58a138c7e23b807614f11eb8b6bffc11d1f50142f",
+    "libvulkan1_1.2.162.0-1_amd64.deb": "8b3a6e5db7d8bdc369a0d276bfae1551ffc0fa31dbd193d56655c8f553868361",
+    "libwavpack1_5.4.0-1_amd64.deb": "51e52731f4a403bd7c11d93bc9c1ded3c02772cd0972c85641f3c555c215c400",
+    "libwayland-bin_1.18.0-2~exp1.1_amd64.deb": "774e97053d524549044b332469d13eec70c989b4bc00a592019512c17a92978e",
+    "libwayland-client0_1.18.0-2~exp1.1_amd64.deb": "4baf16bb3a35823251453368ee078b6be6a14f97b05c19783b5acd4232a608ea",
+    "libwayland-cursor0_1.18.0-2~exp1.1_amd64.deb": "1b48d1d8e17a95b28a2876c7f2a95667ee1618a5f586d4dff05aeb09488172cb",
+    "libwayland-dev_1.18.0-2~exp1.1_amd64.deb": "3265bf05c0cea760d0e8f5fb5fc68b0f154911de23503e02232dfa59f6b6490c",
+    "libwayland-egl1_1.18.0-2~exp1.1_amd64.deb": "b98e636f08eca9e818e326fc8cd75810dbb50b1ed4e3586c2394e11248e29275",
+    "libwayland-server0_1.18.0-2~exp1.1_amd64.deb": "1df9a6e304bdaebdd53e1044c6eadcda95c914119e9426c2866eaa619a49c85b",
+    "libwebp6_0.6.1-2.1_amd64.deb": "52bfd0f8d3a1bbd2c25fcd72fab857d0f24aea35874af68e057dde869ae3902c",
+    "libwebpmux3_0.6.1-2.1_amd64.deb": "78486e53903cbf422dfe04a33e5481c56c82198a2bfa307f2066e616477395f5",
+    "libwebrtc-audio-processing1_0.3-1+b1_amd64.deb": "1f7a097b1d26859acbec99f94d251ddbf825e73169d4d064e95b257ad39570e9",
+    "libwildmidi2_0.4.3-1_amd64.deb": "b6d4c8f96f81d0887241e3fc9fedc46cd0c16b486a0d08ff680b1b5d1cc4f72b",
+    "libwrap0_7.6.q-31_amd64.deb": "c6aa9c653857d807cff31682b5158722e8b16eeb3cec443d34d6eba52312e701",
+    "libx11-6_1.7.2-1_amd64.deb": "086bd667fc07369472a923da015d182bb0c15a72228a5c0e6ddbcbeaab70acd2",
+    "libx11-data_1.7.2-1_all.deb": "049b7eabced516acfdf44a5e81c26d108b16e4987e5d7604ea53eaade74027fb",
+    "libx11-dev_1.7.2-1_amd64.deb": "11e5f9dcded1a1226b3ee02847b86edce525240367b3989274a891a43dc49f5f",
+    "libx11-xcb-dev_1.7.2-1_amd64.deb": "80a2413ace2a0a073f2472059b9e589737cbf8a336fb6862684a5811bf640aa3",
+    "libx11-xcb1_1.7.2-1_amd64.deb": "1f9f2dbe7744a2bb7f855d819f43167df095fe7d5291546bec12865aed045e0c",
+    "libx264-160_0.160.3011+gitcde9a93-2.1_amd64.deb": "12c940c9755d26f73c225641f4d28aadcf55aeae211e933972e00ebfd42c700d",
+    "libx265-192_3.4-2_amd64.deb": "3ebd02002d226aef70e614676774f0c0828d21b22c0743af3b277865e05fcfe7",
+    "libxau-dev_1.0.9-1_amd64.deb": "d1a7f5d484e0879b3b2e8d512894744505e53d078712ce65903fef2ecfd824bb",
+    "libxau6_1.0.9-1_amd64.deb": "679db1c4579ec7c61079adeaae8528adeb2e4bf5465baa6c56233b995d714750",
+    "libxcb-dri2-0_1.14-3_amd64.deb": "fbfc7d55fa00ab7068d015c185363370215c857ac9484d7020c2d9c38c8401b2",
+    "libxcb-dri3-0_1.14-3_amd64.deb": "4dd503b321253f210fe546aae8fe5061fc7d30015cf5580d7843432a71ebc772",
+    "libxcb-glx0_1.14-3_amd64.deb": "61ae35a71148038aad04b021b3adfa0dee4fc06d98e045ec9edfd9e850324876",
+    "libxcb-present0_1.14-3_amd64.deb": "7937af87426de2ed382ba0d6204fee58f4028b332625e2727ebb7ca9a1b32028",
+    "libxcb-render0_1.14-3_amd64.deb": "3d653df34e5cd35a78a9aff1d90c18ec0200e5574e27bc779315b855bea2ecc0",
+    "libxcb-shm0_1.14-3_amd64.deb": "0751b48b1c637b5b0cb080159c29b8dd83af8ec771a21c8cc26d180aaab0d351",
+    "libxcb-sync1_1.14-3_amd64.deb": "53e7f18c8a95b2be2024537a753b6bd914af5f4c7aeed175f61155a5a3c8fe88",
+    "libxcb-xfixes0_1.14-3_amd64.deb": "939b29a4eaad5972ba379c2b5f29cf51d7d947b10e68cc2fe96238efcd3d63c2",
+    "libxcb1-dev_1.14-3_amd64.deb": "b75544f334c8963b8b7b0e8a88f8a7cde95a714dddbcda076d4beb669a961b58",
+    "libxcb1_1.14-3_amd64.deb": "d5e0f047ed766f45eb7473947b70f9e8fddbe45ef22ecfd92ab712c0671a93ac",
+    "libxcomposite1_0.4.5-1_amd64.deb": "4c26ebf519d2ebc22fc1416dee45e12c4c4ef68aa9b2ed890356830df42b652a",
+    "libxcursor1_1.2.0-2_amd64.deb": "d9fee761e4c50572c3ce3c3965b70fcfecd277d0d7d598e102134d12757a3d11",
+    "libxdamage1_1.1.5-2_amd64.deb": "1acf6d6117929a7df346d355caeb579798d75feb7e3b3aae58a2d1af735b444f",
     "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",
+    "libxerces-c3.2_3.2.3+debian-3_amd64.deb": "82aedc43835e2b7da8e12fe12a8a53e0c66c422e3881dcf8c2edcf2e4e8c658e",
+    "libxext6_1.3.3-1.1_amd64.deb": "dc1ff8a2b60c7dd3c8917ffb9aa65ee6cda52648d9150608683c47319d1c0c8c",
+    "libxfixes3_5.0.3-2_amd64.deb": "58622d0d65c3535bd724c4da62ae7acb71e0e8f527bcbd65daf8c97e6f0ef843",
     "libxft2_2.3.2-2_amd64.deb": "cd71384b4d511cba69bcee29af326943c7ca12450765f44c40d246608c779aad",
-    "libxi6_1.7.9-1_amd64.deb": "fe26733adf2025f184bf904caf088a5d3f6aa29a8863b616af9cafaad85b1237",
+    "libxi6_1.7.10-1_amd64.deb": "4d583f43b5396ca5434100a7274613e9983357d80875a47b29a4f3218fe0bec0",
     "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",
+    "libxkbcommon0_1.0.3-2_amd64.deb": "d74d0b9f0a6641b44c279644c7ac627fa7a9b92350b7c6ff37da94352885bcfc",
+    "libxml2-dev_2.9.10+dfsg-6.7+deb11u1_amd64.deb": "b70e2a592fe6239ec00ec6293e706608d6f29b5090cd5f2a399f7e883dac1afe",
+    "libxml2_2.9.10+dfsg-6.7+deb11u1_amd64.deb": "c4335b83890d05760f455393c0ddd9406056d46cebe59c6de4a2876f2db04847",
     "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",
+    "libxvidcore4_1.3.7-1_amd64.deb": "048e4d456992c7f593564e8ad1cecfc4be0464bfb1fc7a8908a643d59d9fbe81",
     "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",
+    "libz3-4_4.8.10-1_amd64.deb": "7a38c2dd985eb9315857588ee06ff297e2b16de159dec85bd2777a43ebe9f458",
+    "libzbar0_0.23.90-1_amd64.deb": "400c37e4eedf8875e39a594c8329962362eb806643dd53d4691fbf0599720eea",
+    "libzmq5_4.3.4-1_amd64.deb": "817977deef176bd5be517690ae56c136cfcc2667ed434b2a8e43ac87437e9926",
+    "libzstd1_1.4.8+dfsg-2.1_amd64.deb": "5dcadfbb743bfa1c1c773bff91c018f835e8e8c821d423d3836f3ab84773507b",
+    "libzvbi-common_0.2.35-18_all.deb": "53ed21370b937a9385e1fcf1626400891bd4fd86a76b31654fb45e0875d8bfb8",
+    "libzvbi0_0.2.35-18_amd64.deb": "41ce82a1c9b629385d837970afaa7aefffb2c923fc54b31ebf582e0d905b9cdd",
+    "libzxingcore1_1.1.1-2_amd64.deb": "306a362b6ca238856a2c02e08f57a5ef21db570c2a7e5f9c5123737a9ac62e49",
+    "lsb-base_11.1.0_all.deb": "89ed6332074d827a65305f9a51e591dff20641d61ff5e11f4e1822a9987e96fe",
+    "mariadb-common_10.5.15-0+deb11u1_all.deb": "a98b12228a79f29c588cb621f0fee395b263fcfc5037b912a22e8b168be3550d",
+    "mysql-common_5.8+1.0.7_all.deb": "22b3130e002c2c2fa6a1124aaccbe3a6ddbbb4d6bf03beed8a6f044027dcb720",
+    "ocl-icd-libopencl1_2.2.14-2_amd64.deb": "fb5624009f0f8015eef1d28c4f88046abc72f093a4a508894f5c043b3e16bcef",
+    "odbcinst1debian2_2.3.6-0.1+b1_amd64.deb": "127e6d2728921dd270e60fae11b0175c2d49fcd0c4845e803c0819dc6e5dc175",
+    "odbcinst_2.3.6-0.1+b1_amd64.deb": "112d552e88ef6fd99e717dd2dae8764fffc11a2787e7452dcc312d34c161a33e",
+    "perl_5.32.1-4+deb11u2_amd64.deb": "1cebc4516ed7c240b812c7bdd7e6ea0810f513152717ca17ce139ee0dfbc7b0d",
+    "pkg-config_0.29.2-1_amd64.deb": "09a05a23c5fd5baacd488255a6b0114909210691b830fb951acd276e9bcd632a",
+    "proj-data_7.2.1-1_all.deb": "40c64f7808d8233c08f3aa2745211e705828b4ae6fc5dbd62a934d8c3e9fd6e5",
+    "python3-distutils_3.9.2-1_all.deb": "05ec4080e0f05ba6b1c339d89c97f6343919be450b66cf4cfb215f54dcb85e58",
+    "python3-lib2to3_3.9.2-1_all.deb": "802c198e5dd0b5af85a6937e426a85d616680785e8d18148fac451281a83a9a9",
+    "python3_3.9.2-3_amd64.deb": "6d9375916c5c0d670df708bed3e8c033ce4b197a580d536ce39d1170c67a4c95",
+    "readline-common_8.1-1_all.deb": "3f947176ef949f93e4ad5d76c067d33fa97cf90b62ee0748acb4f5f64790edc8",
+    "sensible-utils_0.0.14_all.deb": "b9a447dc4ec8714196b037e20a2209e62cd669f5450222952f259bda4416b71f",
+    "shared-mime-info_2.0-1_amd64.deb": "de0a814e186af5a941e1fcd3044da62eb155638fcf9616d6005bcfc6696bbe67",
+    "timgm6mb-soundfont_1.3-5_all.deb": "034abdfb296d9353433513dad5dbdcab46425ee6008fc02fe7039b46e75edc54",
+    "ttf-bitstream-vera_1.10-8.1_all.deb": "ba622edf73744b2951bbd20bfc113a1a875a9b0c6fed1ac9e9c7f4b54dd8a048",
+    "tzdata_2021a-1+deb11u3_all.deb": "61346a9f8cda14c34251d2440c8de8dab7c09bda7ebb96533166b4567359de66",
+    "ucf_3.0043_all.deb": "ebef6bcd777b5c0cc2699926f2159db08433aed07c50cb321fd828b28c5e8d53",
+    "uuid-dev_2.36.1-8+deb11u1_amd64.deb": "90a533bbb3b82f5c9bedc5da28965ca8223913099f8ac67213e4f8828bfdd2a1",
+    "x11-common_7.7+22_all.deb": "5d1c3287826f60c3a82158b803b9c0489b8aad845ca23a53a982eba3dbb82aa3",
+    "x11proto-core-dev_2020.1-1_all.deb": "92941b1b2a7889a67e952a9301339202b6b390b77af939a26ee15c94ef4fad7e",
+    "x11proto-dev_2020.1-1_all.deb": "d5568d587d9ad2664c34c14b0ac538ccb3c567e126ee5291085a8de704a565f5",
+    "xkb-data_2.29-2_all.deb": "9122cccc67e6b3c3aef2fa9c50ef9d793a12f951c76698a02b1f4ceb9e3634e5",
+    "xorg-sgml-doctools_1.11-1.1_all.deb": "168345058319094e475a87ace66f5fb6ae802109650ea8434d672117982b5d0a",
+    "xtrans-dev_1.4.0-1_all.deb": "9ce1af9464faee0c679348dd11cdf63934c12e734a64e0903692b0cb5af38e06",
+    "zlib1g-dev_1.2.11.dfsg-2_amd64.deb": "a36b74415b32513dab9a2fa56e7d215f5e5d0185df6939e483267cef15e2eaf5",
 }
diff --git a/debian/gstreamer_arm64.bzl b/debian/gstreamer_arm64.bzl
new file mode 100644
index 0000000..cd7eb40
--- /dev/null
+++ b/debian/gstreamer_arm64.bzl
@@ -0,0 +1,523 @@
+files = {
+    "adwaita-icon-theme_3.38.0-1_all.deb": "2046876c82fc1c342b38ace9aa0661bcb3e167837c984b4bdc89702bc78df5ac",
+    "coreutils_8.32-4_arm64.deb": "6210c84d6ff84b867dc430f661f22f536e1704c27bdb79de38e26f75b853d9c0",
+    "dconf-gsettings-backend_0.38.0-2_arm64.deb": "8d1df67abd7bca93f361ca26f5bb70821c91b5c4d5924941fef524f76fc6d473",
+    "dconf-service_0.38.0-2_arm64.deb": "b76a7d724cc9f18f91cc22aff58afcb090cc7a28ec9b9a21cdd1aa069c7a4cef",
+    "fluidr3mono-gm-soundfont_2.315-7_all.deb": "4098301bf29f4253c2f5799a844f42dd4aa733d91a210071ad16d7757dea51d6",
+    "fontconfig-config_2.13.1-4.2_all.deb": "48afb6ad7d15e6104a343b789f73697301ad8bff77b69927bc998f5a409d8e90",
+    "fontconfig_2.13.1-4.2_arm64.deb": "166e5e6d47af2e1a48eff6fb66e89f0e6e0f390d04f7c9abe8b4f36812378267",
+    "fonts-croscore_20201225-1_all.deb": "64904820b729ff40038f85683004e3b94b328d969bc0fbba263c58d635452923",
+    "fonts-dejavu-core_2.37-2_all.deb": "1f67421437b6eb18669d2868e3e02cb88668683d635198142f48aacc5b397118",
+    "fonts-freefont-otf_20120503-10_all.deb": "0b63996c80c6c660424af6d3832818e647960d6f65a51de010bb57dd0762faa7",
+    "fonts-freefont-ttf_20120503-10_all.deb": "4ca1c21ebc479198a3a5879d236c8317d6f7b2f1c403f7890e24c02eead05615",
+    "fonts-liberation2_2.1.3-1_all.deb": "e0805f0085132f5e6dd30f88c0d7260caf1e5450832fe2e3988a20fa9fa2150e",
+    "fonts-liberation_1.07.4-11_all.deb": "efd381517f958b01969343634ffcbdd60056be7779af84c6f53a005090430204",
+    "fonts-texgyre_20180621-3.1_all.deb": "cb7e9a4b2471cfdd57194c16364f9102f0639816a2662fed4b30d2a158747076",
+    "fonts-urw-base35_20200910-1_all.deb": "f95a139adb7f1b60626e76d4d45d1b35aad1bc2c2597394c291ef5f84b5dcb43",
+    "gdal-data_3.2.2+dfsg-2+deb11u1_all.deb": "3ae44cc2f51dccc023f9c3cfbea3411508e24f1335651fa0e6cba74b7b9b87aa",
+    "gir1.2-glib-2.0_1.66.1-1+b1_arm64.deb": "c169be0e2d5429521f2a306f5e8359489007cd5fb2ba13ce7b8df82c8c917075",
+    "gir1.2-gst-plugins-bad-1.0_1.20.1-1~bpo11+1_arm64.deb": "ecbfa3ee7bed8783488441414eaab0f205475ce08c17a898539a5177b7c6f69b",
+    "gir1.2-gst-plugins-base-1.0_1.20.1-1~bpo11+1_arm64.deb": "31284a7c37c293c93e2c772b5838bad45a6a03802949de260a92f5d32103a1ea",
+    "gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_arm64.deb": "7cf47e2dc53bda3f98307acefcfe14a3b1751128302408c6794b4fcaee1daa61",
+    "gir1.2-gudev-1.0_234-1_arm64.deb": "0b150144ae6bff0fcaadc8aa47232809db948aef2faacbdaaf49217cc31e9a89",
+    "gir1.2-json-1.0_1.6.2-1_arm64.deb": "f21ff7ca42b997bc178d20b997b7a0d6e4a8ab9ae24c8d9b4953dd0525c5de4d",
+    "gir1.2-soup-2.4_2.72.0-2_arm64.deb": "bc3f96d1bf587783c21048562322533742f7bc8f4b1384233512cf9af4a07a0a",
+    "glib-networking-common_2.66.0-2_all.deb": "a07370151ce5169e48ee7799b9bd9a7a035467a21f5cf3373b2aff090968609c",
+    "glib-networking-services_2.66.0-2_arm64.deb": "7fc232a31f09d8d08242ce08f53286e3b296c7b146834cdb41da75303f5417e1",
+    "glib-networking_2.66.0-2_arm64.deb": "3da5e3ba93a1e9a18a4cb14cfda44f938b270291004f0e88fa7d6316dfc9aebb",
+    "gsettings-desktop-schemas_3.38.0-2_all.deb": "3758968491a770e50cd85122c00d141944ffb43cb7a5c886a37290fef848cee3",
+    "gstreamer1.0-nice_0.1.18-2~bpo11+1_arm64.deb": "be6baa33743f6ba35ed3f2e51b08c9c3437094a1beb975bd2aba2f0c1b7ae9df",
+    "gstreamer1.0-plugins-bad_1.20.1-1~bpo11+1_arm64.deb": "2d29b5ae1986c3da45014986b3ed7be2da8a7179a9f75daa086280d7e66e45b9",
+    "gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_arm64.deb": "b1939a662a68e156edc89b061cee211f586284801f95849b222d2cc65161e480",
+    "gstreamer1.0-plugins-good_1.20.1-1~bpo11+1_arm64.deb": "09b1019953106ebb22904f1694f9f6352395ff815716b31b1fc5619d9ee52d29",
+    "gstreamer1.0-plugins-ugly_1.20.1-1~bpo11+1_arm64.deb": "b5e8a9ac30e154daef5073fec33d2d95d6eba651f64a0cb867d63e627397e664",
+    "gtk-update-icon-cache_3.24.24-4+deb11u2_arm64.deb": "4353a51918639f6adc45b185f06caf14f461e435b3d32e26ed2607d1e786938f",
+    "hicolor-icon-theme_0.17-2_all.deb": "20304d34b85a734ec1e4830badf3a3a70a5dc5f9c1afc0b2230ecd760c81b5e0",
+    "icu-devtools_67.1-7_arm64.deb": "17c0c7d5e1fc10b954aafd7b76474c1d648b3b4034f969bada31195da9a49d59",
+    "iso-codes_4.6.0-1_all.deb": "4e044d72a9f810aabf2c8addf126327fa845eaf8e983b51eb6356b9ed5108348",
+    "liba52-0.7.4_0.7.4-20_arm64.deb": "663eff94315dd15f0531f9a8b92f1778222faa93ebd92c48065246202975cbc8",
+    "libaa1_1.4p5-48_arm64.deb": "507fd5a5b659ac8116a14ee15fed17086aeb43903965d1adcdf053d2682c2eb1",
+    "libaec0_1.0.4-1_arm64.deb": "d71f8f3448d2aa6df31707bb97db9b307305011f9e3ae19a270f2a8858657a45",
+    "libaom0_1.0.0.errata1-3_arm64.deb": "600536f50bf36cbcfabfc8eacb43a8f26bff7a8f8f52304ce35fc1a117dcd06e",
+    "libarchive13_3.4.3-2+deb11u1_arm64.deb": "20ac2c3a178375fd35d17d1621f387461fe530d593970d505985fa4b229df760",
+    "libarmadillo10_10.1.2+dfsg-6_arm64.deb": "d937aa4788605c00a4cb6e1bfcc233ab53981761e7f04cc6b1c324607f397bbe",
+    "libarpack2_3.8.0-1_arm64.deb": "d9baf93cd93b528c331f9fd23ed0ad8fa901cd15ff6b07c914ceffa051eb02fe",
+    "libasound2-data_1.2.4-1.1_all.deb": "76211f5f201ad1069b95d047863e0c1b51d8400c874b59e00f24f31f972b4036",
+    "libasound2_1.2.4-1.1_arm64.deb": "8fabd77256593c42e2ad1578f938b56d0a5ed19aa61c6c72c890fd761fd129a8",
+    "libass9_0.15.0-2_arm64.deb": "01a4324d10e612584f90e246a1376909983866fa24b4bb6b8fb7bf1c61f71d57",
+    "libasyncns0_0.8-6+b2_arm64.deb": "1988ed1b9d48baae39a5ca37416579a8422e687e11df18b540fa28247868935f",
+    "libatk-bridge2.0-0_2.38.0-1_arm64.deb": "a977c4a2be6d46185cd4f529ba226017dfcdc9a4056ff80ae97f7c67799885b2",
+    "libatk1.0-0_2.36.0-2_arm64.deb": "53c9c45ba719f1df44f5477396d276530e7e2fc210bc598e48e624194b8a1173",
+    "libatk1.0-data_2.36.0-2_all.deb": "86c1acae473977f8a78b905090847df654306996324493f9a39d9f27807778b2",
+    "libatspi2.0-0_2.38.0-4_arm64.deb": "ca3db48d64bdd6f6e26931a5d52ddb73bc6d915957a678bc2779e03aeacf467b",
+    "libattr1_2.4.48-6_arm64.deb": "cb9b59be719a6fdbaabaa60e22aa6158b2de7a68c88ccd7c3fb7f41a25fb43d0",
+    "libavahi-client3_0.8-5_arm64.deb": "b255f51854f9984a9e69a52bbce6cab5ce6141ed7dc232cc810053c228514ce3",
+    "libavahi-common-data_0.8-5_arm64.deb": "59e6d7f43ab387989e4cb116fe35292e07cb6e6a9125b944acd417c288f88f35",
+    "libavahi-common3_0.8-5_arm64.deb": "bd51e2159b0350ffbaf653f44131ce522f0a6eeab0d60c2ccd72182a0001cb5b",
+    "libavc1394-0_0.5.4-5_arm64.deb": "5a6ad45da10d69ca6ca7757882fe696ace8af98fccac781697ef280ea13e770f",
+    "libavcodec-dev_4.3.3-0+deb11u1_arm64.deb": "189dae2788f471312d20bc57587a2fda447eaf992d6ebfb63657f75254e8e424",
+    "libavcodec58_4.3.3-0+deb11u1_arm64.deb": "f86160f51ad8e2feb6bd6a4c241e9724f6e48248a7d6f54f844df08867f196a7",
+    "libavformat-dev_4.3.3-0+deb11u1_arm64.deb": "321ffce7b99458ea358112d16c803465821370328cbda19d2c3edc3c574ed7c2",
+    "libavformat58_4.3.3-0+deb11u1_arm64.deb": "1d8dcb480bf88ab3920eecbed6531210fd756748e869222e7ed9eeb1e806355f",
+    "libavutil-dev_4.3.3-0+deb11u1_arm64.deb": "c39d6ede6e43123843db5d25ae006c2e7c8cae16cc07053550d1430e62f53b86",
+    "libavutil56_4.3.3-0+deb11u1_arm64.deb": "b0e732ed95860f3098ddcfa95b52e865376a5ad279fe34fa61324e472406094c",
+    "libblas3_3.9.0-3_arm64.deb": "99806c4a490d55ae1ba50640460a2f62c491a8920af4e804560849fc5fe9d73f",
+    "libblkid-dev_2.36.1-8+deb11u1_arm64.deb": "a7ad72251fbde83e44b82e8031b72c0a439e049b05b278aa2185c4da193161a5",
+    "libblkid1_2.36.1-8+deb11u1_arm64.deb": "f6daca6d84eab01e281bf59d7c06f55125b8af443da936afdd255fa32f939928",
+    "libbluray2_1.2.1-4+deb11u1_arm64.deb": "ccc3d64b9f3d5aa37223ce9f331b137ebeb10eb26d933bf83179772d2571fb2a",
+    "libbrotli-dev_1.0.9-2+b2_arm64.deb": "969b37f09319811600eb14e628786fcc7da3687bd642f1dc82aaa2e41c6e0766",
+    "libbrotli1_1.0.9-2+b2_arm64.deb": "52ca7f90de6cb6576a0a5cf5712fc4ae7344b79c44b8a1548087fd5d92bf1f64",
+    "libbs2b0_3.1.0+dfsg-2.2+b1_arm64.deb": "a900464c3677b73ff7a05524db80f18af3bba1b3ae1bc950ea4a94b27d2000dc",
+    "libbsd0_0.11.3-1_arm64.deb": "b6e7fa54a05e5a3a5e1ec5dceb57a470e9a0883081594aea643ca58264071e7a",
+    "libcaca0_0.99.beta19-2.2_arm64.deb": "83e458423ee3314cc9c28d91105fbd80377dbca763e9fca92f50d1b747a50cb0",
+    "libcairo-gobject2_1.16.0-5_arm64.deb": "971311ba4fa99a4611205b2f7fbb8278b3798faf3409336e6f0b0d32106f7b7b",
+    "libcairo2_1.16.0-5_arm64.deb": "5b18336974b045dda5fbd64799f06247f6b216ee54f3391adc90f0bf81596de5",
+    "libcap2-bin_2.44-1_arm64.deb": "37915f4cc73cfaef7862043f2bdad062d1b1639df04c511665f1f9321c84d0db",
+    "libcap2_2.44-1_arm64.deb": "7c5729a1cfd14876685217c5f0545301e7ff1b839262fb487d6a778e8cd8c05a",
+    "libcdio19_2.1.0-2_arm64.deb": "97b69a7b22d2b924388665d3227a64bc99936b2ad1168683b2cbefa142861af8",
+    "libcdparanoia0_3.10.2+debian-13.1_arm64.deb": "9c3886d3bbea3311fdd7051b9f2fbee54f4f88dba19c1da0cf2c9ae22679f048",
+    "libcfitsio9_3.490-3_arm64.deb": "b538f9fedc7055a6b878f4de16e0a6de96a26c3e03ddc857aeda22da946be7d8",
+    "libcharls2_2.2.0+dfsg-2_arm64.deb": "bbbf7b985fd559d4fb6226dd04a35eb745bbd22285bea98e2550cef785ff55ac",
+    "libchromaprint1_1.5.0-2_arm64.deb": "b2462da2f97fdd9ff14861451309ce3d86c4a579422c41dbdf858b2d1cfd237d",
+    "libcodec2-0.9_0.9.2-4_arm64.deb": "325c2106a587e3c054b31cf6ab8325f61a32f141eee2cea3f2eaa0423b3398d5",
+    "libcolord2_1.4.5-3_arm64.deb": "6b0d8c1debb73d9d4a97f38804e46e384d28869c6d4458d5fe807d207eecf7a6",
+    "libcups2_2.3.3op2-3+deb11u1_arm64.deb": "5badecf1e5c921ae50584378af49e4e185221e855d209ba9f2c4955d825de06d",
+    "libcurl3-gnutls_7.74.0-1.3+deb11u1_arm64.deb": "8e8ae4b7c49ff981aab871069a6457f3413d3648597b26c73500cbe7474141c4",
+    "libcurl4_7.74.0-1.3+deb11u1_arm64.deb": "1ec03ae6ededc45d9bc0906259e72ec27dd91d6d4b50e69904a5c1de13606176",
+    "libdap27_3.20.7-6_arm64.deb": "0df6382891cf0196c94d3d3edfb85991c85cc0345aa007011b69be5f3afce520",
+    "libdapclient6v5_3.20.7-6_arm64.deb": "12dd444ba9a7e2f4949af376dc874cd0cf2bdcb6fc15beba0855de0b8d861088",
+    "libdatrie1_0.2.13-1_arm64.deb": "e2953eec7abf0addebb53d6690a5b52b0e8429492328636e495ce016d819c4c7",
+    "libdav1d4_0.7.1-3_arm64.deb": "4ec26df2faceb35a9d0d821e4ad4a15ff899b07dea83263ccd51d38c57d8c760",
+    "libdb5.3_5.3.28+dfsg1-0.8_arm64.deb": "cf9aa3eae9cfc4c84f93e32f3d11e2707146e4d9707712909e3c61530b50353e",
+    "libdbus-1-3_1.12.20-2_arm64.deb": "2c8e07966435aae522bca50c4d699d940b34931f7906202e83b36982d9783c2b",
+    "libdc1394-25_2.2.6-3_arm64.deb": "09ba9019e1797379a0b662fe24309887a2c6c3e6e8de32f6e33704c65bccdbd4",
+    "libdc1394-dev_2.2.6-3_arm64.deb": "947c7302121d61af8e8b7dfdde910bc2b4342d239fbf5da719bc1b37dc7b5174",
+    "libdca0_0.0.7-2_arm64.deb": "3263ecd4b5aed7031bce65daa7883a1e10c1084e67b10874925d862495809a83",
+    "libdconf1_0.38.0-2_arm64.deb": "c89b11d34fbba55da7fc8a43fda7975eec59cd1b65695d485b7139815c44fb65",
+    "libde265-0_1.0.8-1_arm64.deb": "34097553c0c1075d16e59a8514f318000e26961592e135bf9c0f742a49f9f7d8",
+    "libdeflate-dev_1.7-1_arm64.deb": "782494693fb9e1306dfd0933f16d3be46fb1295666f3f7665a585555fddcf842",
+    "libdeflate0_1.7-1_arm64.deb": "a1adc22600ea5e44e8ea715972ac2af7994cc7ff4d94bba8e8b01abb9ddbdfd0",
+    "libdouble-conversion3_3.1.5-6.1_arm64.deb": "63b41ee55d95505e4dcd7ec6f7bb840fdc5a0527f6ecd29069f6ce7ff8536ba2",
+    "libdpkg-perl_1.20.9_all.deb": "134bd00e60fa30d39d5f676d306d6f1d61c7f6ec6086c1785dbc355ce6190f29",
+    "libdrm-amdgpu1_2.4.104-1_arm64.deb": "660f8dced02cb67826e61a91e09101d70c904ff0231d6696d6b45dbd789e86cb",
+    "libdrm-common_2.4.104-1_all.deb": "60c69026fb8e4cfdf8d80a4a86ee30516c611dcc4de4aa1c8ccbf06dff563e2b",
+    "libdrm-dev_2.4.104-1_arm64.deb": "dbbbc3d05b470d6d35b5f2daed4be893e41b74f9c7e58bf5be3d18849b14f78a",
+    "libdrm-etnaviv1_2.4.104-1_arm64.deb": "94871c2b1fa6ad6dadff5606a293bb3d86faa2215440da9340645d3e303b82c9",
+    "libdrm-freedreno1_2.4.104-1_arm64.deb": "753c888e556952a47bbbfb85823f80eaefa0b23cd8131c69667cbb7c0af13b87",
+    "libdrm-nouveau2_2.4.104-1_arm64.deb": "d54e628289717d6751367767e9b1867ed8b863890a64724cda4e76f3be773cde",
+    "libdrm-radeon1_2.4.104-1_arm64.deb": "e8ecdbc425f3ce293d85eb43bd759f657db9eccef24637c4edb36a147fed93cb",
+    "libdrm-tegra0_2.4.104-1_arm64.deb": "58795c9786d0c925f89724885fc19f1d3bd55126abe7878f0f4766b6b58a7476",
+    "libdrm2_2.4.104-1_arm64.deb": "1dc606aa361307a8c5277c2a5ddedea40a4125874e887c98e82b33dacaa853b0",
+    "libdv4_1.0.0-13_arm64.deb": "cd32532a1ded7f2a159ade41db771845572271a1986b8d6af1ac2ff1c5d507ba",
+    "libdvdnav4_6.1.0-1+b1_arm64.deb": "7f62541bc9116c2074d5f89ddbdf16310ea2d37fe831bea5b2b27940218da335",
+    "libdvdread8_6.1.1-2_arm64.deb": "0a8acd661ee3e12b7c5b484fdce8e98d7d4f42f9714a5e6ca0f1d00927956d16",
+    "libdw-dev_0.183-1_arm64.deb": "368b551d30eeffcd726a3b2d9c6dad17a3dc290a422c19b6dd885523e2d0df57",
+    "libdw1_0.183-1_arm64.deb": "66805e4b00001a0e76ea5291705942b542be53e8d1149867cfbd4cd5fd92285f",
+    "libedit2_3.1-20191231-2+b1_arm64.deb": "43cbfd69ef591a66cfd06aedf930e3fc3c370b3a7ad514a33399d0e1a4d7343e",
+    "libegl-dev_1.3.2-1_arm64.deb": "e2ad8f861b423620dd202ee6dbd1f79ff09e427656d84b395cb19ff43cc77f2d",
+    "libegl-mesa0_20.3.5-1_arm64.deb": "cf0ea87393d134d49145798357725a044c80ee2d48fc8920490e15282aa4dca5",
+    "libegl1_1.3.2-1_arm64.deb": "4b531a79399010d3377ce9b6094c8f5f3508bd18ea5b32008a6a1ec16e019a81",
+    "libelf-dev_0.183-1_arm64.deb": "cf209e957183f4c00d4144069450b9dd937a61e9523bd4952c1b8ea63e983d2b",
+    "libelf1_0.183-1_arm64.deb": "12e14110cd66b3bf0564e3b184985b3e91c9cd76e909531a7f7bd2cb9b35a1f3",
+    "libepoxy0_1.5.5-1_arm64.deb": "cc66fbfd7b08de406873d792b07f17e19e70e37c8255fe33335b57d17379fdd5",
+    "libepsilon1_0.9.2+dfsg-5_arm64.deb": "d39e9ac0d3fe3fc6824c902f9085b70d70af2b40ff4a395bea8937e15516b619",
+    "libexif-dev_0.6.22-3_arm64.deb": "9b002b7311fa12eb59366f77e8c6de17932fad1f7c441f7eec91da2addbd8cd3",
+    "libexif12_0.6.22-3_arm64.deb": "2b2f428d688630275822104dad11d08203640076e91ea2df37dabcf2a6ae7e0e",
+    "libexpat1_2.2.10-2+deb11u3_arm64.deb": "d9bae74ea4fed5a5616c4acf67914a12d485fa50be729468cfbc5d2d54c91cfc",
+    "libfaad2_2.10.0-1_arm64.deb": "bd2fb8e6b423f68588c70584ea1ebc63e6960554be0f07f7198711de84120387",
+    "libffi-dev_3.3-6_arm64.deb": "31166c3c814374c520e4c47cc779291888a2a7eca09fc93b5d2918bd7e2ed085",
+    "libffi7_3.3-6_arm64.deb": "eb748e33ae4ed46f5a4c14b7a2a09792569f2029ede319d0979c373829ba1532",
+    "libflac8_1.3.3-2+deb11u1_arm64.deb": "8da72b767f4c2c30029f31f41d76549d73d0d95a6252e0fef6d144acaedbd7f9",
+    "libflite1_2.2-2_arm64.deb": "263e423db8223ee49b40fedacf64a0c86e0d64e359976fc3582e3961a5c105e7",
+    "libfluidsynth2_2.1.7-1.1_arm64.deb": "58c810eadac43ea2cc50483b851ac02692f6e8373671a34b7a02debbfba4efc1",
+    "libfontconfig1_2.13.1-4.2_arm64.deb": "18b13ef8a46e9d79ba6a6ba2db0c86e42583277b5d47f6942f3223e349c22641",
+    "libfreeaptx0_0.1.1-1_arm64.deb": "1da0a6f6af39846927078e1c5b5ed2b215b36ceeeda01d180b5fb3678a6ba512",
+    "libfreetype6_2.10.4+dfsg-1_arm64.deb": "43e3b1cc3dd70e00f930bca8b69488f5979ab358e53084361424cca7c49a3fa0",
+    "libfreexl1_1.0.6-1_arm64.deb": "8bdabfe5826a662d1082bf58bfb5d15c95082a5438f57239b9e5427b6f4701cf",
+    "libfribidi0_1.0.8-2_arm64.deb": "c6c92f3dbe2f1894b09aa1eeb74933d4dc0df6dacfadaa1ff66d96ee753d41de",
+    "libfyba0_4.1.1-7_arm64.deb": "3a7578a717d1104c55486dbd4a69271b44ce72ce0ac5d34d216417cc171a8dbb",
+    "libgbm-dev_20.3.5-1_arm64.deb": "4ef3b10914072d465282bffaa10e310dcabc88fa8ab02c200771cbe1c37f527d",
+    "libgbm1_20.3.5-1_arm64.deb": "991e5db7978e44ef745a1f7f18100927d304ddf1eaaab840167b5f07e3bdc7e8",
+    "libgcc-s1_10.2.1-6_arm64.deb": "e2fcdb378d3c1ad1bcb64d4fb6b37aab44011152beca12a4944f435a2582df1f",
+    "libgcrypt20_1.8.7-6_arm64.deb": "61ec779149f20923b30adad7bdf4732957e88a5b6a26d94b2210dfe79409959b",
+    "libgd3_2.3.0-2_arm64.deb": "1e6d6af0c90fe62193b3e51e45f69d075b86d7bde3fb4fd30b60da763aeca43f",
+    "libgdal28_3.2.2+dfsg-2+deb11u1_arm64.deb": "bc5ad6148ce06f07397f07f7a96bc865975248821fb01124184049d741e93c1a",
+    "libgdcm-dev_3.0.8-2_arm64.deb": "1795d571d83196ec248fcdfc6153fe2d21599aa1debf40ebdeec00393764ca0c",
+    "libgdcm3.0_3.0.8-2_arm64.deb": "59472494d90363690f34a7cbe3f5ba66028fab6f242161785401ba7d7c223ef7",
+    "libgdk-pixbuf-2.0-0_2.42.2+dfsg-1_arm64.deb": "e35997839534e69d093cef790dc4a43d0b102e7aed5f21492d330f810717f4ce",
+    "libgdk-pixbuf2.0-common_2.42.2+dfsg-1_all.deb": "61ff764860dafbd7e3fe2050b9c17db3ae109dea15ac748212eff56fdb3111e1",
+    "libgeos-3.9.0_3.9.0-1_arm64.deb": "9f8b32c8814c8ad72df61bc8cdf0eb95b4d625f06fb7a945e146e523c664b874",
+    "libgeos-c1v5_3.9.0-1_arm64.deb": "efffd604a25acd4088a2f34e24c779490f1389f0dd34e2b62cb1e3bc364d8ec9",
+    "libgeotiff5_1.6.0-1_arm64.deb": "94e2329dcc4236aa1bb455613f9b1795b8f8974ac40f8a77568312c677bd14fe",
+    "libgfortran5_10.2.1-6_arm64.deb": "967866fc2caf00fd28a3a515234131d7301043288b4a36e46b7dc16050a6f862",
+    "libgif7_5.1.9-2_arm64.deb": "90751dcd054948236424aac6bddc0666013621fc6d3aec9fba62e275ba12db17",
+    "libgirepository-1.0-1_1.66.1-1+b1_arm64.deb": "fb3da6c3d8b79aefa9db09fb9b5054793d5c98bab11504f41a149935849e462d",
+    "libgl-dev_1.3.2-1_arm64.deb": "cb40f975a9bf6916bb435378a1bdab4f2aec93ef4df91c7f2708900c84211e7d",
+    "libgl1-mesa-dri_20.3.5-1_arm64.deb": "13bc343bc0bb7ec8bf52ecceb2c41ac8b70bc80d4d76784252caceadf0c83b73",
+    "libgl1_1.3.2-1_arm64.deb": "18075b79c22b06bb25b9b4f3fa44a42127c40e573b34703af3f4c0b780cae29a",
+    "libgl2ps1.4_1.4.2+dfsg1-1_arm64.deb": "989c066bd91aa9ac18d9d571fba404038e25893da99c0fd86fbecda15be89372",
+    "libglapi-mesa_20.3.5-1_arm64.deb": "2678cf4bcfe4dfeba85deabaf869cb78a2f3263388baaff53e826f52cf17088b",
+    "libgles-dev_1.3.2-1_arm64.deb": "8fd9943bd37d02e0ef9e8983d880fe71aa171f2973c2c19d9fc5d9492bc13fe8",
+    "libgles1_1.3.2-1_arm64.deb": "a607c89ff8d61145f215b4be95814aef7accbfc0bac8c2a840ee9f0aca530068",
+    "libgles2_1.3.2-1_arm64.deb": "ae3fda1556b677519ff13d43295d7df678d3a9c39a21042e87d9255de1bc78b6",
+    "libglew2.1_2.1.0-4+b1_arm64.deb": "68ed07175805f1b827ff4db50387ca8dfb5bdfffa98e23c897de34706c04b42d",
+    "libglib2.0-0_2.66.8-1_arm64.deb": "667d1b891bcf9b8cc47385c19b39271c74f48fd2b6b457474184f85ce63ea261",
+    "libglib2.0-bin_2.66.8-1_arm64.deb": "e577f6ff709ecf2302515bc98b3d71d4d3e59734ae6721b8fffc1d6e4b600eb5",
+    "libglib2.0-data_2.66.8-1_all.deb": "be41a674336cefd00e2a468fe19c8bbf9f3fac86f39379e1b7acbad41f6af644",
+    "libglib2.0-dev-bin_2.66.8-1_arm64.deb": "2ca6490629d6eeb92154ea1b7d91996142a2f2ee81c91e4586d34cd9ddf581e9",
+    "libglib2.0-dev_2.66.8-1_arm64.deb": "e91666fddc4ca1a40325797ff960499ab51c3f4ab4da65ccb0ee92a1352b1f4e",
+    "libglvnd0_1.3.2-1_arm64.deb": "5d7a05a966d1df43ca440245dfc7e18a51fc974f665441fc87180a605a0481d9",
+    "libglx-dev_1.3.2-1_arm64.deb": "0a69e1f459f8bc3e190162e78f8f6aa749dfcc0ad6c77f7ec27c9b55aef8e75c",
+    "libglx-mesa0_20.3.5-1_arm64.deb": "b1f653df820ecda31ed3d6a39429befbced67093dc9b964cbfa5f7991a23eda8",
+    "libglx0_1.3.2-1_arm64.deb": "a53aec87ff8d3ae181c320c01bc190f752a276d5e5ca87e624f9058a4b520bf2",
+    "libgme0_0.6.3-2_arm64.deb": "2324dc1a5c06845f56eac43d9efb2446fb3119758eee46e793e33aa158a3325d",
+    "libgmp10_6.2.1+dfsg-1+deb11u1_arm64.deb": "d52619b6ff8829aa5424dfe3189dd05f22118211e69273e9576030584ffcce80",
+    "libgnutls30_3.7.1-5_arm64.deb": "6b7429aba5f642c258d351b2ebac5a00170f6761331f6b367edfb9401c10d8da",
+    "libgomp1_10.2.1-6_arm64.deb": "813af2e0b8ba0a7cea18c988cd843412ef6d0415700fc860d62816750e741670",
+    "libgpg-error0_1.38-2_arm64.deb": "d1116f4281d6db35279799a21051e0d0e2600d110d7ee2b95b3cca6bec28067c",
+    "libgphoto2-6_2.5.27-1_arm64.deb": "b83525eb32514b47362ab6b8c16f37f002d9a531405b1dbdaed9dd20de19b61a",
+    "libgphoto2-dev_2.5.27-1_arm64.deb": "8a13c502539d0ff3d431f5d1099c784f0ab358c81065af788d0bba65e46ba24f",
+    "libgphoto2-port12_2.5.27-1_arm64.deb": "64892ba635e3f67816748d33700262d73a0b727fd31c89c5f260eceff0c9bbea",
+    "libgpm2_1.20.7-8_arm64.deb": "367a548386f1297c937af6ed548bdcaa4db25f11c1a7495c45978826da61deb8",
+    "libgraphite2-3_1.3.14-1_arm64.deb": "473362a74ba74ae630fc43675460fb5a1058564a635a301875e00f1c6f9b27cb",
+    "libgsm1_1.0.18-2_arm64.deb": "f7ea67da4f664d6c7b49ca7f31bf6c5fee61e2ff679da61d0613efbb0fdd6cb9",
+    "libgssdp-1.2-0_1.2.3-2_arm64.deb": "817b986d52438f86709fcdbe6b8c5b55a3bcbb445c10feeb52824a95d148a322",
+    "libgstreamer-gl1.0-0_1.20.1-1~bpo11+1_arm64.deb": "fcaccf88b57203cd9a494aa60bc8db6ffb43c68525122087a6516f3ec64a2b53",
+    "libgstreamer-opencv1.0-0_1.20.1-1~bpo11+1_arm64.deb": "f4f07dfc3baa0e875e5b4298cc3cfb5749994bb56c3cb34c27ee0151a6e591e1",
+    "libgstreamer-plugins-bad1.0-0_1.20.1-1~bpo11+1_arm64.deb": "b320b3e6c6a7e6a6f5c7036e1fe986ffed90d9b65993753a8823f05fcbc09c41",
+    "libgstreamer-plugins-bad1.0-dev_1.20.1-1~bpo11+1_arm64.deb": "b653071e00823ea0f5fbfeb18d0030fc5545f0cfb329f48daa24db8f722787b6",
+    "libgstreamer-plugins-base1.0-0_1.20.1-1~bpo11+1_arm64.deb": "c0379e4c5adc551c92d8acf8bba476fea71b0c23bb135737db6f878f6403a93d",
+    "libgstreamer-plugins-base1.0-dev_1.20.1-1~bpo11+1_arm64.deb": "5a7b180a7bc2df3ded9635e126a6061789825ba9557813e4e073913fddad665a",
+    "libgstreamer1.0-0_1.20.1-1~bpo11+1_arm64.deb": "92556adf1a343a6f6a9b1b38a8c5dbdb0b16a59bb9f283a1e46c75d2376e13ca",
+    "libgstreamer1.0-dev_1.20.1-1~bpo11+1_arm64.deb": "802c4c5c358120b4f701bf9136b392537681712ecf6c2c553c6b1e7c0319123e",
+    "libgtk-3-0_3.24.24-4+deb11u2_arm64.deb": "af0df7dd4cbee8ece4b7f88c4c700f8255043bfa263a450205a0efd74162728e",
+    "libgtk-3-common_3.24.24-4+deb11u2_all.deb": "172d01f359af8f13cee93dba183e282ea5f059f2a418dfe66d35abf9dd60ddd7",
+    "libgudev-1.0-0_234-1_arm64.deb": "8e0e8990893f9355d7f725e608e948cc9369c850caf163a28f118c0262b63586",
+    "libgudev-1.0-dev_234-1_arm64.deb": "1d93b9914f0d14822d3a84ba648ecbdd6dcc421e26091a1737c6a62a18bd628c",
+    "libgupnp-1.2-0_1.2.4-1_arm64.deb": "9fc8e77b4c28caf6db825a34fa05c2dd105897e4e7213833896236326b9e64aa",
+    "libgupnp-igd-1.0-4_1.2.0-1_arm64.deb": "6852f375f22f60455552e07577c4bd75c3191f23ca2421b7968ff38671e03ee5",
+    "libharfbuzz0b_2.7.4-1_arm64.deb": "d9f0345391cc661503d1508ccd318b3db48add354e706cf9d66fa16bf99e2d03",
+    "libhdf4-0-alt_4.2.15-3_arm64.deb": "5ce23c00a3726049c74e149cf08ea46809143eb106a49e38aad973287abafdb3",
+    "libhdf5-103-1_1.10.6+repack-4+deb11u1_arm64.deb": "5f051dfa0c2f6d369e9322fa0c175717c61dce21d3bfa316e0271c949b3b834d",
+    "libhdf5-hl-100_1.10.6+repack-4+deb11u1_arm64.deb": "d3748cffd412ae9dce22016a3a4106a6e0cd2eedf79ae98a79873a0b2f64d9e7",
+    "libheif1_1.11.0-1_arm64.deb": "a8db0c9a9e0311ad2624319f1ef76e89b1c188f424adf101a581a62c28853381",
+    "libhogweed6_3.7.3-1_arm64.deb": "3e9eea5e474dd98a7de9e4c1ecfbfd6f6efb1d40bf51d6473de9713cf41d2191",
+    "libicu-dev_67.1-7_arm64.deb": "4372e38b47534a24c7d92771b805b4dcfcda8ce98489606859077ae754037257",
+    "libicu67_67.1-7_arm64.deb": "776303db230b275d8a17dfe8f0012bf61093dfc910f0d51f175be36707686efe",
+    "libidn2-0_2.3.0-5_arm64.deb": "0d2e6d39bf65f16861f284be567c1a6c5d4dc6b54dcfdf9dba631546ff4e6796",
+    "libiec61883-0_1.2.0-4_arm64.deb": "4a15ee3d12830c7bdaa5c21e522a69dabbaff3fcb4cf18648b71e37ebd0c324d",
+    "libilmbase-dev_2.5.4-1_arm64.deb": "f7efd0776113ae36e03f08d349ba7c17862746c076f54a27f366d0927579923f",
+    "libilmbase25_2.5.4-1_arm64.deb": "9d09d54ad2ceb8148fd1be0fe2e065a7c35ffecb48b329592271aaf860f620e9",
+    "libinstpatch-1.0-2_1.1.6-1_arm64.deb": "c24d1ef774b0cd16baf38f8c45d6081e38e515674cb4a5b7d27a7e853576be3e",
+    "libjack-jackd2-0_1.9.17~dfsg-1_arm64.deb": "bf6470c02225641ff4f2baaf5b724a41a175dacbc7195d90d695404ce00afdc1",
+    "libjbig-dev_2.1-3.1+b2_arm64.deb": "611c5cdf980db966432c39b8ae3cf6449f84e224e9369b7c8fe492d2d48378b1",
+    "libjbig0_2.1-3.1+b2_arm64.deb": "b71b3e62e162f64cb24466bf7c6e40b05ce2a67ca7fed26d267d498f2896d549",
+    "libjpeg-dev_2.0.6-4_arm64.deb": "ce0f75c7f632be0c980d01f2e2ae863089f99bca47e4cff3015a490e1c886d41",
+    "libjpeg62-turbo-dev_2.0.6-4_arm64.deb": "8535e4bd12e026ff79991f69c62ead5ff1750df980a20a6a7ab540587439f06f",
+    "libjpeg62-turbo_2.0.6-4_arm64.deb": "8903394de23dc6ead5abfc80972c8fd44300c9903ad4589d0df926e71977d881",
+    "libjson-c5_0.15-2_arm64.deb": "451820024e4d1023cc74758bc22beccf1aa235227026b3e24bafa67bf81d215e",
+    "libjson-glib-1.0-0_1.6.2-1_arm64.deb": "a2167dd43e00154f2418f19e819cd90db1559f78cd8d783cfac1063bba7509e8",
+    "libjson-glib-1.0-common_1.6.2-1_all.deb": "a938ec35a20dca2e5878a8750fb44683b67a5f7c2d23d383963803a9fcfac1a3",
+    "libjson-glib-dev_1.6.2-1_arm64.deb": "382233d072ddef848296465e02f5b9314dd2b57ddccb12702335f3e78484f312",
+    "libjsoncpp24_1.9.4-4_arm64.deb": "b8e04a858f2059d4e9d66b79f359995e38ff8c9509175647d6f98ba1f8b29b2a",
+    "libkate1_0.4.1-11_arm64.deb": "97890e66ff7ac0dbc99ab927ffe441feb9c51922cf6aeefd86c504e4ef9613dc",
+    "libkmlbase1_1.3.0-9_arm64.deb": "e8c4b8513c10b763ec97133bc1c6180e027aeddeaba8965cbed8fba790a8a30b",
+    "libkmldom1_1.3.0-9_arm64.deb": "301808035031ac8c3c39baf0a854a3fc4c9e4de36a45ba5cd9abf4b06cbe4432",
+    "libkmlengine1_1.3.0-9_arm64.deb": "a6cf70031c33b85d5be04d1ad0ab4ff4b580674431e1ec0ba0596017b75045cf",
+    "liblapack3_3.9.0-3_arm64.deb": "30858f2b44bd6e24100bf119ab7e4da0e9db9df1ade3957e65952d95d4c308e6",
+    "liblcms2-2_2.12~rc1-2_arm64.deb": "6d92ee1f0d427b88ab9bff32c769b61e2597c8fb289589ca0731a7e77d490d6e",
+    "libldacbt-enc2_2.0.2.3+git20200429+ed310a0-4_arm64.deb": "cf953f7b903344c29f58022894b16426488812d9ea0b8b319143d6b34c84a18c",
+    "libldap-2.4-2_2.4.57+dfsg-3_arm64.deb": "443959ae0679f73a1587e26f0f5a78e2c9909d1328c0f267c71da349dfccfe8e",
+    "liblept5_1.79.0-1.1_arm64.deb": "134c600adc419c058777f87dd5254932a03e4591e6e230a290747657d7557390",
+    "liblilv-0-0_0.24.12-2_arm64.deb": "b36dc0c3b8ba453025b4839c852065abb95c823c4c03376e9ac4db76c56d5e8b",
+    "libllvm11_11.0.1-2_arm64.deb": "0f4d1c3e259e203cc25ca5e07a4f88a018a775bdbf4376f33b06c8ffb7bb688b",
+    "libltc11_1.3.1-1_arm64.deb": "1ff780bc54ed29fd5b638fc4b1cd948c2a34524298fd32175e3abf17a6e8f81f",
+    "libltdl7_2.4.6-15_arm64.deb": "e13bc091b1493c3bbb0e6554e73bd4d913358fdcd267152d1dcb32a5a3b94e27",
+    "liblz4-1_1.9.3-2_arm64.deb": "83f0ee547cd42854e1b2a2e4c1a5705e28259ee5fa6560119f918f961a5dada2",
+    "liblzma-dev_5.2.5-2_arm64.deb": "44c7d3b32401fa0406fd9e271b10038342b7d04bba377a917847b063eb428e9b",
+    "libmariadb3_10.5.15-0+deb11u1_arm64.deb": "d54b4007c430d6d1e968e255f51475e636487942c27ec036937d45ca9e0e25ea",
+    "libmd0_1.0.3-3_arm64.deb": "3c490cdcce9d25e702e6587b6166cd8e7303fce8343642d9d5d99695282a9e5c",
+    "libminizip1_1.1-8+b1_arm64.deb": "9db7fb955cc168a591fa5cc052f02606a39333ce4b15177426baf2d956018400",
+    "libmjpegutils-2.1-0_2.1.0+debian-6_arm64.deb": "e09666538003abdb1646ea2e3f37cf6c1188e9517226fadf2feed52a184b0188",
+    "libmodplug1_0.8.9.0-3_arm64.deb": "31562caee099234947a228d6392156495b10f4b1960182e419554fe58ee50402",
+    "libmount-dev_2.36.1-8+deb11u1_arm64.deb": "46ffa87df906d4a8bcc888d14f69c70d3dd60af8383b04af452f7e3c1e49e818",
+    "libmount1_2.36.1-8+deb11u1_arm64.deb": "fd1dff93bdaba84d3f45f25448e2ada8c867674cd4e8af9fe25604ddf9a2f8de",
+    "libmp3lame0_3.100-3_arm64.deb": "ad02a132721a5569476235b3fbe8f88af0c9e06fa37d8d68f5eda604cfe8ec92",
+    "libmpcdec6_0.1~r495-2_arm64.deb": "f586bbb11f52627bf20846a501296366365ae7c1cd60c2829a8451cbf5ad2cdc",
+    "libmpeg2-4_0.5.1-9_arm64.deb": "abf6f3204f0cce6acde4b20899e58e1a5d1fa20e7dc6a138a39c3bda04e3b96d",
+    "libmpeg2encpp-2.1-0_2.1.0+debian-6_arm64.deb": "69407c9dfb3dfb441ce0083a705416eafcfd86c585f91ff5f96403eba478a7cb",
+    "libmpg123-0_1.26.4-1_arm64.deb": "4a7351593002fc11acc50d4d86a457332b6f3de488491803e1c3eafdeb440630",
+    "libmplex2-2.1-0_2.1.0+debian-6_arm64.deb": "c99fb7e85e43a59672c1dfce1c8e6256a197ab587741ab2ddaab7f287b5313f7",
+    "libncurses6_6.2+20201114-2_arm64.deb": "29e207cc49f7bedfe61f8c41d1c1878fb827df9264b3e2a9cb207998200cac4f",
+    "libncursesw6_6.2+20201114-2_arm64.deb": "4ff84101deab5af44f91df40a8220995a501c0f875f70ede51ce8c7dce7c475c",
+    "libnetcdf18_4.7.4-1_arm64.deb": "48e40a3ed8d48043996d858b8a28277f239cb43685cf13f047b3084aaeec0794",
+    "libnettle8_3.7.3-1_arm64.deb": "5061c931f95dc7277d95fc58bce7c17b1a95c6aa9a9aac781784f3b3dc909047",
+    "libnghttp2-14_1.43.0-1_arm64.deb": "317b58d2654d5875eee1dbf147ea810b8e3eb007f3bf4e2dcbca8ed76e425763",
+    "libnice10_0.1.18-2~bpo11+1_arm64.deb": "d4d1e4b44bb5e41aed8a99fd0174048bc9730b521c075c80a4aa99e30288ee83",
+    "libnorm1_1.5.9+dfsg-2_arm64.deb": "3974ddba57ed9bbfff10a144baae64d647adbb38ebce5fa122e4f0dd16b77866",
+    "libnspr4_4.29-1_arm64.deb": "f83f450a49499e320b1d7723a596be2414bf7adfd20315ba72df0e7735d98bc5",
+    "libnss3_3.61-1+deb11u2_arm64.deb": "c10ac0797a3c5f694ab86251d22d8d1452b03e50fc30999edd91a45bc202696d",
+    "libnuma1_2.0.12-1+b1_arm64.deb": "4eda519ae1f36f6376380fb2798ca0f50e104930845d8c51561ec455e98c57fc",
+    "libodbc1_2.3.6-0.1+b1_arm64.deb": "bd0060e4333b038300fb9f4a225adebbdd5054fb6298960eef36f52c50733bc8",
+    "libogdi4.1_4.1.0+ds-5_arm64.deb": "48903ccacb992d04131def3accceac424f6226aa4059201303131cf89eaeddbe",
+    "libogg0_1.3.4-0.1_arm64.deb": "910d1f3893a9340ea83bf19deebbc4e0d2362f22c274c2c2d3f00e4ba386c871",
+    "libopenal-data_1.19.1-2_all.deb": "695a650803f885459994bb921132d956c2b693759572005351a5b13773c754cd",
+    "libopenal1_1.19.1-2_arm64.deb": "848f7cb93823c780ab58c0da67535316ef797666934d06ed6d64e902d4e2df11",
+    "libopencore-amrnb0_0.1.5-1_arm64.deb": "85c1c3a54c4c85a4c630ef21b86ff1a8278b43613659299715e5b255a9e5614f",
+    "libopencore-amrwb0_0.1.5-1_arm64.deb": "a597315898b997ab702696608756a738701047d70275b681406c91b850a87625",
+    "libopencv-calib3d-dev_4.5.1+dfsg-5_arm64.deb": "9b32b570cb47735db6879a925956168e85155ef048d9895c7cbce014ef9b4529",
+    "libopencv-calib3d4.5_4.5.1+dfsg-5_arm64.deb": "189fbfc0fa850bf8ee4f3484c5a9a71d38bf1ba7694ebe0a218cf398a755f1d5",
+    "libopencv-contrib-dev_4.5.1+dfsg-5_arm64.deb": "36838790208c16db93236aee39bf77fee8ab8bff6ef4b35c22e9ebfd0d900099",
+    "libopencv-contrib4.5_4.5.1+dfsg-5_arm64.deb": "7d80388fc6ce9662a2706cd0371ec1b75b2a0473cc17536d6bb329c597317e65",
+    "libopencv-core-dev_4.5.1+dfsg-5_arm64.deb": "b8d8873d726345a0029f524dd7791b581a8379e1cd6e3aed6cc462b47c495fe0",
+    "libopencv-core4.5_4.5.1+dfsg-5_arm64.deb": "4736fc101e6fffd1f11f7dba48a1fc72aca5683cac123c25160718628a682e9f",
+    "libopencv-dev_4.5.1+dfsg-5_arm64.deb": "cca973f700a3177cfeb77bbdf0fbb34d49a894b93719b3e3fc13dea145ed9d6b",
+    "libopencv-dnn-dev_4.5.1+dfsg-5_arm64.deb": "e506228fc13a8533bec5a88b6b73cc931f31e0032218465dac81d2fb659d8988",
+    "libopencv-dnn4.5_4.5.1+dfsg-5_arm64.deb": "0c121b36ba1cd55c9355aa9f2dc42b78afe83a5ab3734e3a5b16ba68ae5ef7d1",
+    "libopencv-features2d-dev_4.5.1+dfsg-5_arm64.deb": "d9cc52691782a9e0714814f33ce177cfe56d78e2b6a828e920a0739286806ae3",
+    "libopencv-features2d4.5_4.5.1+dfsg-5_arm64.deb": "ac5c82fff13f36eea33fb92e04cfd7078642cca527382ce8cb2e5e7c75b1d698",
+    "libopencv-flann-dev_4.5.1+dfsg-5_arm64.deb": "ef70c57db440c2805be002d5638fb381727e9b0c9faf3462857565884eed81b9",
+    "libopencv-flann4.5_4.5.1+dfsg-5_arm64.deb": "20c490a68d367e4ac47b443850697757d97dc30e74ffd31b0025841f2829ad80",
+    "libopencv-highgui-dev_4.5.1+dfsg-5_arm64.deb": "27c41d0d726870b9245327857e6207657971b023d5fe251fd0a539ff84ce69ba",
+    "libopencv-highgui4.5_4.5.1+dfsg-5_arm64.deb": "5af44ec6da17dc8d000215ff9377a6eff00ab7970c078f3cd46492ceec1be7c8",
+    "libopencv-imgcodecs-dev_4.5.1+dfsg-5_arm64.deb": "27ec650759d661651dbfa9fe9d571b106048f0e6a7863c6fa8e9c224b6c64ad6",
+    "libopencv-imgcodecs4.5_4.5.1+dfsg-5_arm64.deb": "8d68e9a89517628082fb5d58d47d865c69c7e070bd91fcab8240d8f082e85e54",
+    "libopencv-imgproc-dev_4.5.1+dfsg-5_arm64.deb": "22fd1fd47d57f4085472653b420fcfd8d4180d85ccfb8b9ffaab7e110643fbe0",
+    "libopencv-imgproc4.5_4.5.1+dfsg-5_arm64.deb": "fcbf06a539696d53a1386a1609f51c0890a7d58a58fd67e153facd8e13728754",
+    "libopencv-ml-dev_4.5.1+dfsg-5_arm64.deb": "fb7022c93d9c4f4bc050d9f3ca545133d1d5a70efcc692ef7fc78f4d902fcf5c",
+    "libopencv-ml4.5_4.5.1+dfsg-5_arm64.deb": "b7eb23cc968b4409d044addcad4b534282fc208eb4958b811f4404eebb3c52a7",
+    "libopencv-objdetect-dev_4.5.1+dfsg-5_arm64.deb": "db41fa5f2a3eee924d2bb9cad42e8c416a66f87a498562c4399e6690176d61d0",
+    "libopencv-objdetect4.5_4.5.1+dfsg-5_arm64.deb": "9f3784842133a8ffc764a72572b8631049d8627ea8d7c120ced7e44f8a981641",
+    "libopencv-photo-dev_4.5.1+dfsg-5_arm64.deb": "13db446289d550a9dbf7214e49cbb46c895331c3a7fc7813594423b9f9d6bd3c",
+    "libopencv-photo4.5_4.5.1+dfsg-5_arm64.deb": "af684d483ab247c47941682ec6acd5d045438ccca8085c442973529da783cb4f",
+    "libopencv-shape-dev_4.5.1+dfsg-5_arm64.deb": "04313d5b4408784e50e314fcdf27002cba2c19c8b28d1ef3d4b3ea75d92ca303",
+    "libopencv-shape4.5_4.5.1+dfsg-5_arm64.deb": "b30cf313926d1064a676c857b2f57afba78e0a3942bdd0993b83eb17144b9a8d",
+    "libopencv-stitching-dev_4.5.1+dfsg-5_arm64.deb": "e0998b49adf404ff376c725bb6ad62663ef50bac4ec9bacebeda9fc19fe61dc8",
+    "libopencv-stitching4.5_4.5.1+dfsg-5_arm64.deb": "2316563a7b487d0b245f84e87eb0db61423f3f730c474a7776d3910ac1a1b210",
+    "libopencv-superres-dev_4.5.1+dfsg-5_arm64.deb": "f8524762200001fbe8acff2d2dd1e110ac37ed5e124837778d0acb703fdc9d22",
+    "libopencv-superres4.5_4.5.1+dfsg-5_arm64.deb": "1da58f51aa2ca2fe8a425a7f6f84ee5252c90fef10cfc90d2ccd2190ebaa8e43",
+    "libopencv-video-dev_4.5.1+dfsg-5_arm64.deb": "a37ea4ef728084858601f8a61b018e28e146951311dd55b4b75b83e4788b8f49",
+    "libopencv-video4.5_4.5.1+dfsg-5_arm64.deb": "c49598f7fe96b473c8a6c0e091a407a50f1b9514c398800497c0ae2691be3dc7",
+    "libopencv-videoio-dev_4.5.1+dfsg-5_arm64.deb": "60464425514a01b82dc4798d777d17cb9802119b05283cc4a44623a60bc98a48",
+    "libopencv-videoio4.5_4.5.1+dfsg-5_arm64.deb": "ab79662142f6aa93b54c30eb0f388da3445fc11fb56b7888825b9b6bde2a7536",
+    "libopencv-videostab-dev_4.5.1+dfsg-5_arm64.deb": "2c517c7d4394cd702e417c33b7fa5154ba650d2850ee2530dcc3b86943a471a7",
+    "libopencv-videostab4.5_4.5.1+dfsg-5_arm64.deb": "ce078bd6fe7da56efbdb4b03b7bde1db3fdf8923a70e99528e9eaf3a966e89b5",
+    "libopencv-viz-dev_4.5.1+dfsg-5_arm64.deb": "e0ca0f7422c742889831ee3a9836e6efa15beef70784e7a93f03187efd073d34",
+    "libopencv-viz4.5_4.5.1+dfsg-5_arm64.deb": "fb92c16c42a4faf7f3393f82428865929225a073894100f75c4535cf56e06f97",
+    "libopenexr-dev_2.5.4-2_arm64.deb": "fc0a9ffdda657e668ee164ffeebfbc27f9c5fb80556567f9c25bd1cbec05ff8d",
+    "libopenexr25_2.5.4-2_arm64.deb": "15bc59e1f8abf11d17e9b028a54b15f8f9b9448b6ea98175ddfb3acdd6f8ecd4",
+    "libopengl0_1.3.2-1_arm64.deb": "0dcd4846079fd8106f92f943331e4d035b3c79d9eae5a337135ced490a0deb15",
+    "libopenh264-6_2.2.0+dfsg-2~bpo11+1_arm64.deb": "9c2675bca36718ebb64f937a8d5a943056beefd8c77aa98890c9d56a7b99105d",
+    "libopenjp2-7_2.4.0-3_arm64.deb": "4fb3637093bbbde4499f1344b399c6eb61bbe51bdc3c40a09c5fcc1efec660cb",
+    "libopenmpt0_0.4.11-1_arm64.deb": "a78a20536f771b360f757999dc02e2c6c9033491afd5f7875d0561ec28caf19e",
+    "libopenni2-0_2.2.0.33+dfsg-15_arm64.deb": "be1312e6cbb242b8f9886d821df570ab831c50b6c8e68d39b8a7b8eb51ab4af3",
+    "libopus0_1.3.1-0.1_arm64.deb": "86d96e6e99820be150e4e1d335cf8503c5802a3ac47103ba25eebf77a0699a13",
+    "liborc-0.4-0_0.4.32-1_arm64.deb": "45948981ff4afa3890eca22edf709445088b301b593a3ad3eb5f8e524f661261",
+    "liborc-0.4-dev-bin_0.4.32-1_arm64.deb": "c857edaf37f9cc95bcbcb70665a182c4e65cb8e9fa164bca29e8cc3788e518b7",
+    "liborc-0.4-dev_0.4.32-1_arm64.deb": "3ecdc91537147154a7602e9923280f70ff5e4c6fb7f1c7a927c31d1af063ad8f",
+    "libp11-kit0_0.23.22-1_arm64.deb": "ac6e8eda3277708069bc6f03aff06dc319855d64ede9fca219938e52f92ee09c",
+    "libpango-1.0-0_1.46.2-3_arm64.deb": "6bbb5d942bf5075e07ba2290687cf03939e29dd89c67cba4d868a5d5ca94d360",
+    "libpangocairo-1.0-0_1.46.2-3_arm64.deb": "6947061235f6a3fce541b985ae38509298f7b46299e31fd985d7c596e31e4bbf",
+    "libpangoft2-1.0-0_1.46.2-3_arm64.deb": "75c9b7f28b80822b6e4edea5e2235257f877ac6cade7930c6683e24395e952ec",
+    "libpcre16-3_8.39-13_arm64.deb": "8c254c9f1962db4e8c38b077fce549e00f9201c25936f1b9c9ca36d6aa82e2f3",
+    "libpcre2-16-0_10.36-2_arm64.deb": "59f303e7e77e89c6a4ee906e79b312362bd8e4e6bbb0f3aaff881b3e5d0dc577",
+    "libpcre2-32-0_10.36-2_arm64.deb": "6e1695eb7c4947d74c257f55937aece65c7ac077c6f02fca98a88082d8d6d087",
+    "libpcre2-dev_10.36-2_arm64.deb": "059ecaad9508c57513ef1d298d39091fbd5d017e30e10a0702516ec8b02998ac",
+    "libpcre2-posix2_10.36-2_arm64.deb": "3fed2511147e36670758ad4853c2527340cd354e88de2b9f1797b74101f58e34",
+    "libpcre3-dev_8.39-13_arm64.deb": "fc10133538dd98895d466b2f273e5373e5e44cac832ed588c517cf3a795c5724",
+    "libpcre32-3_8.39-13_arm64.deb": "a5eeebe0a2d4106558c6c626b4297055a38bd67059b9a0afcf04c40602e4a6ab",
+    "libpcre3_8.39-13_arm64.deb": "21cac4064a41dbc354295c437f37bf623f9004513a97a6fa030248566aa986e9",
+    "libpcrecpp0v5_8.39-13_arm64.deb": "266be5873eda3f30867f26028636d11d6cf25e0147467bdcac2c65e4a90c4ffd",
+    "libpgm-5.3-0_5.3.128~dfsg-2_arm64.deb": "6a632aa13cafe154d6212a57710f058405dd895fb62d464e16961b28d7325cbb",
+    "libpixman-1-0_0.40.0-1_arm64.deb": "0be923132af8fa7102c09a3c8d200cd8475b633a9a5f1609f7ad653851fb6448",
+    "libpng-dev_1.6.37-3_arm64.deb": "15da2d4389f62ac5daf0f91f0a34db8007986ce77f079ea1f6f2ee92ea6a620d",
+    "libpng16-16_1.6.37-3_arm64.deb": "f5f61274aa5f71b9e44b077bd7b9fa9cd5ff71d8b8295f47dc1b2d45378aa73e",
+    "libpoppler102_20.09.0-3.1_arm64.deb": "cb9d93e535446d9662722614109a3d9c2df89dd970a473623f31895c37be1513",
+    "libpq5_13.5-0+deb11u1_arm64.deb": "84daa5d6407c51e5cab185d053c896c58a6035e05157adb197d077f2d417f7af",
+    "libproj19_7.2.1-1_arm64.deb": "535a698960d6fe73de45099fa289b7e9382319b8ea3ab3ec535db52f893e7af2",
+    "libprotobuf23_3.12.4-1_arm64.deb": "f3935bf86962ef3df68aca17c9f1fa0a55a9a0c74a78a225c2dbf78ed5746cfa",
+    "libproxy1v5_0.4.17-1_arm64.deb": "b81ca7f47e384ed7117a75cfe9798bd11d0085b1b5edce4f9ffcf4e678e24b71",
+    "libpsl-dev_0.21.0-1.2_arm64.deb": "b5aac74d63fae3f6a4e835f3f4cea23f62efb4bb32421fb4fee9eb2d57b1da8c",
+    "libpsl5_0.21.0-1.2_arm64.deb": "12637647316e770c37a4bfec7aef27ed472f2850b5f59dd508505dda32519584",
+    "libpthread-stubs0-dev_0.4-1_arm64.deb": "acd58ed5d26d3f4d4b0856ebec6bc43f9c4357601a532eee0d2de839a8b8952e",
+    "libpulse0_14.2-2_arm64.deb": "bb6533d2f67cd7fb94bf2ae1d7f13ae6b3c5b70c098038f21bc47fce30c88621",
+    "libqhull8.0_2020.2-3_arm64.deb": "e42e31c799c6017158dcc6b5fce9ccc68550b54255df50f52c947a34f2a63943",
+    "libqrencode4_4.1.1-1_arm64.deb": "57cb902bf90c5d939ff6b72d7beb821c8be0d13be11c8eea3341005fdeccd7cf",
+    "librabbitmq4_0.10.0-1_arm64.deb": "9460d0c3c016883bd5bfb5e66a47cae445f93e2e6f43c1c951738ab51d2ec982",
+    "libraw1394-11_2.1.2-2_arm64.deb": "81a1e9bd2b790aa7ce7c426dd1742d3310894b01f124bc41962331b834da6cc3",
+    "libraw1394-dev_2.1.2-2_arm64.deb": "84d7280c5e0d38a77c7d8ccc6d5d2986b42bd64245533947aa9c684077fa7243",
+    "libreadline8_8.1-1_arm64.deb": "500c3cdc00dcaea2c4ed736e00bfcb6cb43c3415e808566c5dfa266dbfc0c5e5",
+    "librest-0.7-0_0.8.1-1.1_arm64.deb": "c1b636d7a66996743d248cb9e6042167b03b442150d39b349b12348a31690ee0",
+    "librsvg2-2_2.50.3+dfsg-1_arm64.deb": "79688f922da4c45e546c77667f1ea9480e3b96dd65e8eef88bcb44ef335cd6bd",
+    "librtmp1_2.4+20151223.gitfa8646d.1-2+b2_arm64.deb": "a3a1a6e4b02bcd3254e932b1972ed744082fd7dd5cc1545eec3dd3d539ce6c93",
+    "librttopo1_1.1.0-2_arm64.deb": "f20685ed048635be16e8abf790e14d7e88bb4e2335d371dff631b4adbdebc6a8",
+    "libsamplerate0_0.2.1+ds0-1_arm64.deb": "2217f60d0e8e10b966811328a50bd6ac4c33ec32c80d9fd92d699a2185dcb629",
+    "libsasl2-2_2.1.27+dfsg-2.1+deb11u1_arm64.deb": "fc4c943224b8fb6aaa86439ff60dcec4ca1aeb4730f121e594c68a37b3e0c88f",
+    "libsasl2-modules-db_2.1.27+dfsg-2.1+deb11u1_arm64.deb": "006239b28681f507db0937125a13810c8cf03e3fffe9b7c8433445af86805d12",
+    "libsbc1_1.5-3_arm64.deb": "42da4d0df79a713822a71abf87e29a7fd56650870e68ee0242db4d78bce8eefb",
+    "libsdl2-2.0-0_2.0.14+dfsg2-3_arm64.deb": "84044621db62626a6e57c0def89db5efb9a5ebda0eec6fa68a2c90e79a7d46c2",
+    "libselinux1-dev_3.1-3_arm64.deb": "91e9c58f4a33e44ea0543d2e2d9e259d6833fbfa13704bcff685ed6a3d7c8eec",
+    "libsensors-config_3.6.0-7_all.deb": "4265811140a591d27c99d026b63707d8235d98c73d7543c66ab9ec73c28523fc",
+    "libsensors5_3.6.0-7_arm64.deb": "fd5533c6293d881d67e488ad0549c0b0351e1b770037a08018e5e88996cb95dc",
+    "libsepol1-dev_3.1-1_arm64.deb": "2b01df0d3a8b1d94a9a4cb925219d7b156ae9e089c2dc07791af6170c7bd44cf",
+    "libsepol1_3.1-1_arm64.deb": "354d36c3084c14f242baba3a06372a3c034cec7a0cb38e626fc03cc4751b2cd3",
+    "libserd-0-0_0.30.10-2_arm64.deb": "d0090cf9f8855b5af6f18758933fb2ae2348d8ca383745510638f6a4cbb3a898",
+    "libshine3_3.1.1-2_arm64.deb": "d64982ddfbdfcef714dc35a760a752ab1f3739aaee8011bc5c3f03afa73f465f",
+    "libshout3_2.4.5-1+b1_arm64.deb": "6b4a6d87737d98ccfbe2b392fed75b3bc492932b7f345ffb8613a79a9f8db367",
+    "libsidplay1v5_1.36.60-1_arm64.deb": "6ed3b3a8903b75bf22ac40d3fcaf0f7a476592913fba38db9ebf0b0d4ce7c328",
+    "libslang2_2.3.2-5_arm64.deb": "61c51c2ff17b38a0791193460df9a09a5f5a90ece01483202d0667059814503f",
+    "libsnappy1v5_1.1.8-1_arm64.deb": "48cb00030ca2e87780f815e6b5e354c76642c6de5c7a287a18d70b24880c70d1",
+    "libsndfile1_1.0.31-2_arm64.deb": "35cd1ede25dda91abdfd23bc02fbfe9afc72e2a11178bebcc5ef76601a2a60b7",
+    "libsndio7.0_1.5.0-3_arm64.deb": "5d3fcdcde0de0021ee217769e0866dcd524734a04f961c2ecc33488db47ba545",
+    "libsocket++1_1.12.13-11_arm64.deb": "ea74ce1260340fedc20131f4f8183c5ee55fdc4e153c2850bceb0fc7173f4de3",
+    "libsodium23_1.0.18-1_arm64.deb": "f00a5c6ccaeb6a70bc07c7a56d61c605281c2b1cbea29611b69dfc789274db0c",
+    "libsord-0-0_0.16.8-2_arm64.deb": "af1aa4057c185c1f7b07d14d1b2711cadd572bbc4dc17ce1f1144180974e83c1",
+    "libsoundtouch1_2.2+ds1-2_arm64.deb": "c44e330c7f54de52a0fa695374536d24a093ff34116c250002066de84570019a",
+    "libsoup-gnome2.4-1_2.72.0-2_arm64.deb": "7e0aacd0e2e484844b86fce2dcaf842ff95f209ad353c52d685ed1b2bc346eb2",
+    "libsoup2.4-1_2.72.0-2_arm64.deb": "c11fd27b2cd0639d642d1b852c6e89f94742228d5a866d1e32f122bdb2b324d1",
+    "libsoup2.4-dev_2.72.0-2_arm64.deb": "f11b45fde07c3f94a756287dfc5d24850ef8ca02c84ced03bd81145c90307195",
+    "libsoxr0_0.1.3-4_arm64.deb": "8f1b1780a5ced7a0821a8040d0f3ab5815acd5d2f38b12cca915a9a9d0ee28b7",
+    "libspandsp2_0.0.6+dfsg-2_arm64.deb": "6c58a18a6a4e9469e677f3dfe256a481db7d2909c16f4ebc4d2d0dd3f869d191",
+    "libspatialite7_5.0.1-2_arm64.deb": "0268c4e54141c222e6ae6984f1c402f78a806e0e70e411235524445b0e2beb2a",
+    "libspeex1_1.2~rc1.2-1.1_arm64.deb": "5bdef83a0a8f0afda640f0166a64f9160c459faa0c2681f9cef7a96933733340",
+    "libsqlite3-0_3.34.1-3_arm64.deb": "1e33cd39dc4fff2a7edd7bb7e90a71e20fb528f6a581fe0287652e4dae77e0d0",
+    "libsqlite3-dev_3.34.1-3_arm64.deb": "02245250f77ea400f0de1532248b8696c8ddb282e1fbba17f1b5410f823624ee",
+    "libsratom-0-0_0.6.8-1_arm64.deb": "32fd27b9233db0895f6da92db5d3d58dbdde0f6d3e58e49683553e4fae46dc41",
+    "libsrt1.4-gnutls_1.4.2-1.3_arm64.deb": "08a94b07becea150a1385614fb7b4c1b09f864db5ab8185b586d099ffdf11b8a",
+    "libsrtp2-1_2.3.0-5_arm64.deb": "0b3195777f70d1a1c9bccb7d2a6bcf6075043ba1d4bdb5d9800946bb49e84b1e",
+    "libssh-gcrypt-4_0.9.5-1+deb11u1_arm64.deb": "ab5a221194b84dbbb75961555efd98eca8d84d568a15dd971a8c5579c0c4d9dd",
+    "libssh2-1_1.9.0-2_arm64.deb": "c3847ce093a395c4425f23c0a1601516248e2d241bedaab94ecd9686536214a7",
+    "libstdc++6_10.2.1-6_arm64.deb": "7869aa540cc46e9f3d4267d5bde2af0e5b429a820c1d6f1a4cfccfe788c31890",
+    "libsuperlu5_5.2.2+dfsg1-2_arm64.deb": "28d7257ec368508de05df08d87bdf1a535469758744e20ed93fc7c401b2740b9",
+    "libswresample-dev_4.3.3-0+deb11u1_arm64.deb": "790d775017119434a6eddbb68b85e29392119aaae5ed52bbe7a2734a45cb0b64",
+    "libswresample3_4.3.3-0+deb11u1_arm64.deb": "58ae7a8c97a8763ef3e3352915f65b9bb2bbfa1c977042fb7761a89c2f1f1e93",
+    "libswscale-dev_4.3.3-0+deb11u1_arm64.deb": "bbf45093f7ca81b3b3f3cfdcac5c01546e0ae5e7f99029361ab1b78672917c73",
+    "libswscale5_4.3.3-0+deb11u1_arm64.deb": "e29e8d8a7e4f1ad8f71e1c0b05859e1fe6840cbe36a3955aaf8536b587dfd028",
+    "libsystemd0_247.3-7_arm64.deb": "365b1d9bc9933d8cdcf8ca2d68e8751719e43c4b2a93a5cfce5ebb124f02decb",
+    "libsz2_1.0.4-1_arm64.deb": "8ed09313ff4408291fd0e3942b4129280babf882f2d3fe8ba48aac4be6def051",
+    "libtag1v5-vanilla_1.11.1+dfsg.1-3_arm64.deb": "5258f2fc45789d1f057e5a4adfff0ee07e1079c8435bf9e38e224e50ac8be45d",
+    "libtag1v5_1.11.1+dfsg.1-3_arm64.deb": "a19a754738f4880143e1fa38aa4540fc7bcca77041da77056b8ee79c63cc9be9",
+    "libtasn1-6_4.16.0-2_arm64.deb": "a89b659a3cf2d040885a7d00f3c547b6e362cdfeb5f89d0c777495d82a58e64f",
+    "libtbb-dev_2020.3-1_arm64.deb": "85bc6b8697aac9f2c481589de3471ca44402c383b3738304fc325ea8ec776b14",
+    "libtbb2_2020.3-1_arm64.deb": "c69b242838b3829c16b9580fa218387f0f0f7f976062df8a66c7d9c0a6e95a9e",
+    "libtcl8.6_8.6.11+dfsg-1_arm64.deb": "39047359624bb8229a0e26291ad56012ae6ad17e443e73dd9f38635102c9a0e4",
+    "libtesseract4_4.1.1-2.1_arm64.deb": "762932cc1bc533cf6d1f6ad08c108fb1f5f13131cc4c5a37ce777e5715fb56a0",
+    "libthai-data_0.1.28-3_all.deb": "64750cb822e54627a25b5a00cde06e233b5dea28571690215f672af97937f01b",
+    "libthai0_0.1.28-3_arm64.deb": "e7ac0d861936385cca0ea7e3b9b04d20e85a7dfbfaa801e093d9f7fcbcf841f6",
+    "libtheora0_1.1.1+dfsg.1-15_arm64.deb": "e1ca65eaa5c90af2f88a5ba157e4b61b38b3cdf7ca8b83f20db4ee2dc271c344",
+    "libtiff-dev_4.2.0-1+deb11u1_arm64.deb": "7a399d4215c787f8f78abec0d96822559a16d33d73750d588079c7445da02bef",
+    "libtiff5_4.2.0-1+deb11u1_arm64.deb": "fdb29f5dcae827ef3485c4395f2937e8880e3022fb72757965888e83bb096b6f",
+    "libtiffxx5_4.2.0-1+deb11u1_arm64.deb": "5126597feca5b7f97699c1f38596be99b9db7422c6c521b8270d5a0389606654",
+    "libtinfo6_6.2+20201114-2_arm64.deb": "21c0c33e00d091d0f326a083a77531270b8c56468500f0948d149f3e20b95385",
+    "libtk8.6_8.6.11-2_arm64.deb": "c189bdef93e4caebd58f952e99dfd1922ca182996fd4d0b4b33fb5fa5828dfa8",
+    "libtwolame0_0.4.0-2_arm64.deb": "73780c555796569a252ec5cf1ea919e59ef4031298bc4be1516a6a0e83f06b5a",
+    "libudev1_247.3-7_arm64.deb": "4c5d5b6736499d39eb87bf135f9bb6bc2051d945eb52644bbf52a26f7f8e6504",
+    "libudfread0_1.1.1-1_arm64.deb": "a54c888952f0eab05024e26a09cef78182144f7f11763af5d24c3947a8002648",
+    "libunistring2_0.9.10-4_arm64.deb": "53ff395ea4d8cf17c52155a452a0dc15af0ee2fa5cb3b0085b9c7335de8d5f7f",
+    "libunwind-dev_1.3.2-2_arm64.deb": "1256cc79e6614668240886eba0dfbc65ffe159a08b420dc2c515237891ab60a9",
+    "libunwind8_1.3.2-2_arm64.deb": "2a93283482cd89b6e6d3e1ee16497c911d6aacdc7414b9d24ca6851e221cf66c",
+    "liburiparser1_0.9.4+dfsg-1+deb11u1_arm64.deb": "4a184076b07eb3f6a409db354b651b5c024e3c783e35702038782c4300dbd037",
+    "libusb-1.0-0_1.0.24-3_arm64.deb": "61ca0a0412c4182cb007f4e447608857cd4c70ddbe730ecda15a495de7d2178f",
+    "libuuid1_2.36.1-8+deb11u1_arm64.deb": "3d677da6a22e9cac519fed5a2ed5b20a4217f51ca420fce57434b5e813c26e03",
+    "libv4l-0_1.20.0-2_arm64.deb": "d56810fc6ac3877da7731681e90fc1dd24d8288ba4ea0f29ff2751c0aa248e34",
+    "libv4lconvert0_1.20.0-2_arm64.deb": "d2da0adaf32562e9733561a67d06ba5d6ecb8a099033c4a3cdb257cc5b205e76",
+    "libva-drm2_2.10.0-1_arm64.deb": "45bf4248d72ce42940ee658bbba3025db0a233587bc49d773b1e3e9606d613a3",
+    "libva-x11-2_2.10.0-1_arm64.deb": "d6a845c9456a8b22366a25cbcd68a4ced425243a924e4b0048dc82f8b1f8853c",
+    "libva2_2.10.0-1_arm64.deb": "6dbb204d43bcaed495719fc6a793dcbce77f4ab12bc0ea4617ae4e816139d5a1",
+    "libvdpau1_1.4-3_arm64.deb": "9f44679cee3e9ed99525b2becff5b0ef040736b1a450e68fd3e2e536de52b99a",
+    "libvisual-0.4-0_0.4.0-17_arm64.deb": "1173621505fdc74b88210c7fffec97a7ffbbdc76b85f2392edeff3e6e7b114d0",
+    "libvo-aacenc0_0.1.3-2_arm64.deb": "b86d13dae80e35813d01492f4056aa924b08b41698468ab1d17425860f3d0151",
+    "libvo-amrwbenc0_0.1.3-2_arm64.deb": "9f359ac83bf72104f29584f0c9efd5c440a592f5d6ad24fb785213718084f624",
+    "libvorbis0a_1.3.7-1_arm64.deb": "2f902ae456bcada7b0d494d7bd7c994feb81c4158209d8a12c0b2d9e255edda7",
+    "libvorbisenc2_1.3.7-1_arm64.deb": "f1089e220c81e267caec859bf2e440bb78ed9f318bbb51cfd6c85d35bf80144b",
+    "libvorbisfile3_1.3.7-1_arm64.deb": "f8f418e15f99905d4a2d532617511a11d700e814f8ead1a883deea2f7241970c",
+    "libvpx6_1.9.0-1_arm64.deb": "878f04bcd1089f8f2aa47d20b7ec4922877e31e562c90f613756d5452b3814fd",
+    "libvtk9_9.0.1+dfsg1-8_arm64.deb": "3271f972b20a26fed4781466dbaff083833c30208ac7b71376d96dec2a402aa2",
+    "libvulkan1_1.2.162.0-1_arm64.deb": "303d48229ccab57342ad9cc36174429ee7b8b0cdb6e6e292b87ce3415fe5d65a",
+    "libwavpack1_5.4.0-1_arm64.deb": "81317be561d0cdb1239225772a8a9260d8178634a41263f072c340448c029490",
+    "libwayland-bin_1.18.0-2~exp1.1_arm64.deb": "79a850a284b308d14d5031c6e78ec566f1ab4437bde889140f96333d96cb0674",
+    "libwayland-client0_1.18.0-2~exp1.1_arm64.deb": "7bde74828c3508b5b81400c4b9ef51d65b566d7645747b777ec171eb8ecfce47",
+    "libwayland-cursor0_1.18.0-2~exp1.1_arm64.deb": "1c201b449abf41e864dd48c960c9ef7f180b5164a3e7562ae5fd467d60fd074e",
+    "libwayland-dev_1.18.0-2~exp1.1_arm64.deb": "2357033bfe3cee735220abb527ca0707a31dc2813defe7e67d738882607ecdb9",
+    "libwayland-egl1_1.18.0-2~exp1.1_arm64.deb": "8b5e2d8975330f308c226e3b51624a76ea8ebe5115dd8e929e05ea23c4169008",
+    "libwayland-server0_1.18.0-2~exp1.1_arm64.deb": "3bbcb9e41159b6268b40ce16b1512e5bcf38e63b8bcf2426eaa54b19c6988257",
+    "libwebp6_0.6.1-2.1_arm64.deb": "c4e7e63f283aaa9913ac78b9871434f543f87ff4641a9a1737e86a0b32a679a7",
+    "libwebpmux3_0.6.1-2.1_arm64.deb": "bf59f5c2e958af997f9754f78de0ed48178b4c33688c354b52ffdcb970876ca8",
+    "libwebrtc-audio-processing1_0.3-1+b1_arm64.deb": "f074066f53da5141ed8b5a221b7a38344f43504f13cda4597ac589e8bca914e9",
+    "libwildmidi2_0.4.3-1_arm64.deb": "09f041820a0e2e5e19ef541a7dc88521067c266561e6f7fa24accb5dcaefafc8",
+    "libwrap0_7.6.q-31_arm64.deb": "8e57d2f27eea81a61ea144e8c0b8301c3ab4a6195dc2366c2c703d49b7b79e7d",
+    "libx11-6_1.7.2-1_arm64.deb": "8872962f2a0a6b9e16cafc6acd2be56cee4ec7a16c2a0abdb9fcda6d0b31be3b",
+    "libx11-data_1.7.2-1_all.deb": "049b7eabced516acfdf44a5e81c26d108b16e4987e5d7604ea53eaade74027fb",
+    "libx11-dev_1.7.2-1_arm64.deb": "be35e106b9dc900b6a2e84fdb346c3a53a8440153a5e1708f0a9af473b31b0de",
+    "libx11-xcb-dev_1.7.2-1_arm64.deb": "20d59a5c097ffaf7cc206d017f8c3364ce5b0f8db39db13e2c57fcc01abe0eff",
+    "libx11-xcb1_1.7.2-1_arm64.deb": "4714e973f35a3fa8e4687ec86d9239bead13ebdc3990173d5520afa7504ab65d",
+    "libx264-160_0.160.3011+gitcde9a93-2.1_arm64.deb": "3e379d7147e548a43f8a4ca4fb291d839d0a3d8cc07df73e5cbc88b856818b92",
+    "libx265-192_3.4-2_arm64.deb": "4da85b00f95645ab01832af0bec627534608481a6f181c0fbbabeb5c04c2a1cc",
+    "libxau-dev_1.0.9-1_arm64.deb": "21db3761a8f388fc83707c00fac645d4332737a859dd727571e1e6ede003b048",
+    "libxau6_1.0.9-1_arm64.deb": "36c2bf400641a80521093771dc2562c903df4065f9eb03add50d90564337ea6c",
+    "libxcb-dri2-0_1.14-3_arm64.deb": "b99d1f4909d0a3fe85dfba6524fb0a8c4d08703366ed99f034417684e91ded5b",
+    "libxcb-dri3-0_1.14-3_arm64.deb": "9a6dc75fd8e9fe720f62f99cfebef0cbe51cf2909aa5e777254cb6a3ddf47f6f",
+    "libxcb-glx0_1.14-3_arm64.deb": "754902f3399fd5c9c851dbf150306f537acab7152b9f32206a7910b7c3f353e1",
+    "libxcb-present0_1.14-3_arm64.deb": "9e84b49e46833fd3fb366b04d19c652a0e8d2ebe3d40f19f9b530763ba2f7b88",
+    "libxcb-render0_1.14-3_arm64.deb": "e794ba2657c5f21dcca327343b41b1997a150b6ac27977970404d60f471be48a",
+    "libxcb-shm0_1.14-3_arm64.deb": "e7f59fc41744fe6b8b9ba97b262a051621173689e2a3e5ebb26dc253c9bdc48b",
+    "libxcb-sync1_1.14-3_arm64.deb": "ba70a2194fa7875f1a3ef6fb31bdd4cca5ca970926e3201994a166c5026ce442",
+    "libxcb-xfixes0_1.14-3_arm64.deb": "8185fca98fda89fc5a020b9a8cd92b093929830a29cf92e6a1eba04bd88bcf5f",
+    "libxcb1-dev_1.14-3_arm64.deb": "43ebe78dd7f66933286fe68fd7ccc650ef14464d479fff13e635e95d1f742efa",
+    "libxcb1_1.14-3_arm64.deb": "48f82b65c93adb7af7757961fdd2730928316459f008d767b7104a56bc20a8f1",
+    "libxcomposite1_0.4.5-1_arm64.deb": "cfe39326fdb822e9d060ed5eb3f95b14459dd6b73793c5290000f9b27f8bad37",
+    "libxcursor1_1.2.0-2_arm64.deb": "2e309833ccf7e6b62560240cd84e325cafcb2ce70b1fb297469957360cee4478",
+    "libxdamage1_1.1.5-2_arm64.deb": "696cb56f414e7c0ea9a3bcbcb63b07f6ed8e980023c1b35006d5c1dc0d0213ed",
+    "libxdmcp-dev_1.1.2-3_arm64.deb": "6fd0bc51f3a4e6b7f944f24f15524a4c4ba68a4b0aa136c18bdb96a2d36988f2",
+    "libxdmcp6_1.1.2-3_arm64.deb": "e92569ac33247261aa09adfadc34ced3994ac301cf8b58de415a2d5dbf15ccfc",
+    "libxerces-c3.2_3.2.3+debian-3_arm64.deb": "59311a05cc1d2c693840db899c02c0e8d1ba2e9e66961e8284053255d4c38866",
+    "libxext6_1.3.3-1.1_arm64.deb": "57237ecf54662372e206b154c0ab6096e05955e048552575b45d3ad14a6ff6e5",
+    "libxfixes3_5.0.3-2_arm64.deb": "eb70f12af1d13e1632ee29ddf103617af00a078faf6c3a2531ab3d01b395606b",
+    "libxft2_2.3.2-2_arm64.deb": "6941176bcc78bf02d1635575a8cc726a7eb0628d8476efca7607718bdc1f50c5",
+    "libxi6_1.7.10-1_arm64.deb": "0a6788844441f160d970fc7d61004607fe92cfad8966d0b371291703201b3971",
+    "libxinerama1_1.1.4-2_arm64.deb": "bbdef95d025fb804797a5b1a71f319f5f74c0114e90d759b3e9ecf7654442598",
+    "libxkbcommon0_1.0.3-2_arm64.deb": "91c19f642af34ae8cb909d00d08bc83f0b4f8e87ddde6984bd8bff0f7bf83204",
+    "libxml2-dev_2.9.10+dfsg-6.7+deb11u1_arm64.deb": "1532024cfe6074d16c558dfbafdd669cc1517e21a5268e130b401f0c409355a4",
+    "libxml2_2.9.10+dfsg-6.7+deb11u1_arm64.deb": "8a1bae68267cc508cd13662f5fd37f9b445e199b21a3931e3f20be8540495950",
+    "libxpm4_3.5.12-1_arm64.deb": "48ae9f8f91e36956e25bf724fc0fb815ce6202ca610570bd6eb5a077f3580b5a",
+    "libxrandr2_1.5.1-1_arm64.deb": "04c9c59ba1e9648e0ec80d66fa34bab7bf039822f32eb31f706656968052e915",
+    "libxrender1_0.9.10-1_arm64.deb": "fcae69900b599e7b31b31eafa203a184e00376ade1d2f74f9b3d7b24991573a0",
+    "libxshmfence1_1.3-1_arm64.deb": "fa2edaae6902681ecd02534dcdf8389ac48714d3385d572cc17747160957acc8",
+    "libxss1_1.2.3-1_arm64.deb": "e0ff80e309eacda5face68b9a3bd56718fed2750af429324c35d7c9491c335f4",
+    "libxvidcore4_1.3.7-1_arm64.deb": "b950edcb42803f158f94cd2ee44850082c98c92d54e20982ad933936f5f1d181",
+    "libxxf86vm1_1.1.4-1+b2_arm64.deb": "8a4826772ac480804d6b8d776a4130d95260c036b9b218de4c8a0f07eb9c5bba",
+    "libz3-4_4.8.10-1_arm64.deb": "ae1ea58ecfdd4b5ec53d734e60ac2df37fddb11888b7730a19335f1a9b09f489",
+    "libzbar0_0.23.90-1_arm64.deb": "954f155683b38978edeb674e1492dbcb1c8987adf23a72f1987074dd6fa25004",
+    "libzmq5_4.3.4-1_arm64.deb": "e5eaecc454d6792848a2ff866d73e3123b9ccdd08c9bd9e6096b52f08e484a13",
+    "libzstd1_1.4.8+dfsg-2.1_arm64.deb": "dd01659c6c122f983a3369a04ede63539f666585d52a03f8aa2c27b307e547e0",
+    "libzvbi-common_0.2.35-18_all.deb": "53ed21370b937a9385e1fcf1626400891bd4fd86a76b31654fb45e0875d8bfb8",
+    "libzvbi0_0.2.35-18_arm64.deb": "40626984d48d486b62f08c255eb9397a04f52ca2983e8d689e5de5e94e29a4b1",
+    "libzxingcore1_1.1.1-2_arm64.deb": "c88bc936cb0ac8bde1bfdae8e9bc1d1e0c8f2e93454245bc884f9b81c49e7246",
+    "lsb-base_11.1.0_all.deb": "89ed6332074d827a65305f9a51e591dff20641d61ff5e11f4e1822a9987e96fe",
+    "mariadb-common_10.5.15-0+deb11u1_all.deb": "a98b12228a79f29c588cb621f0fee395b263fcfc5037b912a22e8b168be3550d",
+    "mysql-common_5.8+1.0.7_all.deb": "22b3130e002c2c2fa6a1124aaccbe3a6ddbbb4d6bf03beed8a6f044027dcb720",
+    "ocl-icd-libopencl1_2.2.14-2_arm64.deb": "988af69eca9c4b7433572d11ecbc048a7680ae15afa78941782945b18ff185d7",
+    "odbcinst1debian2_2.3.6-0.1+b1_arm64.deb": "3b22a944687b3007a3484673a68e2a4483fd9237530b0ec359e423c8a745ca0e",
+    "odbcinst_2.3.6-0.1+b1_arm64.deb": "c0a9b4ade83bc9a28967e1e981cd1c93f9e6654cf6d53f1fa86db84b3bb8ad89",
+    "perl_5.32.1-4+deb11u2_arm64.deb": "625a2d0cafb5089953012d60d3a5ba726b692d9d49955a251b78b8cce884d05b",
+    "pkg-config_0.29.2-1_arm64.deb": "074d64f7a6bb5fb9139661aea20815438d8ffe8d7bb44b7c3f58e220c153fdbd",
+    "proj-data_7.2.1-1_all.deb": "40c64f7808d8233c08f3aa2745211e705828b4ae6fc5dbd62a934d8c3e9fd6e5",
+    "python3-distutils_3.9.2-1_all.deb": "05ec4080e0f05ba6b1c339d89c97f6343919be450b66cf4cfb215f54dcb85e58",
+    "python3-lib2to3_3.9.2-1_all.deb": "802c198e5dd0b5af85a6937e426a85d616680785e8d18148fac451281a83a9a9",
+    "python3_3.9.2-3_arm64.deb": "79197285d25e73a2a07667efe80af152dd932ac5ef3e13717f1ac824d111ea81",
+    "readline-common_8.1-1_all.deb": "3f947176ef949f93e4ad5d76c067d33fa97cf90b62ee0748acb4f5f64790edc8",
+    "sensible-utils_0.0.14_all.deb": "b9a447dc4ec8714196b037e20a2209e62cd669f5450222952f259bda4416b71f",
+    "shared-mime-info_2.0-1_arm64.deb": "33257bc679bee7b2627a001eb747f3378ea8c8063863d2cb6edb6ad7f37f280a",
+    "timgm6mb-soundfont_1.3-5_all.deb": "034abdfb296d9353433513dad5dbdcab46425ee6008fc02fe7039b46e75edc54",
+    "ttf-bitstream-vera_1.10-8.1_all.deb": "ba622edf73744b2951bbd20bfc113a1a875a9b0c6fed1ac9e9c7f4b54dd8a048",
+    "tzdata_2021a-1+deb11u3_all.deb": "61346a9f8cda14c34251d2440c8de8dab7c09bda7ebb96533166b4567359de66",
+    "ucf_3.0043_all.deb": "ebef6bcd777b5c0cc2699926f2159db08433aed07c50cb321fd828b28c5e8d53",
+    "uuid-dev_2.36.1-8+deb11u1_arm64.deb": "efbc2d26a728bb6212b52bf9c1faa7ee51159068072e2db0a383919ded7f308f",
+    "x11-common_7.7+22_all.deb": "5d1c3287826f60c3a82158b803b9c0489b8aad845ca23a53a982eba3dbb82aa3",
+    "x11proto-core-dev_2020.1-1_all.deb": "92941b1b2a7889a67e952a9301339202b6b390b77af939a26ee15c94ef4fad7e",
+    "x11proto-dev_2020.1-1_all.deb": "d5568d587d9ad2664c34c14b0ac538ccb3c567e126ee5291085a8de704a565f5",
+    "xkb-data_2.29-2_all.deb": "9122cccc67e6b3c3aef2fa9c50ef9d793a12f951c76698a02b1f4ceb9e3634e5",
+    "xorg-sgml-doctools_1.11-1.1_all.deb": "168345058319094e475a87ace66f5fb6ae802109650ea8434d672117982b5d0a",
+    "xtrans-dev_1.4.0-1_all.deb": "9ce1af9464faee0c679348dd11cdf63934c12e734a64e0903692b0cb5af38e06",
+    "zlib1g-dev_1.2.11.dfsg-2_arm64.deb": "1e6ed652eebd3761454d75db5459c247cd5d29e58bbe9f6b4b62a62d96f7c279",
+}
diff --git a/debian/gstreamer_backport_notes.txt b/debian/gstreamer_backport_notes.txt
new file mode 100644
index 0000000..c543a0f
--- /dev/null
+++ b/debian/gstreamer_backport_notes.txt
@@ -0,0 +1,67 @@
+Some notes on backporting gstreamer 1.20 from testing (bookworm) to bullseye
+
+Create chroot build environment. This is kept clean - each build step uses this as a base image and overlays files on top of it.
+    sudo sbuild-createchroot --extra-repository="deb http://deb.debian.org/debian bullseye-backports main" --chroot-prefix=bullseye-backports bullseye /srv/chroot/bullseye-backports-${ARCH} http://deb.debian.org/debian
+
+Download sources for each library. I worked out of a directory "$HOME/backports"
+dget -x ...
+    http://deb.debian.org/debian/pool/main/g/gstreamer1.0/gstreamer1.0_1.20.1-1.dsc
+    http://deb.debian.org/debian/pool/main/g/gst-plugins-base1.0/gst-plugins-base1.0_1.20.1-1.dsc
+    http://deb.debian.org/debian/pool/main/g/gst-plugins-good1.0/gst-plugins-good1.0_1.20.1-1.dsc
+    http://deb.debian.org/debian/pool/main/g/gst-plugins-ugly1.0/gst-plugins-ugly1.0_1.20.1-1.dsc
+
+    http://deb.debian.org/debian/pool/main/libn/libnice/libnice_0.1.18-2.dsc
+    http://deb.debian.org/debian/pool/main/o/openh264/openh264_2.2.0+dfsg-2.dsc
+    http://deb.debian.org/debian/pool/main/libf/libfreeaptx/libfreeaptx_0.1.1-1.dsc
+
+    http://deb.debian.org/debian/pool/main/g/gst-plugins-bad1.0/gst-plugins-bad1.0_1.20.1-1.dsc
+
+
+Enter each directory of sources, modify with `dch --bpo`. Use default. This will append '~bpo' which in the debian version string is the "type" of the package. This is by convention and ensures correct version resolution if these packages are upgraded in the future.
+
+Build the packages in this order to resolve dependencies between them. I found this minimal set could be built against bullseye directly - the only dependencies with unmet versions in bullseye are libnice, libopenh264, and libfreeaptx, so we backport those as well.
+
+For each package we append set --debbuildopts="-v<version number>". Version number should be the previous release that found in bullseye. This generates the proper changelog between that version and our backport's version. sbuild will automatically resolve all dependencies possible with apt. For dependencies between our backports we specify --extra-package. We could setup an apt repo as we do in the last step but it's probably not worth the effort. I'd also replace ${HOME} with the absolute path - not sure when exactly this is resolved.
+
+
+
+gstreamer1.0
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v1.18.4-2"
+
+gst-plugins-base
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v1.18.4-2" --extra-package=${HOME}backports/libgstreamer1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_${ARCH}.deb
+
+gst-plugins-good
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v1.18.4-2" --extra-package=${HOME}backports/libgstreamer1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-gl1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-gl_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gst-plugins-base-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_${ARCH}.deb
+
+gst-plugins-ugly
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v1.18.4-2" --extra-package=${HOME}backports/libgstreamer1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-gl1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-gl_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gst-plugins-base-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-good_1.20.1-1~bpo11+1_${ARCH}.deb
+
+libnice
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v0.1.16-1"
+
+openh264
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v0"
+
+libfreeaptx
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v0"
+
+# The opencv headers shipped with debian bullseye are broken - they require tracking.private.hpp. Copy this file into the chroot to help it out. It can be found in the opencv git repo (may be called tracking.detail.hpp).
+
+gst-plugins-bad
+    sbuild --arch=${ARCH} -d bullseye-backports --build-dep-resolver=aptitude --debbuildopts="-v1.18.4-2" --extra-package=${HOME}backports/libgstreamer1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gstreamer-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-dev_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-plugins-base1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libgstreamer-gl1.0-0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-gl_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-gst-plugins-base-1.0_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-good_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gstreamer1.0-plugins-base_1.20.1-1~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libnice-dev_0.1.18-2~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libnice10_0.1.18-2~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/gir1.2-nice-0.1_0.1.18-2~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libopenh264-6_2.2.0+dfsg-2~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libopenh264-dev_2.2.0+dfsg-2~bpo11+1_${ARCH}.deb --extra-package=${HOME}backports/libfreeaptx0_0.1.1-1_${ARCH}.deb --extra-package=${HOME}backports/libfreeaptx-dev_0.1.1-1_${ARCH}.deb --pre-build-commands='%SBUILD_CHROOT_EXEC sh -c "mkdir -p /usr/include/opencv4/opencv2/video/detail"' --pre-build-commands='cat ${HOME}tracking.private.hpp | %SBUILD_CHROOT_EXEC sh -c "cat > /usr/include/opencv4/opencv2/video/detail/tracking.private.hpp"'
+
+
+
+Move all debs for each arch into their own directory and generate package list. I use "$HOME/apt_root_${ARCH}".
+    dpkg-scanpackages --arch=${ARCH} . /dev/null | gzip -9c > Packages.gz
+
+
+Modify download_packages.py and add an apt source line:
+    deb [trusted=yes] file:${HOME}apt_root_amd64 ./
+
+
+download_packages.py is broken when a deb has mutually exclusive dependencies. In our case, we can use either libsoup2.4 or libsoup3. At the time of writing, it would be much more difficult to setup libsoup3 so exclude it.
+
+Finally, run:
+    ./download_packages.py --arch ${ARCH} --release bullseye-backports --exclude=libsoup3.0-0 gstreamer1.0-plugins-bad gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-plugins-base libgstreamer-plugins-bad1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-nice libgstreamer1.0-0 libgstreamer1.0-dev libsoup2.4-dev libjson-glib-dev
diff --git a/frc971/analysis/plot_data_utils.ts b/frc971/analysis/plot_data_utils.ts
index 230e370..80c4cca 100644
--- a/frc971/analysis/plot_data_utils.ts
+++ b/frc971/analysis/plot_data_utils.ts
@@ -5,7 +5,7 @@
 import {ByteBuffer} from 'flatbuffers';
 import {Plot, Point} from 'org_frc971/aos/network/www/plotter';
 import {Connection} from 'org_frc971/aos/network/www/proxy';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 
 export function plotData(conn: Connection, parentDiv: Element) {
   // Set up a selection box to allow the user to choose between plots to show.
diff --git a/frc971/control_loops/capped_test_plant.h b/frc971/control_loops/capped_test_plant.h
index 65bc7cc..f4286cd 100644
--- a/frc971/control_loops/capped_test_plant.h
+++ b/frc971/control_loops/capped_test_plant.h
@@ -29,4 +29,4 @@
 
 }  // namespace frc971
 }  // namespace control_loops
-#endif  // FRC971_CONTROL_LOOPS_CAPPED_TEST_PLANT_H_
\ No newline at end of file
+#endif  // FRC971_CONTROL_LOOPS_CAPPED_TEST_PLANT_H_
diff --git a/frc971/control_loops/hybrid_state_feedback_loop.h b/frc971/control_loops/hybrid_state_feedback_loop.h
index fd39359..46e6a05 100644
--- a/frc971/control_loops/hybrid_state_feedback_loop.h
+++ b/frc971/control_loops/hybrid_state_feedback_loop.h
@@ -39,7 +39,7 @@
       const Eigen::Matrix<Scalar, number_of_outputs, number_of_states> &C,
       const Eigen::Matrix<Scalar, number_of_outputs, number_of_inputs> &D,
       const Eigen::Matrix<Scalar, number_of_inputs, 1> &U_max,
-      const Eigen::Matrix<Scalar, number_of_inputs, 1> &U_min, bool delayed_u)
+      const Eigen::Matrix<Scalar, number_of_inputs, 1> &U_min, size_t delayed_u)
       : A_continuous(A_continuous),
         B_continuous(B_continuous),
         C(C),
@@ -55,7 +55,7 @@
   const Eigen::Matrix<Scalar, number_of_inputs, 1> U_min;
   const Eigen::Matrix<Scalar, number_of_inputs, 1> U_max;
 
-  const bool delayed_u;
+  const size_t delayed_u;
 };
 
 template <int number_of_states, int number_of_inputs, int number_of_outputs,
@@ -230,7 +230,7 @@
   const Eigen::Matrix<Scalar, number_of_states, number_of_states>
       P_steady_state;
 
-  const bool delayed_u;
+  const size_t delayed_u;
 
   HybridKalmanCoefficients(
       const Eigen::Matrix<Scalar, number_of_states, number_of_states>
@@ -238,7 +238,7 @@
       const Eigen::Matrix<Scalar, number_of_outputs, number_of_outputs>
           &R_continuous,
       const Eigen::Matrix<Scalar, number_of_states, number_of_states>
-          &P_steady_state, bool delayed_u)
+          &P_steady_state, size_t delayed_u)
       : Q_continuous(Q_continuous),
         R_continuous(R_continuous),
         P_steady_state(P_steady_state), delayed_u(delayed_u) {
diff --git a/frc971/control_loops/python/control_loop.py b/frc971/control_loops/python/control_loop.py
index f431983..1649dd2 100644
--- a/frc971/control_loops/python/control_loop.py
+++ b/frc971/control_loops/python/control_loop.py
@@ -266,7 +266,7 @@
             name: string, The name of the loop to use when writing the C++ files.
         """
         self._name = name
-        self.delayed_u = False
+        self.delayed_u = 0
 
     @property
     def name(self):
@@ -291,7 +291,7 @@
         self.X = numpy.matrix(numpy.zeros((self.A.shape[0], 1)))
         self.Y = self.C * self.X
         self.X_hat = numpy.matrix(numpy.zeros((self.A.shape[0], 1)))
-        self.last_U = numpy.matrix(numpy.zeros((self.B.shape[1], 1)))
+        self.last_U = numpy.matrix(numpy.zeros((self.B.shape[1], max(1, self.delayed_u))))
 
     def PlaceControllerPoles(self, poles):
         """Places the controller poles.
@@ -314,19 +314,21 @@
     def Update(self, U):
         """Simulates one time step with the provided U."""
         #U = numpy.clip(U, self.U_min, self.U_max)
-        if self.delayed_u:
-            self.X = self.A * self.X + self.B * self.last_U
-            self.Y = self.C * self.X + self.D * self.last_U
-            self.last_U = U.copy()
+        if self.delayed_u > 0:
+            self.X = self.A * self.X + self.B * self.last_U[:, -1]
+            self.Y = self.C * self.X + self.D * self.last_U[:, -1]
+            self.last_U[:, 1:] = self.last_U[:, 0:-1]
+            self.last_U[:, 0] = U.copy()
         else:
             self.X = self.A * self.X + self.B * U
             self.Y = self.C * self.X + self.D * U
 
     def PredictObserver(self, U):
         """Runs the predict step of the observer update."""
-        if self.delayed_u:
-            self.X_hat = (self.A * self.X_hat + self.B * self.last_U)
-            self.last_U = U.copy()
+        if self.delayed_u > 0:
+            self.X_hat = (self.A * self.X_hat + self.B * self.last_U[:, -1])
+            self.last_U[:, 1:] = self.last_U[:, 0:-1]
+            self.last_U[:, 0] = U.copy()
         else:
             self.X_hat = (self.A * self.X_hat + self.B * U)
 
@@ -336,9 +338,9 @@
             KalmanGain = self.KalmanGain
         else:
             KalmanGain = numpy.linalg.inv(self.A) * self.L
-        if self.delayed_u:
+        if self.delayed_u > 0:
             self.X_hat += KalmanGain * (self.Y - self.C * self.X_hat -
-                                        self.D * self.last_U)
+                                        self.D * self.last_U[:, -1])
         else:
             self.X_hat += KalmanGain * (self.Y - self.C * self.X_hat -
                                         self.D * U)
@@ -396,7 +398,7 @@
         ans.append(self._DumpMatrix('U_max', self.U_max, scalar_type))
         ans.append(self._DumpMatrix('U_min', self.U_min, scalar_type))
 
-        delayed_u_string = "true" if self.delayed_u else "false"
+        delayed_u_string = str(self.delayed_u)
         if plant_coefficient_type.startswith('StateFeedbackPlant'):
             ans.append(self._DumpMatrix('A', self.A, scalar_type))
             ans.append(self._DumpMatrix('B', self.B, scalar_type))
@@ -492,7 +494,7 @@
             '%s %s {\n' % (observer_coefficient_type, self.ObserverFunction())
         ]
 
-        delayed_u_string = "true" if self.delayed_u else "false"
+        delayed_u_string = str(self.delayed_u)
         if observer_coefficient_type.startswith('StateFeedbackObserver'):
             if hasattr(self, 'KalmanGain'):
                 KalmanGain = self.KalmanGain
@@ -540,9 +542,10 @@
 
     def PredictHybridObserver(self, U, dt):
         self.Discretize(dt)
-        if self.delayed_u:
-            self.X_hat = self.A * self.X_hat + self.B * self.last_U
-            self.last_U = U.copy()
+        if self.delayed_u > 0:
+            self.X_hat = self.A * self.X_hat + self.B * self.last_U[:, -1]
+            self.last_U[:, 1:] = self.last_U[:, 0:-1]
+            self.last_U[:, 0] = U.copy()
         else:
             self.X_hat = self.A * self.X_hat + self.B * U
 
diff --git a/frc971/control_loops/state_feedback_loop.h b/frc971/control_loops/state_feedback_loop.h
index 53cd6a2..1ea94d9 100644
--- a/frc971/control_loops/state_feedback_loop.h
+++ b/frc971/control_loops/state_feedback_loop.h
@@ -12,6 +12,7 @@
 
 #if defined(__linux__)
 #include "aos/logging/logging.h"
+#include "glog/logging.h"
 #endif
 #include "aos/macros.h"
 
@@ -49,7 +50,7 @@
       const Eigen::Matrix<Scalar, number_of_outputs, number_of_inputs> &D,
       const Eigen::Matrix<Scalar, number_of_inputs, 1> &U_max,
       const Eigen::Matrix<Scalar, number_of_inputs, 1> &U_min,
-      const std::chrono::nanoseconds dt, bool delayed_u)
+      const std::chrono::nanoseconds dt, size_t delayed_u)
       : A(A),
         B(B),
         C(C),
@@ -71,7 +72,7 @@
   // useful for modeling a control loop cycle where you sample, compute, and
   // then queue the outputs to be ready to be executed when the next cycle
   // happens.
-  const bool delayed_u;
+  const size_t delayed_u;
 };
 
 template <int number_of_states, int number_of_inputs, int number_of_outputs,
@@ -85,6 +86,16 @@
           number_of_states, number_of_inputs, number_of_outputs, Scalar>>>
           &&coefficients)
       : coefficients_(::std::move(coefficients)), index_(0) {
+    if (coefficients_.size() > 1u) {
+      for (size_t i = 1; i < coefficients_.size(); ++i) {
+        if (coefficients_[i]->delayed_u != coefficients_[0]->delayed_u) {
+          abort();
+        }
+      }
+    }
+    last_U_ = Eigen::Matrix<Scalar, number_of_inputs, Eigen::Dynamic>(
+        number_of_inputs,
+        std::max(static_cast<size_t>(1u), coefficients_[0]->delayed_u));
     Reset();
   }
 
@@ -175,15 +186,27 @@
     }
   }
 
+  const Eigen::Matrix<Scalar, number_of_inputs, 1> last_U(
+      size_t index = 0) const {
+    return last_U_.template block<number_of_inputs, 1>(0, index);
+  }
+
   // Computes the new X and Y given the control input.
   void Update(const Eigen::Matrix<Scalar, number_of_inputs, 1> &U) {
     // Powers outside of the range are more likely controller bugs than things
     // that the plant should deal with.
     CheckU(U);
-    if (coefficients().delayed_u) {
-      X_ = Update(X(), last_U_);
-      UpdateY(last_U_);
-      last_U_ = U;
+    if (coefficients().delayed_u > 0) {
+#if defined(__linux__)
+      DCHECK_EQ(static_cast<ssize_t>(coefficients().delayed_u), last_U_.cols());
+#endif
+      X_ = Update(X(), last_U(coefficients().delayed_u - 1));
+      UpdateY(last_U(coefficients().delayed_u - 1));
+      for (int i = coefficients().delayed_u; i > 1; --i) {
+        last_U_.template block<number_of_inputs, 1>(0, i - 1) =
+            last_U_.template block<number_of_inputs, 1>(0, i - 2);
+      }
+      last_U_.template block<number_of_inputs, 1>(0, 0) = U;
     } else {
       X_ = Update(X(), U);
       UpdateY(U);
@@ -210,7 +233,7 @@
  private:
   Eigen::Matrix<Scalar, number_of_states, 1> X_;
   Eigen::Matrix<Scalar, number_of_outputs, 1> Y_;
-  Eigen::Matrix<Scalar, number_of_inputs, 1> last_U_;
+  Eigen::Matrix<Scalar, number_of_inputs, Eigen::Dynamic> last_U_;
 
   ::std::vector<::std::unique_ptr<StateFeedbackPlantCoefficients<
       number_of_states, number_of_inputs, number_of_outputs, Scalar>>>
@@ -310,14 +333,14 @@
   // useful for modeling a control loop cycle where you sample, compute, and
   // then queue the outputs to be ready to be executed when the next cycle
   // happens.
-  const bool delayed_u;
+  const size_t delayed_u;
 
   StateFeedbackObserverCoefficients(
       const Eigen::Matrix<Scalar, number_of_states, number_of_outputs>
           &KalmanGain,
       const Eigen::Matrix<Scalar, number_of_states, number_of_states> &Q,
       const Eigen::Matrix<Scalar, number_of_outputs, number_of_outputs> &R,
-      bool delayed_u)
+      size_t delayed_u)
       : KalmanGain(KalmanGain), Q(Q), R(R), delayed_u(delayed_u) {}
 };
 
@@ -331,7 +354,10 @@
       ::std::vector<::std::unique_ptr<StateFeedbackObserverCoefficients<
           number_of_states, number_of_inputs, number_of_outputs, Scalar>>>
           &&observers)
-      : coefficients_(::std::move(observers)) {}
+      : coefficients_(::std::move(observers)) {
+    last_U_ = Eigen::Matrix<Scalar, number_of_inputs, Eigen::Dynamic>(
+        number_of_inputs, std::max(static_cast<size_t>(1u), coefficients().delayed_u));
+  }
 
   StateFeedbackObserver(StateFeedbackObserver &&other)
       : X_hat_(other.X_hat_), last_U_(other.last_U_), index_(other.index_) {
@@ -349,8 +375,9 @@
   }
   Eigen::Matrix<Scalar, number_of_states, 1> &mutable_X_hat() { return X_hat_; }
 
-  const Eigen::Matrix<Scalar, number_of_inputs, 1> &last_U() const {
-    return last_U_;
+  const Eigen::Matrix<Scalar, number_of_inputs, 1> last_U(
+      size_t index = 0) const {
+    return last_U_.template block<number_of_inputs, 1>(0, index);
   }
 
   void Reset(StateFeedbackPlant<number_of_states, number_of_inputs,
@@ -363,9 +390,14 @@
                                   number_of_outputs, Scalar> *plant,
                const Eigen::Matrix<Scalar, number_of_inputs, 1> &new_u,
                ::std::chrono::nanoseconds /*dt*/) {
-    if (plant->coefficients().delayed_u) {
-      mutable_X_hat() = plant->Update(X_hat(), last_U_);
-      last_U_ = new_u;
+    if (plant->coefficients().delayed_u > 0) {
+      mutable_X_hat() =
+          plant->Update(X_hat(), last_U(coefficients().delayed_u - 1));
+      for (int i = coefficients().delayed_u; i > 1; --i) {
+        last_U_.template block<number_of_inputs, 1>(0, i - 1) =
+            last_U_.template block<number_of_inputs, 1>(0, i - 2);
+      }
+      last_U_.template block<number_of_inputs, 1>(0, 0) = new_u;
     } else {
       mutable_X_hat() = plant->Update(X_hat(), new_u);
     }
@@ -406,7 +438,7 @@
  private:
   // Internal state estimate.
   Eigen::Matrix<Scalar, number_of_states, 1> X_hat_;
-  Eigen::Matrix<Scalar, number_of_inputs, 1> last_U_;
+  Eigen::Matrix<Scalar, number_of_inputs, Eigen::Dynamic> last_U_;
 
   int index_ = 0;
   ::std::vector<::std::unique_ptr<StateFeedbackObserverCoefficients<
diff --git a/frc971/input/drivetrain_input.cc b/frc971/input/drivetrain_input.cc
index b1adf1c..ab3afef 100644
--- a/frc971/input/drivetrain_input.cc
+++ b/frc971/input/drivetrain_input.cc
@@ -207,6 +207,13 @@
     high_gear_ = true;
   }
 
+  // Emprically, the current pistol grip tends towards steady-state errors at
+  // ~0.01-0.02 on both the wheel/throttle. Having the throttle correctly snap
+  // to zero is more important than the wheel for our internal logic, so force a
+  // deadband there.
+  constexpr double kThrottleDeadband = 0.05;
+  throttle = aos::Deadband(throttle, kThrottleDeadband, 1.0);
+
   return DrivetrainInputReader::WheelAndThrottle{
       wheel,     wheel_velocity,    wheel_torque,
       throttle,  throttle_velocity, throttle_torque,
diff --git a/frc971/raspi/rootfs/chrt.sh b/frc971/raspi/rootfs/chrt.sh
index 535f4c6..d5edafe 100755
--- a/frc971/raspi/rootfs/chrt.sh
+++ b/frc971/raspi/rootfs/chrt.sh
@@ -35,3 +35,7 @@
 chrtirq "irq/[0-9]*-vc4 crtc" -o 0
 chrtirq "irq/23-uart-pl0" -o 0
 chrtirq "irq/[0-9]*-eth0" -f 10
+
+# Route data-ready interrupts to the second core
+SPI_IRQ="$(cat /proc/interrupts | grep fe204000.spi | awk '{print $1}' | grep '[0-9]*' -o)"
+echo 2 > /proc/irq/"${SPI_IRQ}"/smp_affinity
diff --git a/frc971/raspi/rootfs/target_configure.sh b/frc971/raspi/rootfs/target_configure.sh
index 06f481b..3b71112 100755
--- a/frc971/raspi/rootfs/target_configure.sh
+++ b/frc971/raspi/rootfs/target_configure.sh
@@ -17,6 +17,8 @@
 chown -R pi.pi /home/pi/.ssh
 chown -R pi.pi /home/pi/bin
 
+echo 'deb [trusted=yes] https://software.frc971.org/Build-Dependencies/gstreamer_bullseye_arm64_deps ./' >> /etc/apt/sources.list
+
 apt-get update
 
 apt-get install -y vim-nox \
@@ -43,9 +45,17 @@
   libnice10 \
   pmount \
   libnice-dev \
-  feh
+  feh \
+  libgstreamer1.0-0 \
+  libgstreamer-plugins-base1.0-0 \
+  libgstreamer-plugins-bad1.0-0 \
+  gstreamer1.0-plugins-base \
+  gstreamer1.0-plugins-good \
+  gstreamer1.0-plugins-bad \
+  gstreamer1.0-plugins-ugly \
+  gstreamer1.0-nice
 
-dpkg -i /tmp/wiringpi-2.70-1.deb
+PATH=$PATH:/sbin dpkg -i /tmp/wiringpi-2.70-1.deb
 
 echo 'GOVERNOR="performance"' > /etc/default/cpufrequtils
 
diff --git a/frc971/wpilib/ahal/Spark.cc b/frc971/wpilib/ahal/Spark.cc
index deb891b..502ff31 100644
--- a/frc971/wpilib/ahal/Spark.cc
+++ b/frc971/wpilib/ahal/Spark.cc
@@ -26,7 +26,7 @@
    *   0.999ms = full "reverse"
    */
   SetBounds(2.003, 1.55, 1.50, 1.46, .999);
-  SetPeriodMultiplier(kPeriodMultiplier_1X);
+  SetPeriodMultiplier(kPeriodMultiplier_2X);
   SetSpeed(0.0);
   SetZeroLatch();
 
diff --git a/frc971/wpilib/ahal/Talon.cc b/frc971/wpilib/ahal/Talon.cc
index e3af567..7caabc0 100644
--- a/frc971/wpilib/ahal/Talon.cc
+++ b/frc971/wpilib/ahal/Talon.cc
@@ -32,7 +32,7 @@
    *   0.989ms = full "reverse"
    */
   SetBounds(2.037, 1.539, 1.513, 1.487, .989);
-  SetPeriodMultiplier(kPeriodMultiplier_1X);
+  SetPeriodMultiplier(kPeriodMultiplier_2X);
   SetSpeed(0.0);
   SetZeroLatch();
 
diff --git a/frc971/wpilib/ahal/TalonFX.cc b/frc971/wpilib/ahal/TalonFX.cc
index 93dc62e..fe29089 100644
--- a/frc971/wpilib/ahal/TalonFX.cc
+++ b/frc971/wpilib/ahal/TalonFX.cc
@@ -33,7 +33,7 @@
    *   0.997ms = full "reverse"
    */
   SetBounds(2.004, 1.52, 1.50, 1.48, .997);
-  SetPeriodMultiplier(kPeriodMultiplier_1X);
+  SetPeriodMultiplier(kPeriodMultiplier_2X);
   SetSpeed(0.0);
   SetZeroLatch();
 
diff --git a/frc971/wpilib/ahal/VictorSP.cc b/frc971/wpilib/ahal/VictorSP.cc
index fee03a9..249202d 100644
--- a/frc971/wpilib/ahal/VictorSP.cc
+++ b/frc971/wpilib/ahal/VictorSP.cc
@@ -33,7 +33,7 @@
    *   0.997ms = full "reverse"
    */
   SetBounds(2.004, 1.52, 1.50, 1.48, .997);
-  SetPeriodMultiplier(kPeriodMultiplier_1X);
+  SetPeriodMultiplier(kPeriodMultiplier_2X);
   SetSpeed(0.0);
   SetZeroLatch();
 
diff --git a/frc971/wpilib/imu_plot_utils.ts b/frc971/wpilib/imu_plot_utils.ts
index 08b3c13..c4b516b 100644
--- a/frc971/wpilib/imu_plot_utils.ts
+++ b/frc971/wpilib/imu_plot_utils.ts
@@ -5,7 +5,7 @@
 import {Point} from 'org_frc971/aos/network/www/plotter';
 import {Table} from 'org_frc971/aos/network/www/reflection';
 import {ByteBuffer} from 'flatbuffers';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 
 const FILTER_WINDOW_SIZE = 100;
 
diff --git a/frc971/wpilib/sensor_reader.cc b/frc971/wpilib/sensor_reader.cc
index 6c47214..bb99dc8 100644
--- a/frc971/wpilib/sensor_reader.cc
+++ b/frc971/wpilib/sensor_reader.cc
@@ -14,6 +14,9 @@
 #include "frc971/wpilib/wpilib_interface.h"
 #include "hal/PWM.h"
 
+DEFINE_int32(pwm_offset, 5050 / 2,
+             "Offset of reading the sensors from the start of the PWM cycle");
+
 namespace frc971 {
 namespace wpilib {
 
@@ -125,12 +128,17 @@
     last_monotonic_now_ = monotonic_now;
 
     monotonic_clock::time_point last_tick_timepoint = GetPWMStartTime();
+    VLOG(1) << "Start time " << last_tick_timepoint << " period " << period_.count();
     if (last_tick_timepoint == monotonic_clock::min_time) {
       return;
     }
 
     last_tick_timepoint +=
-        ((monotonic_now - last_tick_timepoint) / period_) * period_;
+        ((monotonic_now - chrono::microseconds(FLAGS_pwm_offset) -
+          last_tick_timepoint) /
+         period_) *
+        period_ + chrono::microseconds(FLAGS_pwm_offset);
+    VLOG(1) << "Now " << monotonic_now << " tick " << last_tick_timepoint;
     // If it's over 1/2 of a period back in time, that's wrong.  Move it
     // forwards to now.
     if (last_tick_timepoint - monotonic_now < -period_ / 2) {
@@ -142,7 +150,7 @@
     // errors in waking up.  The PWM cycle starts at the falling edge of the
     // PWM pulse.
     const auto next_time =
-        last_tick_timepoint + period_ + chrono::microseconds(50);
+        last_tick_timepoint + period_;
 
     timer_handler_->Setup(next_time, period_);
   }
diff --git a/scouting/BUILD b/scouting/BUILD
index c543c17..f58a157 100644
--- a/scouting/BUILD
+++ b/scouting/BUILD
@@ -1,10 +1,5 @@
 load("//tools/build_rules:apache.bzl", "apache_wrapper")
-load("//tools/build_rules:js.bzl", "protractor_ts_test", "turn_files_into_runfiles")
-
-turn_files_into_runfiles(
-    name = "main_bundle_compiled_runfiles",
-    files = "//scouting/www:main_bundle_compiled",
-)
+load("//tools/build_rules:js.bzl", "protractor_ts_test")
 
 sh_binary(
     name = "scouting",
@@ -12,7 +7,6 @@
         "scouting.sh",
     ],
     data = [
-        ":main_bundle_compiled_runfiles",
         "//scouting/webserver",
         "//scouting/www:static_files",
     ],
diff --git a/scouting/webserver/requests/debug/BUILD b/scouting/webserver/requests/debug/BUILD
index e5f5234..6adaee5 100644
--- a/scouting/webserver/requests/debug/BUILD
+++ b/scouting/webserver/requests/debug/BUILD
@@ -15,5 +15,6 @@
         "//scouting/webserver/requests/messages:request_notes_for_team_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_data_scouting_response_go_fbs",
         "//scouting/webserver/requests/messages:submit_notes_response_go_fbs",
+        "@com_github_google_flatbuffers//go:go_default_library",
     ],
 )
diff --git a/scouting/webserver/requests/debug/cli/main.go b/scouting/webserver/requests/debug/cli/main.go
index 6f2de1d..f6fb38a 100644
--- a/scouting/webserver/requests/debug/cli/main.go
+++ b/scouting/webserver/requests/debug/cli/main.go
@@ -65,6 +65,18 @@
 	return binaryFb
 }
 
+func maybePerformRequest[T interface{}](fbName, fbsPath, requestJsonPath, address string, requester func(string, []byte) (*T, error)) {
+	if requestJsonPath != "" {
+		log.Printf("Sending %s to %s", fbName, address)
+		binaryRequest := parseJson(fbsPath, requestJsonPath)
+		response, err := requester(address, binaryRequest)
+		if err != nil {
+			log.Fatalf("Failed %s: %v", fbName, err)
+		}
+		spew.Dump(*response)
+	}
+}
+
 func main() {
 	// Parse command line arguments.
 	indentPtr := flag.String("indent", " ",
@@ -86,59 +98,38 @@
 	spew.Config.Indent = *indentPtr
 
 	// Handle the actual arguments.
-	if *submitDataScoutingPtr != "" {
-		log.Printf("Sending SubmitDataScouting to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/submit_data_scouting.fbs",
-			*submitDataScoutingPtr)
-		response, err := debug.SubmitDataScouting(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed SubmitDataScouting: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestAllMatchesPtr != "" {
-		log.Printf("Sending RequestAllMatches to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_all_matches.fbs",
-			*requestAllMatchesPtr)
-		response, err := debug.RequestAllMatches(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestAllMatches: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestMatchesForTeamPtr != "" {
-		log.Printf("Sending RequestMatchesForTeam to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_matches_for_team.fbs",
-			*requestMatchesForTeamPtr)
-		response, err := debug.RequestMatchesForTeam(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestMatchesForTeam: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *requestDataScoutingPtr != "" {
-		log.Printf("Sending RequestDataScouting to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/request_data_scouting.fbs",
-			*requestDataScoutingPtr)
-		response, err := debug.RequestDataScouting(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RequestDataScouting: ", err)
-		}
-		spew.Dump(*response)
-	}
-	if *refreshMatchListPtr != "" {
-		log.Printf("Sending RefreshMatchList to %s", *addressPtr)
-		binaryRequest := parseJson(
-			"scouting/webserver/requests/messages/refresh_match_list.fbs",
-			*refreshMatchListPtr)
-		response, err := debug.RefreshMatchList(*addressPtr, binaryRequest)
-		if err != nil {
-			log.Fatal("Failed RefreshMatchList: ", err)
-		}
-		spew.Dump(*response)
-	}
+	maybePerformRequest(
+		"SubmitDataScouting",
+		"scouting/webserver/requests/messages/submit_data_scouting.fbs",
+		*submitDataScoutingPtr,
+		*addressPtr,
+		debug.SubmitDataScouting)
+
+	maybePerformRequest(
+		"RequestAllMatches",
+		"scouting/webserver/requests/messages/request_all_matches.fbs",
+		*requestAllMatchesPtr,
+		*addressPtr,
+		debug.RequestAllMatches)
+
+	maybePerformRequest(
+		"RequestMatchesForTeam",
+		"scouting/webserver/requests/messages/request_matches_for_team.fbs",
+		*requestMatchesForTeamPtr,
+		*addressPtr,
+		debug.RequestMatchesForTeam)
+
+	maybePerformRequest(
+		"RequestDataScouting",
+		"scouting/webserver/requests/messages/request_data_scouting.fbs",
+		*requestDataScoutingPtr,
+		*addressPtr,
+		debug.RequestDataScouting)
+
+	maybePerformRequest(
+		"RefreshMatchList",
+		"scouting/webserver/requests/messages/refresh_match_list.fbs",
+		*refreshMatchListPtr,
+		*addressPtr,
+		debug.RefreshMatchList)
 }
diff --git a/scouting/webserver/requests/debug/debug.go b/scouting/webserver/requests/debug/debug.go
index 63ece72..21f0f05 100644
--- a/scouting/webserver/requests/debug/debug.go
+++ b/scouting/webserver/requests/debug/debug.go
@@ -17,6 +17,7 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/request_notes_for_team_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_data_scouting_response"
 	"github.com/frc971/971-Robot-Code/scouting/webserver/requests/messages/submit_notes_response"
+	flatbuffers "github.com/google/flatbuffers/go"
 )
 
 // The username to submit the various requests as.
@@ -90,82 +91,55 @@
 	return responseBytes, nil
 }
 
-// Sends a `SubmitDataScouting` message to the server and returns the
-// deserialized response.
+// Sends a message to the server and returns the deserialized response.
+// The first generic argument must be specified.
+func sendMessage[FbT interface{}, Fb interface{ UnPack() *FbT }](url string, requestBytes []byte, parser func([]byte, flatbuffers.UOffsetT) Fb) (*FbT, error) {
+	responseBytes, err := performPost(url, requestBytes)
+	if err != nil {
+		return nil, err
+	}
+	response := parser(responseBytes, 0)
+	return response.UnPack(), nil
+}
+
 func SubmitDataScouting(server string, requestBytes []byte) (*submit_data_scouting_response.SubmitDataScoutingResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/submit/data_scouting", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-	log.Printf("Parsing SubmitDataScoutingResponse")
-	response := submit_data_scouting_response.GetRootAsSubmitDataScoutingResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[submit_data_scouting_response.SubmitDataScoutingResponseT](
+		server+"/requests/submit/data_scouting", requestBytes,
+		submit_data_scouting_response.GetRootAsSubmitDataScoutingResponse)
 }
 
-// Sends a `RequestAllMatches` message to the server and returns the
-// deserialized response.
 func RequestAllMatches(server string, requestBytes []byte) (*request_all_matches_response.RequestAllMatchesResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/request/all_matches", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-	log.Printf("Parsing RequestAllMatchesResponse")
-	response := request_all_matches_response.GetRootAsRequestAllMatchesResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[request_all_matches_response.RequestAllMatchesResponseT](
+		server+"/requests/request/all_matches", requestBytes,
+		request_all_matches_response.GetRootAsRequestAllMatchesResponse)
 }
 
-// Sends a `RequestMatchesForTeam` message to the server and returns the
-// deserialized response.
 func RequestMatchesForTeam(server string, requestBytes []byte) (*request_matches_for_team_response.RequestMatchesForTeamResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/request/matches_for_team", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-	log.Printf("Parsing RequestMatchesForTeamResponse")
-	response := request_matches_for_team_response.GetRootAsRequestMatchesForTeamResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[request_matches_for_team_response.RequestMatchesForTeamResponseT](
+		server+"/requests/request/matches_for_team", requestBytes,
+		request_matches_for_team_response.GetRootAsRequestMatchesForTeamResponse)
 }
 
-// Sends a `RequestDataScouting` message to the server and returns the
-// deserialized response.
 func RequestDataScouting(server string, requestBytes []byte) (*request_data_scouting_response.RequestDataScoutingResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/request/data_scouting", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-	log.Printf("Parsing RequestDataScoutingResponse")
-	response := request_data_scouting_response.GetRootAsRequestDataScoutingResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[request_data_scouting_response.RequestDataScoutingResponseT](
+		server+"/requests/request/data_scouting", requestBytes,
+		request_data_scouting_response.GetRootAsRequestDataScoutingResponse)
 }
 
-// Sends a `RefreshMatchList` message to the server and returns the
-// deserialized response.
 func RefreshMatchList(server string, requestBytes []byte) (*refresh_match_list_response.RefreshMatchListResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/refresh_match_list", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-	log.Printf("Parsing RefreshMatchListResponse")
-	response := refresh_match_list_response.GetRootAsRefreshMatchListResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[refresh_match_list_response.RefreshMatchListResponseT](
+		server+"/requests/refresh_match_list", requestBytes,
+		refresh_match_list_response.GetRootAsRefreshMatchListResponse)
 }
 
 func SubmitNotes(server string, requestBytes []byte) (*submit_notes_response.SubmitNotesResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/submit/submit_notes", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-
-	response := submit_notes_response.GetRootAsSubmitNotesResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[submit_notes_response.SubmitNotesResponseT](
+		server+"/requests/submit/submit_notes", requestBytes,
+		submit_notes_response.GetRootAsSubmitNotesResponse)
 }
 
 func RequestNotes(server string, requestBytes []byte) (*request_notes_for_team_response.RequestNotesForTeamResponseT, error) {
-	responseBytes, err := performPost(server+"/requests/request/notes_for_team", requestBytes)
-	if err != nil {
-		return nil, err
-	}
-
-	response := request_notes_for_team_response.GetRootAsRequestNotesForTeamResponse(responseBytes, 0)
-	return response.UnPack(), nil
+	return sendMessage[request_notes_for_team_response.RequestNotesForTeamResponseT](
+		server+"/requests/request/notes_for_team", requestBytes,
+		request_notes_for_team_response.GetRootAsRequestNotesForTeamResponse)
 }
diff --git a/scouting/webserver/requests/requests.go b/scouting/webserver/requests/requests.go
index c9a6880..d67f5e9 100644
--- a/scouting/webserver/requests/requests.go
+++ b/scouting/webserver/requests/requests.go
@@ -79,16 +79,15 @@
 	respondWithError(w, http.StatusNotImplemented, "")
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseSubmitDataScouting(w http.ResponseWriter, buf []byte) (*SubmitDataScouting, bool) {
+func parseRequest[T interface{}](w http.ResponseWriter, buf []byte, requestName string, parser func([]byte, flatbuffers.UOffsetT) *T) (*T, bool) {
 	success := true
 	defer func() {
 		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
+			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse %s: %v", requestName, r))
 			success = false
 		}
 	}()
-	result := submit_data_scouting.GetRootAsSubmitDataScouting(buf, 0)
+	result := parser(buf, 0)
 	return result, success
 }
 
@@ -135,7 +134,7 @@
 		return
 	}
 
-	request, success := parseSubmitDataScouting(w, requestBytes)
+	request, success := parseRequest[SubmitDataScouting](w, requestBytes, "SubmitDataScouting", submit_data_scouting.GetRootAsSubmitDataScouting)
 	if !success {
 		return
 	}
@@ -181,19 +180,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestAllMatches(w http.ResponseWriter, buf []byte) (*RequestAllMatches, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_all_matches.GetRootAsRequestAllMatches(buf, 0)
-	return result, success
-}
-
 // Handles a RequestAllMaches request.
 type requestAllMatchesHandler struct {
 	db Database
@@ -206,7 +192,7 @@
 		return
 	}
 
-	_, success := parseRequestAllMatches(w, requestBytes)
+	_, success := parseRequest(w, requestBytes, "RequestAllMatches", request_all_matches.GetRootAsRequestAllMatches)
 	if !success {
 		return
 	}
@@ -237,19 +223,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestMatchesForTeam(w http.ResponseWriter, buf []byte) (*RequestMatchesForTeam, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_matches_for_team.GetRootAsRequestMatchesForTeam(buf, 0)
-	return result, success
-}
-
 // Handles a RequestMatchesForTeam request.
 type requestMatchesForTeamHandler struct {
 	db Database
@@ -262,7 +235,7 @@
 		return
 	}
 
-	request, success := parseRequestMatchesForTeam(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RequestMatchesForTeam", request_matches_for_team.GetRootAsRequestMatchesForTeam)
 	if !success {
 		return
 	}
@@ -293,19 +266,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRequestDataScouting(w http.ResponseWriter, buf []byte) (*RequestDataScouting, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse SubmitDataScouting: %v", r))
-			success = false
-		}
-	}()
-	result := request_data_scouting.GetRootAsRequestDataScouting(buf, 0)
-	return result, success
-}
-
 // Handles a RequestDataScouting request.
 type requestDataScoutingHandler struct {
 	db Database
@@ -318,7 +278,7 @@
 		return
 	}
 
-	_, success := parseRequestDataScouting(w, requestBytes)
+	_, success := parseRequest(w, requestBytes, "RequestDataScouting", request_data_scouting.GetRootAsRequestDataScouting)
 	if !success {
 		return
 	}
@@ -359,19 +319,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-// TODO(phil): Can we turn this into a generic?
-func parseRefreshMatchList(w http.ResponseWriter, buf []byte) (*RefreshMatchList, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := refresh_match_list.GetRootAsRefreshMatchList(buf, 0)
-	return result, success
-}
-
 func parseTeamKey(teamKey string) (int, error) {
 	// TBA prefixes teams with "frc". Not sure why. Get rid of that.
 	teamKey = strings.TrimPrefix(teamKey, "frc")
@@ -422,7 +369,7 @@
 		return
 	}
 
-	request, success := parseRefreshMatchList(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RefreshMatchList", refresh_match_list.GetRootAsRefreshMatchList)
 	if !success {
 		return
 	}
@@ -467,18 +414,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-func parseSubmitNotes(w http.ResponseWriter, buf []byte) (*SubmitNotes, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := submit_notes.GetRootAsSubmitNotes(buf, 0)
-	return result, success
-}
-
 type submitNoteScoutingHandler struct {
 	db Database
 }
@@ -490,7 +425,7 @@
 		return
 	}
 
-	request, success := parseSubmitNotes(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "SubmitNotes", submit_notes.GetRootAsSubmitNotes)
 	if !success {
 		return
 	}
@@ -510,18 +445,6 @@
 	w.Write(builder.FinishedBytes())
 }
 
-func parseRequestNotesForTeam(w http.ResponseWriter, buf []byte) (*RequestNotesForTeam, bool) {
-	success := true
-	defer func() {
-		if r := recover(); r != nil {
-			respondWithError(w, http.StatusBadRequest, fmt.Sprintf("Failed to parse RefreshMatchList: %v", r))
-			success = false
-		}
-	}()
-	result := request_notes_for_team.GetRootAsRequestNotesForTeam(buf, 0)
-	return result, success
-}
-
 type requestNotesForTeamHandler struct {
 	db Database
 }
@@ -533,7 +456,7 @@
 		return
 	}
 
-	request, success := parseRequestNotesForTeam(w, requestBytes)
+	request, success := parseRequest(w, requestBytes, "RequestNotesForTeam", request_notes_for_team.GetRootAsRequestNotesForTeam)
 	if !success {
 		return
 	}
diff --git a/scouting/webserver/static/BUILD b/scouting/webserver/static/BUILD
index 3166853..0cf22f3 100644
--- a/scouting/webserver/static/BUILD
+++ b/scouting/webserver/static/BUILD
@@ -13,6 +13,7 @@
     name = "static_test",
     srcs = ["static_test.go"],
     data = [
+        "test_pages/index.html",
         "test_pages/page.txt",
         "test_pages/root.txt",
     ],
diff --git a/scouting/webserver/static/static.go b/scouting/webserver/static/static.go
index e921b0b..4c46fe7 100644
--- a/scouting/webserver/static/static.go
+++ b/scouting/webserver/static/static.go
@@ -2,13 +2,137 @@
 
 // A year agnostic way to serve static http files.
 import (
+	"crypto/sha256"
+	"errors"
+	"fmt"
+	"io"
+	"log"
 	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
 
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 )
 
-// Serve pages given a port, directory to serve from, and an channel to pass the errors back to the caller.
+// We want the static files (which include JS that is modified over time), to not be cached.
+// This ensures users get updated versions when uploaded to the server.
+// Based on https://stackoverflow.com/a/33881296, this disables cache for most browsers.
+var epoch = time.Unix(0, 0).Format(time.RFC1123)
+
+var noCacheHeaders = map[string]string{
+	"Expires":         epoch,
+	"Cache-Control":   "no-cache, private, max-age=0",
+	"Pragma":          "no-cache",
+	"X-Accel-Expires": "0",
+}
+
+func MaybeNoCache(h http.Handler) http.Handler {
+	fn := func(w http.ResponseWriter, r *http.Request) {
+		// We force the browser not to cache index.html so that
+		// browsers will notice when the bundle gets updated.
+		if r.URL.Path == "/" || r.URL.Path == "/index.html" {
+			for k, v := range noCacheHeaders {
+				w.Header().Set(k, v)
+			}
+		}
+
+		h.ServeHTTP(w, r)
+	}
+
+	return http.HandlerFunc(fn)
+}
+
+// Computes the sha256 of the specified file.
+func computeSha256(path string) (string, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return "", errors.New(fmt.Sprint("Failed to open ", path, ": ", err))
+	}
+	defer file.Close()
+
+	hash := sha256.New()
+	if _, err := io.Copy(hash, file); err != nil {
+		return "", errors.New(fmt.Sprint("Failed to compute sha256 of ", path, ": ", err))
+	}
+	return fmt.Sprintf("%x", hash.Sum(nil)), nil
+}
+
+// Finds the checksums for all the files in the specified directory. This is a
+// best effort only. If for some reason we fail to compute the checksum of
+// something, we just move on.
+func findAllFileShas(directory string) map[string]string {
+	shaSums := make(map[string]string)
+
+	// Find the checksums for all the files.
+	err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			log.Println("Walk() didn't want to deal with ", path, ":", err)
+			return nil
+		}
+		if info.IsDir() {
+			// We only care about computing checksums of files.
+			// Ignore directories.
+			return nil
+		}
+		hash, err := computeSha256(path)
+		if err != nil {
+			log.Println(err)
+			return nil
+		}
+		shaSums[hash] = "/" + strings.TrimPrefix(path, directory)
+		return nil
+	})
+	if err != nil {
+		log.Fatal("Got unexpected error from Walk(): ", err)
+	}
+
+	return shaSums
+}
+
+func HandleShaUrl(directory string, h http.Handler) http.Handler {
+	shaSums := findAllFileShas(directory)
+
+	fn := func(w http.ResponseWriter, r *http.Request) {
+		// We expect the path portion to look like this:
+		// /sha256/<checksum>/path...
+		// Splitting on / means we end up with this list:
+		// [0] ""
+		// [1] "sha256"
+		// [2] "<checksum>"
+		// [3-] path...
+		parts := strings.Split(r.URL.Path, "/")
+		if len(parts) < 4 {
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+		if parts[0] != "" || parts[1] != "sha256" {
+			// Something is fundamentally wrong. We told the
+			// framework to only give is /sha256/ requests.
+			log.Fatal("This handler should not be called for " + r.URL.Path)
+		}
+		hash := parts[2]
+		if path, ok := shaSums[hash]; ok {
+			// We found a file with this checksum. Serve that file.
+			r.URL.Path = path
+		} else {
+			// No file with this checksum found.
+			w.WriteHeader(http.StatusNotFound)
+			return
+		}
+
+		h.ServeHTTP(w, r)
+	}
+
+	return http.HandlerFunc(fn)
+}
+
+// Serve pages in the specified directory.
 func ServePages(scoutingServer server.ScoutingServer, directory string) {
 	// Serve the / endpoint given a folder of pages.
-	scoutingServer.Handle("/", http.FileServer(http.Dir(directory)))
+	scoutingServer.Handle("/", MaybeNoCache(http.FileServer(http.Dir(directory))))
+
+	// Also serve files in a checksum-addressable manner.
+	scoutingServer.Handle("/sha256/", HandleShaUrl(directory, http.FileServer(http.Dir(directory))))
 }
diff --git a/scouting/webserver/static/static_test.go b/scouting/webserver/static/static_test.go
index 15bd872..09ed940 100644
--- a/scouting/webserver/static/static_test.go
+++ b/scouting/webserver/static/static_test.go
@@ -9,6 +9,12 @@
 	"github.com/frc971/971-Robot-Code/scouting/webserver/server"
 )
 
+func expectEqual(t *testing.T, actual string, expected string) {
+	if actual != expected {
+		t.Error("Expected ", actual, " to equal ", expected)
+	}
+}
+
 func TestServing(t *testing.T) {
 	cases := []struct {
 		// The path to request from the server.
@@ -17,6 +23,7 @@
 		// specified path.
 		expectedData string
 	}{
+		{"/", "<h1>This is the index</h1>\n"},
 		{"/root.txt", "Hello, this is the root page!"},
 		{"/page.txt", "Hello from a page!"},
 	}
@@ -24,16 +31,67 @@
 	scoutingServer := server.NewScoutingServer()
 	ServePages(scoutingServer, "test_pages")
 	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
 
 	// Go through all the test cases, and run them against the running webserver.
 	for _, c := range cases {
 		dataReceived := getData(c.path, t)
-		if dataReceived != c.expectedData {
-			t.Errorf("Got %q, but expected %q", dataReceived, c.expectedData)
-		}
+		expectEqual(t, dataReceived, c.expectedData)
 	}
+}
 
-	scoutingServer.Stop()
+// Makes sure that requesting / sets the proper headers so it doesn't get
+// cached.
+func TestDisallowedCache(t *testing.T) {
+	scoutingServer := server.NewScoutingServer()
+	ServePages(scoutingServer, "test_pages")
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	resp, err := http.Get("http://localhost:8080/")
+	if err != nil {
+		t.Fatal("Failed to get data ", err)
+	}
+	expectEqual(t, resp.Header.Get("Expires"), "Thu, 01 Jan 1970 00:00:00 UTC")
+	expectEqual(t, resp.Header.Get("Cache-Control"), "no-cache, private, max-age=0")
+	expectEqual(t, resp.Header.Get("Pragma"), "no-cache")
+	expectEqual(t, resp.Header.Get("X-Accel-Expires"), "0")
+}
+
+// Makes sure that requesting anything other than / doesn't set the "do not
+// cache" headers.
+func TestAllowedCache(t *testing.T) {
+	scoutingServer := server.NewScoutingServer()
+	ServePages(scoutingServer, "test_pages")
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	resp, err := http.Get("http://localhost:8080/root.txt")
+	if err != nil {
+		t.Fatalf("Failed to get data ", err)
+	}
+	expectEqual(t, resp.Header.Get("Expires"), "")
+	expectEqual(t, resp.Header.Get("Cache-Control"), "")
+	expectEqual(t, resp.Header.Get("Pragma"), "")
+	expectEqual(t, resp.Header.Get("X-Accel-Expires"), "")
+}
+
+func TestSha256(t *testing.T) {
+	scoutingServer := server.NewScoutingServer()
+	ServePages(scoutingServer, "test_pages")
+	scoutingServer.Start(8080)
+	defer scoutingServer.Stop()
+
+	// Validate a valid checksum.
+	dataReceived := getData("sha256/553b9b29647a112136986cf93c57b988d1f12dc43d3b774f14a24e58d272dbff/root.txt", t)
+	expectEqual(t, dataReceived, "Hello, this is the root page!")
+
+	// Make a request with an invalid checksum and make sure we get a 404.
+	resp, err := http.Get("http://localhost:8080/sha256/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/root.txt")
+	if err != nil {
+		t.Fatal("Failed to get data ", err)
+	}
+	expectEqual(t, resp.Status, "404 Not Found")
 }
 
 // Retrieves the data at the specified path. If an error occurs, the test case
@@ -45,7 +103,7 @@
 	}
 	// Error out if the return status is anything other than 200 OK.
 	if resp.Status != "200 OK" {
-		t.Fatalf("Received a status code other than 200")
+		t.Fatal("Received a status code other than 200:", resp.Status)
 	}
 	// Read the response body.
 	body, err := ioutil.ReadAll(resp.Body)
diff --git a/scouting/webserver/static/test_pages/index.html b/scouting/webserver/static/test_pages/index.html
new file mode 100644
index 0000000..d769db4
--- /dev/null
+++ b/scouting/webserver/static/test_pages/index.html
@@ -0,0 +1 @@
+<h1>This is the index</h1>
diff --git a/scouting/www/BUILD b/scouting/www/BUILD
index 99dcde6..647af14 100644
--- a/scouting/www/BUILD
+++ b/scouting/www/BUILD
@@ -1,6 +1,5 @@
 load("@npm//@bazel/typescript:index.bzl", "ts_library")
 load("//tools/build_rules:js.bzl", "rollup_bundle")
-load("@npm//@bazel/concatjs:index.bzl", "concatjs_devserver")
 load("@npm//@babel/cli:index.bzl", "babel")
 
 ts_library(
@@ -21,6 +20,7 @@
         "//scouting/www/import_match_list",
         "//scouting/www/match_list",
         "//scouting/www/notes",
+        "//scouting/www/shift_schedule",
         "@npm//@angular/animations",
         "@npm//@angular/common",
         "@npm//@angular/core",
@@ -39,19 +39,54 @@
 babel(
     name = "main_bundle_compiled",
     args = [
-        "$(execpath :main_bundle)",
+        "$(execpath :main_bundle.min.js)",
         "--no-babelrc",
         "--source-maps",
+        "--minified",
+        "--no-comments",
         "--plugins=@angular/compiler-cli/linker/babel",
         "--out-dir",
         "$(@D)",
     ],
     data = [
-        ":main_bundle",
+        ":main_bundle.min.js",
         "@npm//@angular/compiler-cli",
     ],
     output_dir = True,
-    visibility = ["//visibility:public"],
+)
+
+# The babel() rule above puts everything into a directory without telling bazel
+# what's in the directory. That makes it annoying to work with from other
+# rules. This genrule() here copies the one file in the directory we care about
+# so that other rules have an easier time using the file.
+genrule(
+    name = "main_bundle_file",
+    srcs = [":main_bundle_compiled"],
+    outs = ["main_bundle_file.js"],
+    cmd = "cp $(location :main_bundle_compiled)/main_bundle.min.js $(OUTS)",
+)
+
+py_binary(
+    name = "index_html_generator",
+    srcs = ["index_html_generator.py"],
+)
+
+genrule(
+    name = "generate_index_html",
+    srcs = [
+        "index.template.html",
+        "main_bundle_file.js",
+    ],
+    outs = ["index.html"],
+    cmd = " ".join([
+        "$(location :index_html_generator)",
+        "--template $(location index.template.html)",
+        "--bundle $(location main_bundle_file.js)",
+        "--output $(location index.html)",
+    ]),
+    tools = [
+        ":index_html_generator",
+    ],
 )
 
 # Create a copy of zone.js here so that we can have a predictable path to
@@ -82,20 +117,12 @@
     srcs = [
         "index.html",
         ":field_pictures_copy",
+        ":main_bundle_file.js",
         ":zonejs_copy",
     ],
     visibility = ["//visibility:public"],
 )
 
-concatjs_devserver(
-    name = "devserver",
-    serving_path = "/main_bundle.js",
-    static_files = [
-        ":static_files",
-    ],
-    deps = [":main_bundle_compiled"],
-)
-
 filegroup(
     name = "common_css",
     srcs = ["common.css"],
diff --git a/scouting/www/app.ng.html b/scouting/www/app.ng.html
index ab1e433..1357228 100644
--- a/scouting/www/app.ng.html
+++ b/scouting/www/app.ng.html
@@ -46,6 +46,15 @@
       Import Match List
     </a>
   </li>
+  <li class="nav-item">
+    <a
+      class="nav-link"
+      [class.active]="tabIs('ShiftSchedule')"
+      (click)="switchTabToGuarded('ShiftSchedule')"
+    >
+      Shift Schedule
+    </a>
+  </li>
 </ul>
 
 <ng-container [ngSwitch]="tab">
@@ -63,4 +72,5 @@
   <app-import-match-list
     *ngSwitchCase="'ImportMatchList'"
   ></app-import-match-list>
+  <shift-schedule *ngSwitchCase="'ShiftSchedule'"></shift-schedule>
 </ng-container>
diff --git a/scouting/www/app.ts b/scouting/www/app.ts
index 02af125..ab9c229 100644
--- a/scouting/www/app.ts
+++ b/scouting/www/app.ts
@@ -1,6 +1,15 @@
 import {Component, ElementRef, ViewChild} from '@angular/core';
 
-type Tab = 'MatchList' | 'Notes' | 'Entry' | 'ImportMatchList';
+type Tab =
+  | 'MatchList'
+  | 'Notes'
+  | 'Entry'
+  | 'ImportMatchList'
+  | 'ShiftSchedule';
+
+// Ignore the guard for tabs that don't require the user to enter any data.
+const unguardedTabs: Tab[] = ['MatchList', 'ImportMatchList'];
+
 type TeamInMatch = {
   teamNumber: number;
   matchNumber: number;
@@ -24,12 +33,14 @@
 
   constructor() {
     window.addEventListener('beforeunload', (e) => {
-      if (!this.block_alerts.nativeElement.checked) {
-        // Based on
-        // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
-        // This combination ensures a dialog will be shown on most browsers.
-        e.preventDefault();
-        e.returnValue = '';
+      if (!unguardedTabs.includes(this.tab)) {
+        if (!this.block_alerts.nativeElement.checked) {
+          // Based on
+          // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
+          // This combination ensures a dialog will be shown on most browsers.
+          e.preventDefault();
+          e.returnValue = '';
+        }
       }
     });
   }
@@ -46,8 +57,12 @@
   switchTabToGuarded(tab: Tab) {
     let shouldSwitch = true;
     if (this.tab !== tab) {
-      if (!this.block_alerts.nativeElement.checked) {
-        shouldSwitch = window.confirm('Leave current page?');
+      if (!unguardedTabs.includes(this.tab)) {
+        if (!this.block_alerts.nativeElement.checked) {
+          shouldSwitch = window.confirm(
+            'Leave current page? You will lose all data.'
+          );
+        }
       }
     }
     if (shouldSwitch) {
diff --git a/scouting/www/app_module.ts b/scouting/www/app_module.ts
index 9c762f6..b3c788f 100644
--- a/scouting/www/app_module.ts
+++ b/scouting/www/app_module.ts
@@ -7,6 +7,7 @@
 import {ImportMatchListModule} from './import_match_list/import_match_list.module';
 import {MatchListModule} from './match_list/match_list.module';
 import {NotesModule} from './notes/notes.module';
+import {ShiftScheduleModule} from './shift_schedule/shift_schedule.module';
 
 @NgModule({
   declarations: [App],
@@ -17,6 +18,7 @@
     NotesModule,
     ImportMatchListModule,
     MatchListModule,
+    ShiftScheduleModule,
   ],
   exports: [App],
   bootstrap: [App],
diff --git a/scouting/www/entry/entry.ng.html b/scouting/www/entry/entry.ng.html
index 0e76268..d0805d4 100644
--- a/scouting/www/entry/entry.ng.html
+++ b/scouting/www/entry/entry.ng.html
@@ -37,7 +37,10 @@
 
   <div *ngSwitchCase="'Auto'" id="auto" class="container-fluid">
     <div class="row">
-      <img src="/pictures/field/quadrants.jpeg" alt="Quadrants Image" />
+      <img
+        src="/sha256/cbb99a057a2504e80af526dae7a0a04121aed84c56a6f4889e9576fe1c20c61e/pictures/field/quadrants.jpeg"
+        alt="Quadrants Image"
+      />
       <form>
         <input
           type="radio"
@@ -75,7 +78,10 @@
       </form>
     </div>
     <div class="row">
-      <img src="/pictures/field/balls.jpeg" alt="Image" />
+      <img
+        src="/sha256/cbb99a057a2504e80af526dae7a0a04121aed84c56a6f4889e9576fe1c20c61e/pictures/field/balls.jpeg"
+        alt="Image"
+      />
       <form>
         <!--Choice for each ball location-->
         <input
diff --git a/scouting/www/index.html b/scouting/www/index.template.html
similarity index 79%
rename from scouting/www/index.html
rename to scouting/www/index.template.html
index 84777d1..303dca1 100644
--- a/scouting/www/index.html
+++ b/scouting/www/index.template.html
@@ -14,6 +14,7 @@
   </head>
   <body>
     <my-app></my-app>
-    <script src="./main_bundle_compiled/main_bundle.js"></script>
+    <!-- The path here is auto-generated to be /sha256/<checksum>/main_bundle_file.js. -->
+    <script src="{MAIN_BUNDLE_FILE}"></script>
   </body>
 </html>
diff --git a/scouting/www/index_html_generator.py b/scouting/www/index_html_generator.py
new file mode 100644
index 0000000..3b057fd
--- /dev/null
+++ b/scouting/www/index_html_generator.py
@@ -0,0 +1,28 @@
+"""Generates index.html with the right checksum for main_bundle_file.js filled in."""
+
+import argparse
+import hashlib
+import sys
+from pathlib import Path
+
+def compute_sha256(filepath):
+    return hashlib.sha256(filepath.read_bytes()).hexdigest()
+
+def main(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--template", type=str)
+    parser.add_argument("--bundle", type=str)
+    parser.add_argument("--output", type=str)
+    args = parser.parse_args(argv[1:])
+
+    template = Path(args.template).read_text()
+    bundle_path = Path(args.bundle)
+    bundle_sha256 = compute_sha256(bundle_path)
+
+    output = template.format(
+        MAIN_BUNDLE_FILE = f"/sha256/{bundle_sha256}/{bundle_path.name}",
+    )
+    Path(args.output).write_text(output)
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/scouting/www/shift_schedule/BUILD b/scouting/www/shift_schedule/BUILD
new file mode 100644
index 0000000..8fe99e4
--- /dev/null
+++ b/scouting/www/shift_schedule/BUILD
@@ -0,0 +1,27 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+
+ts_library(
+    name = "shift_schedule",
+    srcs = [
+        "shift_schedule.component.ts",
+        "shift_schedule.module.ts",
+    ],
+    angular_assets = [
+        "shift_schedule.component.css",
+        "shift_schedule.ng.html",
+        "//scouting/www:common_css",
+    ],
+    compiler = "//tools:tsc_wrapped_with_angular",
+    target_compatible_with = ["@platforms//cpu:x86_64"],
+    use_angular_plugin = True,
+    visibility = ["//visibility:public"],
+    deps = [
+        "//scouting/webserver/requests/messages:error_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_response_ts_fbs",
+        "//scouting/webserver/requests/messages:request_all_matches_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+        "@npm//@angular/common",
+        "@npm//@angular/core",
+        "@npm//@angular/forms",
+    ],
+)
diff --git a/scouting/www/shift_schedule/shift_schedule.component.css b/scouting/www/shift_schedule/shift_schedule.component.css
new file mode 100644
index 0000000..bafeb4c
--- /dev/null
+++ b/scouting/www/shift_schedule/shift_schedule.component.css
@@ -0,0 +1,17 @@
+* {
+  padding: 5px;
+}
+
+.badge {
+  height: 20px;
+}
+
+.red {
+  background-color: #dc3545;
+  border-radius: 5px;
+}
+
+.blue {
+  background-color: #0d6efd;
+  border-radius: 5px;
+}
diff --git a/scouting/www/shift_schedule/shift_schedule.component.ts b/scouting/www/shift_schedule/shift_schedule.component.ts
new file mode 100644
index 0000000..2f22653
--- /dev/null
+++ b/scouting/www/shift_schedule/shift_schedule.component.ts
@@ -0,0 +1,15 @@
+import {Component, OnInit} from '@angular/core';
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {ErrorResponse} from 'org_frc971/scouting/webserver/requests/messages/error_response_generated';
+
+@Component({
+  selector: 'shift-schedule',
+  templateUrl: './shift_schedule.ng.html',
+  styleUrls: ['../common.css', './shift_schedule.component.css'],
+})
+export class ShiftsComponent {
+  progressMessage: string = '';
+  errorMessage: string = '';
+  // used to calculate shift blocks from starting match to ending match
+  numMatches: number[] = [20, 40, 60, 80, 100, 120, 140, 160, 180, 200];
+}
diff --git a/scouting/www/shift_schedule/shift_schedule.module.ts b/scouting/www/shift_schedule/shift_schedule.module.ts
new file mode 100644
index 0000000..0e5aa48
--- /dev/null
+++ b/scouting/www/shift_schedule/shift_schedule.module.ts
@@ -0,0 +1,12 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+
+import {ShiftsComponent} from './shift_schedule.component';
+
+@NgModule({
+  declarations: [ShiftsComponent],
+  exports: [ShiftsComponent],
+  imports: [CommonModule, FormsModule],
+})
+export class ShiftScheduleModule {}
diff --git a/scouting/www/shift_schedule/shift_schedule.ng.html b/scouting/www/shift_schedule/shift_schedule.ng.html
new file mode 100644
index 0000000..3a9cdf0
--- /dev/null
+++ b/scouting/www/shift_schedule/shift_schedule.ng.html
@@ -0,0 +1,33 @@
+<div class="header">
+  <h2>Shift Schedule</h2>
+</div>
+
+<div class="container-fluid">
+  <div class="row">
+    <div *ngFor="let num of numMatches">
+      <span class="badge bg-secondary rounded-left">
+        Scouting matches from {{num-19}} to {{num}}
+      </span>
+      <div class="list-group list-group-horizontal-sm">
+        <div class="redColumn">
+          <div *ngFor="let allianceNum of [1, 2, 3]">
+            <input
+              class="red text-center text-white fw-bold list-group-item list-group-item-action"
+              type="text"
+            />
+          </div>
+        </div>
+        <div class="blueColumn">
+          <div *ngFor="let allianceNum of [1, 2, 3]">
+            <input
+              class="blue text-center text-white fw-bold list-group-item list-group-item-action"
+              type="text"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <button class="btn btn-primary">Save</button>
+  <span class="error_message" role="alert">{{ errorMessage }}</span>
+</div>
diff --git a/third_party/BUILD b/third_party/BUILD
index 8dbb529..a550a24 100644
--- a/third_party/BUILD
+++ b/third_party/BUILD
@@ -55,6 +55,7 @@
     deps = select({
         "//tools:cpu_k8": ["@gstreamer_k8//:gstreamer"],
         "//tools:cpu_armhf": ["@gstreamer_armhf//:gstreamer"],
+        "//tools:cpu_arm64": ["@gstreamer_arm64//:gstreamer"],
         "//conditions:default": [":unavailable"],
     }),
 )
diff --git a/third_party/allwpilib/hal/src/main/native/athena/PWM.cpp b/third_party/allwpilib/hal/src/main/native/athena/PWM.cpp
index 19a3b83..83a0ca5 100644
--- a/third_party/allwpilib/hal/src/main/native/athena/PWM.cpp
+++ b/third_party/allwpilib/hal/src/main/native/athena/PWM.cpp
@@ -430,6 +430,7 @@
 
 void HAL_SetPWMPeriodScale(HAL_DigitalHandle pwmPortHandle, int32_t squelchMask,
                            int32_t* status) {
+  pwmSystem->writeConfig_Period(5050 / 2, status);
   auto port = digitalChannelHandles->Get(pwmPortHandle, HAL_HandleEnum::PWM);
   if (port == nullptr) {
     *status = HAL_HANDLE_ERROR;
diff --git a/third_party/flatbuffers/build_defs.bzl b/third_party/flatbuffers/build_defs.bzl
index cb50726..7eaa4d3 100644
--- a/third_party/flatbuffers/build_defs.bzl
+++ b/third_party/flatbuffers/build_defs.bzl
@@ -379,7 +379,8 @@
         flatc_args = DEFAULT_FLATC_TS_ARGS,
         visibility = None,
         restricted_to = None,
-        include_reflection = True):
+        include_reflection = True,
+        package_name = None):
     """Generates a ts_library rule for a given flatbuffer definition.
 
     Args:
@@ -401,6 +402,7 @@
       include_reflection: Optional, Whether to depend on the flatbuffer
         reflection library automatically. Only really relevant for the
         target that builds the reflection library itself.
+      package_name: Optional, Package name to use for the generated code.
     """
     srcs_lib = "%s_srcs" % (name)
 
@@ -428,7 +430,7 @@
         "SRCS=($(SRCS));",
         "OUTS=($(OUTS));",
         "for i in $${!SRCS[@]}; do",
-        "sed 's/third_party\\/flatbuffers/external\\/com_github_google_flatbuffers/' $${SRCS[i]} > $${OUTS[i]};",
+        "sed \"s/'.*reflection\\/reflection_pregenerated/'flatbuffers_reflection\\/reflection_generated/\" $${SRCS[i]} > $${OUTS[i]};",
         "sed -i 's/_pregenerated/_generated/' $${OUTS[i]};",
         "done",
     ])
@@ -469,6 +471,7 @@
         restricted_to = restricted_to,
         target_compatible_with = target_compatible_with,
         deps = [name + "_ts"],
+        package_name = package_name,
     )
     native.filegroup(
         name = "%s_includes" % (name),
diff --git a/third_party/flatbuffers/reflection/BUILD.bazel b/third_party/flatbuffers/reflection/BUILD.bazel
index 7948e12..aa421db 100644
--- a/third_party/flatbuffers/reflection/BUILD.bazel
+++ b/third_party/flatbuffers/reflection/BUILD.bazel
@@ -8,6 +8,7 @@
 
 flatbuffer_ts_library(
     name = "reflection_ts_fbs",
+    package_name = "flatbuffers_reflection",
     srcs = ["reflection.fbs"],
     include_reflection = False,
     visibility = ["//visibility:public"],
diff --git a/third_party/rawrtc/rawrtc/BUILD b/third_party/rawrtc/rawrtc/BUILD
index dc278f7..92fb15f 100644
--- a/third_party/rawrtc/rawrtc/BUILD
+++ b/third_party/rawrtc/rawrtc/BUILD
@@ -34,6 +34,7 @@
     includes = ["include/"],
     local_defines = [
         "RAWRTC_VERSION=\\\"0.5.1\\\"",
+        "RAWRTC_SKIP_DH_CHECK",
     ],
     visibility = ["//visibility:public"],
     deps = [
diff --git a/third_party/rawrtc/rawrtc/src/diffie_hellman_parameters/parameters.c b/third_party/rawrtc/rawrtc/src/diffie_hellman_parameters/parameters.c
index 952511f..702b8c9 100644
--- a/third_party/rawrtc/rawrtc/src/diffie_hellman_parameters/parameters.c
+++ b/third_party/rawrtc/rawrtc/src/diffie_hellman_parameters/parameters.c
@@ -25,7 +25,7 @@
   // optimized); just YOLO it. Note that this could probably be moved to
   // somewhere where the cost could be incurred at startup instead of
   // on connection (or even cached at build-time).
-#ifndef AOS_ARCHITECTURE_arm_frc
+#if !defined(AOS_ARCHITECTURE_arm_frc) && !defined(RAWRTC_SKIP_DH_CHECK)
     int codes;
 
     // Check that the parameters are "likely enough to be valid"
diff --git a/tools/build_rules/js.bzl b/tools/build_rules/js.bzl
index eeb5594..f5a7543 100644
--- a/tools/build_rules/js.bzl
+++ b/tools/build_rules/js.bzl
@@ -63,25 +63,6 @@
     },
 )
 
-# Some rules (e.g. babel()) do not expose their files as runfiles. So we need
-# to do this step manually.
-def _turn_files_into_runfiles_impl(ctx):
-    files = ctx.attr.files.files
-    return [DefaultInfo(
-        files = files,
-        runfiles = ctx.runfiles(transitive_files = files),
-    )]
-
-turn_files_into_runfiles = rule(
-    implementation = _turn_files_into_runfiles_impl,
-    attrs = {
-        "files": attr.label(
-            mandatory = True,
-            doc = "The target whose files should be turned into runfiles.",
-        ),
-    },
-)
-
 def protractor_ts_test(name, srcs, deps = None, **kwargs):
     """Wraps upstream protractor_web_test_suite() to reduce boilerplate.
 
diff --git a/tools/python/runtime_binary.sh b/tools/python/runtime_binary.sh
index 0bc832c..a36f085 100755
--- a/tools/python/runtime_binary.sh
+++ b/tools/python/runtime_binary.sh
@@ -34,7 +34,7 @@
   exit 1
 fi
 
-export LD_LIBRARY_PATH="${BASE_PATH}/usr/lib/lapack:${BASE_PATH}/usr/lib/libblas:${BASE_PATH}/usr/lib/x86_64-linux-gnu:${BASE_PATH}/../matplotlib_repo/rpathed3/usr/lib"
+export LD_LIBRARY_PATH="${BASE_PATH}/usr/lib/lapack:${BASE_PATH}/usr/lib/libblas:${BASE_PATH}/usr/lib/x86_64-linux-gnu:${BASE_PATH}/../matplotlib_repo/rpathed3/usr/lib:${BASE_PATH}/usr/lib/x86_64-linux-gnu/lapack:${BASE_PATH}/usr/lib/x86_64-linux-gnu/blas"
 
 # Prevent Python from importing the host's installed packages.
 exec "$BASE_PATH"/usr/bin/python3 -sS "$@"
diff --git a/y2020/control_loops/drivetrain/localizer_plotter.ts b/y2020/control_loops/drivetrain/localizer_plotter.ts
index 9fe80de..b2120ef 100644
--- a/y2020/control_loops/drivetrain/localizer_plotter.ts
+++ b/y2020/control_loops/drivetrain/localizer_plotter.ts
@@ -6,7 +6,7 @@
 import {Point} from 'org_frc971/aos/network/www/plotter';
 import {Table} from 'org_frc971/aos/network/www/reflection';
 import {ByteBuffer} from 'flatbuffers';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 
 const TIME = AosPlotter.TIME;
 
diff --git a/y2020/control_loops/drivetrain/localizer_test.cc b/y2020/control_loops/drivetrain/localizer_test.cc
index cc04428..f0bcd88 100644
--- a/y2020/control_loops/drivetrain/localizer_test.cc
+++ b/y2020/control_loops/drivetrain/localizer_test.cc
@@ -603,7 +603,7 @@
 // Tests that we don't blow up if we stop getting updates for an extended period
 // of time and fall behind on fetching fron the cameras.
 TEST_F(LocalizedDrivetrainTest, FetchersHandleTimeGap) {
-  set_enable_cameras(true);
+  set_enable_cameras(false);
   set_send_delay(std::chrono::seconds(0));
   event_loop_factory()->set_network_delay(std::chrono::nanoseconds(1));
   test_event_loop_
diff --git a/y2020/control_loops/superstructure/superstructure_lib_test.cc b/y2020/control_loops/superstructure/superstructure_lib_test.cc
index 1bbf42b..b13263a 100644
--- a/y2020/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2020/control_loops/superstructure/superstructure_lib_test.cc
@@ -686,8 +686,7 @@
     goal_builder.add_turret(turret_offset);
     goal_builder.add_shooter(shooter_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(10));
   VerifyNearGoal();
@@ -733,8 +732,7 @@
     goal_builder.add_turret(turret_offset);
     goal_builder.add_shooter(shooter_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   // Give it a lot of time to get there.
@@ -775,8 +773,7 @@
     goal_builder.add_turret(turret_offset);
     goal_builder.add_shooter(shooter_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(8));
   VerifyNearGoal();
@@ -810,8 +807,7 @@
     goal_builder.add_turret(turret_offset);
     goal_builder.add_shooter(shooter_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   superstructure_plant_.set_peak_hood_velocity(23.0);
   // 30 hz sin wave on the hood causes acceleration to be ignored.
@@ -865,8 +861,7 @@
     goal_builder.add_shooter(shooter_offset);
     goal_builder.add_shooting(true);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   // In the beginning, the finisher and accelerator should not be ready
@@ -911,8 +906,7 @@
     goal_builder.add_intake(intake_offset);
     goal_builder.add_shooter(shooter_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   // Give it a lot of time to get there.
@@ -961,8 +955,7 @@
     goal_builder.add_climber_voltage(-10.0);
     goal_builder.add_turret(turret_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   // The turret needs to move out of the way first.  This takes some time.
@@ -986,8 +979,7 @@
     goal_builder.add_climber_voltage(10.0);
     goal_builder.add_turret(turret_offset);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(1));
 
@@ -1175,6 +1167,8 @@
                                "y2020.control_loops.superstructure.Status");
     reader_.RemapLoggedChannel("/superstructure",
                                "y2020.control_loops.superstructure.Output");
+    reader_.RemapLoggedChannel("/drivetrain",
+                               "frc971.control_loops.drivetrain.Status");
     reader_.Register();
 
     roborio_ = aos::configuration::GetNode(reader_.configuration(), "roborio");
@@ -1221,32 +1215,27 @@
   constexpr double kShotDistance = 2.5;
   const auto target = turret::OuterPortPose(aos::Alliance::kRed);
 
-  // There was no target when this log was taken so send a position within range
-  // of the interpolation table.
-  test_event_loop_->AddPhasedLoop(
-      [&](int) {
-        auto builder = drivetrain_status_sender_.MakeBuilder();
+  // There was no target when this log was taken, so send a position within
+  // range of the interpolation table.
+  {
+    auto builder = drivetrain_status_sender_.MakeBuilder();
 
-        const auto localizer_offset =
-            builder
-                .MakeBuilder<
-                    frc971::control_loops::drivetrain::LocalizerState>()
-                .Finish();
+    const auto localizer_offset =
+        builder.MakeBuilder<frc971::control_loops::drivetrain::LocalizerState>()
+            .Finish();
 
-        auto drivetrain_status_builder =
-            builder.MakeBuilder<DrivetrainStatus>();
+    auto drivetrain_status_builder = builder.MakeBuilder<DrivetrainStatus>();
 
-        // Set the robot up at kShotAngle off from the target, kShotDistance
-        // away.
-        drivetrain_status_builder.add_x(target.abs_pos().x() +
-                                        std::cos(kShotAngle) * kShotDistance);
-        drivetrain_status_builder.add_y(target.abs_pos().y() +
-                                        std::sin(kShotAngle) * kShotDistance);
-        drivetrain_status_builder.add_localizer(localizer_offset);
+    // Set the robot up at kShotAngle off from the target, kShotDistance
+    // away.
+    drivetrain_status_builder.add_x(target.abs_pos().x() +
+                                    std::cos(kShotAngle) * kShotDistance);
+    drivetrain_status_builder.add_y(target.abs_pos().y() +
+                                    std::sin(kShotAngle) * kShotDistance);
+    drivetrain_status_builder.add_localizer(localizer_offset);
 
-        builder.CheckOk(builder.Send(drivetrain_status_builder.Finish()));
-      },
-      frc971::controls::kLoopFrequency);
+    builder.CheckOk(builder.Send(drivetrain_status_builder.Finish()));
+  }
 
   reader_.event_loop_factory()->Run();
 
@@ -1289,8 +1278,7 @@
 
     goal_builder.add_turret_tracking(true);
 
-    ASSERT_EQ(builder.Send(goal_builder.Finish()),
-              aos::RawSender::Error::kOk);
+    ASSERT_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   {
@@ -1347,8 +1335,7 @@
     goal_builder.add_shooter(shooter_goal);
     goal_builder.add_hood(hood_offset);
 
-    CHECK_EQ(builder.Send(goal_builder.Finish()),
-             aos::RawSender::Error::kOk);
+    CHECK_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
 
   RunFor(chrono::seconds(10));
@@ -1415,8 +1402,7 @@
     goal_builder.add_shooter_tracking(true);
     goal_builder.add_hood_tracking(true);
 
-    CHECK_EQ(builder.Send(goal_builder.Finish()),
-             aos::RawSender::Error::kOk);
+    CHECK_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(10));
 
@@ -1486,8 +1472,7 @@
     goal_builder.add_shooter_tracking(true);
     goal_builder.add_hood_tracking(true);
 
-    CHECK_EQ(builder.Send(goal_builder.Finish()),
-             aos::RawSender::Error::kOk);
+    CHECK_EQ(builder.Send(goal_builder.Finish()), aos::RawSender::Error::kOk);
   }
   RunFor(chrono::seconds(10));
 
diff --git a/y2020/control_loops/superstructure/turret_plotter.ts b/y2020/control_loops/superstructure/turret_plotter.ts
index 2c3fb01..5cb15c6 100644
--- a/y2020/control_loops/superstructure/turret_plotter.ts
+++ b/y2020/control_loops/superstructure/turret_plotter.ts
@@ -7,7 +7,7 @@
 import {Point} from 'org_frc971/aos/network/www/plotter';
 import {Table} from 'org_frc971/aos/network/www/reflection';
 import {ByteBuffer} from 'flatbuffers';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 
 import Connection = proxy.Connection;
 
diff --git a/y2020/vision/sift/BUILD b/y2020/vision/sift/BUILD
index c6afc98..feecb62 100644
--- a/y2020/vision/sift/BUILD
+++ b/y2020/vision/sift/BUILD
@@ -32,7 +32,7 @@
         "fast_gaussian_generator.cc",
         "get_gaussian_kernel.h",
         "@amd64_debian_sysroot//:sysroot_files",
-        "@deb_zlib1g_dev_1_2_11_dfsg_1_amd64_deb_repo//file",
+        "@deb_zlib1g_dev_1_2_11_dfsg_2_amd64_deb_repo//file",
         "@halide_k8//:build_files",
         "@llvm_toolchain//:all-components-x86_64-linux",
     ],
diff --git a/y2020/vision/sift/fast_gaussian_halide_generator.sh b/y2020/vision/sift/fast_gaussian_halide_generator.sh
index a90f218..cec9995 100755
--- a/y2020/vision/sift/fast_gaussian_halide_generator.sh
+++ b/y2020/vision/sift/fast_gaussian_halide_generator.sh
@@ -25,7 +25,7 @@
 SOURCE="$(rlocation org_frc971/y2020/vision/sift/fast_gaussian_generator.cc)"
 HALIDE="$(rlocation halide_k8)"
 SYSROOT="$(rlocation amd64_debian_sysroot)"
-ZLIB1G_DEV_AMD64_DEB="$(rlocation deb_zlib1g_dev_1_2_11_dfsg_1_amd64_deb_repo/file/zlib1g-dev_1.2.11.dfsg-1_amd64.deb)"
+ZLIB1G_DEV_AMD64_DEB="$(rlocation deb_zlib1g_dev_1_2_11_dfsg_2_amd64_deb_repo/file/zlib1g-dev_1.2.11.dfsg-2_amd64.deb)"
 
 ZLIB1G_DEV="$(mktemp -d)"
 
diff --git a/y2020/y2020_pi_template.json b/y2020/y2020_pi_template.json
index 5ad87eb..0f8d268 100644
--- a/y2020/y2020_pi_template.json
+++ b/y2020/y2020_pi_template.json
@@ -120,7 +120,7 @@
       "name": "/pi{{ NUM }}/camera/detailed",
       "type": "frc971.vision.sift.ImageMatchResult",
       "source_node": "pi{{ NUM }}",
-      "frequency": 25,
+      "frequency": 30,
       "max_size": 1000000
     },
     {
diff --git a/y2022/BUILD b/y2022/BUILD
index 83438ec..5d29f04 100644
--- a/y2022/BUILD
+++ b/y2022/BUILD
@@ -7,9 +7,11 @@
     binaries = [
         ":setpoint_setter",
         "//aos/network:web_proxy_main",
+        "//aos/events/logging:log_cat",
     ],
     data = [
         ":aos_config",
+        ":message_bridge_client.sh",
         "@ctre_phoenix_api_cpp_athena//:shared_libraries",
         "@ctre_phoenix_cci_athena//:shared_libraries",
     ],
@@ -40,12 +42,16 @@
         "//y2022/localizer:imu_main",
         "//y2022/localizer:localizer_main",
         "//y2022/vision:image_decimator",
+        "//y2022/image_streamer:image_streamer",
+        "//aos/events/logging:log_cat",
     ],
     data = [
         ":aos_config",
+        "//y2022/image_streamer:image_streamer_start",
     ],
     dirs = [
         "//y2022/www:www_files",
+        "//y2022/image_streamer/www:www_files",
     ],
     start_binaries = [
         "//aos/events/logging:logger_main",
@@ -53,6 +59,7 @@
         "//aos/network:message_bridge_server",
         "//aos/network:web_proxy_main",
         "//y2022/vision:camera_reader",
+        "//y2022/vision:ball_color_detector",
     ],
     target_compatible_with = ["//tools/platforms/hardware:raspberry_pi"],
     target_type = "pi",
@@ -95,6 +102,7 @@
             "//y2022/localizer:localizer_output_fbs",
             "//y2022/vision:calibration_fbs",
             "//y2022/vision:target_estimate_fbs",
+            "//y2022/vision:ball_color_fbs",
         ],
         target_compatible_with = ["@platforms//os:linux"],
         visibility = ["//visibility:public"],
@@ -142,6 +150,7 @@
         "//aos/network:remote_message_fbs",
         "//frc971/vision:vision_fbs",
         "//y2022/vision:calibration_fbs",
+        "//y2022/vision:ball_color_fbs",
     ],
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
@@ -161,6 +170,7 @@
         "//aos/network:message_bridge_client_fbs",
         "//aos/network:message_bridge_server_fbs",
         "//aos/network:timestamp_fbs",
+        "//y2022/vision:ball_color_fbs",
         "//y2019/control_loops/drivetrain:target_selector_fbs",
         "//y2022/control_loops/superstructure:superstructure_goal_fbs",
         "//y2022/control_loops/superstructure:superstructure_output_fbs",
diff --git a/y2022/actors/autonomous_actor.cc b/y2022/actors/autonomous_actor.cc
index 8bfb2d9..a8a2905 100644
--- a/y2022/actors/autonomous_actor.cc
+++ b/y2022/actors/autonomous_actor.cc
@@ -13,7 +13,7 @@
 #include "y2022/control_loops/drivetrain/drivetrain_base.h"
 
 DEFINE_bool(spline_auto, false, "If true, define a spline autonomous mode");
-DEFINE_bool(rapid_react, false,
+DEFINE_bool(rapid_react, true,
             "If true, run the main rapid react autonomous mode");
 
 namespace y2022 {
@@ -21,7 +21,7 @@
 namespace {
 constexpr double kExtendIntakeGoal = -0.02;
 constexpr double kRetractIntakeGoal = 1.47;
-constexpr double kIntakeRollerVoltage = 8.0;
+constexpr double kIntakeRollerVoltage = 12.0;
 constexpr double kRollerVoltage = 12.0;
 constexpr double kCatapultReturnPosition = -0.908;
 }  // namespace
@@ -209,15 +209,20 @@
   if (!WaitForPreloaded()) return;
 
   // Fire preloaded ball
-  set_turret_goal(constants::Values::kTurretFrontIntakePos());
+  set_turret_goal(constants::Values::kTurretBackIntakePos());
   set_fire_at_will(true);
   SendSuperstructureGoal();
   if (!WaitForBallsShot()) return;
+  LOG(INFO) << "Shot first ball "
+            << chrono::duration<double>(aos::monotonic_clock::now() -
+                                        start_time)
+                   .count()
+            << 's';
   set_fire_at_will(false);
   SendSuperstructureGoal();
 
   // Drive and intake the 2 balls in nearest to the starting zonei
-  set_turret_goal(constants::Values::kTurretBackIntakePos());
+  set_turret_goal(constants::Values::kTurretFrontIntakePos());
   ExtendBackIntake();
   if (!splines[0].WaitForPlan()) return;
   splines[0].Start();
@@ -228,6 +233,11 @@
   set_fire_at_will(true);
   SendSuperstructureGoal();
   if (!WaitForBallsShot()) return;
+  LOG(INFO) << "Shot first 3 balls "
+            << chrono::duration<double>(aos::monotonic_clock::now() -
+                                        start_time)
+                   .count()
+            << 's';
   set_fire_at_will(false);
   SendSuperstructureGoal();
 
@@ -238,6 +248,11 @@
   if (!splines[1].WaitForPlan()) return;
   splines[1].Start();
   if (!splines[1].WaitForSplineDistanceRemaining(0.02)) return;
+  LOG(INFO) << "At balls 4/5 "
+            << chrono::duration<double>(aos::monotonic_clock::now() -
+                                        start_time)
+                   .count()
+            << 's';
 
   // Drive to the shooting position
   if (!splines[2].WaitForPlan()) return;
@@ -246,6 +261,11 @@
   RetractFrontIntake();
 
   if (!splines[2].WaitForSplineDistanceRemaining(0.02)) return;
+  LOG(INFO) << "Shooting last balls "
+            << chrono::duration<double>(aos::monotonic_clock::now() -
+                                        start_time)
+                   .count()
+            << 's';
 
   // Fire the two balls once we stopped
   set_fire_at_will(true);
@@ -325,21 +345,18 @@
 
   superstructure_builder.add_intake_front(intake_front_offset);
   superstructure_builder.add_intake_back(intake_back_offset);
-  superstructure_builder.add_roller_speed_compensation(1.5);
+  superstructure_builder.add_roller_speed_compensation(0.0);
   superstructure_builder.add_roller_speed_front(roller_front_voltage_);
   superstructure_builder.add_roller_speed_back(roller_back_voltage_);
   if (requested_intake_.has_value()) {
     superstructure_builder.add_turret_intake(*requested_intake_);
   }
-  superstructure_builder.add_transfer_roller_speed_front(
-      transfer_roller_front_voltage_);
-  superstructure_builder.add_transfer_roller_speed_back(
-      transfer_roller_back_voltage_);
+  superstructure_builder.add_transfer_roller_speed(transfer_roller_voltage_);
   superstructure_builder.add_turret(turret_offset);
   superstructure_builder.add_catapult(catapult_goal_offset);
   superstructure_builder.add_fire(fire_);
   superstructure_builder.add_preloaded(preloaded_);
-  superstructure_builder.add_auto_aim(false);
+  superstructure_builder.add_auto_aim(true);
 
   if (builder.Send(superstructure_builder.Finish()) !=
       aos::RawSender::Error::kOk) {
@@ -351,8 +368,7 @@
   set_requested_intake(RequestedIntake::kFront);
   set_intake_front_goal(kExtendIntakeGoal);
   set_roller_front_voltage(kIntakeRollerVoltage);
-  set_transfer_roller_front_voltage(kRollerVoltage);
-  set_transfer_roller_back_voltage(-kRollerVoltage);
+  set_transfer_roller_voltage(kRollerVoltage);
   SendSuperstructureGoal();
 }
 
@@ -360,8 +376,7 @@
   set_requested_intake(std::nullopt);
   set_intake_front_goal(kRetractIntakeGoal);
   set_roller_front_voltage(0.0);
-  set_transfer_roller_front_voltage(0.0);
-  set_transfer_roller_back_voltage(0.0);
+  set_transfer_roller_voltage(0.0);
   SendSuperstructureGoal();
 }
 
@@ -369,8 +384,7 @@
   set_requested_intake(RequestedIntake::kBack);
   set_intake_back_goal(kExtendIntakeGoal);
   set_roller_back_voltage(kIntakeRollerVoltage);
-  set_transfer_roller_back_voltage(kRollerVoltage);
-  set_transfer_roller_front_voltage(-kRollerVoltage);
+  set_transfer_roller_voltage(-kRollerVoltage);
   SendSuperstructureGoal();
 }
 
@@ -378,13 +392,13 @@
   set_requested_intake(std::nullopt);
   set_intake_back_goal(kRetractIntakeGoal);
   set_roller_back_voltage(0.0);
-  set_transfer_roller_front_voltage(0.0);
-  set_transfer_roller_back_voltage(0.0);
+  set_transfer_roller_voltage(0.0);
   SendSuperstructureGoal();
 }
 
 [[nodiscard]] bool AutonomousActor::WaitForBallsShot() {
-  CHECK(superstructure_status_fetcher_.Fetch());
+  superstructure_status_fetcher_.Fetch();
+  CHECK(superstructure_status_fetcher_.get());
 
   // Don't do anything if we aren't loaded
   if (superstructure_status_fetcher_->state() !=
diff --git a/y2022/actors/autonomous_actor.h b/y2022/actors/autonomous_actor.h
index 0168630..ec66fb3 100644
--- a/y2022/actors/autonomous_actor.h
+++ b/y2022/actors/autonomous_actor.h
@@ -41,11 +41,8 @@
   void set_roller_back_voltage(double roller_back_voltage) {
     roller_back_voltage_ = roller_back_voltage;
   }
-  void set_transfer_roller_front_voltage(double voltage) {
-    transfer_roller_front_voltage_ = voltage;
-  }
-  void set_transfer_roller_back_voltage(double voltage) {
-    transfer_roller_back_voltage_ = voltage;
+  void set_transfer_roller_voltage(double voltage) {
+    transfer_roller_voltage_ = voltage;
   }
   void set_requested_intake(std::optional<RequestedIntake> requested_intake) {
     requested_intake_ = requested_intake;
@@ -78,8 +75,7 @@
   double intake_back_goal_ = 0.0;
   double roller_front_voltage_ = 0.0;
   double roller_back_voltage_ = 0.0;
-  double transfer_roller_front_voltage_ = 0.0;
-  double transfer_roller_back_voltage_ = 0.0;
+  double transfer_roller_voltage_ = 0.0;
   std::optional<RequestedIntake> requested_intake_ = std::nullopt;
   double turret_goal_ = 0.0;
   bool fire_ = false;
diff --git a/y2022/actors/splines/spline_5_ball_1.json b/y2022/actors/splines/spline_5_ball_1.json
index 03b5644..da3e4cf 100644
--- a/y2022/actors/splines/spline_5_ball_1.json
+++ b/y2022/actors/splines/spline_5_ball_1.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [-0.18145693702491972, -0.20576409082414582, 0.49914336935341463, 5.522928566665585, 2.880988556589501, 1.845502911614626], "spline_y": [2.346189480782648, 3.695236516639704, 4.837672745203337, 2.7973591802504263, 2.3618745632049176, 1.471550457245212], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.0}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.0}, {"constraint_type": "VOLTAGE", "value": 10.0}]}
\ No newline at end of file
+{"spline_count": 1, "spline_x": [-0.18145693702491972, -0.1806686149879133, -0.05595918014581436, 5.762204620882601, 2.7805678460726355, 1.6146169804687496], "spline_y": [2.346189480782648, 3.6925675615333544, 4.41262134323365, 2.4753395126953124, 2.2341888067461992, 1.3005395681218328], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.0}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2.5}, {"constraint_type": "VOLTAGE", "value": 12.0}, {"constraint_type": "VELOCITY", "value": 0.8, "start_distance": 1.0, "end_distance": 1.15}]}
\ No newline at end of file
diff --git a/y2022/actors/splines/spline_5_ball_2.json b/y2022/actors/splines/spline_5_ball_2.json
index 975d6ed..3efd1ee 100644
--- a/y2022/actors/splines/spline_5_ball_2.json
+++ b/y2022/actors/splines/spline_5_ball_2.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [1.845502911614626, 2.4171345421461163, 3.004385177342393, 5.2666273627598565, 5.91167736359207, 6.7133296469650885], "spline_y": [1.471550457245212, 2.0368843425108123, 1.4829129663110887, 1.7324754229926884, 1.9201953550863622, 2.6087941113170316], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2}, {"constraint_type": "VOLTAGE", "value": 10}]}
+{"spline_count": 1, "spline_x": [1.6037446032516893, 2.2055167265625, 2.8212725389450344, 6.148134261553881, 5.92062789622044, 6.7046250148859805], "spline_y": [1.2861465107685808, 1.7993420469805743, 1.286805497714088, 2.0935212995201415, 1.9849658141364017, 2.755576908889358], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3.0}, {"constraint_type": "LATERAL_ACCELERATION", "value": 3.0}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
diff --git a/y2022/actors/splines/spline_5_ball_3.json b/y2022/actors/splines/spline_5_ball_3.json
index e9e1a41..6b239fc 100644
--- a/y2022/actors/splines/spline_5_ball_3.json
+++ b/y2022/actors/splines/spline_5_ball_3.json
@@ -1 +1 @@
-{"spline_count": 1, "spline_x": [6.7133296469650885, 6.343336285408311, 5.737641975476239, 3.4208648418667504, 2.51547308321015, 1.841750386467515], "spline_y": [2.6087941113170316, 2.2490783431368313, 1.931858867758041, 1.9600940870614552, 2.031396541720077, 1.4679812465169668], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 3}, {"constraint_type": "LATERAL_ACCELERATION", "value": 2}, {"constraint_type": "VOLTAGE", "value": 10}]}
+{"spline_count": 1, "spline_x": [6.702231375950168, 6.373881457031249, 5.758688966174009, 3.1788453508620487, 2.273453592205448, 1.6114305300886826], "spline_y": [2.7438724869219806, 2.4293757261929896, 2.0768880836927197, 1.7809922274871859, 1.852294682145808, 1.2821724076488596], "constraints": [{"constraint_type": "LONGITUDINAL_ACCELERATION", "value": 4.0}, {"constraint_type": "LATERAL_ACCELERATION", "value": 3}, {"constraint_type": "VOLTAGE", "value": 12.0}]}
\ No newline at end of file
diff --git a/y2022/constants.cc b/y2022/constants.cc
index 0b4a591..cb85e94 100644
--- a/y2022/constants.cc
+++ b/y2022/constants.cc
@@ -70,7 +70,7 @@
   turret_params->zeroing_voltage = 4.0;
   turret_params->operating_voltage = 12.0;
   turret_params->zeroing_profile_params = {0.5, 2.0};
-  turret_params->default_profile_params = {15.0, 40.0};
+  turret_params->default_profile_params = {15.0, 20.0};
   turret_params->range = Values::kTurretRange();
   turret_params->make_integral_loop =
       control_loops::superstructure::turret::MakeIntegralTurretLoop;
@@ -132,8 +132,19 @@
 
   // Interpolation table for comp and practice robots
   r.shot_interpolation_table = InterpolationTable<Values::ShotParams>({
-      {2, {0.08, 8.0}},
-      {5, {0.6, 10.0}},
+      {1.0, {0.0, 19.0}},
+      {1.6, {0.0, 19.0}},
+      {1.9, {0.1, 19.0}},
+      {2.12, {0.15, 18.8}},
+      {2.9, {0.25, 19.2}},
+
+      {3.8, {0.35, 20.6}},
+      {4.9, {0.4,  21.9}},
+      {6.0, {0.40, 24.0}},
+      {7.0, {0.40, 25.5}},
+
+      {7.8, {0.35, 26.9}},
+      {10.0, {0.35, 26.9}},
   });
 
   switch (team) {
@@ -162,20 +173,24 @@
       break;
 
     case kCompTeamNumber:
-      climber->potentiometer_offset = -0.0463847608752;
+      climber->potentiometer_offset =
+          -0.0463847608752 - 0.0376876182111 + 0.0629263851579;
 
-      intake_front->potentiometer_offset = 2.79628370453323;
+      intake_front->potentiometer_offset =
+          2.79628370453323 - 0.0250288114832881 + 0.577152542437606;
       intake_front->subsystem_params.zeroing_constants
-          .measured_absolute_position = 0.248921954833972;
+          .measured_absolute_position = 0.26963366701647;
 
-      intake_back->potentiometer_offset = 3.1409576474047;
+      intake_back->potentiometer_offset =
+          3.1409576474047 + 0.278653334013286 + 0.00879137908308503;
       intake_back->subsystem_params.zeroing_constants
-          .measured_absolute_position = 0.280099007470002;
+          .measured_absolute_position = 0.242434593996789;
 
       turret->potentiometer_offset = -9.99970387166721 + 0.06415943 +
-                                     0.073290115367682 - 0.0634440443622909;
+                                     0.073290115367682 - 0.0634440443622909 +
+                                     0.213601224728352 + 0.0657973101027296;
       turret->subsystem_params.zeroing_constants.measured_absolute_position =
-          0.568649928102931;
+          0.27787064956636;
 
       flipper_arm_left->potentiometer_offset = -6.4;
       flipper_arm_right->potentiometer_offset = 5.56;
diff --git a/y2022/constants.h b/y2022/constants.h
index 35ca0ae..610ddc0 100644
--- a/y2022/constants.h
+++ b/y2022/constants.h
@@ -114,13 +114,13 @@
   static constexpr ::frc971::constants::Range kTurretRange() {
     return ::frc971::constants::Range{
         .lower_hard = -7.0,  // Back Hard
-        .upper_hard = 4.0,   // Front Hard
+        .upper_hard = 3.0,   // Front Hard
         .lower = -6.5,       // Back Soft
-        .upper = 3.25        // Front Soft
+        .upper = 2.5         // Front Soft
     };
   }
 
-  static constexpr double kTurretBackIntakePos() { return M_PI; }
+  static constexpr double kTurretBackIntakePos() { return -M_PI; }
   static constexpr double kTurretFrontIntakePos() { return 0; }
 
   static constexpr double kTurretPotRatio() { return 27.0 / 110.0; }
diff --git a/y2022/control_loops/python/catapult.py b/y2022/control_loops/python/catapult.py
index 2d0588a..9e9adf7 100755
--- a/y2022/control_loops/python/catapult.py
+++ b/y2022/control_loops/python/catapult.py
@@ -41,7 +41,7 @@
 J_cup = M_cup * lever**2.0 + M_cup * (ball_diameter / 2.)**2.0
 
 
-J = (0.6 * J_ball + J_bar + J_cup * 0.0)
+J = (0.0 * J_ball + J_bar + J_cup * 0.0)
 JEmpty = (J_bar + J_cup * 0.0)
 
 kCatapultWithBall = catapult_lib.CatapultParams(
diff --git a/y2022/control_loops/python/catapult_lib.py b/y2022/control_loops/python/catapult_lib.py
index d6040d1..4a88b14 100644
--- a/y2022/control_loops/python/catapult_lib.py
+++ b/y2022/control_loops/python/catapult_lib.py
@@ -15,9 +15,9 @@
 class Catapult(angular_system.AngularSystem):
     def __init__(self, params, name="Catapult"):
         super(Catapult, self).__init__(params, name)
-        # Signal that we have a single cycle output delay to compensate for in
+        # Signal that we have a 2 cycle output delay to compensate for in
         # our observer.
-        self.delayed_u = True
+        self.delayed_u = 2
 
         self.InitializeState()
 
@@ -25,9 +25,9 @@
 class IntegralCatapult(angular_system.IntegralAngularSystem):
     def __init__(self, params, name="IntegralCatapult"):
         super(IntegralCatapult, self).__init__(params, name=name)
-        # Signal that we have a single cycle output delay to compensate for in
+        # Signal that we have a 2 cycle output delay to compensate for in
         # our observer.
-        self.delayed_u = True
+        self.delayed_u = 2
 
         self.InitializeState()
 
diff --git a/y2022/control_loops/superstructure/BUILD b/y2022/control_loops/superstructure/BUILD
index eb713c2..ebd9ace 100644
--- a/y2022/control_loops/superstructure/BUILD
+++ b/y2022/control_loops/superstructure/BUILD
@@ -177,6 +177,7 @@
         "//frc971/control_loops:control_loops_fbs",
         "//frc971/control_loops:profiled_subsystem_fbs",
         "//frc971/control_loops/drivetrain:drivetrain_output_fbs",
+        "//frc971/queues:gyro_fbs",
         "//third_party:phoenix",
         "//third_party:wpilib",
     ],
diff --git a/y2022/control_loops/superstructure/catapult/catapult.cc b/y2022/control_loops/superstructure/catapult/catapult.cc
index 2093174..a04d8c9 100644
--- a/y2022/control_loops/superstructure/catapult/catapult.cc
+++ b/y2022/control_loops/superstructure/catapult/catapult.cc
@@ -342,7 +342,9 @@
     latched_shot_velocity = catapult_goal->shot_velocity();
   }
 
-  if (catapult_.running()) {
+  // Don't update last_firing_ if the catapult is disabled, so that we actually
+  // end up firing once it's enabled
+  if (catapult_.running() && !catapult_disabled) {
     last_firing_ = fire;
   }
 
@@ -359,10 +361,13 @@
       // hardware applies it, we need to run the optimizer for the position at
       // the *next* control loop cycle.
 
-      const Eigen::Vector3d next_X =
-          catapult_.controller().plant().A() * catapult_.estimated_state() +
-          catapult_.controller().plant().B() *
-              catapult_.controller().observer().last_U();
+      Eigen::Vector3d next_X = catapult_.estimated_state();
+      for (int i = catapult_.controller().plant().coefficients().delayed_u;
+           i > 1; --i) {
+        next_X = catapult_.controller().plant().A() * next_X +
+                 catapult_.controller().plant().B() *
+                     catapult_.controller().observer().last_U(i - 1);
+      }
 
       catapult_mpc_.SetState(
           next_X.block<2, 1>(0, 0),
diff --git a/y2022/control_loops/superstructure/catapult_plotter.ts b/y2022/control_loops/superstructure/catapult_plotter.ts
index 6c66340..90db40b 100644
--- a/y2022/control_loops/superstructure/catapult_plotter.ts
+++ b/y2022/control_loops/superstructure/catapult_plotter.ts
@@ -14,6 +14,7 @@
   const goal = aosPlotter.addMessageSource('/superstructure', 'y2022.control_loops.superstructure.Goal');
   const output = aosPlotter.addMessageSource('/superstructure', 'y2022.control_loops.superstructure.Output');
   const status = aosPlotter.addMessageSource('/superstructure', 'y2022.control_loops.superstructure.Status');
+  const position = aosPlotter.addMessageSource('/superstructure', 'y2022.control_loops.superstructure.Position');
   const robotState = aosPlotter.addMessageSource('/aos', 'aos.RobotState');
 
   // Robot Enabled/Disabled and Mode
@@ -34,6 +35,7 @@
   positionPlot.addMessageLine(status, ['catapult', 'position']).setColor(GREEN).setPointSize(4.0);
   positionPlot.addMessageLine(status, ['catapult', 'velocity']).setColor(PINK).setPointSize(1.0);
   positionPlot.addMessageLine(status, ['catapult', 'calculated_velocity']).setColor(BROWN).setPointSize(1.0);
+  positionPlot.addMessageLine(position, ['catapult', 'pot']).setColor(WHITE).setPointSize(4.0);
   positionPlot.addMessageLine(status, ['catapult', 'estimator_state', 'position']).setColor(CYAN).setPointSize(1.0);
 
   const voltagePlot =
diff --git a/y2022/control_loops/superstructure/led_indicator.cc b/y2022/control_loops/superstructure/led_indicator.cc
index 54b0b00..94d0506 100644
--- a/y2022/control_loops/superstructure/led_indicator.cc
+++ b/y2022/control_loops/superstructure/led_indicator.cc
@@ -5,24 +5,28 @@
 namespace y2022::control_loops::superstructure {
 
 LedIndicator::LedIndicator(aos::EventLoop *event_loop)
-    : drivetrain_output_fetcher_(
-          event_loop->MakeFetcher<frc971::control_loops::drivetrain::Output>(
+    : event_loop_(event_loop),
+      drivetrain_output_fetcher_(
+          event_loop_->MakeFetcher<frc971::control_loops::drivetrain::Output>(
               "/drivetrain")),
       superstructure_status_fetcher_(
-          event_loop->MakeFetcher<Status>("/superstructure")),
+          event_loop_->MakeFetcher<Status>("/superstructure")),
       server_statistics_fetcher_(
-          event_loop->MakeFetcher<aos::message_bridge::ServerStatistics>(
+          event_loop_->MakeFetcher<aos::message_bridge::ServerStatistics>(
               "/roborio/aos")),
       client_statistics_fetcher_(
-          event_loop->MakeFetcher<aos::message_bridge::ClientStatistics>(
-              "/roborio/aos")) {
+          event_loop_->MakeFetcher<aos::message_bridge::ClientStatistics>(
+              "/roborio/aos")),
+      gyro_reading_fetcher_(
+          event_loop_->MakeFetcher<frc971::sensors::GyroReading>(
+              "/drivetrain")) {
   led::CANdleConfiguration config;
   config.statusLedOffWhenActive = true;
   config.disableWhenLOS = false;
   config.brightnessScalar = 1.0;
   candle_.ConfigAllSettings(config, 0);
 
-  event_loop->AddPhasedLoop([&](int) { DecideColor(); },
+  event_loop_->AddPhasedLoop([&](int) { DecideColor(); },
                             std::chrono::milliseconds(20));
 }
 
@@ -36,7 +40,8 @@
 bool DisconnectedPiServer(
     const aos::message_bridge::ServerStatistics &server_stats) {
   for (const auto *pi_server_status : *server_stats.connections()) {
-    if (pi_server_status->state() == aos::message_bridge::State::DISCONNECTED) {
+    if (pi_server_status->state() == aos::message_bridge::State::DISCONNECTED &&
+        pi_server_status->node()->name()->string_view() != "logger") {
       return true;
     }
   }
@@ -46,7 +51,8 @@
 bool DisconnectedPiClient(
     const aos::message_bridge::ClientStatistics &client_stats) {
   for (const auto *pi_client_status : *client_stats.connections()) {
-    if (pi_client_status->state() == aos::message_bridge::State::DISCONNECTED) {
+    if (pi_client_status->state() == aos::message_bridge::State::DISCONNECTED &&
+        pi_client_status->node()->name()->string_view() != "logger") {
       return true;
     }
   }
@@ -65,6 +71,7 @@
   server_statistics_fetcher_.Fetch();
   drivetrain_output_fetcher_.Fetch();
   client_statistics_fetcher_.Fetch();
+  gyro_reading_fetcher_.Fetch();
 
   // Estopped
   if (superstructure_status_fetcher_.get() &&
@@ -80,6 +87,23 @@
     return;
   }
 
+  // If the imu gyro readings are not being sent/updated recently
+  if (!gyro_reading_fetcher_.get() ||
+      gyro_reading_fetcher_.context().monotonic_event_time <
+          event_loop_->monotonic_now() - frc971::controls::kLoopFrequency * 10) {
+    if (imu_flash_) {
+      DisplayLed(255, 0, 0);
+    } else {
+      DisplayLed(255, 255, 255);
+    }
+
+    if (imu_counter_ % kFlashIterations == 0) {
+      imu_flash_ = !imu_flash_;
+    }
+    imu_counter_++;
+    return;
+  }
+
   // Pi disconnected
   if ((server_statistics_fetcher_.get() &&
        DisconnectedPiServer(*server_statistics_fetcher_)) ||
diff --git a/y2022/control_loops/superstructure/led_indicator.h b/y2022/control_loops/superstructure/led_indicator.h
index 680b875..0f44788 100644
--- a/y2022/control_loops/superstructure/led_indicator.h
+++ b/y2022/control_loops/superstructure/led_indicator.h
@@ -9,6 +9,7 @@
 #include "frc971/control_loops/control_loops_generated.h"
 #include "frc971/control_loops/drivetrain/drivetrain_output_generated.h"
 #include "frc971/control_loops/profiled_subsystem_generated.h"
+#include "frc971/queues/gyro_generated.h"
 #include "y2022/control_loops/superstructure/superstructure_output_generated.h"
 #include "y2022/control_loops/superstructure/superstructure_status_generated.h"
 
@@ -22,6 +23,7 @@
   //
   // Red: estopped
   // Yellow: not zeroed
+  // Flash red/white: imu disconnected
   // Flash red/green: pi disconnected
   // Purple: driving fast
   //
@@ -48,6 +50,7 @@
 
   ctre::phoenix::led::CANdle candle_{0, ""};
 
+  aos::EventLoop *event_loop_;
   aos::Fetcher<frc971::control_loops::drivetrain::Output>
       drivetrain_output_fetcher_;
   aos::Fetcher<Status> superstructure_status_fetcher_;
@@ -55,7 +58,10 @@
       server_statistics_fetcher_;
   aos::Fetcher<aos::message_bridge::ClientStatistics>
       client_statistics_fetcher_;
+  aos::Fetcher<frc971::sensors::GyroReading> gyro_reading_fetcher_;
 
+  size_t imu_counter_ = 0;
+  bool imu_flash_ = false;
   size_t disconnected_counter_ = 0;
   bool disconnected_flash_ = false;
 };
diff --git a/y2022/control_loops/superstructure/superstructure.cc b/y2022/control_loops/superstructure/superstructure.cc
index 6daea37..b21dd0d 100644
--- a/y2022/control_loops/superstructure/superstructure.cc
+++ b/y2022/control_loops/superstructure/superstructure.cc
@@ -70,8 +70,7 @@
   const CatapultGoal *catapult_goal = nullptr;
   double roller_speed_compensated_front = 0.0;
   double roller_speed_compensated_back = 0.0;
-  double transfer_roller_speed_front = 0.0;
-  double transfer_roller_speed_back = 0.0;
+  double transfer_roller_speed = 0.0;
   double flipper_arms_voltage = 0.0;
   bool have_active_intake_request = false;
 
@@ -84,8 +83,7 @@
         unsafe_goal->roller_speed_back() -
         std::min(velocity * unsafe_goal->roller_speed_compensation(), 0.0);
 
-    transfer_roller_speed_front = unsafe_goal->transfer_roller_speed_front();
-    transfer_roller_speed_back = unsafe_goal->transfer_roller_speed_back();
+    transfer_roller_speed = unsafe_goal->transfer_roller_speed();
 
     turret_goal =
         unsafe_goal->auto_aim() ? auto_aim_goal : unsafe_goal->turret();
@@ -175,13 +173,13 @@
   }
 
   // Check if we're either spitting of have lost the ball.
-  if (transfer_roller_speed_front < 0.0 ||
+  if ((transfer_roller_speed < 0.0 && front_intake_has_ball_) ||
       timestamp >
           front_intake_beambreak_timer_ + constants::Values::kBallLostTime()) {
     front_intake_has_ball_ = false;
   }
 
-  if (transfer_roller_speed_back < 0.0 ||
+  if ((transfer_roller_speed > 0.0 && back_intake_has_ball_) ||
       timestamp >
           back_intake_beambreak_timer_ + constants::Values::kBallLostTime()) {
     back_intake_has_ball_ = false;
@@ -194,18 +192,18 @@
   if (front_intake_has_ball_) {
     roller_speed_compensated_front = 0.0;
     if (position->intake_beambreak_front()) {
-      transfer_roller_speed_front = -wiggle_voltage;
+      transfer_roller_speed = -wiggle_voltage;
     } else {
-      transfer_roller_speed_front = wiggle_voltage;
+      transfer_roller_speed = wiggle_voltage;
     }
   }
 
   if (back_intake_has_ball_) {
     roller_speed_compensated_back = 0.0;
     if (position->intake_beambreak_back()) {
-      transfer_roller_speed_back = -wiggle_voltage;
+      transfer_roller_speed = wiggle_voltage;
     } else {
-      transfer_roller_speed_back = wiggle_voltage;
+      transfer_roller_speed = -wiggle_voltage;
     }
   }
 
@@ -244,6 +242,10 @@
        .turret_position = turret_.estimated_position(),
        .shooting = true});
 
+  // Dont shoot if the robot is moving faster than this
+  constexpr double kMaxShootSpeed = 1.7;
+  const bool moving_too_fast = std::abs(robot_velocity()) > kMaxShootSpeed;
+
   switch (state_) {
     case SuperstructureState::IDLE: {
       // Only change the turret's goal loading position when idle, to prevent us
@@ -257,11 +259,12 @@
       }
       // When IDLE with no specific intake button pressed, allow the goal
       // message to override the intaking stuff.
-      if (have_active_intake_request || turret_goal == nullptr) {
+      if (have_active_intake_request || (turret_goal == nullptr)) {
         turret_goal = &turret_loading_goal_buffer.message();
       }
 
       if (!front_intake_has_ball_ && !back_intake_has_ball_) {
+        last_shot_angle_ = std::nullopt;
         break;
       }
 
@@ -291,11 +294,9 @@
 
       // Transfer rollers and flipper arm belt on
       if (turret_intake_state_ == RequestedIntake::kFront) {
-        transfer_roller_speed_front =
-            constants::Values::kTransferRollerVoltage();
+        transfer_roller_speed = constants::Values::kTransferRollerVoltage();
       } else {
-        transfer_roller_speed_back =
-            constants::Values::kTransferRollerVoltage();
+        transfer_roller_speed = -constants::Values::kTransferRollerVoltage();
       }
       flipper_arms_voltage = constants::Values::kFlipperFeedVoltage();
 
@@ -330,6 +331,13 @@
     }
     case SuperstructureState::LOADED: {
       if (unsafe_goal != nullptr) {
+        if (turret_goal == nullptr) {
+          if (last_shot_angle_) {
+            turret_loading_goal_buffer.mutable_message()->mutate_unsafe_goal(
+                *last_shot_angle_);
+          }
+          turret_goal = &turret_loading_goal_buffer.message();
+        }
         if (unsafe_goal->cancel_shot()) {
           // Cancel the shot process
           state_ = SuperstructureState::IDLE;
@@ -345,9 +353,24 @@
       break;
     }
     case SuperstructureState::SHOOTING: {
+      if (turret_goal == nullptr) {
+        if (last_shot_angle_) {
+          turret_loading_goal_buffer.mutable_message()->mutate_unsafe_goal(
+              *last_shot_angle_);
+        }
+        turret_goal = &turret_loading_goal_buffer.message();
+        last_shot_angle_ = turret_goal->unsafe_goal();
+      } else {
+        last_shot_angle_ = std::nullopt;
+      }
+      const bool turret_near_goal =
+          turret_goal != nullptr &&
+          std::abs(turret_goal->unsafe_goal() - turret_.position()) <
+              kTurretGoalThreshold;
+
       // Don't open the flippers until the turret's ready: give them as little
-      // time to get bumped as possible.
-      if (!turret_near_goal || collided) {
+      // time to get bumped as possible. Or moving to fast.
+      if (!turret_near_goal || collided || moving_too_fast) {
         break;
       }
 
@@ -386,11 +409,6 @@
         reseating_in_catapult_ = true;
         break;
       }
-      // If we started firing and the flippers closed a bit, estop to prevent
-      // damage
-      if (fire_ && !flippers_open_) {
-        catapult_.Estop();
-      }
 
       // If the turret reached the aiming goal and the catapult is safe to move
       // up, fire!
@@ -473,8 +491,7 @@
   if (output != nullptr) {
     output_struct.roller_voltage_front = roller_speed_compensated_front;
     output_struct.roller_voltage_back = roller_speed_compensated_back;
-    output_struct.transfer_roller_voltage_front = transfer_roller_speed_front;
-    output_struct.transfer_roller_voltage_back = transfer_roller_speed_back;
+    output_struct.transfer_roller_voltage = transfer_roller_speed;
     output_struct.flipper_arms_voltage = flipper_arms_voltage;
 
     output->CheckOk(output->Send(Output::Pack(*output->fbb(), &output_struct)));
@@ -509,6 +526,7 @@
   status_builder.add_flippers_open(flippers_open_);
   status_builder.add_reseating_in_catapult(reseating_in_catapult_);
   status_builder.add_fire(fire_);
+  status_builder.add_moving_too_fast(moving_too_fast);
   status_builder.add_ready_to_fire(state_ == SuperstructureState::LOADED &&
                                    turret_near_goal && !collided);
   status_builder.add_state(state_);
diff --git a/y2022/control_loops/superstructure/superstructure.h b/y2022/control_loops/superstructure/superstructure.h
index 11cd38f..14fa8ab 100644
--- a/y2022/control_loops/superstructure/superstructure.h
+++ b/y2022/control_loops/superstructure/superstructure.h
@@ -94,6 +94,7 @@
   SuperstructureState state_ = SuperstructureState::IDLE;
   bool front_intake_has_ball_ = false;
   bool back_intake_has_ball_ = false;
+  std::optional<double> last_shot_angle_ = std::nullopt;
   RequestedIntake turret_intake_state_ = RequestedIntake::kFront;
 
   DISALLOW_COPY_AND_ASSIGN(Superstructure);
diff --git a/y2022/control_loops/superstructure/superstructure_goal.fbs b/y2022/control_loops/superstructure/superstructure_goal.fbs
index 7227dc2..0bb51e1 100644
--- a/y2022/control_loops/superstructure/superstructure_goal.fbs
+++ b/y2022/control_loops/superstructure/superstructure_goal.fbs
@@ -37,8 +37,10 @@
   roller_speed_front:double (id: 3);
   roller_speed_back:double (id: 4);
 
-  transfer_roller_speed_front:double (id: 5);
-  transfer_roller_speed_back:double (id: 12);
+  // Positive is intaking front and spitting back, negative is the opposite
+  transfer_roller_speed:double (id: 5);
+  // Not used anymore - just one transfer roller speed to control both
+  transfer_roller_speed_back:double (id: 12, deprecated);
 
   // Factor to multiply robot velocity by and add to roller voltage.
   roller_speed_compensation:double (id: 6);
diff --git a/y2022/control_loops/superstructure/superstructure_lib_test.cc b/y2022/control_loops/superstructure/superstructure_lib_test.cc
index 6c19052..02fef22 100644
--- a/y2022/control_loops/superstructure/superstructure_lib_test.cc
+++ b/y2022/control_loops/superstructure/superstructure_lib_test.cc
@@ -753,9 +753,9 @@
   SetEnabled(true);
   WaitUntilZeroed();
 
-  SendRobotVelocity(3.0);
+  SendRobotVelocity(1.0);
 
-  constexpr double kTurretGoal = 3.0;
+  constexpr double kTurretGoal = 2.0;
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
@@ -778,10 +778,7 @@
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 12.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 12.0);
 
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_front(),
-            0.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_back(),
-            0.0);
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
   EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
             IntakeState::NO_BALL);
@@ -822,10 +819,8 @@
             IntakeState::INTAKE_FRONT_BALL);
   EXPECT_EQ(superstructure_output_fetcher_->flipper_arms_voltage(),
             constants::Values::kFlipperFeedVoltage());
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_front(),
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(),
             constants::Values::kTransferRollerVoltage());
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_back(),
-            0.0);
   EXPECT_NEAR(superstructure_status_fetcher_->turret()->position(),
               constants::Values::kTurretFrontIntakePos(), 0.001);
   EXPECT_EQ(superstructure_status_fetcher_->shot_count(), 0);
@@ -843,10 +838,7 @@
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 12.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 12.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_front(),
-            0.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_back(),
-            0.0);
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::LOADING);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
@@ -865,10 +857,7 @@
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 12.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 12.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_front(),
-            0.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_back(),
-            0.0);
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::LOADED);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
@@ -884,10 +873,7 @@
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 12.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 12.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_front(),
-            0.0);
-  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage_back(),
-            0.0);
+  EXPECT_EQ(superstructure_output_fetcher_->transfer_roller_voltage(), 0.0);
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::LOADED);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
@@ -908,14 +894,12 @@
   ASSERT_TRUE(superstructure_output_fetcher_.Fetch());
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 12.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 0.0);
-  EXPECT_TRUE(superstructure_output_fetcher_->transfer_roller_voltage_back() !=
+  EXPECT_TRUE(superstructure_output_fetcher_->transfer_roller_voltage() !=
                   0.0 &&
-              superstructure_output_fetcher_->transfer_roller_voltage_back() <=
+              superstructure_output_fetcher_->transfer_roller_voltage() <=
                   constants::Values::kTransferRollerWiggleVoltage() &&
-              superstructure_output_fetcher_->transfer_roller_voltage_back() >=
+              superstructure_output_fetcher_->transfer_roller_voltage() >=
                   -constants::Values::kTransferRollerWiggleVoltage());
-  EXPECT_EQ(0.0,
-            superstructure_output_fetcher_->transfer_roller_voltage_front());
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::LOADED);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
@@ -963,14 +947,12 @@
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_front(), 0.0);
   EXPECT_EQ(superstructure_output_fetcher_->roller_voltage_back(), 0.0);
-  EXPECT_TRUE(superstructure_output_fetcher_->transfer_roller_voltage_back() !=
+  EXPECT_TRUE(superstructure_output_fetcher_->transfer_roller_voltage() !=
                   0.0 &&
-              superstructure_output_fetcher_->transfer_roller_voltage_back() <=
+              superstructure_output_fetcher_->transfer_roller_voltage() <=
                   constants::Values::kTransferRollerWiggleVoltage() &&
-              superstructure_output_fetcher_->transfer_roller_voltage_back() >=
+              superstructure_output_fetcher_->transfer_roller_voltage() >=
                   -constants::Values::kTransferRollerWiggleVoltage());
-  EXPECT_EQ(0.0,
-            superstructure_output_fetcher_->transfer_roller_voltage_front());
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::SHOOTING);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
@@ -1015,7 +997,7 @@
   SetEnabled(true);
   WaitUntilZeroed();
 
-  constexpr double kTurretGoal = -4.0;
+  constexpr double kTurretGoal = -6.0;
   {
     auto builder = superstructure_goal_sender_.MakeBuilder();
     flatbuffers::Offset<StaticZeroingSingleDOFProfiledSubsystemGoal>
@@ -1033,20 +1015,20 @@
   EXPECT_NEAR(superstructure_status_fetcher_->turret()->position(), kTurretGoal,
               0.001);
 
-  superstructure_plant_.set_intake_beambreak_back(true);
+  superstructure_plant_.set_intake_beambreak_front(true);
   RunFor(dt() * 2);
 
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_EQ(superstructure_status_fetcher_->state(),
             SuperstructureState::TRANSFERRING);
   EXPECT_EQ(superstructure_status_fetcher_->intake_state(),
-            IntakeState::INTAKE_BACK_BALL);
+            IntakeState::INTAKE_FRONT_BALL);
 
   RunFor(std::chrono::seconds(3));
 
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
   EXPECT_NEAR(superstructure_status_fetcher_->turret()->position(),
-              -constants::Values::kTurretBackIntakePos(), 0.001);
+              -constants::Values::kTurretFrontIntakePos() - 2.0 * M_PI, 0.001);
   // it chooses -pi because -pi is closer to -4 than positive pi
 }
 
@@ -1212,8 +1194,7 @@
 
   RunFor(chrono::milliseconds(2000));
   ASSERT_TRUE(superstructure_status_fetcher_.Fetch());
-  EXPECT_EQ(superstructure_status_fetcher_->state(),
-            SuperstructureState::IDLE);
+  EXPECT_EQ(superstructure_status_fetcher_->state(), SuperstructureState::IDLE);
 }
 
 // Test that we are able to signal that the ball was preloaded
diff --git a/y2022/control_loops/superstructure/superstructure_output.fbs b/y2022/control_loops/superstructure/superstructure_output.fbs
index af6c292..a4460e2 100644
--- a/y2022/control_loops/superstructure/superstructure_output.fbs
+++ b/y2022/control_loops/superstructure/superstructure_output.fbs
@@ -26,8 +26,9 @@
   // positive is pulling into the robot
   roller_voltage_front:double (id: 6);
   roller_voltage_back:double (id: 7);
-  transfer_roller_voltage_front:double (id: 8);
-  transfer_roller_voltage_back:double (id: 9);
+  transfer_roller_voltage:double (id: 8);
+  // Only using one transfer roller voltage now
+  transfer_roller_voltage_back:double (id: 9, deprecated);
 }
 
 root_type Output;
diff --git a/y2022/control_loops/superstructure/superstructure_status.fbs b/y2022/control_loops/superstructure/superstructure_status.fbs
index 06fddd0..9cf9a5f 100644
--- a/y2022/control_loops/superstructure/superstructure_status.fbs
+++ b/y2022/control_loops/superstructure/superstructure_status.fbs
@@ -57,6 +57,8 @@
   reseating_in_catapult:bool (id: 13);
   // Whether the turret is ready for firing
   ready_to_fire:bool (id: 20);
+  // Whether the robot is moving too fast to shoot
+  moving_too_fast:bool (id: 21);
   // Whether the catapult was told to fire,
   // meaning that the turret and flippers are ready for firing
   // and we were asked to fire. Different from fire flag in goal.
diff --git a/y2022/control_loops/superstructure/turret/aiming.cc b/y2022/control_loops/superstructure/turret/aiming.cc
index e1d24f8..643adbc 100644
--- a/y2022/control_loops/superstructure/turret/aiming.cc
+++ b/y2022/control_loops/superstructure/turret/aiming.cc
@@ -15,7 +15,7 @@
 namespace {
 // Average speed-over-ground of the ball on its way to the target. Our current
 // model assumes constant ball velocity regardless of shot distance.
-constexpr double kBallSpeedOverGround = 12.0;  // m/s
+constexpr double kBallSpeedOverGround = 2.0;  // m/s
 
 // If the turret is at zero, then it will be at this angle at which the shot
 // will leave the robot. I.e., if the turret is at zero, then the shot will go
diff --git a/y2022/image_streamer/BUILD b/y2022/image_streamer/BUILD
new file mode 100644
index 0000000..0305dc1
--- /dev/null
+++ b/y2022/image_streamer/BUILD
@@ -0,0 +1,55 @@
+load("//aos/seasocks:gen_embedded.bzl", "gen_embedded")
+
+gen_embedded(
+    name = "gen_embedded",
+    srcs = glob(
+        include = ["www_defaults/**/*"],
+        exclude = ["www/**/*"],
+    ),
+)
+
+filegroup(
+    name = "image_streamer_start",
+    srcs = ["image_streamer_start.sh"],
+    visibility = ["//visibility:public"],
+)
+
+cc_binary(
+    name = "image_streamer",
+    srcs = ["image_streamer.cc"],
+    args = [
+        "--data_dir=y2022/image_streamer/www",
+    ],
+    copts = [
+        "-Wno-cast-align",
+    ],
+    data = [
+        "//y2022:aos_config",
+        "//y2022/image_streamer/www:files",
+        "//y2022/image_streamer/www:main_bundle.min.js",
+    ],
+    target_compatible_with = select({
+        "@platforms//cpu:x86_64": [],
+        "@platforms//cpu:aarch64": [],
+        "//conditions:default": ["@platforms//:incompatible"],
+    }) + [
+        "@platforms//os:linux",
+    ],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":gen_embedded",
+        "//aos:configuration_fbs",
+        "//aos:init",
+        "//aos/events:glib_main_loop",
+        "//aos/events:shm_event_loop",
+        "//aos/network:connect_fbs",
+        "//aos/network:web_proxy_fbs",
+        "//aos/seasocks:seasocks_logger",
+        "//frc971/vision:vision_fbs",
+        "//third_party:gstreamer",
+        "//third_party/seasocks",
+        "@com_github_google_flatbuffers//:flatbuffers",
+        "@com_github_google_glog//:glog",
+        "@com_google_absl//absl/strings:str_format",
+    ],
+)
diff --git a/y2022/image_streamer/image_streamer.cc b/y2022/image_streamer/image_streamer.cc
new file mode 100644
index 0000000..9a990ed
--- /dev/null
+++ b/y2022/image_streamer/image_streamer.cc
@@ -0,0 +1,597 @@
+#define GST_USE_UNSTABLE_API
+#define GST_DISABLE_REGISTRY 1
+
+#include <glib-unix.h>
+#include <glib.h>
+#include <gst/app/app.h>
+#include <gst/gst.h>
+#include <gst/sdp/sdp.h>
+#include <gst/webrtc/icetransport.h>
+#include <gst/webrtc/webrtc.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <map>
+#include <thread>
+
+#include "absl/strings/str_format.h"
+#include "aos/events/glib_main_loop.h"
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "aos/network/web_proxy_generated.h"
+#include "aos/seasocks/seasocks_logger.h"
+#include "flatbuffers/flatbuffers.h"
+#include "frc971/vision/vision_generated.h"
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+#include "internal/Embedded.h"
+#include "seasocks/Server.h"
+#include "seasocks/StringUtil.h"
+#include "seasocks/WebSocket.h"
+
+extern "C" {
+GST_PLUGIN_STATIC_DECLARE(app);
+GST_PLUGIN_STATIC_DECLARE(coreelements);
+GST_PLUGIN_STATIC_DECLARE(dtls);
+GST_PLUGIN_STATIC_DECLARE(nice);
+GST_PLUGIN_STATIC_DECLARE(rtp);
+GST_PLUGIN_STATIC_DECLARE(rtpmanager);
+GST_PLUGIN_STATIC_DECLARE(srtp);
+GST_PLUGIN_STATIC_DECLARE(webrtc);
+GST_PLUGIN_STATIC_DECLARE(video4linux2);
+GST_PLUGIN_STATIC_DECLARE(videoconvert);
+GST_PLUGIN_STATIC_DECLARE(videoparsersbad);
+GST_PLUGIN_STATIC_DECLARE(videorate);
+GST_PLUGIN_STATIC_DECLARE(videoscale);
+GST_PLUGIN_STATIC_DECLARE(videotestsrc);
+GST_PLUGIN_STATIC_DECLARE(x264);
+}
+DEFINE_string(config, "y2022/aos_config.json",
+              "Name of the config file to replay using.");
+DEFINE_string(device, "/dev/video0", "Camera fd");
+DEFINE_string(data_dir, "image_streamer_www",
+              "Directory to serve data files from");
+DEFINE_int32(width, 400, "Image width");
+DEFINE_int32(height, 300, "Image height");
+DEFINE_int32(framerate, 25, "Framerate (FPS)");
+DEFINE_int32(brightness, 50, "Camera brightness");
+DEFINE_int32(exposure, 300, "Manual exposure");
+DEFINE_int32(bitrate, 500000, "H264 encode bitrate");
+DEFINE_int32(min_port, 5800, "Min rtp port");
+DEFINE_int32(max_port, 5810, "Max rtp port");
+
+class Connection;
+
+using aos::web_proxy::Payload;
+using aos::web_proxy::SdpType;
+using aos::web_proxy::WebSocketIce;
+using aos::web_proxy::WebSocketMessage;
+using aos::web_proxy::WebSocketSdp;
+
+// Basic class that handles receiving new websocket connections. Creates a new
+// Connection to manage the rest of the negotiation and data passing. When the
+// websocket closes, it deletes the Connection.
+class WebsocketHandler : public ::seasocks::WebSocket::Handler {
+ public:
+  WebsocketHandler(aos::ShmEventLoop *event_loop, ::seasocks::Server *server);
+  ~WebsocketHandler() override;
+
+  void onConnect(::seasocks::WebSocket *sock) override;
+  void onData(::seasocks::WebSocket *sock, const uint8_t *data,
+              size_t size) override;
+  void onDisconnect(::seasocks::WebSocket *sock) override;
+
+ private:
+  static GstFlowReturn OnSampleCallback(GstElement *, gpointer user_data) {
+    static_cast<WebsocketHandler *>(user_data)->OnSample();
+    return GST_FLOW_OK;
+  }
+
+  void OnSample();
+
+  std::map<::seasocks::WebSocket *, std::unique_ptr<Connection>> connections_;
+  ::seasocks::Server *server_;
+  GstElement *pipeline_;
+  GstElement *appsink_;
+
+  aos::Sender<frc971::vision::CameraImage> sender_;
+};
+
+// Seasocks requires that sends happen on the correct thread. This class takes a
+// detached buffer to send on a specific websocket connection and sends it when
+// seasocks is ready.
+class UpdateData : public ::seasocks::Server::Runnable {
+ public:
+  UpdateData(::seasocks::WebSocket *websocket,
+             flatbuffers::DetachedBuffer &&buffer)
+      : sock_(websocket), buffer_(std::move(buffer)) {}
+  ~UpdateData() override = default;
+  UpdateData(const UpdateData &) = delete;
+  UpdateData &operator=(const UpdateData &) = delete;
+
+  void run() override { sock_->send(buffer_.data(), buffer_.size()); }
+
+ private:
+  ::seasocks::WebSocket *sock_;
+  const flatbuffers::DetachedBuffer buffer_;
+};
+
+class Connection {
+ public:
+  Connection(::seasocks::WebSocket *sock, ::seasocks::Server *server);
+
+  ~Connection();
+
+  void HandleWebSocketData(const uint8_t *data, size_t size);
+
+  void OnSample(GstSample *sample);
+
+ private:
+  static void OnOfferCreatedCallback(GstPromise *promise, gpointer user_data) {
+    static_cast<Connection *>(user_data)->OnOfferCreated(promise);
+  }
+
+  static void OnNegotiationNeededCallback(GstElement *, gpointer user_data) {
+    static_cast<Connection *>(user_data)->OnNegotiationNeeded();
+  }
+
+  static void OnIceCandidateCallback(GstElement *, guint mline_index,
+                                     gchar *candidate, gpointer user_data) {
+    static_cast<Connection *>(user_data)->OnIceCandidate(mline_index,
+                                                         candidate);
+  }
+
+  void OnOfferCreated(GstPromise *promise);
+  void OnNegotiationNeeded();
+  void OnIceCandidate(guint mline_index, gchar *candidate);
+
+  ::seasocks::WebSocket *sock_;
+  ::seasocks::Server *server_;
+
+  GstElement *pipeline_;
+  GstElement *webrtcbin_;
+  GstElement *appsrc_;
+
+  bool first_sample_ = true;
+};
+
+WebsocketHandler::WebsocketHandler(aos::ShmEventLoop *event_loop,
+                                   ::seasocks::Server *server)
+    : server_(server),
+      sender_(event_loop->MakeSender<frc971::vision::CameraImage>("/camera")) {
+  GError *error = NULL;
+
+  // Create pipeline to read from camera, pack into rtp stream, and dump stream
+  // to callback.
+  // v4l2 device should already be configured with correct bitrate from
+  // v4l2-ctl. do-timestamp marks the time the frame was taken to track when it
+  // should be dropped under latency.
+
+  // With the Pi's hardware encoder, we can encode and package the stream once
+  // and the clients will jump in at any point unsynchronized. With the stream
+  // from x264enc this doesn't seem to work. For now, just reencode for each
+  // client since we don't expect more than 1 or 2.
+
+  pipeline_ = gst_parse_launch(
+      absl::StrFormat("v4l2src device=%s do-timestamp=true "
+                      "extra-controls=\"c,brightness=%d,auto_exposure=1,"
+                      "exposure_time_absolute=%d\" ! "
+                      "video/x-raw,width=%d,height=%d,framerate=%d/"
+                      "1,format=YUY2 ! appsink "
+                      "name=appsink "
+                      "emit-signals=true sync=false async=false "
+                      "caps=video/x-raw,format=YUY2",
+                      FLAGS_device, FLAGS_brightness, FLAGS_exposure,
+                      FLAGS_width, FLAGS_height, FLAGS_framerate)
+          .c_str(),
+      &error);
+
+  if (error != NULL) {
+    LOG(FATAL) << "Could not create v4l2 pipeline: " << error->message;
+  }
+
+  appsink_ = gst_bin_get_by_name(GST_BIN(pipeline_), "appsink");
+  if (appsink_ == NULL) {
+    LOG(FATAL) << "Could not get appsink";
+  }
+
+  g_signal_connect(appsink_, "new-sample",
+                   G_CALLBACK(WebsocketHandler::OnSampleCallback),
+                   static_cast<gpointer>(this));
+
+  gst_element_set_state(pipeline_, GST_STATE_PLAYING);
+}
+
+WebsocketHandler::~WebsocketHandler() {
+  if (pipeline_ != NULL) {
+    gst_element_set_state(GST_ELEMENT(pipeline_), GST_STATE_NULL);
+    gst_object_unref(GST_OBJECT(pipeline_));
+    gst_object_unref(GST_OBJECT(appsink_));
+  }
+}
+
+void WebsocketHandler::onConnect(::seasocks::WebSocket *sock) {
+  std::unique_ptr<Connection> conn =
+      std::make_unique<Connection>(sock, server_);
+  connections_.insert({sock, std::move(conn)});
+}
+
+void WebsocketHandler::onData(::seasocks::WebSocket *sock, const uint8_t *data,
+                              size_t size) {
+  connections_[sock]->HandleWebSocketData(data, size);
+}
+
+void WebsocketHandler::OnSample() {
+  GstSample *sample = gst_app_sink_pull_sample(GST_APP_SINK(appsink_));
+  if (sample == NULL) {
+    LOG(WARNING) << "Received null sample";
+    return;
+  }
+
+  for (auto iter = connections_.begin(); iter != connections_.end(); ++iter) {
+    iter->second->OnSample(sample);
+  }
+
+  {
+    const GstCaps *caps = CHECK_NOTNULL(gst_sample_get_caps(sample));
+    CHECK_GT(gst_caps_get_size(caps), 0U);
+    const GstStructure *str = gst_caps_get_structure(caps, 0);
+
+    gint width;
+    gint height;
+
+    CHECK(gst_structure_get_int(str, "width", &width));
+    CHECK(gst_structure_get_int(str, "height", &height));
+
+    GstBuffer *buffer = CHECK_NOTNULL(gst_sample_get_buffer(sample));
+
+    const gsize size = gst_buffer_get_size(buffer);
+
+    auto builder = sender_.MakeBuilder();
+
+    uint8_t *image_data;
+    auto image_offset =
+        builder.fbb()->CreateUninitializedVector(size, &image_data);
+    gst_buffer_extract(buffer, 0, image_data, size);
+
+    auto image_builder = builder.MakeBuilder<frc971::vision::CameraImage>();
+    image_builder.add_rows(height);
+    image_builder.add_cols(width);
+    image_builder.add_data(image_offset);
+
+    builder.CheckOk(builder.Send(image_builder.Finish()));
+  }
+
+  gst_sample_unref(sample);
+}
+
+void WebsocketHandler::onDisconnect(::seasocks::WebSocket *sock) {
+  connections_.erase(sock);
+}
+
+Connection::Connection(::seasocks::WebSocket *sock, ::seasocks::Server *server)
+    : sock_(sock), server_(server) {
+  GError *error = NULL;
+
+  // Build pipeline to read data from application into pipeline, place in
+  // webrtcbin group, and stream.
+
+  pipeline_ = gst_parse_launch(
+      // aggregate-mode should be zero-latency but this drops the stream on
+      // bitrate spikes for some reason - probably the weak CPU on the pi.
+      absl::StrFormat(
+          "webrtcbin name=webrtcbin appsrc "
+          "name=appsrc block=false "
+          "is-live=true "
+          "format=3 max-buffers=0 leaky-type=2 "
+          "caps=video/x-raw,width=%d,height=%d,format=YUY2 ! videoconvert ! "
+          "x264enc bitrate=%d speed-preset=ultrafast "
+          "tune=zerolatency key-int-max=15 sliced-threads=true ! "
+          "video/x-h264,profile=constrained-baseline ! h264parse ! "
+          "rtph264pay "
+          "config-interval=-1 name=payloader aggregate-mode=none ! "
+          "application/"
+          "x-rtp,media=video,encoding-name=H264,payload=96,clock-rate=90000 !"
+          "webrtcbin. ",
+          FLAGS_width, FLAGS_height, FLAGS_bitrate / 1000)
+          .c_str(),
+      &error);
+
+  if (error != NULL) {
+    LOG(FATAL) << "Could not create WebRTC pipeline: " << error->message;
+  }
+
+  webrtcbin_ = gst_bin_get_by_name(GST_BIN(pipeline_), "webrtcbin");
+  if (webrtcbin_ == NULL) {
+    LOG(FATAL) << "Could not initialize webrtcbin";
+  }
+
+  appsrc_ = gst_bin_get_by_name(GST_BIN(pipeline_), "appsrc");
+  if (appsrc_ == NULL) {
+    LOG(FATAL) << "Could not initialize appsrc";
+  }
+
+  {
+    GArray *transceivers;
+    g_signal_emit_by_name(webrtcbin_, "get-transceivers", &transceivers);
+    if (transceivers == NULL || transceivers->len <= 0) {
+      LOG(FATAL) << "Could not initialize transceivers";
+    }
+
+    GstWebRTCRTPTransceiver *trans =
+        g_array_index(transceivers, GstWebRTCRTPTransceiver *, 0);
+    g_object_set(trans, "direction",
+                 GST_WEBRTC_RTP_TRANSCEIVER_DIRECTION_SENDONLY, nullptr);
+
+    g_array_unref(transceivers);
+  }
+
+  {
+    GstObject *ice = nullptr;
+    g_object_get(G_OBJECT(webrtcbin_), "ice-agent", &ice, nullptr);
+    CHECK_NOTNULL(ice);
+
+    g_object_set(ice, "min-rtp-port", FLAGS_min_port, "max-rtp-port",
+                 FLAGS_max_port, nullptr);
+
+    // We don't need upnp on a local network.
+    {
+      GstObject *nice = nullptr;
+      g_object_get(ice, "agent", &nice, nullptr);
+      CHECK_NOTNULL(nice);
+
+      g_object_set(nice, "upnp", false, nullptr);
+      g_object_unref(nice);
+    }
+
+    gst_object_unref(ice);
+  }
+
+  g_signal_connect(webrtcbin_, "on-negotiation-needed",
+                   G_CALLBACK(Connection::OnNegotiationNeededCallback),
+                   static_cast<gpointer>(this));
+
+  g_signal_connect(webrtcbin_, "on-ice-candidate",
+                   G_CALLBACK(Connection::OnIceCandidateCallback),
+                   static_cast<gpointer>(this));
+
+  gst_element_set_state(pipeline_, GST_STATE_READY);
+  gst_element_set_state(pipeline_, GST_STATE_PLAYING);
+}
+
+Connection::~Connection() {
+  if (pipeline_ != NULL) {
+    gst_element_set_state(pipeline_, GST_STATE_NULL);
+
+    gst_object_unref(GST_OBJECT(webrtcbin_));
+    gst_object_unref(GST_OBJECT(pipeline_));
+    gst_object_unref(GST_OBJECT(appsrc_));
+  }
+}
+
+void Connection::OnSample(GstSample *sample) {
+  GstFlowReturn response =
+      gst_app_src_push_sample(GST_APP_SRC(appsrc_), sample);
+  if (response != GST_FLOW_OK) {
+    LOG(WARNING) << "Sample pushed, did not receive OK";
+  }
+
+  // Since the stream is already running (the camera turns on with
+  // image_streamer) we need to tell the new appsrc where
+  // we are starting in the stream so it can catch up immediately.
+  if (first_sample_) {
+    GstPad *src = gst_element_get_static_pad(appsrc_, "src");
+    if (src == NULL) {
+      return;
+    }
+
+    GstSegment *segment = gst_sample_get_segment(sample);
+    GstBuffer *buffer = gst_sample_get_buffer(sample);
+
+    guint64 offset = gst_segment_to_running_time(segment, GST_FORMAT_TIME,
+                                                 GST_BUFFER_PTS(buffer));
+    LOG(INFO) << "Fixing offset " << offset;
+    gst_pad_set_offset(src, -offset);
+
+    gst_object_unref(GST_OBJECT(src));
+    first_sample_ = false;
+  }
+}
+
+void Connection::OnOfferCreated(GstPromise *promise) {
+  LOG(INFO) << "OnOfferCreated";
+
+  GstWebRTCSessionDescription *offer = NULL;
+  gst_structure_get(gst_promise_get_reply(promise), "offer",
+                    GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &offer, NULL);
+  gst_promise_unref(promise);
+
+  {
+    std::unique_ptr<GstPromise, decltype(&gst_promise_unref)>
+        local_desc_promise(gst_promise_new(), &gst_promise_unref);
+    g_signal_emit_by_name(webrtcbin_, "set-local-description", offer,
+                          local_desc_promise.get());
+    gst_promise_interrupt(local_desc_promise.get());
+  }
+
+  GstSDPMessage *sdp_msg = offer->sdp;
+  std::string sdp_str(gst_sdp_message_as_text(sdp_msg));
+
+  LOG(INFO) << "Negotiation offer created:\n" << sdp_str;
+
+  flatbuffers::FlatBufferBuilder fbb(512);
+  flatbuffers::Offset<WebSocketSdp> sdp_fb =
+      CreateWebSocketSdpDirect(fbb, SdpType::OFFER, sdp_str.c_str());
+  flatbuffers::Offset<WebSocketMessage> answer_message =
+      CreateWebSocketMessage(fbb, Payload::WebSocketSdp, sdp_fb.Union());
+  fbb.Finish(answer_message);
+
+  server_->execute(std::make_shared<UpdateData>(sock_, fbb.Release()));
+}
+
+void Connection::OnNegotiationNeeded() {
+  LOG(INFO) << "OnNegotiationNeeded";
+
+  GstPromise *promise;
+  promise = gst_promise_new_with_change_func(Connection::OnOfferCreatedCallback,
+                                             static_cast<gpointer>(this), NULL);
+  g_signal_emit_by_name(G_OBJECT(webrtcbin_), "create-offer", NULL, promise);
+}
+
+void Connection::OnIceCandidate(guint mline_index, gchar *candidate) {
+  LOG(INFO) << "OnIceCandidate";
+
+  flatbuffers::FlatBufferBuilder fbb(512);
+
+  auto ice_fb_builder = WebSocketIce::Builder(fbb);
+  ice_fb_builder.add_sdp_m_line_index(mline_index);
+  ice_fb_builder.add_sdp_mid(fbb.CreateString("video0"));
+  ice_fb_builder.add_candidate(
+      fbb.CreateString(static_cast<char *>(candidate)));
+  flatbuffers::Offset<WebSocketIce> ice_fb = ice_fb_builder.Finish();
+
+  flatbuffers::Offset<WebSocketMessage> ice_message =
+      CreateWebSocketMessage(fbb, Payload::WebSocketIce, ice_fb.Union());
+  fbb.Finish(ice_message);
+
+  server_->execute(std::make_shared<UpdateData>(sock_, fbb.Release()));
+
+  g_signal_emit_by_name(webrtcbin_, "add-ice-candidate", mline_index,
+                        candidate);
+}
+
+void Connection::HandleWebSocketData(const uint8_t *data, size_t /* size*/) {
+  LOG(INFO) << "HandleWebSocketData";
+
+  const WebSocketMessage *message =
+      flatbuffers::GetRoot<WebSocketMessage>(data);
+
+  switch (message->payload_type()) {
+    case Payload::WebSocketSdp: {
+      const WebSocketSdp *offer = message->payload_as_WebSocketSdp();
+      if (offer->type() != SdpType::ANSWER) {
+        LOG(WARNING) << "Expected SDP message type \"answer\"";
+        break;
+      }
+      const flatbuffers::String *sdp_string = offer->payload();
+
+      LOG(INFO) << "Received SDP:\n" << sdp_string->c_str();
+
+      GstSDPMessage *sdp;
+      GstSDPResult status = gst_sdp_message_new(&sdp);
+      if (status != GST_SDP_OK) {
+        LOG(WARNING) << "Could not create SDP message";
+        break;
+      }
+
+      status = gst_sdp_message_parse_buffer((const guint8 *)sdp_string->c_str(),
+                                            sdp_string->size(), sdp);
+
+      if (status != GST_SDP_OK) {
+        LOG(WARNING) << "Could not parse SDP string";
+        break;
+      }
+
+      std::unique_ptr<GstWebRTCSessionDescription,
+                      decltype(&gst_webrtc_session_description_free)>
+          answer(gst_webrtc_session_description_new(GST_WEBRTC_SDP_TYPE_ANSWER,
+                                                    sdp),
+                 &gst_webrtc_session_description_free);
+      std::unique_ptr<GstPromise, decltype(&gst_promise_unref)> promise(
+          gst_promise_new(), &gst_promise_unref);
+      g_signal_emit_by_name(webrtcbin_, "set-remote-description", answer.get(),
+                            promise.get());
+      gst_promise_interrupt(promise.get());
+
+      break;
+    }
+    case Payload::WebSocketIce: {
+      const WebSocketIce *ice = message->payload_as_WebSocketIce();
+      if (!ice->has_candidate() || ice->candidate()->size() == 0) {
+        LOG(WARNING) << "Received ICE message without candidate";
+        break;
+      }
+
+      const gchar *candidate =
+          static_cast<const gchar *>(ice->candidate()->c_str());
+      guint mline_index = ice->sdp_m_line_index();
+
+      LOG(INFO) << "Received ICE candidate with mline index " << mline_index
+                << "; candidate: " << candidate;
+
+      g_signal_emit_by_name(webrtcbin_, "add-ice-candidate", mline_index,
+                            candidate);
+
+      break;
+    }
+    default:
+      break;
+  }
+}
+
+void RegisterPlugins() {
+  GST_PLUGIN_STATIC_REGISTER(app);
+  GST_PLUGIN_STATIC_REGISTER(coreelements);
+  GST_PLUGIN_STATIC_REGISTER(dtls);
+  GST_PLUGIN_STATIC_REGISTER(nice);
+  GST_PLUGIN_STATIC_REGISTER(rtp);
+  GST_PLUGIN_STATIC_REGISTER(rtpmanager);
+  GST_PLUGIN_STATIC_REGISTER(srtp);
+  GST_PLUGIN_STATIC_REGISTER(webrtc);
+  GST_PLUGIN_STATIC_REGISTER(video4linux2);
+  GST_PLUGIN_STATIC_REGISTER(videoconvert);
+  GST_PLUGIN_STATIC_REGISTER(videoparsersbad);
+  GST_PLUGIN_STATIC_REGISTER(videorate);
+  GST_PLUGIN_STATIC_REGISTER(videoscale);
+  GST_PLUGIN_STATIC_REGISTER(videotestsrc);
+  GST_PLUGIN_STATIC_REGISTER(x264);
+}
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+
+  findEmbeddedContent("");
+
+  std::string openssl_env = "OPENSSL_CONF=\"\"";
+  putenv(const_cast<char *>(openssl_env.c_str()));
+
+  putenv(const_cast<char *>("GST_REGISTRY_DISABLE=yes"));
+
+  gst_init(&argc, &argv);
+  RegisterPlugins();
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+  aos::ShmEventLoop event_loop(&config.message());
+
+  {
+    aos::GlibMainLoop main_loop(&event_loop);
+
+    seasocks::Server server(::std::shared_ptr<seasocks::Logger>(
+        new ::aos::seasocks::SeasocksLogger(seasocks::Logger::Level::Info)));
+
+    LOG(INFO) << "Serving from " << FLAGS_data_dir;
+
+    auto websocket_handler =
+        std::make_shared<WebsocketHandler>(&event_loop, &server);
+    server.addWebSocketHandler("/ws", websocket_handler);
+
+    server.startListening(1180);
+    server.setStaticPath(FLAGS_data_dir.c_str());
+
+    aos::internal::EPoll *epoll = event_loop.epoll();
+
+    epoll->OnReadable(server.fd(), [&server] {
+      CHECK(::seasocks::Server::PollResult::Continue == server.poll(0));
+    });
+
+    event_loop.Run();
+
+    epoll->DeleteFd(server.fd());
+    server.terminate();
+  }
+
+  gst_deinit();
+
+  return 0;
+}
diff --git a/y2022/image_streamer/image_streamer_start.sh b/y2022/image_streamer/image_streamer_start.sh
new file mode 100755
index 0000000..9e6de58
--- /dev/null
+++ b/y2022/image_streamer/image_streamer_start.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# Some configurations to avoid dropping frames
+# 640x480@30fps, 400x300@60fps.
+# Bitrate 500000-1500000
+DEVICE=/dev/video0
+WIDTH=640
+HEIGHT=480
+BITRATE=1500000
+FRAMERATE=30
+EXPOSURE=200
+
+# Handle weirdness with openssl and gstreamer
+export OPENSSL_CONF=""
+
+# Enable for verbose logging
+#export GST_DEBUG=4
+
+export LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0
+
+exec ./image_streamer --device=$DEVICE --width=$WIDTH --height=$HEIGHT --framerate=$FRAMERATE --bitrate=$BITRATE --exposure=$EXPOSURE --config=$HOME/bin/aos_config.json
+
diff --git a/y2022/image_streamer/www/BUILD b/y2022/image_streamer/www/BUILD
new file mode 100644
index 0000000..908c5b4
--- /dev/null
+++ b/y2022/image_streamer/www/BUILD
@@ -0,0 +1,52 @@
+load("@npm//@bazel/typescript:index.bzl", "ts_library")
+load("//tools/build_rules:js.bzl", "rollup_bundle")
+load("//frc971/downloader:downloader.bzl", "aos_downloader_dir")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "files",
+    srcs = glob([
+        "**/*.html",
+        "**/*.css",
+    ]),
+)
+
+ts_library(
+    name = "proxy",
+    srcs = [
+        "proxy.ts",
+    ],
+    deps = [
+        "//aos/network:web_proxy_ts_fbs",
+        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
+    ],
+)
+
+ts_library(
+    name = "main",
+    srcs = [
+        "main.ts",
+    ],
+    deps = [
+        ":proxy",
+    ],
+)
+
+rollup_bundle(
+    name = "main_bundle",
+    entry_point = "main.ts",
+    deps = [
+        "main",
+    ],
+)
+
+aos_downloader_dir(
+    name = "www_files",
+    srcs = [
+        ":files",
+        ":main_bundle.min.js",
+    ],
+    dir = "image_streamer_www",
+    visibility = ["//visibility:public"],
+)
diff --git a/y2022/image_streamer/www/index.html b/y2022/image_streamer/www/index.html
new file mode 100644
index 0000000..f71750d
--- /dev/null
+++ b/y2022/image_streamer/www/index.html
@@ -0,0 +1,89 @@
+<html>
+  <head>
+    <script type="text/javascript"></script>
+    <script src="main_bundle.min.js" defer></script>
+    <style>
+      code {
+        background-color: #eee;
+      }
+    </style>
+  </head>
+  <body>
+    <div>
+      <video id="stream" autoplay playsinline muted style="height: 95%">
+        Your browser does not support video
+      </video>
+    </div>
+    <p>
+      Stats: <span id="stats_protocol">Not connected</span>, <span id="stats_bps"></span> Kibit/s, <span id="stats_fps"></span> fps
+    </p>
+    <span>
+      <div id="bad_remote_port_error" style="display: none">
+        <p>
+          Remote emitted a port <span id="bad_remote_port_port"></span> which may not be supported over the FMS.
+          Try running <code>image_streamer</code> with <code>-min_port=</code> and <code>-max_port=</code>
+        </p>
+      </div>
+      <div id="bad_local_port_error" style="display: none">
+        <p>
+          Local emitted a port <span id="bad_local_port_port"></span> which may not be supported over the FMS, or may fallback to TCP.
+        </p>
+        <p>To fix:</p>
+        <ul>
+          <li>
+            Firefox: Not supported
+          </li>
+          <li>Chrome:
+            <ul>
+              <li>
+                Windows:
+                <br>
+                Add registry entry <code>Software\Policies\Google\Chrome\WebRtcUdpPortRange = "5800-5810"</code>
+                <br>
+                (For Chromium, <code>Software\Policies\Chromium\WebRtcUdpPortRange</code>)
+              </li>
+              <li>
+                Linux:
+                <br>
+                <code>mkdir -p /etc/opt/chrome/policies/managed</code> OR <code>mkdir -p /etc/chromium/policies/managed</code> OR, ON SOME DISTROS <code>mkdir -p /etc/chromium-browser/policies/managed</code>
+                <br>
+                <code>echo '{"WebRtcUdpPortRange": "5800-5810"}' &gt; /etc/&lt;&gt;/policies/managed/webrtc_port_policy.json</code>
+              </li>
+            </ul>
+          </li>
+        </ul>
+      </div>
+      <div id="bad_remote_address_error" style="display: none">
+        <p>Remote emitted an address <span id="bad_remote_address_address"></span> which requires mDNS resolution. This may not be supported.</p>
+      </div>
+      <div id="bad_local_address_error" style="display: none">
+        <p>Local emitted an address <span id="bad_local_address_address"></span> which requires mDNS resolution. This may not be supported.</p>
+        <p>To fix:</p>
+        <ul>
+          <li>
+            Firefox: Navigate to <a href="about:config">about:config</a> (May need to select and copy into new tab).
+            Set <code>media.peerconnection.ice.obfuscate_host_addresses</code> to <code>false</code>.
+          </li>
+          <li>Chrome:
+            <ul>
+              <li>
+                Windows:
+                <br>
+                Add registry entry <code>Software\Policies\Google\Chrome\WebRtcLocalIpsAllowedUrls\1 = "*<span class="page_hostname"></span>*"</code>
+                <br>
+                (For Chromium, <code>Software\Policies\Chromium\WebRtcLocalIpsAllowedUrls\1</code>)
+              </li>
+              <li>
+                Linux:
+                <br>
+                <code>mkdir -p /etc/opt/chrome/policies/managed</code> OR <code>mkdir -p /etc/chromium/policies/managed</code> OR, ON SOME DISTROS <code>mkdir -p /etc/chromium-browser/policies/managed</code>
+                <br>
+                <code>echo '{"WebRtcLocalIpsAllowedUrls": ["*<span class="page_hostname"></span>*"]}' &gt; /etc/&lt;&gt;/policies/managed/webrtc_policy.json</code>
+              </li>
+            </ul>
+          </li>
+        </ul>
+      </div>
+    </span>
+  </body>
+</html>
diff --git a/y2022/image_streamer/www/main.ts b/y2022/image_streamer/www/main.ts
new file mode 100644
index 0000000..5a3165e
--- /dev/null
+++ b/y2022/image_streamer/www/main.ts
@@ -0,0 +1,5 @@
+import {Connection} from './proxy';
+
+const conn = new Connection();
+
+conn.connect();
diff --git a/y2022/image_streamer/www/proxy.ts b/y2022/image_streamer/www/proxy.ts
new file mode 100644
index 0000000..bfe8135
--- /dev/null
+++ b/y2022/image_streamer/www/proxy.ts
@@ -0,0 +1,248 @@
+import {Builder, ByteBuffer} from 'flatbuffers';
+import {Payload, SdpType, WebSocketIce, WebSocketMessage, WebSocketSdp} from 'org_frc971/aos/network/web_proxy_generated';
+
+// Port 9 is used to indicate an active (outgoing) TCP connection. The server
+// would send a corresponding candidate with the actual TCP port it is
+// listening on. Ignore NaN since it doesn't tell us anything about whether the
+// port selected will have issues through the FMS firewall.
+function validPort(port: number): boolean {
+  return Number.isNaN(port) || port == 9 || (port >= 1180 && port <= 1190) ||
+      (port >= 5800 && port <= 5810);
+}
+
+// Some browsers don't support the port property so provide our own function
+// to get it.
+function getIcePort(candidate: RTCIceCandidate): number {
+  if (candidate.port === undefined) {
+    return Number(candidate.candidate.split(' ')[5]);
+  } else {
+    return candidate.port;
+  }
+}
+
+function isMdnsAddress(address: string): boolean {
+  return address.includes('.local');
+}
+
+function getIceAddress(candidate: RTCIceCandidate): string {
+  if (candidate.address === undefined) {
+    return candidate.candidate.split(' ')[4];
+  } else {
+    return candidate.address;
+  }
+}
+
+export class Connection {
+  private webSocketConnection: WebSocket|null = null;
+  private rtcPeerConnection: RTCPeerConnection|null = null;
+  private html5VideoElement: HTMLMediaElement|null = null;
+  private webSocketUrl: string;
+  private statsInterval: number;
+
+  private candidateNominatedId: string;
+  private lastRtpTimestamp: number = 0;
+  private lastBytesReceived: number = 0;
+  private lastFramesDecoded: number = 0;
+
+
+  constructor() {
+    const server = location.host;
+    this.webSocketUrl = `ws://${server}/ws`;
+
+    for (let elem of document.getElementsByClassName('page_hostname')) {
+      (elem as HTMLElement).innerText = location.hostname;
+    }
+  }
+
+  connect(): void {
+    this.html5VideoElement =
+        (document.getElementById('stream') as HTMLMediaElement);
+
+    this.webSocketConnection = new WebSocket(this.webSocketUrl);
+    this.webSocketConnection.binaryType = 'arraybuffer';
+    this.webSocketConnection.addEventListener(
+        'message', (e) => this.onWebSocketMessage(e));
+  }
+
+
+  checkRemoteCandidate(candidate: RTCIceCandidate) {
+    const port = getIcePort(candidate);
+    if (!validPort(port)) {
+      document.getElementById('bad_remote_port_port').innerText =
+          port.toString();
+      document.getElementById('bad_remote_port_error').style['display'] =
+          'inherit';
+    }
+    const address = getIceAddress(candidate);
+    if (isMdnsAddress(address)) {
+      document.getElementById('bad_remote_address_address').innerText = address;
+      document.getElementById('bad_remote_address_error').style['display'] =
+          'inherit';
+    }
+  }
+
+  checkLocalCandidate(candidate: RTCIceCandidate) {
+    const port = getIcePort(candidate);
+    if (!validPort(port)) {
+      document.getElementById('bad_local_port_port').innerText =
+          port.toString();
+      document.getElementById('bad_local_port_error').style['display'] =
+          'inherit';
+    }
+    const address = getIceAddress(candidate);
+    if (isMdnsAddress(address)) {
+      document.getElementById('bad_local_address_address').innerText = address;
+      document.getElementById('bad_local_address_error').style['display'] =
+          'inherit';
+    }
+  }
+
+  onLocalDescription(desc: RTCSessionDescriptionInit): void {
+    console.log('Local description: ' + JSON.stringify(desc));
+    this.rtcPeerConnection.setLocalDescription(desc).then(() => {
+      const builder = new Builder(512);
+      const sdpFb = WebSocketSdp.createWebSocketSdp(
+          builder, SdpType.ANSWER, builder.createString(desc.sdp));
+      const message = WebSocketMessage.createWebSocketMessage(
+          builder, Payload.WebSocketSdp, sdpFb);
+      builder.finish(message);
+      const array = builder.asUint8Array();
+
+      this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
+    });
+  }
+
+  onIncomingSDP(sdp: RTCSessionDescriptionInit): void {
+    console.log('Incoming SDP: ' + JSON.stringify(sdp));
+    this.rtcPeerConnection.setRemoteDescription(sdp);
+    this.rtcPeerConnection.createAnswer().then(
+        (e) => this.onLocalDescription(e));
+  }
+
+  onIncomingICE(ice: RTCIceCandidateInit): void {
+    let candidate = new RTCIceCandidate(ice);
+    console.log('Incoming ICE: ' + JSON.stringify(ice));
+    this.rtcPeerConnection.addIceCandidate(candidate);
+
+    // If end of candidates, won't have a port.
+    if (candidate.candidate !== '') {
+      this.checkRemoteCandidate(candidate);
+    }
+  }
+
+  onRequestStats(track: MediaStreamTrack): void {
+    this.rtcPeerConnection.getStats(track).then((stats) => {
+      // getStats returns a list of stats of various types in an implementation
+      // defined order. We would like to get the protocol in use. This is found
+      // in remote-candidate. However, (again, implementation defined), some
+      // browsers return only remote-candidate's in use, while others return all
+      // of them that attempted negotiation. To figure this out, look at the
+      // currently nominated candidate-pair, then match up it's remote with a
+      // remote-candidate we see later. Since the order isn't defined, store the
+      // id in this in case the remote-candidate comes before candidate-pair.
+
+      for (let dict of stats.values()) {
+        if (dict.type === 'candidate-pair' && dict.nominated) {
+          this.candidateNominatedId = dict.remoteCandidateId;
+        }
+        if (dict.type === 'remote-candidate' &&
+            dict.id === this.candidateNominatedId) {
+          document.getElementById('stats_protocol').innerText = dict.protocol;
+        }
+        if (dict.type === 'inbound-rtp') {
+          const timestamp = dict.timestamp;
+          const bytes_now = dict.bytesReceived;
+          const frames_decoded = dict.framesDecoded;
+
+          document.getElementById('stats_bps').innerText =
+              Math.round(
+                      (bytes_now - this.lastBytesReceived) * 8 /* bits */ /
+                      1024 /* kbits */ / (timestamp - this.lastRtpTimestamp) *
+                      1000 /* ms */)
+                  .toString();
+
+          document.getElementById('stats_fps').innerText =
+              (Math.round(
+                   (frames_decoded - this.lastFramesDecoded) /
+                   (timestamp - this.lastRtpTimestamp) * 1000 /* ms */ * 10) /
+               10).toString();
+
+
+          this.lastRtpTimestamp = timestamp;
+          this.lastBytesReceived = bytes_now;
+          this.lastFramesDecoded = frames_decoded;
+        }
+      }
+    });
+  }
+
+  onAddRemoteStream(event: RTCTrackEvent): void {
+    const stream = event.streams[0];
+    this.html5VideoElement.srcObject = stream;
+
+    const track = stream.getTracks()[0];
+    this.statsInterval =
+        window.setInterval(() => this.onRequestStats(track), 1000);
+  }
+
+  onIceCandidate(event: RTCPeerConnectionIceEvent): void {
+    if (event.candidate == null) {
+      return;
+    }
+
+    console.log(
+        'Sending ICE candidate out: ' + JSON.stringify(event.candidate));
+
+    const builder = new Builder(512);
+    const iceFb = WebSocketIce.createWebSocketIce(
+        builder, builder.createString(event.candidate.candidate), null,
+        event.candidate.sdpMLineIndex);
+    const message = WebSocketMessage.createWebSocketMessage(
+        builder, Payload.WebSocketIce, iceFb);
+    builder.finish(message);
+    const array = builder.asUint8Array();
+
+    this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
+
+    // If end of candidates, won't have a port.
+    if (event.candidate.candidate !== '') {
+      this.checkLocalCandidate(event.candidate);
+    }
+  }
+
+  // When we receive a websocket message, we need to determine what type it is
+  // and handle appropriately. Either by setting the remote description or
+  // adding the remote ice candidate.
+  onWebSocketMessage(e: MessageEvent): void {
+    const buffer = new Uint8Array(e.data)
+    const fbBuffer = new ByteBuffer(buffer);
+    const message = WebSocketMessage.getRootAsWebSocketMessage(fbBuffer);
+
+    if (!this.rtcPeerConnection) {
+      this.rtcPeerConnection = new RTCPeerConnection();
+      this.rtcPeerConnection.ontrack = (e) => this.onAddRemoteStream(e);
+      this.rtcPeerConnection.onicecandidate = (e) => this.onIceCandidate(e);
+    }
+
+    switch (message.payloadType()) {
+      case Payload.WebSocketSdp:
+        const sdpFb = message.payload(new WebSocketSdp());
+        const sdp:
+            RTCSessionDescriptionInit = {type: 'offer', sdp: sdpFb.payload()};
+
+        this.onIncomingSDP(sdp);
+        break;
+      case Payload.WebSocketIce:
+        const iceFb = message.payload(new WebSocketIce());
+        const candidate = {} as RTCIceCandidateInit;
+        candidate.candidate = iceFb.candidate();
+        candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
+        candidate.sdpMid = iceFb.sdpMid();
+        this.onIncomingICE(candidate);
+        break;
+      default:
+        console.log('got an unknown message');
+        break;
+    }
+  }
+}
diff --git a/y2022/image_streamer/www_defaults/_404.png b/y2022/image_streamer/www_defaults/_404.png
new file mode 100644
index 0000000..8a43cb8
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_404.png
Binary files differ
diff --git a/y2022/image_streamer/www_defaults/_error.css b/y2022/image_streamer/www_defaults/_error.css
new file mode 100644
index 0000000..8238d6d
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_error.css
@@ -0,0 +1,33 @@
+body {
+    font-family: segoe ui, tahoma, arial, sans-serif;
+    color: #ffffff;
+    background-color: #c21e29;
+    text-align: center;
+}
+
+a {
+    color: #ffff00;
+}
+
+.footer {
+    font-style: italic;
+}
+
+.message {
+    display: inline-block;
+    border: 1px solid white;
+    padding: 50px;
+    font-size: 20px;
+}
+
+.headline {
+    padding: 50px;
+    font-weight: bold;
+    font-size: 32px;
+}
+
+.footer {
+    padding-top: 50px;
+    font-size: 12px;
+}
+
diff --git a/y2022/image_streamer/www_defaults/_error.html b/y2022/image_streamer/www_defaults/_error.html
new file mode 100644
index 0000000..ecf5e32
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_error.html
@@ -0,0 +1,15 @@
+<html DOCTYPE=html>
+<head>
+  <title>%%ERRORCODE%% - %%MESSAGE%% - Keep Calm And Carry On!</title>
+  <link href="/_error.css" rel="stylesheet">
+</head>
+<body>
+  <div class="message">
+    <img src="/_404.png" height="200" width="107">
+    <div class="headline">%%ERRORCODE%% &#8212; %%MESSAGE%%</div>
+    <div class="info">%%BODY%%</div>
+  </div>
+
+  <div class="footer">Powered by <a href="https://github.com/mattgodbolt/seasocks">SeaSocks</a></div>
+</body>
+</html>
diff --git a/y2022/image_streamer/www_defaults/_jquery.min.js b/y2022/image_streamer/www_defaults/_jquery.min.js
new file mode 100644
index 0000000..f78f96a
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_jquery.min.js
@@ -0,0 +1,16 @@
+/*!
+ * jQuery JavaScript Library v1.5.2
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Thu Mar 31 15:28:23 2011 -0400
+ */
+(function(a,b){function ci(a){return d.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cf(a){if(!b_[a]){var b=d("<"+a+">").appendTo("body"),c=b.css("display");b.remove();if(c==="none"||c==="")c="block";b_[a]=c}return b_[a]}function ce(a,b){var c={};d.each(cd.concat.apply([],cd.slice(0,b)),function(){c[this]=a});return c}function b$(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function bZ(){try{return new a.XMLHttpRequest}catch(b){}}function bY(){d(a).unload(function(){for(var a in bW)bW[a](0,1)})}function bS(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var e=a.dataTypes,f={},g,h,i=e.length,j,k=e[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h==="string"&&(f[h.toLowerCase()]=a.converters[h]);l=k,k=e[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=f[m]||f["* "+k];if(!n){p=b;for(o in f){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=f[j[1]+" "+k];if(p){o=f[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&d.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bR(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bQ(a,b,c,e){if(d.isArray(b)&&b.length)d.each(b,function(b,f){c||bs.test(a)?e(a,f):bQ(a+"["+(typeof f==="object"||d.isArray(f)?b:"")+"]",f,c,e)});else if(c||b==null||typeof b!=="object")e(a,b);else if(d.isArray(b)||d.isEmptyObject(b))e(a,"");else for(var f in b)bQ(a+"["+f+"]",b[f],c,e)}function bP(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bJ,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l==="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bP(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bP(a,c,d,e,"*",g));return l}function bO(a){return function(b,c){typeof b!=="string"&&(c=b,b="*");if(d.isFunction(c)){var e=b.toLowerCase().split(bD),f=0,g=e.length,h,i,j;for(;f<g;f++)h=e[f],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bq(a,b,c){var e=b==="width"?bk:bl,f=b==="width"?a.offsetWidth:a.offsetHeight;if(c==="border")return f;d.each(e,function(){c||(f-=parseFloat(d.css(a,"padding"+this))||0),c==="margin"?f+=parseFloat(d.css(a,"margin"+this))||0:f-=parseFloat(d.css(a,"border"+this+"Width"))||0});return f}function bc(a,b){b.src?d.ajax({url:b.src,async:!1,dataType:"script"}):d.globalEval(b.text||b.textContent||b.innerHTML||""),b.parentNode&&b.parentNode.removeChild(b)}function bb(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function ba(a,b){if(b.nodeType===1){var c=b.nodeName.toLowerCase();b.clearAttributes(),b.mergeAttributes(a);if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(d.expando)}}function _(a,b){if(b.nodeType===1&&d.hasData(a)){var c=d.expando,e=d.data(a),f=d.data(b,e);if(e=e[c]){var g=e.events;f=f[c]=d.extend({},e);if(g){delete f.handle,f.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)d.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function $(a,b){return d.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Q(a,b,c){if(d.isFunction(b))return d.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return d.grep(a,function(a,d){return a===b===c});if(typeof b==="string"){var e=d.grep(a,function(a){return a.nodeType===1});if(L.test(b))return d.filter(b,e,!c);b=d.filter(b,e)}return d.grep(a,function(a,e){return d.inArray(a,b)>=0===c})}function P(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function H(a,b){return(a&&a!=="*"?a+".":"")+b.replace(t,"`").replace(u,"&")}function G(a){var b,c,e,f,g,h,i,j,k,l,m,n,o,p=[],q=[],s=d._data(this,"events");if(a.liveFired!==this&&s&&s.live&&!a.target.disabled&&(!a.button||a.type!=="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var t=s.live.slice(0);for(i=0;i<t.length;i++)g=t[i],g.origType.replace(r,"")===a.type?q.push(g.selector):t.splice(i--,1);f=d(a.target).closest(q,a.currentTarget);for(j=0,k=f.length;j<k;j++){m=f[j];for(i=0;i<t.length;i++){g=t[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,e=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,e=d(a.relatedTarget).closest(g.selector)[0];(!e||e!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){f=p[j];if(c&&f.level>c)break;a.currentTarget=f.elem,a.data=f.handleObj.data,a.handleObj=f.handleObj,o=f.handleObj.origHandler.apply(f.elem,arguments);if(o===!1||a.isPropagationStopped()){c=f.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function E(a,c,e){var f=d.extend({},e[0]);f.type=a,f.originalEvent={},f.liveFired=b,d.event.handle.call(c,f),f.isDefaultPrevented()&&e[0].preventDefault()}function y(){return!0}function x(){return!1}function i(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function h(a,c,e){if(e===b&&a.nodeType===1){e=a.getAttribute("data-"+c);if(typeof e==="string"){try{e=e==="true"?!0:e==="false"?!1:e==="null"?null:d.isNaN(e)?g.test(e)?d.parseJSON(e):e:parseFloat(e)}catch(f){}d.data(a,c,e)}else e=b}return e}var c=a.document,d=function(){function G(){if(!d.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(G,1);return}d.ready()}}var d=function(a,b){return new d.fn.init(a,b,g)},e=a.jQuery,f=a.$,g,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,i=/\S/,j=/^\s+/,k=/\s+$/,l=/\d/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=navigator.userAgent,w,x,y,z=Object.prototype.toString,A=Object.prototype.hasOwnProperty,B=Array.prototype.push,C=Array.prototype.slice,D=String.prototype.trim,E=Array.prototype.indexOf,F={};d.fn=d.prototype={constructor:d,init:function(a,e,f){var g,i,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!e&&c.body){this.context=c,this[0]=c.body,this.selector="body",this.length=1;return this}if(typeof a==="string"){g=h.exec(a);if(!g||!g[1]&&e)return!e||e.jquery?(e||f).find(a):this.constructor(e).find(a);if(g[1]){e=e instanceof d?e[0]:e,k=e?e.ownerDocument||e:c,j=m.exec(a),j?d.isPlainObject(e)?(a=[c.createElement(j[1])],d.fn.attr.call(a,e,!0)):a=[k.createElement(j[1])]:(j=d.buildFragment([g[1]],[k]),a=(j.cacheable?d.clone(j.fragment):j.fragment).childNodes);return d.merge(this,a)}i=c.getElementById(g[2]);if(i&&i.parentNode){if(i.id!==g[2])return f.find(a);this.length=1,this[0]=i}this.context=c,this.selector=a;return this}if(d.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return d.makeArray(a,this)},selector:"",jquery:"1.5.2",length:0,size:function(){return this.length},toArray:function(){return C.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var e=this.constructor();d.isArray(a)?B.apply(e,a):d.merge(e,a),e.prevObject=this,e.context=this.context,b==="find"?e.selector=this.selector+(this.selector?" ":"")+c:b&&(e.selector=this.selector+"."+b+"("+c+")");return e},each:function(a,b){return d.each(this,a,b)},ready:function(a){d.bindReady(),x.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(C.apply(this,arguments),"slice",C.call(arguments).join(","))},map:function(a){return this.pushStack(d.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:B,sort:[].sort,splice:[].splice},d.fn.init.prototype=d.fn,d.extend=d.fn.extend=function(){var a,c,e,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i==="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!=="object"&&!d.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){e=i[c],f=a[c];if(i===f)continue;l&&f&&(d.isPlainObject(f)||(g=d.isArray(f)))?(g?(g=!1,h=e&&d.isArray(e)?e:[]):h=e&&d.isPlainObject(e)?e:{},i[c]=d.extend(l,h,f)):f!==b&&(i[c]=f)}return i},d.extend({noConflict:function(b){a.$=f,b&&(a.jQuery=e);return d},isReady:!1,readyWait:1,ready:function(a){a===!0&&d.readyWait--;if(!d.readyWait||a!==!0&&!d.isReady){if(!c.body)return setTimeout(d.ready,1);d.isReady=!0;if(a!==!0&&--d.readyWait>0)return;x.resolveWith(c,[d]),d.fn.trigger&&d(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!x){x=d._Deferred();if(c.readyState==="complete")return setTimeout(d.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",y,!1),a.addEventListener("load",d.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",y),a.attachEvent("onload",d.ready);var b=!1;try{b=a.frameElement==null}catch(e){}c.documentElement.doScroll&&b&&G()}}},isFunction:function(a){return d.type(a)==="function"},isArray:Array.isArray||function(a){return d.type(a)==="array"},isWindow:function(a){return a&&typeof a==="object"&&"setInterval"in a},isNaN:function(a){return a==null||!l.test(a)||isNaN(a)},type:function(a){return a==null?String(a):F[z.call(a)]||"object"},isPlainObject:function(a){if(!a||d.type(a)!=="object"||a.nodeType||d.isWindow(a))return!1;if(a.constructor&&!A.call(a,"constructor")&&!A.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a){}return c===b||A.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!=="string"||!b)return null;b=d.trim(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return a.JSON&&a.JSON.parse?a.JSON.parse(b):(new Function("return "+b))();d.error("Invalid JSON: "+b)},parseXML:function(b,c,e){a.DOMParser?(e=new DOMParser,c=e.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),e=c.documentElement,(!e||!e.nodeName||e.nodeName==="parsererror")&&d.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(a){if(a&&i.test(a)){var b=c.head||c.getElementsByTagName("head")[0]||c.documentElement,e=c.createElement("script");d.support.scriptEval()?e.appendChild(c.createTextNode(a)):e.text=a,b.insertBefore(e,b.firstChild),b.removeChild(e)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,e){var f,g=0,h=a.length,i=h===b||d.isFunction(a);if(e){if(i){for(f in a)if(c.apply(a[f],e)===!1)break}else for(;g<h;)if(c.apply(a[g++],e)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(var j=a[0];g<h&&c.call(j,g,j)!==!1;j=a[++g]){}return a},trim:D?function(a){return a==null?"":D.call(a)}:function(a){return a==null?"":(a+"").replace(j,"").replace(k,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var e=d.type(a);a.length==null||e==="string"||e==="function"||e==="regexp"||d.isWindow(a)?B.call(c,a):d.merge(c,a)}return c},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length==="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,b,c){var d=[],e;for(var f=0,g=a.length;f<g;f++)e=b(a[f],f,c),e!=null&&(d[d.length]=e);return d.concat.apply([],d)},guid:1,proxy:function(a,c,e){arguments.length===2&&(typeof c==="string"?(e=a,a=e[c],c=b):c&&!d.isFunction(c)&&(e=c,c=b)),!c&&a&&(c=function(){return a.apply(e||this,arguments)}),a&&(c.guid=a.guid=a.guid||c.guid||d.guid++);return c},access:function(a,c,e,f,g,h){var i=a.length;if(typeof c==="object"){for(var j in c)d.access(a,j,c[j],f,g,e);return a}if(e!==b){f=!h&&f&&d.isFunction(e);for(var k=0;k<i;k++)g(a[k],c,f?e.call(a[k],k,g(a[k],c)):e,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}d.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.subclass=this.subclass,a.fn.init=function b(b,c){c&&c instanceof d&&!(c instanceof a)&&(c=a(c));return d.fn.init.call(this,b,c,e)},a.fn.init.prototype=a.fn;var e=a(c);return a},browser:{}}),d.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){F["[object "+b+"]"]=b.toLowerCase()}),w=d.uaMatch(v),w.browser&&(d.browser[w.browser]=!0,d.browser.version=w.version),d.browser.webkit&&(d.browser.safari=!0),E&&(d.inArray=function(a,b){return E.call(b,a)}),i.test(" ")&&(j=/^[\s\xA0]+/,k=/[\s\xA0]+$/),g=d(c),c.addEventListener?y=function(){c.removeEventListener("DOMContentLoaded",y,!1),d.ready()}:c.attachEvent&&(y=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",y),d.ready())});return d}(),e="then done fail isResolved isRejected promise".split(" "),f=[].slice;d.extend({_Deferred:function(){var a=[],b,c,e,f={done:function(){if(!e){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=d.type(i),j==="array"?f.done.apply(f,i):j==="function"&&a.push(i);k&&f.resolveWith(k[0],k[1])}return this},resolveWith:function(d,f){if(!e&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(d,f)}finally{b=[d,f],c=0}}return this},resolve:function(){f.resolveWith(this,arguments);return this},isResolved:function(){return c||b},cancel:function(){e=1,a=[];return this}};return f},Deferred:function(a){var b=d._Deferred(),c=d._Deferred(),f;d.extend(b,{then:function(a,c){b.done(a).fail(c);return this},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,promise:function(a){if(a==null){if(f)return f;f=a={}}var c=e.length;while(c--)a[e[c]]=b[e[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?f.call(arguments,0):c,--g||h.resolveWith(h,f.call(b,0))}}var b=arguments,c=0,e=b.length,g=e,h=e<=1&&a&&d.isFunction(a.promise)?a:d.Deferred();if(e>1){for(;c<e;c++)b[c]&&d.isFunction(b[c].promise)?b[c].promise().then(i(c),h.reject):--g;g||h.resolveWith(h,b)}else h!==a&&h.resolveWith(h,e?[a]:[]);return h.promise()}}),function(){d.support={};var b=c.createElement("div");b.style.display="none",b.innerHTML="   <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=b.getElementsByTagName("*"),f=b.getElementsByTagName("a")[0],g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=b.getElementsByTagName("input")[0];if(e&&e.length&&f){d.support={leadingWhitespace:b.firstChild.nodeType===3,tbody:!b.getElementsByTagName("tbody").length,htmlSerialize:!!b.getElementsByTagName("link").length,style:/red/.test(f.getAttribute("style")),hrefNormalized:f.getAttribute("href")==="/a",opacity:/^0.55$/.test(f.style.opacity),cssFloat:!!f.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,deleteExpando:!0,optDisabled:!1,checkClone:!1,noCloneEvent:!0,noCloneChecked:!0,boxModel:null,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableHiddenOffsets:!0,reliableMarginRight:!0},i.checked=!0,d.support.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,d.support.optDisabled=!h.disabled;var j=null;d.support.scriptEval=function(){if(j===null){var b=c.documentElement,e=c.createElement("script"),f="script"+d.now();try{e.appendChild(c.createTextNode("window."+f+"=1;"))}catch(g){}b.insertBefore(e,b.firstChild),a[f]?(j=!0,delete a[f]):j=!1,b.removeChild(e)}return j};try{delete b.test}catch(k){d.support.deleteExpando=!1}!b.addEventListener&&b.attachEvent&&b.fireEvent&&(b.attachEvent("onclick",function l(){d.support.noCloneEvent=!1,b.detachEvent("onclick",l)}),b.cloneNode(!0).fireEvent("onclick")),b=c.createElement("div"),b.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";var m=c.createDocumentFragment();m.appendChild(b.firstChild),d.support.checkClone=m.cloneNode(!0).cloneNode(!0).lastChild.checked,d(function(){var a=c.createElement("div"),b=c.getElementsByTagName("body")[0];if(b){a.style.width=a.style.paddingLeft="1px",b.appendChild(a),d.boxModel=d.support.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,d.support.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",d.support.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";var e=a.getElementsByTagName("td");d.support.reliableHiddenOffsets=e[0].offsetHeight===0,e[0].style.display="",e[1].style.display="none",d.support.reliableHiddenOffsets=d.support.reliableHiddenOffsets&&e[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(a.style.width="1px",a.style.marginRight="0",d.support.reliableMarginRight=(parseInt(c.defaultView.getComputedStyle(a,null).marginRight,10)||0)===0),b.removeChild(a).style.display="none",a=e=null}});var n=function(a){var b=c.createElement("div");a="on"+a;if(!b.attachEvent)return!0;var d=a in b;d||(b.setAttribute(a,"return;"),d=typeof b[a]==="function");return d};d.support.submitBubbles=n("submit"),d.support.changeBubbles=n("change"),b=e=f=null}}();var g=/^(?:\{.*\}|\[.*\])$/;d.extend({cache:{},uuid:0,expando:"jQuery"+(d.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?d.cache[a[d.expando]]:a[d.expando];return!!a&&!i(a)},data:function(a,c,e,f){if(d.acceptData(a)){var g=d.expando,h=typeof c==="string",i,j=a.nodeType,k=j?d.cache:a,l=j?a[d.expando]:a[d.expando]&&d.expando;if((!l||f&&l&&!k[l][g])&&h&&e===b)return;l||(j?a[d.expando]=l=++d.uuid:l=d.expando),k[l]||(k[l]={},j||(k[l].toJSON=d.noop));if(typeof c==="object"||typeof c==="function")f?k[l][g]=d.extend(k[l][g],c):k[l]=d.extend(k[l],c);i=k[l],f&&(i[g]||(i[g]={}),i=i[g]),e!==b&&(i[c]=e);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[c]:i}},removeData:function(b,c,e){if(d.acceptData(b)){var f=d.expando,g=b.nodeType,h=g?d.cache:b,j=g?b[d.expando]:d.expando;if(!h[j])return;if(c){var k=e?h[j][f]:h[j];if(k){delete k[c];if(!i(k))return}}if(e){delete h[j][f];if(!i(h[j]))return}var l=h[j][f];d.support.deleteExpando||h!=a?delete h[j]:h[j]=null,l?(h[j]={},g||(h[j].toJSON=d.noop),h[j][f]=l):g&&(d.support.deleteExpando?delete b[d.expando]:b.removeAttribute?b.removeAttribute(d.expando):b[d.expando]=null)}},_data:function(a,b,c){return d.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=d.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),d.fn.extend({data:function(a,c){var e=null;if(typeof a==="undefined"){if(this.length){e=d.data(this[0]);if(this[0].nodeType===1){var f=this[0].attributes,g;for(var i=0,j=f.length;i<j;i++)g=f[i].name,g.indexOf("data-")===0&&(g=g.substr(5),h(this[0],g,e[g]))}}return e}if(typeof a==="object")return this.each(function(){d.data(this,a)});var k=a.split(".");k[1]=k[1]?"."+k[1]:"";if(c===b){e=this.triggerHandler("getData"+k[1]+"!",[k[0]]),e===b&&this.length&&(e=d.data(this[0],a),e=h(this[0],a,e));return e===b&&k[1]?this.data(k[0]):e}return this.each(function(){var b=d(this),e=[k[0],c];b.triggerHandler("setData"+k[1]+"!",e),d.data(this,a,c),b.triggerHandler("changeData"+k[1]+"!",e)})},removeData:function(a){return this.each(function(){d.removeData(this,a)})}}),d.extend({queue:function(a,b,c){if(a){b=(b||"fx")+"queue";var e=d._data(a,b);if(!c)return e||[];!e||d.isArray(c)?e=d._data(a,b,d.makeArray(c)):e.push(c);return e}},dequeue:function(a,b){b=b||"fx";var c=d.queue(a,b),e=c.shift();e==="inprogress"&&(e=c.shift()),e&&(b==="fx"&&c.unshift("inprogress"),e.call(a,function(){d.dequeue(a,b)})),c.length||d.removeData(a,b+"queue",!0)}}),d.fn.extend({queue:function(a,c){typeof a!=="string"&&(c=a,a="fx");if(c===b)return d.queue(this[0],a);return this.each(function(b){var e=d.queue(this,a,c);a==="fx"&&e[0]!=="inprogress"&&d.dequeue(this,a)})},dequeue:function(a){return this.each(function(){d.dequeue(this,a)})},delay:function(a,b){a=d.fx?d.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){d.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var j=/[\n\t\r]/g,k=/\s+/,l=/\r/g,m=/^(?:href|src|style)$/,n=/^(?:button|input)$/i,o=/^(?:button|input|object|select|textarea)$/i,p=/^a(?:rea)?$/i,q=/^(?:radio|checkbox)$/i;d.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"},d.fn.extend({attr:function(a,b){return d.access(this,a,b,!0,d.attr)},removeAttr:function(a,b){return this.each(function(){d.attr(this,a,""),this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.addClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"){var b=(a||"").split(k);for(var c=0,e=this.length;c<e;c++){var f=this[c];if(f.nodeType===1)if(f.className){var g=" "+f.className+" ",h=f.className;for(var i=0,j=b.length;i<j;i++)g.indexOf(" "+b[i]+" ")<0&&(h+=" "+b[i]);f.className=d.trim(h)}else f.className=a}}return this},removeClass:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.removeClass(a.call(this,b,c.attr("class")))});if(a&&typeof a==="string"||a===b){var c=(a||"").split(k);for(var e=0,f=this.length;e<f;e++){var g=this[e];if(g.nodeType===1&&g.className)if(a){var h=(" "+g.className+" ").replace(j," ");for(var i=0,l=c.length;i<l;i++)h=h.replace(" "+c[i]+" "," ");g.className=d.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,e=typeof b==="boolean";if(d.isFunction(a))return this.each(function(c){var e=d(this);e.toggleClass(a.call(this,c,e.attr("class"),b),b)});return this.each(function(){if(c==="string"){var f,g=0,h=d(this),i=b,j=a.split(k);while(f=j[g++])i=e?i:!h.hasClass(f),h[i?"addClass":"removeClass"](f)}else if(c==="undefined"||c==="boolean")this.className&&d._data(this,"__className__",this.className),this.className=this.className||a===!1?"":d._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(j," ").indexOf(b)>-1)return!0;return!1},val:function(a){if(!arguments.length){var c=this[0];if(c){if(d.nodeName(c,"option")){var e=c.attributes.value;return!e||e.specified?c.value:c.text}if(d.nodeName(c,"select")){var f=c.selectedIndex,g=[],h=c.options,i=c.type==="select-one";if(f<0)return null;for(var j=i?f:0,k=i?f+1:h.length;j<k;j++){var m=h[j];if(m.selected&&(d.support.optDisabled?!m.disabled:m.getAttribute("disabled")===null)&&(!m.parentNode.disabled||!d.nodeName(m.parentNode,"optgroup"))){a=d(m).val();if(i)return a;g.push(a)}}if(i&&!g.length&&h.length)return d(h[f]).val();return g}if(q.test(c.type)&&!d.support.checkOn)return c.getAttribute("value")===null?"on":c.value;return(c.value||"").replace(l,"")}return b}var n=d.isFunction(a);return this.each(function(b){var c=d(this),e=a;if(this.nodeType===1){n&&(e=a.call(this,b,c.val())),e==null?e="":typeof e==="number"?e+="":d.isArray(e)&&(e=d.map(e,function(a){return a==null?"":a+""}));if(d.isArray(e)&&q.test(this.type))this.checked=d.inArray(c.val(),e)>=0;else if(d.nodeName(this,"select")){var f=d.makeArray(e);d("option",this).each(function(){this.selected=d.inArray(d(this).val(),f)>=0}),f.length||(this.selectedIndex=-1)}else this.value=e}})}}),d.extend({attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,e,f){if(!a||a.nodeType===3||a.nodeType===8||a.nodeType===2)return b;if(f&&c in d.attrFn)return d(a)[c](e);var g=a.nodeType!==1||!d.isXMLDoc(a),h=e!==b;c=g&&d.props[c]||c;if(a.nodeType===1){var i=m.test(c);if(c==="selected"&&!d.support.optSelected){var j=a.parentNode;j&&(j.selectedIndex,j.parentNode&&j.parentNode.selectedIndex)}if((c in a||a[c]!==b)&&g&&!i){h&&(c==="type"&&n.test(a.nodeName)&&a.parentNode&&d.error("type property can't be changed"),e===null?a.nodeType===1&&a.removeAttribute(c):a[c]=e);if(d.nodeName(a,"form")&&a.getAttributeNode(c))return a.getAttributeNode(c).nodeValue;if(c==="tabIndex"){var k=a.getAttributeNode("tabIndex");return k&&k.specified?k.value:o.test(a.nodeName)||p.test(a.nodeName)&&a.href?0:b}return a[c]}if(!d.support.style&&g&&c==="style"){h&&(a.style.cssText=""+e);return a.style.cssText}h&&a.setAttribute(c,""+e);if(!a.attributes[c]&&(a.hasAttribute&&!a.hasAttribute(c)))return b;var l=!d.support.hrefNormalized&&g&&i?a.getAttribute(c,2):a.getAttribute(c);return l===null?b:l}h&&(a[c]=e);return a[c]}});var r=/\.(.*)$/,s=/^(?:textarea|input|select)$/i,t=/\./g,u=/ /g,v=/[^\w\s.|`]/g,w=function(a){return a.replace(v,"\\$&")};d.event={add:function(c,e,f,g){if(c.nodeType!==3&&c.nodeType!==8){try{d.isWindow(c)&&(c!==a&&!c.frameElement)&&(c=a)}catch(h){}if(f===!1)f=x;else if(!f)return;var i,j;f.handler&&(i=f,f=i.handler),f.guid||(f.guid=d.guid++);var k=d._data(c);if(!k)return;var l=k.events,m=k.handle;l||(k.events=l={}),m||(k.handle=m=function(a){return typeof d!=="undefined"&&d.event.triggered!==a.type?d.event.handle.apply(m.elem,arguments):b}),m.elem=c,e=e.split(" ");var n,o=0,p;while(n=e[o++]){j=i?d.extend({},i):{handler:f,data:g},n.indexOf(".")>-1?(p=n.split("."),n=p.shift(),j.namespace=p.slice(0).sort().join(".")):(p=[],j.namespace=""),j.type=n,j.guid||(j.guid=f.guid);var q=l[n],r=d.event.special[n]||{};if(!q){q=l[n]=[];if(!r.setup||r.setup.call(c,g,p,m)===!1)c.addEventListener?c.addEventListener(n,m,!1):c.attachEvent&&c.attachEvent("on"+n,m)}r.add&&(r.add.call(c,j),j.handler.guid||(j.handler.guid=f.guid)),q.push(j),d.event.global[n]=!0}c=null}},global:{},remove:function(a,c,e,f){if(a.nodeType!==3&&a.nodeType!==8){e===!1&&(e=x);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=d.hasData(a)&&d._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(e=c.handler,c=c.type);if(!c||typeof c==="string"&&c.charAt(0)==="."){c=c||"";for(h in t)d.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+d.map(m.slice(0).sort(),w).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!e){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))d.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=d.event.special[h]||{};for(j=f||0;j<p.length;j++){q=p[j];if(e.guid===q.guid){if(l||n.test(q.namespace))f==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(f!=null)break}}if(p.length===0||f!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&d.removeEvent(a,h,s.handle),g=null,delete t[h]}if(d.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,d.isEmptyObject(s)&&d.removeData(a,b,!0)}}},trigger:function(a,c,e){var f=a.type||a,g=arguments[3];if(!g){a=typeof a==="object"?a[d.expando]?a:d.extend(d.Event(f),a):d.Event(f),f.indexOf("!")>=0&&(a.type=f=f.slice(0,-1),a.exclusive=!0),e||(a.stopPropagation(),d.event.global[f]&&d.each(d.cache,function(){var b=d.expando,e=this[b];e&&e.events&&e.events[f]&&d.event.trigger(a,c,e.handle.elem)}));if(!e||e.nodeType===3||e.nodeType===8)return b;a.result=b,a.target=e,c=d.makeArray(c),c.unshift(a)}a.currentTarget=e;var h=d._data(e,"handle");h&&h.apply(e,c);var i=e.parentNode||e.ownerDocument;try{e&&e.nodeName&&d.noData[e.nodeName.toLowerCase()]||e["on"+f]&&e["on"+f].apply(e,c)===!1&&(a.result=!1,a.preventDefault())}catch(j){}if(!a.isPropagationStopped()&&i)d.event.trigger(a,c,i,!0);else if(!a.isDefaultPrevented()){var k,l=a.target,m=f.replace(r,""),n=d.nodeName(l,"a")&&m==="click",o=d.event.special[m]||{};if((!o._default||o._default.call(e,a)===!1)&&!n&&!(l&&l.nodeName&&d.noData[l.nodeName.toLowerCase()])){try{l[m]&&(k=l["on"+m],k&&(l["on"+m]=null),d.event.triggered=a.type,l[m]())}catch(p){}k&&(l["on"+m]=k),d.event.triggered=b}}},handle:function(c){var e,f,g,h,i,j=[],k=d.makeArray(arguments);c=k[0]=d.event.fix(c||a.event),c.currentTarget=this,e=c.type.indexOf(".")<0&&!c.exclusive,e||(g=c.type.split("."),c.type=g.shift(),j=g.slice(0).sort(),h=new RegExp("(^|\\.)"+j.join("\\.(?:.*\\.)?")+"(\\.|$)")),c.namespace=c.namespace||j.join("."),i=d._data(this,"events"),f=(i||{})[c.type];if(i&&f){f=f.slice(0);for(var l=0,m=f.length;l<m;l++){var n=f[l];if(e||h.test(n.namespace)){c.handler=n.handler,c.data=n.data,c.handleObj=n;var o=n.handler.apply(this,k);o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[d.expando])return a;var e=a;a=d.Event(e);for(var f=this.props.length,g;f;)g=this.props[--f],a[g]=e[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=c.documentElement,i=c.body;a.pageX=a.clientX+(h&&h.scrollLeft||i&&i.scrollLeft||0)-(h&&h.clientLeft||i&&i.clientLeft||0),a.pageY=a.clientY+(h&&h.scrollTop||i&&i.scrollTop||0)-(h&&h.clientTop||i&&i.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:d.proxy,special:{ready:{setup:d.bindReady,teardown:d.noop},live:{add:function(a){d.event.add(this,H(a.origType,a.selector),d.extend({},a,{handler:G,guid:a.handler.guid}))},remove:function(a){d.event.remove(this,H(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){d.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},d.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},d.Event=function(a){if(!this.preventDefault)return new d.Event(a);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?y:x):this.type=a,this.timeStamp=d.now(),this[d.expando]=!0},d.Event.prototype={preventDefault:function(){this.isDefaultPrevented=y;var a=this.originalEvent;a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=y;var a=this.originalEvent;a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=y,this.stopPropagation()},isDefaultPrevented:x,isPropagationStopped:x,isImmediatePropagationStopped:x};var z=function(a){var b=a.relatedTarget;try{if(b&&b!==c&&!b.parentNode)return;while(b&&b!==this)b=b.parentNode;b!==this&&(a.type=a.data,d.event.handle.apply(this,arguments))}catch(e){}},A=function(a){a.type=a.data,d.event.handle.apply(this,arguments)};d.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){d.event.special[a]={setup:function(c){d.event.add(this,b,c&&c.selector?A:z,a)},teardown:function(a){d.event.remove(this,b,a&&a.selector?A:z)}}}),d.support.submitBubbles||(d.event.special.submit={setup:function(a,b){if(this.nodeName&&this.nodeName.toLowerCase()!=="form")d.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&d(b).closest("form").length&&E("submit",this,arguments)}),d.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&d(b).closest("form").length&&a.keyCode===13&&E("submit",this,arguments)});else return!1},teardown:function(a){d.event.remove(this,".specialSubmit")}});if(!d.support.changeBubbles){var B,C=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?d.map(a.options,function(a){return a.selected}).join("-"):"":a.nodeName.toLowerCase()==="select"&&(c=a.selectedIndex);return c},D=function D(a){var c=a.target,e,f;if(s.test(c.nodeName)&&!c.readOnly){e=d._data(c,"_change_data"),f=C(c),(a.type!=="focusout"||c.type!=="radio")&&d._data(c,"_change_data",f);if(e===b||f===e)return;if(e!=null||f)a.type="change",a.liveFired=b,d.event.trigger(a,arguments[1],c)}};d.event.special.change={filters:{focusout:D,beforedeactivate:D,click:function(a){var b=a.target,c=b.type;(c==="radio"||c==="checkbox"||b.nodeName.toLowerCase()==="select")&&D.call(this,a)},keydown:function(a){var b=a.target,c=b.type;(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&D.call(this,a)},beforeactivate:function(a){var b=a.target;d._data(b,"_change_data",C(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in B)d.event.add(this,c+".specialChange",B[c]);return s.test(this.nodeName)},teardown:function(a){d.event.remove(this,".specialChange");return s.test(this.nodeName)}},B=d.event.special.change.filters,B.focus=B.beforeactivate}c.addEventListener&&d.each({focus:"focusin",blur:"focusout"},function(a,b){function f(a){var c=d.event.fix(a);c.type=b,c.originalEvent={},d.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var e=0;d.event.special[b]={setup:function(){e++===0&&c.addEventListener(a,f,!0)},teardown:function(){--e===0&&c.removeEventListener(a,f,!0)}}}),d.each(["bind","one"],function(a,c){d.fn[c]=function(a,e,f){if(typeof a==="object"){for(var g in a)this[c](g,e,a[g],f);return this}if(d.isFunction(e)||e===!1)f=e,e=b;var h=c==="one"?d.proxy(f,function(a){d(this).unbind(a,h);return f.apply(this,arguments)}):f;if(a==="unload"&&c!=="one")this.one(a,e,f);else for(var i=0,j=this.length;i<j;i++)d.event.add(this[i],a,h,e);return this}}),d.fn.extend({unbind:function(a,b){if(typeof a!=="object"||a.preventDefault)for(var e=0,f=this.length;e<f;e++)d.event.remove(this[e],a,b);else for(var c in a)this.unbind(c,a[c]);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){d.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){var c=d.Event(a);c.preventDefault(),c.stopPropagation(),d.event.trigger(c,b,this[0]);return c.result}},toggle:function(a){var b=arguments,c=1;while(c<b.length)d.proxy(a,b[c++]);return this.click(d.proxy(a,function(e){var f=(d._data(this,"lastToggle"+a.guid)||0)%c;d._data(this,"lastToggle"+a.guid,f+1),e.preventDefault();return b[f].apply(this,arguments)||!1}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var F={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};d.each(["live","die"],function(a,c){d.fn[c]=function(a,e,f,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:d(this.context);if(typeof a==="object"&&!a.preventDefault){for(var o in a)n[c](o,e,a[o],m);return this}d.isFunction(e)&&(f=e,e=b),a=(a||"").split(" ");while((h=a[i++])!=null){j=r.exec(h),k="",j&&(k=j[0],h=h.replace(r,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,h==="focus"||h==="blur"?(a.push(F[h]+k),h=h+k):h=(F[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)d.event.add(n[p],"live."+H(h,m),{data:e,selector:m,handler:f,origType:h,origHandler:f,preType:l});else n.unbind("live."+H(h,m),f)}return this}}),d.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){d.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},d.attrFn&&(d.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!=="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,e,g){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!=="string")return e;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(f.call(n)==="[object Array]")if(u)if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&e.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&e.push(j[t]);else e.push.apply(e,n);else p(n,e);o&&(k(o,h,e,g),k.uniqueSort(e));return e};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!=="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(f){if(f===!0)continue}else g=o=!0}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b==="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1){}a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b==="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=u;typeof b==="string"&&!j.test(b)&&(b=b.toLowerCase(),d=b,g=t),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!=="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!=="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!=="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return"text"===c&&(b===c||b===null)},radio:function(a){return"radio"===a.type},checkbox:function(a){return"checkbox"===a.type},file:function(a){return"file"===a.type},password:function(a){return"password"===a.type},submit:function(a){return"submit"===a.type},image:function(a){return"image"===a.type},reset:function(a){return"reset"===a.type},button:function(a){return"button"===a.type||a.nodeName.toLowerCase()==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(f.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length==="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(a===b){g=!0;return 0}if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!=="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!=="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!=="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!=="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!=="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};d.find=k,d.expr=k.selectors,d.expr[":"]=d.expr.filters,d.unique=k.uniqueSort,d.text=k.getText,d.isXMLDoc=k.isXML,d.contains=k.contains}();var I=/Until$/,J=/^(?:parents|prevUntil|prevAll)/,K=/,/,L=/^.[^:#\[\.,]*$/,M=Array.prototype.slice,N=d.expr.match.POS,O={children:!0,contents:!0,next:!0,prev:!0};d.fn.extend({find:function(a){var b=this.pushStack("","find",a),c=0;for(var e=0,f=this.length;e<f;e++){c=b.length,d.find(a,this[e],b);if(e>0)for(var g=c;g<b.length;g++)for(var h=0;h<c;h++)if(b[h]===b[g]){b.splice(g--,1);break}}return b},has:function(a){var b=d(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(d.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(Q(this,a,!1),"not",a)},filter:function(a){return this.pushStack(Q(this,a,!0),"filter",a)},is:function(a){return!!a&&d.filter(a,this).length>0},closest:function(a,b){var c=[],e,f,g=this[0];if(d.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(e=0,f=a.length;e<f;e++)i=a[e],j[i]||(j[i]=d.expr.match.POS.test(i)?d(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:d(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=N.test(a)?d(a,b||this.context):null;for(e=0,f=this.length;e<f;e++){g=this[e];while(g){if(l?l.index(g)>-1:d.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b)break}}c=c.length>1?d.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a==="string")return d.inArray(this[0],a?d(a):this.parent().children());return d.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a==="string"?d(a,b):d.makeArray(a),e=d.merge(this.get(),c);return this.pushStack(P(c[0])||P(e[0])?e:d.unique(e))},andSelf:function(){return this.add(this.prevObject)}}),d.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return d.dir(a,"parentNode")},parentsUntil:function(a,b,c){return d.dir(a,"parentNode",c)},next:function(a){return d.nth(a,2,"nextSibling")},prev:function(a){return d.nth(a,2,"previousSibling")},nextAll:function(a){return d.dir(a,"nextSibling")},prevAll:function(a){return d.dir(a,"previousSibling")},nextUntil:function(a,b,c){return d.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return d.dir(a,"previousSibling",c)},siblings:function(a){return d.sibling(a.parentNode.firstChild,a)},children:function(a){return d.sibling(a.firstChild)},contents:function(a){return d.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:d.makeArray(a.childNodes)}},function(a,b){d.fn[a]=function(c,e){var f=d.map(this,b,c),g=M.call(arguments);I.test(a)||(e=c),e&&typeof e==="string"&&(f=d.filter(e,f)),f=this.length>1&&!O[a]?d.unique(f):f,(this.length>1||K.test(e))&&J.test(a)&&(f=f.reverse());return this.pushStack(f,a,g.join(","))}}),d.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?d.find.matchesSelector(b[0],a)?[b[0]]:[]:d.find.matches(a,b)},dir:function(a,c,e){var f=[],g=a[c];while(g&&g.nodeType!==9&&(e===b||g.nodeType!==1||!d(g).is(e)))g.nodeType===1&&f.push(g),g=g[c];return f},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var R=/ jQuery\d+="(?:\d+|null)"/g,S=/^\s+/,T=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,U=/<([\w:]+)/,V=/<tbody/i,W=/<|&#?\w+;/,X=/<(?:script|object|embed|option|style)/i,Y=/checked\s*(?:[^=]|=\s*.checked.)/i,Z={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};Z.optgroup=Z.option,Z.tbody=Z.tfoot=Z.colgroup=Z.caption=Z.thead,Z.th=Z.td,d.support.htmlSerialize||(Z._default=[1,"div<div>","</div>"]),d.fn.extend({text:function(a){if(d.isFunction(a))return this.each(function(b){var c=d(this);c.text(a.call(this,b,c.text()))});if(typeof a!=="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return d.text(this)},wrapAll:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapAll(a.call(this,b))});if(this[0]){var b=d(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(d.isFunction(a))return this.each(function(b){d(this).wrapInner(a.call(this,b))});return this.each(function(){var b=d(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){d(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){d.nodeName(this,"body")||d(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=d(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,d(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,e;(e=this[c])!=null;c++)if(!a||d.filter(a,[e]).length)!b&&e.nodeType===1&&(d.cleanData(e.getElementsByTagName("*")),d.cleanData([e])),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&d.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return d.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(R,""):null;if(typeof a!=="string"||X.test(a)||!d.support.leadingWhitespace&&S.test(a)||Z[(U.exec(a)||["",""])[1].toLowerCase()])d.isFunction(a)?this.each(function(b){var c=d(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);else{a=a.replace(T,"<$1></$2>");try{for(var c=0,e=this.length;c<e;c++)this[c].nodeType===1&&(d.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(f){this.empty().append(a)}}return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(d.isFunction(a))return this.each(function(b){var c=d(this),e=c.html();c.replaceWith(a.call(this,b,e))});typeof a!=="string"&&(a=d(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;d(this).remove(),b?d(b).before(a):d(c).append(a)})}return this.length?this.pushStack(d(d.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,e){var f,g,h,i,j=a[0],k=[];if(!d.support.checkClone&&arguments.length===3&&typeof j==="string"&&Y.test(j))return this.each(function(){d(this).domManip(a,c,e,!0)});if(d.isFunction(j))return this.each(function(f){var g=d(this);a[0]=j.call(this,f,c?g.html():b),g.domManip(a,c,e)});if(this[0]){i=j&&j.parentNode,d.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?f={fragment:i}:f=d.buildFragment(a,this,k),h=f.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&d.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)e.call(c?$(this[l],g):this[l],f.cacheable||m>1&&l<n?d.clone(h,!0,!0):h)}k.length&&d.each(k,bc)}return this}}),d.buildFragment=function(a,b,e){var f,g,h,i=b&&b[0]?b[0].ownerDocument||b[0]:c;a.length===1&&typeof a[0]==="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!X.test(a[0])&&(d.support.checkClone||!Y.test(a[0]))&&(g=!0,h=d.fragments[a[0]],h&&(h!==1&&(f=h))),f||(f=i.createDocumentFragment(),d.clean(a,i,f,e)),g&&(d.fragments[a[0]]=h?f:1);return{fragment:f,cacheable:g}},d.fragments={},d.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){d.fn[a]=function(c){var e=[],f=d(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&f.length===1){f[b](this[0]);return this}for(var h=0,i=f.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();d(f[h])[b](j),e=e.concat(j)}return this.pushStack(e,a,f.selector)}}),d.extend({clone:function(a,b,c){var e=a.cloneNode(!0),f,g,h;if((!d.support.noCloneEvent||!d.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!d.isXMLDoc(a)){ba(a,e),f=bb(a),g=bb(e);for(h=0;f[h];++h)ba(f[h],g[h])}if(b){_(a,e);if(c){f=bb(a),g=bb(e);for(h=0;f[h];++h)_(f[h],g[h])}}return e},clean:function(a,b,e,f){b=b||c,typeof b.createElement==="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var g=[];for(var h=0,i;(i=a[h])!=null;h++){typeof i==="number"&&(i+="");if(!i)continue;if(typeof i!=="string"||W.test(i)){if(typeof i==="string"){i=i.replace(T,"<$1></$2>");var j=(U.exec(i)||["",""])[1].toLowerCase(),k=Z[j]||Z._default,l=k[0],m=b.createElement("div");m.innerHTML=k[1]+i+k[2];while(l--)m=m.lastChild;if(!d.support.tbody){var n=V.test(i),o=j==="table"&&!n?m.firstChild&&m.firstChild.childNodes:k[1]==="<table>"&&!n?m.childNodes:[];for(var p=o.length-1;p>=0;--p)d.nodeName(o[p],"tbody")&&!o[p].childNodes.length&&o[p].parentNode.removeChild(o[p])}!d.support.leadingWhitespace&&S.test(i)&&m.insertBefore(b.createTextNode(S.exec(i)[0]),m.firstChild),i=m.childNodes}}else i=b.createTextNode(i);i.nodeType?g.push(i):g=d.merge(g,i)}if(e)for(h=0;g[h];h++)!f||!d.nodeName(g[h],"script")||g[h].type&&g[h].type.toLowerCase()!=="text/javascript"?(g[h].nodeType===1&&g.splice.apply(g,[h+1,0].concat(d.makeArray(g[h].getElementsByTagName("script")))),e.appendChild(g[h])):f.push(g[h].parentNode?g[h].parentNode.removeChild(g[h]):g[h]);return g},cleanData:function(a){var b,c,e=d.cache,f=d.expando,g=d.event.special,h=d.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&d.noData[j.nodeName.toLowerCase()])continue;c=j[d.expando];if(c){b=e[c]&&e[c][f];if(b&&b.events){for(var k in b.events)g[k]?d.event.remove(j,k):d.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[d.expando]:j.removeAttribute&&j.removeAttribute(d.expando),delete e[c]}}}});var bd=/alpha\([^)]*\)/i,be=/opacity=([^)]*)/,bf=/-([a-z])/ig,bg=/([A-Z]|^ms)/g,bh=/^-?\d+(?:px)?$/i,bi=/^-?\d/,bj={position:"absolute",visibility:"hidden",display:"block"},bk=["Left","Right"],bl=["Top","Bottom"],bm,bn,bo,bp=function(a,b){return b.toUpperCase()};d.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return d.access(this,a,c,!0,function(a,c,e){return e!==b?d.style(a,c,e):d.css(a,c)})},d.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bm(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{zIndex:!0,fontWeight:!0,opacity:!0,zoom:!0,lineHeight:!0},cssProps:{"float":d.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,e,f){if(a&&a.nodeType!==3&&a.nodeType!==8&&a.style){var g,h=d.camelCase(c),i=a.style,j=d.cssHooks[h];c=d.cssProps[h]||h;if(e===b){if(j&&"get"in j&&(g=j.get(a,!1,f))!==b)return g;return i[c]}if(typeof e==="number"&&isNaN(e)||e==null)return;typeof e==="number"&&!d.cssNumber[h]&&(e+="px");if(!j||!("set"in j)||(e=j.set(a,e))!==b)try{i[c]=e}catch(k){}}},css:function(a,c,e){var f,g=d.camelCase(c),h=d.cssHooks[g];c=d.cssProps[g]||g;if(h&&"get"in h&&(f=h.get(a,!0,e))!==b)return f;if(bm)return bm(a,c,g)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]},camelCase:function(a){return a.replace(bf,bp)}}),d.curCSS=d.css,d.each(["height","width"],function(a,b){d.cssHooks[b]={get:function(a,c,e){var f;if(c){a.offsetWidth!==0?f=bq(a,b,e):d.swap(a,bj,function(){f=bq(a,b,e)});if(f<=0){f=bm(a,b,b),f==="0px"&&bo&&(f=bo(a,b,b));if(f!=null)return f===""||f==="auto"?"0px":f}if(f<0||f==null){f=a.style[b];return f===""||f==="auto"?"0px":f}return typeof f==="string"?f:f+"px"}},set:function(a,b){if(!bh.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),d.support.opacity||(d.cssHooks.opacity={get:function(a,b){return be.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style;c.zoom=1;var e=d.isNaN(b)?"":"alpha(opacity="+b*100+")",f=c.filter||"";c.filter=bd.test(f)?f.replace(bd,e):c.filter+" "+e}}),d(function(){d.support.reliableMarginRight||(d.cssHooks.marginRight={get:function(a,b){var c;d.swap(a,{display:"inline-block"},function(){b?c=bm(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bn=function(a,c,e){var f,g,h;e=e.replace(bg,"-$1").toLowerCase();if(!(g=a.ownerDocument.defaultView))return b;if(h=g.getComputedStyle(a,null))f=h.getPropertyValue(e),f===""&&!d.contains(a.ownerDocument.documentElement,a)&&(f=d.style(a,e));return f}),c.documentElement.currentStyle&&(bo=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bh.test(d)&&bi.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bm=bn||bo,d.expr&&d.expr.filters&&(d.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!d.support.reliableHiddenOffsets&&(a.style.display||d.css(a,"display"))==="none"},d.expr.filters.visible=function(a){return!d.expr.filters.hidden(a)});var br=/%20/g,bs=/\[\]$/,bt=/\r?\n/g,bu=/#.*$/,bv=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bw=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bx=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,by=/^(?:GET|HEAD)$/,bz=/^\/\//,bA=/\?/,bB=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bC=/^(?:select|textarea)/i,bD=/\s+/,bE=/([?&])_=[^&]*/,bF=/(^|\-)([a-z])/g,bG=function(a,b,c){return b+c.toUpperCase()},bH=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bI=d.fn.load,bJ={},bK={},bL,bM;try{bL=c.location.href}catch(bN){bL=c.createElement("a"),bL.href="",bL=bL.href}bM=bH.exec(bL.toLowerCase())||[],d.fn.extend({load:function(a,c,e){if(typeof a!=="string"&&bI)return bI.apply(this,arguments);if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var g=a.slice(f,a.length);a=a.slice(0,f)}var h="GET";c&&(d.isFunction(c)?(e=c,c=b):typeof c==="object"&&(c=d.param(c,d.ajaxSettings.traditional),h="POST"));var i=this;d.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?d("<div>").append(c.replace(bB,"")).find(g):c)),e&&i.each(e,[c,b,a])}});return this},serialize:function(){return d.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?d.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bC.test(this.nodeName)||bw.test(this.type))}).map(function(a,b){var c=d(this).val();return c==null?null:d.isArray(c)?d.map(c,function(a,c){return{name:b.name,value:a.replace(bt,"\r\n")}}):{name:b.name,value:c.replace(bt,"\r\n")}}).get()}}),d.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){d.fn[b]=function(a){return this.bind(b,a)}}),d.each(["get","post"],function(a,c){d[c]=function(a,e,f,g){d.isFunction(e)&&(g=g||f,f=e,e=b);return d.ajax({type:c,url:a,data:e,success:f,dataType:g})}}),d.extend({getScript:function(a,c){return d.get(a,b,c,"script")},getJSON:function(a,b,c){return d.get(a,b,c,"json")},ajaxSetup:function(a,b){b?d.extend(!0,a,d.ajaxSettings,b):(b=a,a=d.extend(!0,d.ajaxSettings,b));for(var c in {context:1,url:1})c in b?a[c]=b[c]:c in d.ajaxSettings&&(a[c]=d.ajaxSettings[c]);return a},ajaxSettings:{url:bL,isLocal:bx.test(bM[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":d.parseJSON,"text xml":d.parseXML}},ajaxPrefilter:bO(bJ),ajaxTransport:bO(bK),ajax:function(a,c){function v(a,c,l,n){if(r!==2){r=2,p&&clearTimeout(p),o=b,m=n||"",u.readyState=a?4:0;var q,t,v,w=l?bR(e,u,l):b,x,y;if(a>=200&&a<300||a===304){if(e.ifModified){if(x=u.getResponseHeader("Last-Modified"))d.lastModified[k]=x;if(y=u.getResponseHeader("Etag"))d.etag[k]=y}if(a===304)c="notmodified",q=!0;else try{t=bS(e,w),c="success",q=!0}catch(z){c="parsererror",v=z}}else{v=c;if(!c||a)c="error",a<0&&(a=0)}u.status=a,u.statusText=c,q?h.resolveWith(f,[t,c,u]):h.rejectWith(f,[u,c,v]),u.statusCode(j),j=b,s&&g.trigger("ajax"+(q?"Success":"Error"),[u,e,q?t:v]),i.resolveWith(f,[u,c]),s&&(g.trigger("ajaxComplete",[u,e]),--d.active||d.event.trigger("ajaxStop"))}}typeof a==="object"&&(c=a,a=b),c=c||{};var e=d.ajaxSetup({},c),f=e.context||e,g=f!==e&&(f.nodeType||f instanceof d)?d(f):d.event,h=d.Deferred(),i=d._Deferred(),j=e.statusCode||{},k,l={},m,n,o,p,q,r=0,s,t,u={readyState:0,setRequestHeader:function(a,b){r||(l[a.toLowerCase().replace(bF,bG)]=b);return this},getAllResponseHeaders:function(){return r===2?m:null},getResponseHeader:function(a){var c;if(r===2){if(!n){n={};while(c=bv.exec(m))n[c[1].toLowerCase()]=c[2]}c=n[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){r||(e.mimeType=a);return this},abort:function(a){a=a||"abort",o&&o.abort(a),v(0,a);return this}};h.promise(u),u.success=u.done,u.error=u.fail,u.complete=i.done,u.statusCode=function(a){if(a){var b;if(r<2)for(b in a)j[b]=[j[b],a[b]];else b=a[u.status],u.then(b,b)}return this},e.url=((a||e.url)+"").replace(bu,"").replace(bz,bM[1]+"//"),e.dataTypes=d.trim(e.dataType||"*").toLowerCase().split(bD),e.crossDomain==null&&(q=bH.exec(e.url.toLowerCase()),e.crossDomain=q&&(q[1]!=bM[1]||q[2]!=bM[2]||(q[3]||(q[1]==="http:"?80:443))!=(bM[3]||(bM[1]==="http:"?80:443)))),e.data&&e.processData&&typeof e.data!=="string"&&(e.data=d.param(e.data,e.traditional)),bP(bJ,e,c,u);if(r===2)return!1;s=e.global,e.type=e.type.toUpperCase(),e.hasContent=!by.test(e.type),s&&d.active++===0&&d.event.trigger("ajaxStart");if(!e.hasContent){e.data&&(e.url+=(bA.test(e.url)?"&":"?")+e.data),k=e.url;if(e.cache===!1){var w=d.now(),x=e.url.replace(bE,"$1_="+w);e.url=x+(x===e.url?(bA.test(e.url)?"&":"?")+"_="+w:"")}}if(e.data&&e.hasContent&&e.contentType!==!1||c.contentType)l["Content-Type"]=e.contentType;e.ifModified&&(k=k||e.url,d.lastModified[k]&&(l["If-Modified-Since"]=d.lastModified[k]),d.etag[k]&&(l["If-None-Match"]=d.etag[k])),l.Accept=e.dataTypes[0]&&e.accepts[e.dataTypes[0]]?e.accepts[e.dataTypes[0]]+(e.dataTypes[0]!=="*"?", */*; q=0.01":""):e.accepts["*"];for(t in e.headers)u.setRequestHeader(t,e.headers[t]);if(e.beforeSend&&(e.beforeSend.call(f,u,e)===!1||r===2)){u.abort();return!1}for(t in {success:1,error:1,complete:1})u[t](e[t]);o=bP(bK,e,c,u);if(o){u.readyState=1,s&&g.trigger("ajaxSend",[u,e]),e.async&&e.timeout>0&&(p=setTimeout(function(){u.abort("timeout")},e.timeout));try{r=1,o.send(l,v)}catch(y){status<2?v(-1,y):d.error(y)}}else v(-1,"No Transport");return u},param:function(a,c){var e=[],f=function(a,b){b=d.isFunction(b)?b():b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=d.ajaxSettings.traditional);if(d.isArray(a)||a.jquery&&!d.isPlainObject(a))d.each(a,function(){f(this.name,this.value)});else for(var g in a)bQ(g,a[g],c,f);return e.join("&").replace(br,"+")}}),d.extend({active:0,lastModified:{},etag:{}});var bT=d.now(),bU=/(\=)\?(&|$)|\?\?/i;d.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return d.expando+"_"+bT++}}),d.ajaxPrefilter("json jsonp",function(b,c,e){var f=typeof b.data==="string";if(b.dataTypes[0]==="jsonp"||c.jsonpCallback||c.jsonp!=null||b.jsonp!==!1&&(bU.test(b.url)||f&&bU.test(b.data))){var g,h=b.jsonpCallback=d.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2",m=function(){a[h]=i,g&&d.isFunction(i)&&a[h](g[0])};b.jsonp!==!1&&(j=j.replace(bU,l),b.url===j&&(f&&(k=k.replace(bU,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},e.then(m,m),b.converters["script json"]=function(){g||d.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),d.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){d.globalEval(a);return a}}}),d.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),d.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var bV=d.now(),bW,bX;d.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&bZ()||b$()}:bZ,bX=d.ajaxSettings.xhr(),d.support.ajax=!!bX,d.support.cors=bX&&"withCredentials"in bX,bX=b,d.support.ajax&&d.ajaxTransport(function(a){if(!a.crossDomain||d.support.cors){var c;return{send:function(e,f){var g=a.xhr(),h,i;a.username?g.open(a.type,a.url,a.async,a.username,a.password):g.open(a.type,a.url,a.async);if(a.xhrFields)for(i in a.xhrFields)g[i]=a.xhrFields[i];a.mimeType&&g.overrideMimeType&&g.overrideMimeType(a.mimeType),!a.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(i in e)g.setRequestHeader(i,e[i])}catch(j){}g.send(a.hasContent&&a.data||null),c=function(e,i){var j,k,l,m,n;try{if(c&&(i||g.readyState===4)){c=b,h&&(g.onreadystatechange=d.noop,delete bW[h]);if(i)g.readyState!==4&&g.abort();else{j=g.status,l=g.getAllResponseHeaders(),m={},n=g.responseXML,n&&n.documentElement&&(m.xml=n),m.text=g.responseText;try{k=g.statusText}catch(o){k=""}j||!a.isLocal||a.crossDomain?j===1223&&(j=204):j=m.text?200:404}}}catch(p){i||f(-1,p)}m&&f(j,k,m,l)},a.async&&g.readyState!==4?(bW||(bW={},bY()),h=bV++,g.onreadystatechange=bW[h]=c):c()},abort:function(){c&&c(0,1)}}}});var b_={},ca=/^(?:toggle|show|hide)$/,cb=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cc,cd=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];d.fn.extend({show:function(a,b,c){var e,f;if(a||a===0)return this.animate(ce("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)e=this[g],f=e.style.display,!d._data(e,"olddisplay")&&f==="none"&&(f=e.style.display=""),f===""&&d.css(e,"display")==="none"&&d._data(e,"olddisplay",cf(e.nodeName));for(g=0;g<h;g++){e=this[g],f=e.style.display;if(f===""||f==="none")e.style.display=d._data(e,"olddisplay")||""}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ce("hide",3),a,b,c);for(var e=0,f=this.length;e<f;e++){var g=d.css(this[e],"display");g!=="none"&&!d._data(this[e],"olddisplay")&&d._data(this[e],"olddisplay",g)}for(e=0;e<f;e++)this[e].style.display="none";return this},_toggle:d.fn.toggle,toggle:function(a,b,c){var e=typeof a==="boolean";d.isFunction(a)&&d.isFunction(b)?this._toggle.apply(this,arguments):a==null||e?this.each(function(){var b=e?a:d(this).is(":hidden");d(this)[b?"show":"hide"]()}):this.animate(ce("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,e){var f=d.speed(b,c,e);if(d.isEmptyObject(a))return this.each(f.complete);return this[f.queue===!1?"each":"queue"](function(){var b=d.extend({},f),c,e=this.nodeType===1,g=e&&d(this).is(":hidden"),h=this;for(c in a){var i=d.camelCase(c);c!==i&&(a[i]=a[c],delete a[c],c=i);if(a[c]==="hide"&&g||a[c]==="show"&&!g)return b.complete.call(this);if(e&&(c==="height"||c==="width")){b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY];if(d.css(this,"display")==="inline"&&d.css(this,"float")==="none")if(d.support.inlineBlockNeedsLayout){var j=cf(this.nodeName);j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)}else this.style.display="inline-block"}d.isArray(a[c])&&((b.specialEasing=b.specialEasing||{})[c]=a[c][1],a[c]=a[c][0])}b.overflow!=null&&(this.style.overflow="hidden"),b.curAnim=d.extend({},a),d.each(a,function(c,e){var f=new d.fx(h,b,c);if(ca.test(e))f[e==="toggle"?g?"show":"hide":e](a);else{var i=cb.exec(e),j=f.cur();if(i){var k=parseFloat(i[2]),l=i[3]||(d.cssNumber[c]?"":"px");l!=="px"&&(d.style(h,c,(k||1)+l),j=(k||1)/f.cur()*j,d.style(h,c,j+l)),i[1]&&(k=(i[1]==="-="?-1:1)*k+j),f.custom(j,k,l)}else f.custom(j,e,"")}});return!0})},stop:function(a,b){var c=d.timers;a&&this.queue([]),this.each(function(){for(var a=c.length-1;a>=0;a--)c[a].elem===this&&(b&&c[a](!0),c.splice(a,1))}),b||this.dequeue();return this}}),d.each({slideDown:ce("show",1),slideUp:ce("hide",1),slideToggle:ce("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){d.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),d.extend({speed:function(a,b,c){var e=a&&typeof a==="object"?d.extend({},a):{complete:c||!c&&b||d.isFunction(a)&&a,duration:a,easing:c&&b||b&&!d.isFunction(b)&&b};e.duration=d.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in d.fx.speeds?d.fx.speeds[e.duration]:d.fx.speeds._default,e.old=e.complete,e.complete=function(){e.queue!==!1&&d(this).dequeue(),d.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig||(b.orig={})}}),d.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(d.fx.step[this.prop]||d.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=d.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function g(a){return e.step(a)}var e=this,f=d.fx;this.startTime=d.now(),this.start=a,this.end=b,this.unit=c||this.unit||(d.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,g.elem=this.elem,g()&&d.timers.push(g)&&!cc&&(cc=setInterval(f.tick,f.interval))},show:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),d(this.elem).show()},hide:function(){this.options.orig[this.prop]=d.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=d.now(),c=!0;if(a||b>=this.options.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),this.options.curAnim[this.prop]=!0;for(var e in this.options.curAnim)this.options.curAnim[e]!==!0&&(c=!1);if(c){if(this.options.overflow!=null&&!d.support.shrinkWrapBlocks){var f=this.elem,g=this.options;d.each(["","X","Y"],function(a,b){f.style["overflow"+b]=g.overflow[a]})}this.options.hide&&d(this.elem).hide();if(this.options.hide||this.options.show)for(var h in this.options.curAnim)d.style(this.elem,h,this.options.orig[h]);this.options.complete.call(this.elem)}return!1}var i=b-this.startTime;this.state=i/this.options.duration;var j=this.options.specialEasing&&this.options.specialEasing[this.prop],k=this.options.easing||(d.easing.swing?"swing":"linear");this.pos=d.easing[j||k](this.state,i,0,1,this.options.duration),this.now=this.start+(this.end-this.start)*this.pos,this.update();return!0}},d.extend(d.fx,{tick:function(){var a=d.timers;for(var b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||d.fx.stop()},interval:13,stop:function(){clearInterval(cc),cc=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){d.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),d.expr&&d.expr.filters&&(d.expr.filters.animated=function(a){return d.grep(d.timers,function(b){return a===b.elem}).length});var cg=/^t(?:able|d|h)$/i,ch=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?d.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(e){}var f=b.ownerDocument,g=f.documentElement;if(!c||!d.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=f.body,i=ci(f),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||d.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||d.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:d.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){d.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return d.offset.bodyOffset(b);d.offset.initialize();var c,e=b.offsetParent,f=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(d.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===e&&(l+=b.offsetTop,m+=b.offsetLeft,d.offset.doesNotAddBorder&&(!d.offset.doesAddBorderForTableAndCells||!cg.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),f=e,e=b.offsetParent),d.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;d.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},d.offset={initialize:function(){var a=c.body,b=c.createElement("div"),e,f,g,h,i=parseFloat(d.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";d.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),e=b.firstChild,f=e.firstChild,h=e.nextSibling.firstChild.firstChild,this.doesNotAddBorder=f.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,f.style.position="fixed",f.style.top="20px",this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15,f.style.position=f.style.top="",e.style.overflow="hidden",e.style.position="relative",this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),d.offset.initialize=d.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;d.offset.initialize(),d.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(d.css(a,"marginTop"))||0,c+=parseFloat(d.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var e=d.css(a,"position");e==="static"&&(a.style.position="relative");var f=d(a),g=f.offset(),h=d.css(a,"top"),i=d.css(a,"left"),j=(e==="absolute"||e==="fixed")&&d.inArray("auto",[h,i])>-1,k={},l={},m,n;j&&(l=f.position()),m=j?l.top:parseInt(h,10)||0,n=j?l.left:parseInt(i,10)||0,d.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):f.css(k)}},d.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),e=ch.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(d.css(a,"marginTop"))||0,c.left-=parseFloat(d.css(a,"marginLeft"))||0,e.top+=parseFloat(d.css(b[0],"borderTopWidth"))||0,e.left+=parseFloat(d.css(b[0],"borderLeftWidth"))||0;return{top:c.top-e.top,left:c.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&(!ch.test(a.nodeName)&&d.css(a,"position")==="static"))a=a.offsetParent;return a})}}),d.each(["Left","Top"],function(a,c){var e="scroll"+c;d.fn[e]=function(c){var f=this[0],g;if(!f)return null;if(c!==b)return this.each(function(){g=ci(this),g?g.scrollTo(a?d(g).scrollLeft():c,a?c:d(g).scrollTop()):this[e]=c});g=ci(f);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:d.support.boxModel&&g.document.documentElement[e]||g.document.body[e]:f[e]}}),d.each(["Height","Width"],function(a,c){var e=c.toLowerCase();d.fn["inner"+c]=function(){return this[0]?parseFloat(d.css(this[0],e,"padding")):null},d.fn["outer"+c]=function(a){return this[0]?parseFloat(d.css(this[0],e,a?"margin":"border")):null},d.fn[e]=function(a){var f=this[0];if(!f)return a==null?null:this;if(d.isFunction(a))return this.each(function(b){var c=d(this);c[e](a.call(this,b,c[e]()))});if(d.isWindow(f)){var g=f.document.documentElement["client"+c];return f.document.compatMode==="CSS1Compat"&&g||f.document.body["client"+c]||g}if(f.nodeType===9)return Math.max(f.documentElement["client"+c],f.body["scroll"+c],f.documentElement["scroll"+c],f.body["offset"+c],f.documentElement["offset"+c]);if(a===b){var h=d.css(f,e),i=parseFloat(h);return d.isNaN(i)?h:i}return this.css(e,typeof a==="string"?a:a+"px")}}),a.jQuery=a.$=d})(window);
\ No newline at end of file
diff --git a/y2022/image_streamer/www_defaults/_seasocks.css b/y2022/image_streamer/www_defaults/_seasocks.css
new file mode 100644
index 0000000..03a7287
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_seasocks.css
@@ -0,0 +1,22 @@
+body {
+    font-family: segoe ui, tahoma, arial, sans-serif;
+    font-size: 12px;
+    color: #ffffff;
+    background-color: #333333;
+    margin: 0;
+}
+
+a {
+    color: #ffff00;
+}
+
+table {
+    border-collapse: collapse;
+    width: 100%;
+    text-align: center;
+}
+
+.template {
+    display: none;
+}
+
diff --git a/y2022/image_streamer/www_defaults/_stats.html b/y2022/image_streamer/www_defaults/_stats.html
new file mode 100644
index 0000000..d34e932
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/_stats.html
@@ -0,0 +1,60 @@
+<html DOCTYPE=html>
+<head>
+  <title>SeaSocks Stats</title>
+  <link href="/_seasocks.css" rel="stylesheet">
+  <script src="/_jquery.min.js" type="text/javascript"></script>
+  <script>
+  function clear() {
+    $('#cx tbody tr:visible').remove();
+  }
+  function connection(stats) {
+  	c = $('#cx .template').clone().removeClass('template').appendTo('#cx');
+  	for (stat in stats) {
+  	  c.find('.' + stat).text(stats[stat]);
+    }
+  }
+  function refresh() {
+  		var stats = new XMLHttpRequest();
+		stats.open("GET", "/_livestats.js", false);
+		stats.send(null);
+		eval(stats.responseText);
+  }
+  $(function() {
+  	setInterval(refresh, 1000);
+  	refresh();		
+  });
+  </script>
+</head>
+<body><h1>SeaSocks Stats</h1></body>
+
+<h2>Connections</h2>
+<table id="cx">
+  <thead>
+    <tr>
+      <th>Connection time</th>
+      <th>Fd</th>
+      <th>Addr</th>
+      <th>URI</th>
+      <th>Username</th>
+      <th>Pending read</th>
+      <th>Bytes read</th>
+      <th>Pending send</th>
+      <th>Bytes sent</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr class="template">
+      <td class="since"></td>
+      <td class="fd"></td>
+      <td class="addr"></td>
+      <td class="uri"></td>
+      <td class="user"></td>
+      <td class="input"></td>
+      <td class="read"></td>
+      <td class="output"></td>
+      <td class="written"></td>
+    </tr>
+  </tbody>
+</table>
+
+</body></html>
diff --git a/y2022/image_streamer/www_defaults/favicon.ico b/y2022/image_streamer/www_defaults/favicon.ico
new file mode 100644
index 0000000..30a95b9
--- /dev/null
+++ b/y2022/image_streamer/www_defaults/favicon.ico
Binary files differ
diff --git a/y2022/joystick_reader.cc b/y2022/joystick_reader.cc
index 653e1c1..baaa2ee 100644
--- a/y2022/joystick_reader.cc
+++ b/y2022/joystick_reader.cc
@@ -45,7 +45,8 @@
 
 const ButtonLocation kIntakeFrontOut(4, 10);
 const ButtonLocation kIntakeBackOut(4, 9);
-const ButtonLocation kSpit(3, 3);
+const ButtonLocation kSpitFront(3, 3);
+const ButtonLocation kSpitBack(2, 3);
 
 const ButtonLocation kRedLocalizerReset(3, 13);
 const ButtonLocation kBlueLocalizerReset(3, 14);
@@ -55,14 +56,15 @@
 const ButtonLocation kCatapultPos(4, 3);
 const ButtonLocation kFire(4, 1);
 const ButtonLocation kTurret(4, 15);
-const ButtonLocation kAutoAim(4, 2);
+const ButtonLocation kAutoAim(4, 16);
 
 const ButtonLocation kClimberExtend(4, 6);
 const ButtonLocation kClimberIntakes(4, 5);
 
 const ButtonLocation kIntakeFrontOut(4, 10);
 const ButtonLocation kIntakeBackOut(4, 9);
-const ButtonLocation kSpit(3, 3);
+const ButtonLocation kSpitFront(3, 3);
+const ButtonLocation kSpitBack(3, 1);
 
 const ButtonLocation kRedLocalizerReset(4, 14);
 const ButtonLocation kBlueLocalizerReset(4, 13);
@@ -165,15 +167,14 @@
     constexpr double kIntakeUpPosition = 1.47;
     double intake_front_pos = kIntakeUpPosition;
     double intake_back_pos = kIntakeUpPosition;
-    double transfer_roller_front_speed = 0.0;
-    double transfer_roller_back_speed = 0.0;
+    double transfer_roller_speed = 0.0;
     std::optional<control_loops::superstructure::RequestedIntake>
         requested_intake;
 
     double roller_front_speed = 0.0;
     double roller_back_speed = 0.0;
 
-    std::optional<double> turret_pos = 0.0;
+    std::optional<double> turret_pos = std::nullopt;
 
     double climber_position = 0.01;
 
@@ -215,7 +216,7 @@
     // Keep the catapult return position at the shot one if kCatapultPos is
     // pressed
     if (data.IsPressed(kCatapultPos)) {
-      catapult_return_pos = 0.3;
+      catapult_return_pos = 0.7;
     } else {
       catapult_return_pos = -0.908;
     }
@@ -225,24 +226,28 @@
     constexpr double kIntakePosition = -0.02;
     constexpr size_t kIntakeCounterIterations = 25;
 
-    // Extend the intakes and spin the rollers
-    if (data.IsPressed(kIntakeFrontOut)) {
+    // Extend the intakes and spin the rollers.
+    // Don't let this happen if there is a ball in the other intake, because
+    // that would spit this one out.
+    if (data.IsPressed(kIntakeFrontOut) &&
+        !superstructure_status_fetcher_->back_intake_has_ball()) {
       intake_front_pos = kIntakePosition;
-      transfer_roller_front_speed = kTransferRollerSpeed;
+      transfer_roller_speed = kTransferRollerSpeed;
 
       intake_front_counter_ = kIntakeCounterIterations;
       intake_back_counter_ = 0;
-    } else if (data.IsPressed(kIntakeBackOut)) {
+    } else if (data.IsPressed(kIntakeBackOut) &&
+               !superstructure_status_fetcher_->front_intake_has_ball()) {
       intake_back_pos = kIntakePosition;
-      transfer_roller_back_speed = kTransferRollerSpeed;
+      transfer_roller_speed = -kTransferRollerSpeed;
 
       intake_back_counter_ = kIntakeCounterIterations;
       intake_front_counter_ = 0;
-    } else if (data.IsPressed(kSpit)) {
-      transfer_roller_front_speed = -kTransferRollerSpeed;
-      transfer_roller_back_speed = -kTransferRollerSpeed;
-
+    } else if (data.IsPressed(kSpitFront)) {
+      transfer_roller_speed = -kTransferRollerSpeed;
       intake_front_counter_ = 0;
+    } else if (data.IsPressed(kSpitBack)) {
+      transfer_roller_speed = kTransferRollerSpeed;
       intake_back_counter_ = 0;
     }
 
@@ -261,7 +266,6 @@
     if (data.IsPressed(kFire)) {
       fire = true;
       // Provide a default turret goal.
-      turret_pos = 0.0;
     }
 
     if (data.IsPressed(kClimberIntakes)) {
@@ -324,10 +328,8 @@
 
       superstructure_goal_builder.add_roller_speed_front(roller_front_speed);
       superstructure_goal_builder.add_roller_speed_back(roller_back_speed);
-      superstructure_goal_builder.add_transfer_roller_speed_front(
-          transfer_roller_front_speed);
-      superstructure_goal_builder.add_transfer_roller_speed_back(
-          transfer_roller_back_speed);
+      superstructure_goal_builder.add_transfer_roller_speed(
+          transfer_roller_speed);
       superstructure_goal_builder.add_auto_aim(data.IsPressed(kAutoAim));
       if (requested_intake.has_value()) {
         superstructure_goal_builder.add_turret_intake(requested_intake.value());
diff --git a/y2022/localizer/localizer.cc b/y2022/localizer/localizer.cc
index 24df19b..954bffc 100644
--- a/y2022/localizer/localizer.cc
+++ b/y2022/localizer/localizer.cc
@@ -258,16 +258,15 @@
   state->model_state += K * (Z - H * state->model_state);
 }
 
-void ModelBasedLocalizer::HandleImu(aos::monotonic_clock::time_point t,
-                                    const Eigen::Vector3d &gyro,
-                                    const Eigen::Vector3d &accel,
-                                    const Eigen::Vector2d encoders,
-                                    const Eigen::Vector2d voltage) {
+void ModelBasedLocalizer::HandleImu(
+    aos::monotonic_clock::time_point t, const Eigen::Vector3d &gyro,
+    const Eigen::Vector3d &accel, const std::optional<Eigen::Vector2d> encoders,
+    const Eigen::Vector2d voltage) {
   VLOG(2) << t;
   if (t_ == aos::monotonic_clock::min_time) {
     t_ = t;
   }
-  if (t_ + 2 * kNominalDt < t) {
+  if (t_ + 10 * kNominalDt < t) {
     t_ = t;
     ++clock_resets_;
   }
@@ -323,13 +322,17 @@
     R.diagonal() << 1e-9, 1e-9, 1e-13;
   }
 
-  const Eigen::Matrix<double, kNModelOutputs, 1> Z(encoders(0), encoders(1),
-                                                   yaw_rate);
+  const Eigen::Matrix<double, kNModelOutputs, 1> Z =
+      encoders.has_value()
+          ? Eigen::Vector3d(encoders.value()(0), encoders.value()(1), yaw_rate)
+          : Eigen::Vector3d(current_state_.model_state(kLeftEncoder),
+                            current_state_.model_state(kRightEncoder),
+                            yaw_rate);
 
   if (branches_.empty()) {
     VLOG(2) << "Initializing";
-    current_state_.model_state(kLeftEncoder) = encoders(0);
-    current_state_.model_state(kRightEncoder) = encoders(1);
+    current_state_.model_state(kLeftEncoder) = Z(0);
+    current_state_.model_state(kRightEncoder) = Z(1);
     current_state_.branch_time = t;
     branches_.Push(current_state_);
   }
@@ -389,7 +392,7 @@
       current_state_.accel_state = branches_[0].accel_state;
       current_state_.model_state = branches_[0].model_state;
       current_state_.model_state = ModelStateForAccelState(
-          current_state_.accel_state, encoders, yaw_rate);
+          current_state_.accel_state, Z.topRows<2>(), yaw_rate);
     } else {
       VLOG(2) << "Normal branching";
       current_state_.accel_state =
@@ -407,14 +410,15 @@
       using_model_ = true;
       // Grab the model-based state from back when we stopped diverging.
       current_state_.model_state.topRows<kShareStates>() =
-          ModelStateForAccelState(branches_[0].accel_state, encoders, yaw_rate)
+          ModelStateForAccelState(branches_[0].accel_state, Z.topRows<2>(),
+                                  yaw_rate)
               .topRows<kShareStates>();
       current_state_.accel_state =
           AccelStateForModelState(current_state_.model_state);
     } else {
       // TODO(james): Why was I leaving the encoders/wheel velocities in place?
       current_state_.model_state = ModelStateForAccelState(
-          current_state_.accel_state, encoders, yaw_rate);
+          current_state_.accel_state, Z.topRows<2>(), yaw_rate);
       current_state_.branch_time = t;
     }
   }
@@ -449,7 +453,7 @@
   VLOG(2) << "Input acce " << accel.transpose();
   VLOG(2) << "Input gyro " << gyro.transpose();
   VLOG(2) << "Input voltage " << voltage.transpose();
-  VLOG(2) << "Input encoder " << encoders.transpose();
+  VLOG(2) << "Input encoder " << Z.topRows<2>().transpose();
   VLOG(2) << "yaw rate " << yaw_rate;
 
   CHECK(std::isfinite(last_residual_));
@@ -638,7 +642,7 @@
   H_model(1, kY) = 1.0;
   H_accel(0, kX) = 1.0;
   H_accel(1, kY) = 1.0;
-  R.diagonal() << 1e-2, 1e-2;
+  R.diagonal() << 1e-0, 1e-0;
 
   const Eigen::Matrix<double, kNModelStates, 2> K_model =
       P_model_ * H_model.transpose() *
@@ -966,12 +970,51 @@
         output_fetcher_.Fetch();
         for (const IMUValues *value : *values.readings()) {
           zeroer_.InsertAndProcessMeasurement(*value);
-          const Eigen::Vector2d encoders{
-              left_encoder_.Unwrap(value->left_encoder()),
-              right_encoder_.Unwrap(value->right_encoder())};
+          if (zeroer_.Faulted()) {
+            if (value->checksum_failed()) {
+              imu_fault_tracker_.pico_to_pi_checksum_mismatch++;
+            } else if (value->previous_reading_diag_stat()->checksum_mismatch()) {
+              imu_fault_tracker_.imu_to_pico_checksum_mismatch++;
+            } else {
+              imu_fault_tracker_.other_zeroing_faults++;
+            }
+          } else {
+            if (!first_valid_data_counter_.has_value()) {
+              first_valid_data_counter_ = value->data_counter();
+            }
+          }
+          if (first_valid_data_counter_.has_value()) {
+            total_imu_messages_received_++;
+            // Only update when we have good checksums, since the data counter
+            // could get corrupted.
+            if (!zeroer_.Faulted()) {
+              if (value->data_counter() < last_data_counter_) {
+                data_counter_offset_ += 1 << 16;
+              }
+              imu_fault_tracker_.missed_messages =
+                  (1 + value->data_counter() + data_counter_offset_ -
+                   first_valid_data_counter_.value()) -
+                  total_imu_messages_received_;
+              last_data_counter_ = value->data_counter();
+            }
+          }
+          const std::optional<Eigen::Vector2d> encoders =
+              zeroer_.Faulted()
+                  ? std::nullopt
+                  : std::make_optional(Eigen::Vector2d{
+                        left_encoder_.Unwrap(value->left_encoder()),
+                        right_encoder_.Unwrap(value->right_encoder())});
           {
-            const aos::monotonic_clock::time_point pico_timestamp{
-                std::chrono::microseconds(value->pico_timestamp_us())};
+            // If we can't trust the imu reading, just naively increment the
+            // pico timestamp.
+            const aos::monotonic_clock::time_point pico_timestamp =
+                zeroer_.Faulted()
+                    ? (last_pico_timestamp_.has_value()
+                           ? last_pico_timestamp_.value() + kNominalDt
+                           : aos::monotonic_clock::epoch())
+                    : aos::monotonic_clock::time_point(
+                          std::chrono::microseconds(
+                              value->pico_timestamp_us()));
             // TODO(james): If we get large enough drift off of the pico,
             // actually do something about it.
             if (!pico_offset_.has_value()) {
@@ -992,9 +1035,11 @@
                      std::chrono::milliseconds(10) <
                  event_loop_->context().monotonic_event_time);
             const bool zeroed = zeroer_.Zeroed();
+            // For gyros, use the most recent gyro reading if we aren't zeroed,
+            // to avoid missing integration cycles.
             model_based_.HandleImu(
                 sample_timestamp,
-                zeroed ? zeroer_.ZeroedGyro().value() : Eigen::Vector3d::Zero(),
+                zeroed ? zeroer_.ZeroedGyro().value() : last_gyro_,
                 zeroed ? zeroer_.ZeroedAccel().value()
                        : dt_config_.imu_transform.transpose() *
                              Eigen::Vector3d::UnitZ(),
@@ -1003,6 +1048,10 @@
                          : Eigen::Vector2d{output_fetcher_->left_voltage(),
                                            output_fetcher_->right_voltage()});
             last_pico_timestamp_ = pico_timestamp;
+
+            if (zeroed) {
+              last_gyro_ = zeroer_.ZeroedGyro().value();
+            }
           }
           {
             auto builder = status_sender_.MakeBuilder();
@@ -1010,14 +1059,19 @@
                 model_based_.PopulateStatus(builder.fbb());
             const flatbuffers::Offset<control_loops::drivetrain::ImuZeroerState>
                 zeroer_status = zeroer_.PopulateStatus(builder.fbb());
+            const flatbuffers::Offset<ImuFailures> imu_failures =
+                ImuFailures::Pack(*builder.fbb(), &imu_fault_tracker_);
             LocalizerStatus::Builder status_builder =
                 builder.MakeBuilder<LocalizerStatus>();
             status_builder.add_model_based(model_based_status);
             status_builder.add_zeroed(zeroer_.Zeroed());
             status_builder.add_faulted_zero(zeroer_.Faulted());
             status_builder.add_zeroing(zeroer_status);
-            status_builder.add_left_encoder(encoders(0));
-            status_builder.add_right_encoder(encoders(1));
+            status_builder.add_imu_failures(imu_failures);
+            if (encoders.has_value()) {
+              status_builder.add_left_encoder(encoders.value()(0));
+              status_builder.add_right_encoder(encoders.value()(1));
+            }
             if (pico_offset_.has_value()) {
               status_builder.add_pico_offset_ns(pico_offset_.value().count());
               status_builder.add_pico_offset_error_ns(
diff --git a/y2022/localizer/localizer.h b/y2022/localizer/localizer.h
index be14b45..f8205d7 100644
--- a/y2022/localizer/localizer.h
+++ b/y2022/localizer/localizer.h
@@ -110,7 +110,8 @@
       const control_loops::drivetrain::DrivetrainConfig<double> &dt_config);
   void HandleImu(aos::monotonic_clock::time_point t,
                  const Eigen::Vector3d &gyro, const Eigen::Vector3d &accel,
-                 const Eigen::Vector2d encoders, const Eigen::Vector2d voltage);
+                 const std::optional<Eigen::Vector2d> encoders,
+                 const Eigen::Vector2d voltage);
   void HandleTurret(aos::monotonic_clock::time_point sample_time,
                     double turret_position, double turret_velocity);
   void HandleImageMatch(aos::monotonic_clock::time_point sample_time,
@@ -344,6 +345,14 @@
   // Note that this can drift over sufficiently long time periods!
   std::optional<std::chrono::nanoseconds> pico_offset_;
 
+  ImuFailuresT imu_fault_tracker_;
+  std::optional<size_t> first_valid_data_counter_;
+  size_t total_imu_messages_received_ = 0;
+  size_t data_counter_offset_ = 0;
+  int last_data_counter_ = 0;
+
+  Eigen::Vector3d last_gyro_ = Eigen::Vector3d::Zero();
+
   zeroing::UnwrapSensor left_encoder_;
   zeroing::UnwrapSensor right_encoder_;
 };
diff --git a/y2022/localizer/localizer_plotter.ts b/y2022/localizer/localizer_plotter.ts
index 239d2cb..bcd8894 100644
--- a/y2022/localizer/localizer_plotter.ts
+++ b/y2022/localizer/localizer_plotter.ts
@@ -268,10 +268,34 @@
 
   const timingPlot =
       aosPlotter.addPlot(element, [DEFAULT_WIDTH, DEFAULT_HEIGHT]);
-  timingPlot.plot.getAxisLabels().setTitle('Timing');
+  timingPlot.plot.getAxisLabels().setTitle('Fault Counting');
   timingPlot.plot.getAxisLabels().setXLabel(TIME);
 
   timingPlot.addMessageLine(localizer, ['model_based', 'clock_resets'])
       .setColor(GREEN)
       .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu_failures', 'imu_to_pico_checksum_mismatch'])
+      .setColor(BLUE)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu_failures', 'pico_to_pi_checksum_mismatch'])
+      .setColor(RED)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu_failures', 'other_zeroing_faults'])
+      .setColor(CYAN)
+      .setDrawLine(false);
+
+  timingPlot
+      .addMessageLine(
+          localizer, ['imu_failures', 'missed_messages'])
+      .setColor(PINK)
+      .setDrawLine(false);
 }
diff --git a/y2022/localizer/localizer_replay.cc b/y2022/localizer/localizer_replay.cc
index d328948..08479eb 100644
--- a/y2022/localizer/localizer_replay.cc
+++ b/y2022/localizer/localizer_replay.cc
@@ -59,6 +59,13 @@
   // open logfiles
   aos::logger::LogReader reader(logfiles, &config.message());
 
+  reader.RemapLoggedChannel("/localizer",
+                            "frc971.controls.LocalizerStatus");
+  reader.RemapLoggedChannel("/localizer",
+                            "frc971.controls.LocalizerOutput");
+  reader.RemapLoggedChannel("/localizer",
+                            "frc971.controls.LocalizerVisualization");
+
   auto factory =
       std::make_unique<aos::SimulatedEventLoopFactory>(reader.configuration());
 
diff --git a/y2022/localizer/localizer_status.fbs b/y2022/localizer/localizer_status.fbs
index ae96b63..05cfc73 100644
--- a/y2022/localizer/localizer_status.fbs
+++ b/y2022/localizer/localizer_status.fbs
@@ -92,6 +92,13 @@
   statistics:CumulativeStatistics (id: 18);
 }
 
+table ImuFailures {
+  imu_to_pico_checksum_mismatch:uint (id: 0);
+  pico_to_pi_checksum_mismatch:uint (id: 1);
+  missed_messages:uint (id: 2);
+  other_zeroing_faults:uint (id: 3);
+}
+
 table LocalizerStatus {
   model_based:ModelBasedStatus (id: 0);
   // Whether the IMU is zeroed or not.
@@ -110,6 +117,7 @@
   pico_offset_error_ns:int64 (id: 5);
   left_encoder:double (id: 6);
   right_encoder:double (id: 7);
+  imu_failures:ImuFailures (id: 8);
 }
 
 root_type LocalizerStatus;
diff --git a/y2022/localizer/localizer_test.cc b/y2022/localizer/localizer_test.cc
index 6e9cd1e..ef14972 100644
--- a/y2022/localizer/localizer_test.cc
+++ b/y2022/localizer/localizer_test.cc
@@ -784,7 +784,7 @@
   event_loop_factory_.RunFor(std::chrono::seconds(4));
   CHECK(status_fetcher_.Fetch());
   ASSERT_TRUE(status_fetcher_->model_based()->using_model());
-  EXPECT_TRUE(VerifyEstimatorAccurate(1e-1));
+  EXPECT_TRUE(VerifyEstimatorAccurate(5e-1));
   ASSERT_TRUE(status_fetcher_->model_based()->has_statistics());
   ASSERT_LT(10,
             status_fetcher_->model_based()->statistics()->total_candidates());
@@ -832,7 +832,7 @@
   event_loop_factory_.RunFor(std::chrono::seconds(4));
   CHECK(status_fetcher_.Fetch());
   ASSERT_TRUE(status_fetcher_->model_based()->using_model());
-  EXPECT_TRUE(VerifyEstimatorAccurate(1e-1));
+  EXPECT_TRUE(VerifyEstimatorAccurate(5e-1));
   ASSERT_TRUE(status_fetcher_->model_based()->has_statistics());
   ASSERT_EQ(status_fetcher_->model_based()->statistics()->total_candidates(),
             rejected_count +
diff --git a/y2022/message_bridge_client.sh b/y2022/message_bridge_client.sh
new file mode 100755
index 0000000..c81076a
--- /dev/null
+++ b/y2022/message_bridge_client.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+while true;
+do
+  ping -c 1 pi1 -W 1 && break;
+  sleep 1
+done
+
+echo Pinged
+
+exec /home/admin/bin/message_bridge_client "$@"
diff --git a/y2022/vision/BUILD b/y2022/vision/BUILD
index 389bb48..65ab20c 100644
--- a/y2022/vision/BUILD
+++ b/y2022/vision/BUILD
@@ -149,6 +149,73 @@
     ],
 )
 
+cc_binary(
+    name = "ball_color_detector",
+    srcs = [
+        "ball_color_main.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2022:__subpackages__"],
+    deps = [
+        ":ball_color_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
+
+cc_test(
+    name = "ball_color_test",
+    srcs = [
+        "ball_color_test.cc",
+    ],
+    data = [
+        "test_ball_color_image.jpg",
+    ],
+    deps = [
+        ":ball_color_lib",
+        "//aos:json_to_flatbuffer",
+        "//aos/events:simulated_event_loop",
+        "//aos/testing:googletest",
+        "//aos/testing:test_logging",
+        "//y2022:constants",
+    ],
+)
+
+cc_library(
+    name = "ball_color_lib",
+    srcs = [
+        "ball_color.cc",
+    ],
+    hdrs = [
+        "ball_color.h",
+    ],
+    data = [
+        "//y2022:aos_config",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2022:__subpackages__"],
+    deps = [
+        ":ball_color_fbs",
+        "//aos/events:event_loop",
+        "//aos/events:shm_event_loop",
+        "//aos/network:team_number",
+        "//frc971/input:joystick_state_fbs",
+        "//frc971/vision:vision_fbs",
+        "//third_party:opencv",
+    ],
+)
+
+flatbuffer_cc_library(
+    name = "ball_color_fbs",
+    srcs = ["ball_color.fbs"],
+    gen_reflections = 1,
+    includes = [
+        "//frc971/input:joystick_state_fbs_includes",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    visibility = ["//y2022:__subpackages__"],
+)
+
 cc_library(
     name = "geometry_lib",
     hdrs = [
@@ -273,8 +340,13 @@
         "//aos:init",
         "//aos/events:simulated_event_loop",
         "//aos/events/logging:log_reader",
+        "//frc971/control_loops:control_loops_fbs",
+        "//frc971/control_loops:profiled_subsystem_fbs",
+        "//frc971/control_loops/drivetrain:drivetrain_status_fbs",
+        "//frc971/input:joystick_state_fbs",
         "//frc971/vision:vision_fbs",
         "//third_party:opencv",
+        "//y2022/control_loops/superstructure:superstructure_status_fbs",
     ],
 )
 
diff --git a/y2022/vision/ball_color.cc b/y2022/vision/ball_color.cc
new file mode 100644
index 0000000..e896da5
--- /dev/null
+++ b/y2022/vision/ball_color.cc
@@ -0,0 +1,138 @@
+#include "y2022/vision/ball_color.h"
+
+#include <chrono>
+#include <cmath>
+#include <opencv2/highgui/highgui.hpp>
+#include <thread>
+
+#include "aos/events/event_loop.h"
+#include "aos/events/shm_event_loop.h"
+#include "frc971/input/joystick_state_generated.h"
+#include "frc971/vision/vision_generated.h"
+#include "glog/logging.h"
+#include "opencv2/imgproc.hpp"
+
+namespace y2022 {
+namespace vision {
+
+BallColorDetector::BallColorDetector(aos::EventLoop *event_loop)
+    : ball_color_sender_(event_loop->MakeSender<BallColor>("/superstructure")) {
+  event_loop->MakeWatcher("/camera", [this](const CameraImage &camera_image) {
+    this->ProcessImage(camera_image);
+  });
+}
+
+void BallColorDetector::ProcessImage(const CameraImage &image) {
+  cv::Mat image_color_mat(cv::Size(image.cols(), image.rows()), CV_8UC2,
+                          (void *)image.data()->data());
+  cv::Mat image_mat(cv::Size(image.cols(), image.rows()), CV_8UC3);
+  cv::cvtColor(image_color_mat, image_mat, cv::COLOR_YUV2BGR_YUYV);
+
+  aos::Alliance detected_color = DetectColor(image_mat);
+
+  auto builder = ball_color_sender_.MakeBuilder();
+  auto ball_color_builder = builder.MakeBuilder<BallColor>();
+  ball_color_builder.add_ball_color(detected_color);
+  builder.CheckOk(builder.Send(ball_color_builder.Finish()));
+}
+
+aos::Alliance BallColorDetector::DetectColor(cv::Mat image) {
+  cv::Mat hsv(cv::Size(image.cols, image.rows), CV_8UC3);
+
+  cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
+
+  // Look at 3 chunks of the image
+  cv::Mat reference_red =
+      BallColorDetector::SubImage(hsv, BallColorDetector::kReferenceRed());
+
+  cv::Mat reference_blue =
+      BallColorDetector::SubImage(hsv, BallColorDetector::kReferenceBlue());
+  cv::Mat ball_location =
+      BallColorDetector::SubImage(hsv, BallColorDetector::kBallLocation());
+
+  // OpenCV HSV hues go from [0 to 179]
+  // Average the average color of each patch in both directions
+  // Rejecting pixels that have too low saturation or to bright or dark value
+  // And dealing with the wrapping of the red hues by shifting the wrap to be
+  // around 90 instead of 180. 90 is a color we don't care about.
+  double red = BallColorDetector::mean_hue(reference_red);
+  double blue = BallColorDetector::mean_hue(reference_blue);
+  double ball = BallColorDetector::mean_hue(ball_location);
+
+  // Just look at the hue values for distance
+  const double distance_to_blue = std::abs(ball - blue);
+  const double distance_to_red = std::abs(ball - red);
+
+  VLOG(1) << "\n"
+          << "Red: " << red << " deg\n"
+          << "Blue: " << blue << " deg\n"
+          << "Ball: " << ball << " deg\n"
+          << "distance to blue: " << distance_to_blue << " "
+          << "distance_to_red: " << distance_to_red;
+
+  // Is the ball location close enough to being the same hue as the blue
+  // reference or the red reference?
+
+  if (distance_to_blue < distance_to_red &&
+      distance_to_blue < kMaxHueDistance) {
+    return aos::Alliance::kBlue;
+  } else if (distance_to_red < distance_to_blue &&
+             distance_to_red < kMaxHueDistance) {
+    return aos::Alliance::kRed;
+  }
+
+  return aos::Alliance::kInvalid;
+}
+
+cv::Mat BallColorDetector::SubImage(cv::Mat image, cv::Rect location) {
+  cv::Rect new_location = BallColorDetector::RescaleRect(
+      image, location, BallColorDetector::kMeasurementsImageSize());
+  return image(new_location);
+}
+
+// Handle varying size images by scaling our constants rectangles
+cv::Rect BallColorDetector::RescaleRect(cv::Mat image, cv::Rect location,
+                                        cv::Size original_size) {
+  const double x_scale = static_cast<double>(image.cols) / original_size.width;
+  const double y_scale = static_cast<double>(image.rows) / original_size.height;
+
+  cv::Rect new_location(location.x * x_scale, location.y * y_scale,
+                        location.width * x_scale, location.height * y_scale);
+
+  return new_location;
+}
+
+double BallColorDetector::mean_hue(cv::Mat hsv_image) {
+  double num_pixels_selected = 0;
+  double sum = 0;
+
+  for (int i = 0; i < hsv_image.rows; ++i) {
+    for (int j = 0; j < hsv_image.cols; ++j) {
+      const cv::Vec3b &color = hsv_image.at<cv::Vec3b>(i, j);
+      double value = static_cast<double>(color(2));
+      double saturation = static_cast<double>(color(1));
+
+      if (value < kMinValue || value > kMaxValue ||
+          saturation < kMinSaturation) {
+        continue;
+      }
+
+      // unwrap hue so that break is around 90 instead of 180
+      // ex. a hue of 180 goes to 0, a hue of 120 goes to -60
+      // but there's still a break around 90 where it will be either +- 90
+      // depending on which side it's on
+      double hue = static_cast<double>(color(0));
+      if (hue > 90) {
+        hue = hue - 180;
+      }
+
+      num_pixels_selected++;
+      sum += hue;
+    }
+  }
+
+  return sum / num_pixels_selected;
+}
+
+}  // namespace vision
+}  // namespace y2022
diff --git a/y2022/vision/ball_color.fbs b/y2022/vision/ball_color.fbs
new file mode 100644
index 0000000..7eb93e0
--- /dev/null
+++ b/y2022/vision/ball_color.fbs
@@ -0,0 +1,12 @@
+include "frc971/input/joystick_state.fbs";
+
+namespace y2022.vision;
+
+table BallColor {
+  // The color of the ball represented as which alliance it belongs to
+  // it will be unpredictable when there is no ball and it will be kInvalid
+  // if the color is not close enough to either of the two references.
+  ball_color:aos.Alliance (id: 0);
+}
+
+root_type BallColor;
diff --git a/y2022/vision/ball_color.h b/y2022/vision/ball_color.h
new file mode 100644
index 0000000..ef3bdd2
--- /dev/null
+++ b/y2022/vision/ball_color.h
@@ -0,0 +1,58 @@
+#ifndef Y2022_VISION_BALL_COLOR_H_
+#define Y2022_VISION_BALL_COLOR_H_
+
+#include <opencv2/imgproc.hpp>
+
+#include "aos/events/shm_event_loop.h"
+#include "frc971/input/joystick_state_generated.h"
+#include "frc971/vision/vision_generated.h"
+#include "y2022/vision/ball_color_generated.h"
+
+namespace y2022 {
+namespace vision {
+
+using namespace frc971::vision;
+
+// Takes in camera images and detects what color the loaded ball is
+// Does not detect if there is a ball, and will output bad measurements in
+// the case that that there is not a ball.
+class BallColorDetector {
+ public:
+  // The size image that the reference rectangles were measure with
+  // These constants will be scaled if the image sent is not the same size
+  static const cv::Size kMeasurementsImageSize() { return {640, 480}; };
+  static const cv::Rect kReferenceRed() { return {440, 150, 50, 130}; };
+  static const cv::Rect kReferenceBlue() { return {440, 350, 30, 100}; };
+  static const cv::Rect kBallLocation() { return {100, 400, 140, 50}; };
+
+  // Constants used to filter out pixels that don't have good color information
+  static constexpr double kMinSaturation = 128;
+  static constexpr double kMinValue = 25;
+  static constexpr double kMaxValue = 230;
+
+  static constexpr double kMaxHueDistance = 10;
+
+  BallColorDetector(aos::EventLoop *event_loop);
+
+  void ProcessImage(const CameraImage &camera_image);
+
+  // We look at three parts of the image: two reference locations where there
+  // will be red and blue markers that should match the ball, and then the
+  // location in the catapult where we expect to see the ball. We then compute
+  // the average hue of each patch but discard pixels that we deem not colorful
+  // enough. Then we decide whether the ball color looks close enough to either
+  // of the reference colors. If no good color is detected, outputs kInvalid.
+  static aos::Alliance DetectColor(cv::Mat image);
+
+  static cv::Mat SubImage(cv::Mat image, cv::Rect location);
+
+  static cv::Rect RescaleRect(cv::Mat image, cv::Rect location,
+                              cv::Size original_size);
+  static double mean_hue(cv::Mat hsv_image);
+
+ private:
+  aos::Sender<BallColor> ball_color_sender_;
+};
+}  // namespace vision
+}  // namespace y2022
+#endif
diff --git a/y2022/vision/ball_color_main.cc b/y2022/vision/ball_color_main.cc
new file mode 100644
index 0000000..63f9d06
--- /dev/null
+++ b/y2022/vision/ball_color_main.cc
@@ -0,0 +1,35 @@
+#include "aos/events/shm_event_loop.h"
+#include "aos/init.h"
+#include "y2022/vision/ball_color.h"
+
+// config used to allow running ball_color_detector independently.  E.g.,
+// bazel run //y2022/vision:ball_color_detector -- --config
+// y2022/aos_config.json
+//   --override_hostname pi-7971-1  --ignore_timestamps true
+DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
+
+namespace y2022 {
+namespace vision {
+namespace {
+
+using namespace frc971::vision;
+
+void BallColorDetectorMain() {
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config =
+      aos::configuration::ReadConfig(FLAGS_config);
+
+  aos::ShmEventLoop event_loop(&config.message());
+
+  BallColorDetector ball_color_detector(&event_loop);
+
+  event_loop.Run();
+}
+
+}  // namespace
+}  // namespace vision
+}  // namespace y2022
+
+int main(int argc, char **argv) {
+  aos::InitGoogle(&argc, &argv);
+  y2022::vision::BallColorDetectorMain();
+}
diff --git a/y2022/vision/ball_color_test.cc b/y2022/vision/ball_color_test.cc
new file mode 100644
index 0000000..695791b
--- /dev/null
+++ b/y2022/vision/ball_color_test.cc
@@ -0,0 +1,147 @@
+#include "y2022/vision/ball_color.h"
+
+#include <cmath>
+#include <opencv2/highgui/highgui.hpp>
+#include <opencv2/imgproc.hpp>
+
+#include "aos/events/simulated_event_loop.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/testing/test_logging.h"
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+#include "y2022/constants.h"
+
+DEFINE_string(output_folder, "",
+              "If set, logs all channels to the provided logfile.");
+
+namespace y2022::vision::testing {
+
+class BallColorTest : public ::testing::Test {
+ public:
+  BallColorTest()
+      : config_(aos::configuration::ReadConfig("y2022/aos_config.json")),
+        event_loop_factory_(&config_.message()),
+        logger_pi_(aos::configuration::GetNode(
+            event_loop_factory_.configuration(), "logger")),
+        roborio_(aos::configuration::GetNode(
+            event_loop_factory_.configuration(), "roborio")),
+        camera_event_loop_(
+            event_loop_factory_.MakeEventLoop("Camera", logger_pi_)),
+        color_detector_event_loop_(event_loop_factory_.MakeEventLoop(
+            "Ball color detector", logger_pi_)),
+        superstructure_event_loop_(
+            event_loop_factory_.MakeEventLoop("Superstructure", roborio_)),
+        ball_color_fetcher_(superstructure_event_loop_->MakeFetcher<BallColor>(
+            "/superstructure")),
+        image_sender_(camera_event_loop_->MakeSender<CameraImage>("/camera"))
+
+  {}
+
+  // copied from camera_reader.cc
+  void SendImage(cv::Mat bgr_image) {
+    cv::Mat image_color_mat;
+    cv::cvtColor(bgr_image, image_color_mat, cv::COLOR_BGR2YUV);
+
+    // Convert YUV (3 channels) to YUYV (stacked format)
+    std::vector<uint8_t> yuyv;
+    for (int i = 0; i < image_color_mat.rows; i++) {
+      for (int j = 0; j < image_color_mat.cols; j++) {
+        // Always push a Y value
+        yuyv.emplace_back(image_color_mat.at<cv::Vec3b>(i, j)[0]);
+        if ((j % 2) == 0) {
+          // If column # is even, push a U value.
+          yuyv.emplace_back(image_color_mat.at<cv::Vec3b>(i, j)[1]);
+        } else {
+          // If column # is odd, push a V value.
+          yuyv.emplace_back(image_color_mat.at<cv::Vec3b>(i, j)[2]);
+        }
+      }
+    }
+
+    CHECK_EQ(static_cast<int>(yuyv.size()),
+             image_color_mat.rows * image_color_mat.cols * 2);
+
+    auto builder = image_sender_.MakeBuilder();
+    auto image_offset = builder.fbb()->CreateVector(yuyv);
+    auto image_builder = builder.MakeBuilder<CameraImage>();
+
+    int64_t timestamp = aos::monotonic_clock::now().time_since_epoch().count();
+
+    image_builder.add_rows(image_color_mat.rows);
+    image_builder.add_cols(image_color_mat.cols);
+    image_builder.add_data(image_offset);
+    image_builder.add_monotonic_timestamp_ns(timestamp);
+
+    builder.CheckOk(builder.Send(image_builder.Finish()));
+  }
+
+  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
+  aos::SimulatedEventLoopFactory event_loop_factory_;
+  const aos::Node *const logger_pi_;
+  const aos::Node *const roborio_;
+  ::std::unique_ptr<::aos::EventLoop> camera_event_loop_;
+  ::std::unique_ptr<::aos::EventLoop> color_detector_event_loop_;
+  ::std::unique_ptr<::aos::EventLoop> superstructure_event_loop_;
+  aos::Fetcher<BallColor> ball_color_fetcher_;
+  aos::Sender<CameraImage> image_sender_;
+};
+
+TEST_F(BallColorTest, DetectColorFromTestImage) {
+  cv::Mat bgr_image =
+      cv::imread("y2022/vision/test_ball_color_image.jpg", cv::IMREAD_COLOR);
+
+  ASSERT_TRUE(bgr_image.data != nullptr);
+
+  aos::Alliance detected_color = BallColorDetector::DetectColor(bgr_image);
+
+  EXPECT_EQ(detected_color, aos::Alliance::kRed);
+}
+
+TEST_F(BallColorTest, DetectColorFromTestImageInEventLoop) {
+  cv::Mat bgr_image =
+      cv::imread("y2022/vision/test_ball_color_image.jpg", cv::IMREAD_COLOR);
+  ASSERT_TRUE(bgr_image.data != nullptr);
+
+  BallColorDetector detector(color_detector_event_loop_.get());
+
+  camera_event_loop_->OnRun([this, bgr_image]() { SendImage(bgr_image); });
+
+  event_loop_factory_.RunFor(std::chrono::milliseconds(5));
+
+  ASSERT_TRUE(ball_color_fetcher_.Fetch());
+
+  EXPECT_TRUE(ball_color_fetcher_->has_ball_color());
+  EXPECT_EQ(ball_color_fetcher_->ball_color(), aos::Alliance::kRed);
+}
+
+TEST_F(BallColorTest, TestRescaling) {
+  cv::Mat mat(cv::Size(320, 240), CV_8UC3);
+  cv::Rect new_rect = BallColorDetector::RescaleRect(
+      mat, cv::Rect(30, 30, 30, 30), cv::Size(1920, 1080));
+
+  EXPECT_EQ(new_rect, cv::Rect(5, 6, 5, 6));
+}
+
+TEST_F(BallColorTest, TestAreas) {
+  cv::Mat bgr_image =
+      cv::imread("y2022/vision/test_ball_color_image.jpg", cv::IMREAD_COLOR);
+  ASSERT_TRUE(bgr_image.data != nullptr);
+
+  cv::Rect reference_red = BallColorDetector::RescaleRect(
+      bgr_image, BallColorDetector::kReferenceRed(),
+      BallColorDetector::kMeasurementsImageSize());
+  cv::Rect reference_blue = BallColorDetector::RescaleRect(
+      bgr_image, BallColorDetector::kReferenceBlue(),
+      BallColorDetector::kMeasurementsImageSize());
+  cv::Rect ball_location = BallColorDetector::RescaleRect(
+      bgr_image, BallColorDetector::kBallLocation(),
+      BallColorDetector::kMeasurementsImageSize());
+
+  cv::rectangle(bgr_image, reference_red, cv::Scalar(0, 0, 255));
+  cv::rectangle(bgr_image, reference_blue, cv::Scalar(255, 0, 0));
+  cv::rectangle(bgr_image, ball_location, cv::Scalar(0, 255, 0));
+
+  cv::imwrite("/tmp/rectangles.jpg", bgr_image);
+}
+
+}  // namespace y2022::vision::testing
diff --git a/y2022/vision/blob_detector.cc b/y2022/vision/blob_detector.cc
index efe7961..60af1f8 100644
--- a/y2022/vision/blob_detector.cc
+++ b/y2022/vision/blob_detector.cc
@@ -11,15 +11,30 @@
 #include "opencv2/imgproc.hpp"
 #include "y2022/vision/geometry.h"
 
+DEFINE_bool(
+    use_outdoors, true,
+    "If set, use the color filters and exposure for an outdoor setting.");
 DEFINE_int32(red_delta, 50, "Required difference between green pixels vs. red");
 DEFINE_int32(blue_delta, -20,
              "Required difference between green pixels vs. blue");
+DEFINE_int32(outdoors_red_delta, 70,
+             "Required difference between green pixels vs. red when using "
+             "--use_outdoors");
+DEFINE_int32(outdoors_blue_delta, -10,
+             "Required difference between green pixels vs. blue when using "
+             "--use_outdoors");
 
 namespace y2022 {
 namespace vision {
 
 cv::Mat BlobDetector::ThresholdImage(cv::Mat bgr_image) {
   cv::Mat binarized_image(cv::Size(bgr_image.cols, bgr_image.rows), CV_8UC1);
+
+  const int red_delta =
+      (FLAGS_use_outdoors ? FLAGS_outdoors_red_delta : FLAGS_red_delta);
+  const int blue_delta =
+      (FLAGS_use_outdoors ? FLAGS_outdoors_blue_delta : FLAGS_blue_delta);
+
   for (int row = 0; row < bgr_image.rows; row++) {
     for (int col = 0; col < bgr_image.cols; col++) {
       cv::Vec3b pixel = bgr_image.at<cv::Vec3b>(row, col);
@@ -28,8 +43,7 @@
       int red = pixel.val[2];
       // Simple filter that looks for green pixels sufficiently brigher than
       // red and blue
-      if ((green > blue + FLAGS_blue_delta) &&
-          (green > red + FLAGS_red_delta)) {
+      if ((green > blue + blue_delta) && (green > red + red_delta)) {
         binarized_image.at<uint8_t>(row, col) = 255;
       } else {
         binarized_image.at<uint8_t>(row, col) = 0;
@@ -233,10 +247,6 @@
     cv::circle(view_image, stats.centroid, kCircleRadius, cv::Scalar(0, 255, 0),
                cv::FILLED);
   }
-
-  // Draw average centroid
-  cv::circle(view_image, blob_result.centroid, kCircleRadius,
-             cv::Scalar(255, 255, 0), cv::FILLED);
 }
 
 void BlobDetector::ExtractBlobs(cv::Mat bgr_image,
diff --git a/y2022/vision/camera_definition.py b/y2022/vision/camera_definition.py
index 68549a4..61789cb 100644
--- a/y2022/vision/camera_definition.py
+++ b/y2022/vision/camera_definition.py
@@ -100,13 +100,13 @@
 
     if pi_number == "pi1":
         camera_yaw = 90.0 * np.pi / 180.0
-        T = np.array([-7.0 * 0.0254, 3.5 * 0.0254, 32.0 * 0.0254])
+        T = np.array([-8.25 * 0.0254, 3.25 * 0.0254, 32.0 * 0.0254])
     elif pi_number == "pi2":
         camera_yaw = 0.0
-        T = np.array([-7.0 * 0.0254, -3.0 * 0.0254, 34.0 * 0.0254])
+        T = np.array([-7.5 * 0.0254, -3.5 * 0.0254, 34.0 * 0.0254])
     elif pi_number == "pi3":
-        camera_yaw = 180.0 * np.pi / 180.0
-        T = np.array([-1.0 * 0.0254, 8.5 * 0.0254, 34.0 * 0.0254])
+        camera_yaw = 179.0 * np.pi / 180.0
+        T = np.array([-1.0 * 0.0254, 8.5 * 0.0254, 34.25 * 0.0254])
     elif pi_number == "pi4":
         camera_yaw = -90.0 * np.pi / 180.0
         T = np.array([-9.0 * 0.0254, -5 * 0.0254, 27.5 * 0.0254])
diff --git a/y2022/vision/camera_reader.cc b/y2022/vision/camera_reader.cc
index 2fd581a..92d3727 100644
--- a/y2022/vision/camera_reader.cc
+++ b/y2022/vision/camera_reader.cc
@@ -90,8 +90,12 @@
 }
 }  // namespace
 
-void CameraReader::ProcessImage(cv::Mat image_mat,
+void CameraReader::ProcessImage(cv::Mat image_mat_distorted,
                                 int64_t image_monotonic_timestamp_ns) {
+  cv::Mat image_mat;
+  cv::undistort(image_mat_distorted, image_mat, CameraIntrinsics(),
+                CameraDistCoeffs());
+
   BlobDetector::BlobResult blob_result;
   BlobDetector::ExtractBlobs(image_mat, &blob_result);
   auto builder = target_estimate_sender_.MakeBuilder();
diff --git a/y2022/vision/camera_reader.h b/y2022/vision/camera_reader.h
index a777bcc..7128890 100644
--- a/y2022/vision/camera_reader.h
+++ b/y2022/vision/camera_reader.h
@@ -63,7 +63,8 @@
   const calibration::CameraCalibration *FindCameraCalibration() const;
 
   // Processes an image (including sending the results).
-  void ProcessImage(cv::Mat image, int64_t image_monotonic_timestamp_ns);
+  void ProcessImage(cv::Mat image_mat_distorted,
+                    int64_t image_monotonic_timestamp_ns);
 
   // Reads an image, and then performs all of our processing on it.
   void ReadImage();
diff --git a/y2022/vision/camera_reader_main.cc b/y2022/vision/camera_reader_main.cc
index dac2100..22794d3 100644
--- a/y2022/vision/camera_reader_main.cc
+++ b/y2022/vision/camera_reader_main.cc
@@ -5,10 +5,14 @@
 // config used to allow running camera_reader independently.  E.g.,
 // bazel run //y2022/vision:camera_reader -- --config y2022/aos_config.json
 //   --override_hostname pi-7971-1  --ignore_timestamps true
+DECLARE_bool(use_outdoors);
 DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
 DEFINE_double(duty_cycle, 0.6, "Duty cycle of the LEDs");
 DEFINE_uint32(exposure, 5,
               "Exposure time, in 100us increments; 0 implies auto exposure");
+DEFINE_uint32(outdoors_exposure, 2,
+              "Exposure time when using --use_outdoors, in 100us increments; 0 "
+              "implies auto exposure");
 
 namespace y2022 {
 namespace vision {
@@ -35,8 +39,10 @@
   }
 
   V4L2Reader v4l2_reader(&event_loop, "/dev/video0");
-  if (FLAGS_exposure > 0) {
-    v4l2_reader.SetExposure(FLAGS_exposure);
+  const uint32_t exposure =
+      (FLAGS_use_outdoors ? FLAGS_outdoors_exposure : FLAGS_exposure);
+  if (exposure > 0) {
+    v4l2_reader.SetExposure(exposure);
   }
 
   CameraReader camera_reader(&event_loop, &calibration_data.message(),
diff --git a/y2022/vision/geometry.h b/y2022/vision/geometry.h
index 31e8629..c27eb4d 100644
--- a/y2022/vision/geometry.h
+++ b/y2022/vision/geometry.h
@@ -1,3 +1,6 @@
+#ifndef Y2022_VISION_GEOMETRY_H_
+#define Y2022_VISION_GEOMETRY_H_
+
 #include "aos/util/math.h"
 #include "glog/logging.h"
 #include "opencv2/core/types.hpp"
@@ -129,3 +132,5 @@
 };
 
 }  // namespace y2022::vision
+
+#endif  // Y2022_VISION_GEOMETRY_H_
diff --git a/y2022/vision/target_estimator.cc b/y2022/vision/target_estimator.cc
index b495b23..9eef390 100644
--- a/y2022/vision/target_estimator.cc
+++ b/y2022/vision/target_estimator.cc
@@ -24,6 +24,8 @@
               "Maximum number of iterations for the ceres solver");
 DEFINE_bool(solver_output, false,
             "If true, log the solver progress and results");
+DEFINE_bool(draw_projected_hub, true,
+            "If true, draw the projected hub when drawing an estimate");
 
 namespace y2022::vision {
 
@@ -36,7 +38,7 @@
 // Height of the center of the tape (m)
 constexpr double kTapeCenterHeight = 2.58 + (kTapePieceHeight / 2);
 // Horizontal distance from tape to center of hub (m)
-constexpr double kUpperHubRadius = 1.22 / 2;
+constexpr double kUpperHubRadius = 1.36 / 2;
 
 std::vector<cv::Point3d> ComputeTapePoints() {
   std::vector<cv::Point3d> tape_points;
@@ -96,14 +98,22 @@
 const std::array<cv::Point3d, 4> TargetEstimator::kMiddleTapePiecePoints =
     ComputeMiddleTapePiecePoints();
 
+namespace {
+constexpr double kDefaultDistance = 3.0;
+constexpr double kDefaultYaw = M_PI;
+constexpr double kDefaultAngleToCamera = 0.0;
+}  // namespace
+
 TargetEstimator::TargetEstimator(cv::Mat intrinsics, cv::Mat extrinsics)
     : blob_stats_(),
+      middle_blob_index_(0),
+      max_blob_area_(0.0),
       image_(std::nullopt),
       roll_(0.0),
       pitch_(0.0),
-      yaw_(M_PI),
-      distance_(3.0),
-      angle_to_camera_(0.0),
+      yaw_(kDefaultYaw),
+      distance_(kDefaultDistance),
+      angle_to_camera_(kDefaultAngleToCamera),
       // Seed camera height
       camera_height_(extrinsics.at<double>(2, 3) +
                      constants::Values::kImuHeight()) {
@@ -151,11 +161,17 @@
                    blob_stats_[2].centroid});
   CHECK(circle.has_value());
 
+  max_blob_area_ = 0.0;
+
   // Find the middle blob, which is the one with the angle closest to the
   // average
   double theta_avg = 0.0;
   for (const auto &stats : blob_stats_) {
     theta_avg += circle->AngleOf(stats.centroid);
+
+    if (stats.area > max_blob_area_) {
+      max_blob_area_ = stats.area;
+    }
   }
   theta_avg /= blob_stats_.size();
 
@@ -183,23 +199,47 @@
                                                ceres::DO_NOT_TAKE_OWNERSHIP);
 
   // TODO(milind): add loss function when we get more noisy data
-  problem.AddResidualBlock(cost_function, nullptr, &roll_, &pitch_, &yaw_,
-                           &distance_, &angle_to_camera_, &camera_height_);
+  problem.AddResidualBlock(cost_function, new ceres::HuberLoss(2.0), &roll_,
+                           &pitch_, &yaw_, &distance_, &angle_to_camera_,
+                           &camera_height_);
 
   // Compute the estimated rotation of the camera using the robot rotation.
-  const Eigen::Vector3d ypr_extrinsics =
-      (Eigen::Affine3d(extrinsics_).rotation() * kHubToCameraAxes)
-          .eulerAngles(2, 1, 0);
+  const Eigen::Matrix3d extrinsics_rot =
+      Eigen::Affine3d(extrinsics_).rotation() * kHubToCameraAxes;
+  // asin returns a pitch in [-pi/2, pi/2] so this will be the correct euler
+  // angles.
+  const double pitch_seed = -std::asin(extrinsics_rot(2, 0));
+  const double roll_seed =
+      std::atan2(extrinsics_rot(2, 1) / std::cos(pitch_seed),
+                 extrinsics_rot(2, 2) / std::cos(pitch_seed));
+
   // TODO(milind): seed with localizer output as well
-  const double roll_seed = ypr_extrinsics.z();
-  const double pitch_seed = ypr_extrinsics.y();
+
+  // If we didn't solve well last time, seed everything at the defaults so we
+  // don't get stuck in a bad state.
+  // Copied from localizer.cc
+  constexpr double kMinConfidence = 0.75;
+  if (confidence_ < kMinConfidence) {
+    roll_ = roll_seed;
+    pitch_ = pitch_seed;
+    yaw_ = kDefaultYaw;
+    distance_ = kDefaultDistance;
+    angle_to_camera_ = kDefaultAngleToCamera;
+    camera_height_ = extrinsics_(2, 3) + constants::Values::kImuHeight();
+  }
 
   // Constrain the rotation to be around the localizer's, otherwise there can be
   // multiple solutions. There shouldn't be too much roll or pitch
+  if (FLAGS_freeze_roll) {
+    roll_ = roll_seed;
+  }
   constexpr double kMaxRollDelta = 0.1;
   SetBoundsOrFreeze(&roll_, FLAGS_freeze_roll, roll_seed - kMaxRollDelta,
                     roll_seed + kMaxRollDelta, &problem);
 
+  if (FLAGS_freeze_pitch) {
+    pitch_ = pitch_seed;
+  }
   constexpr double kMaxPitchDelta = 0.15;
   SetBoundsOrFreeze(&pitch_, FLAGS_freeze_pitch, pitch_seed - kMaxPitchDelta,
                     pitch_seed + kMaxPitchDelta, &problem);
@@ -237,7 +277,7 @@
           << std::chrono::duration<double, std::milli>(end - start).count()
           << " ms";
 
-  // For computing the confidence, find the standard deviation in pixels
+  // For computing the confidence, find the standard deviation in pixels.
   std::vector<double> residual(num_residuals);
   (*this)(&roll_, &pitch_, &yaw_, &distance_, &angle_to_camera_,
           &camera_height_, residual.data());
@@ -251,12 +291,12 @@
   // Use a sigmoid to convert the deviation into a confidence for the
   // localizer. Fit a sigmoid to the points of (0, 1) and two other
   // reasonable deviation-confidence combinations using
-  // https://www.desmos.com/calculator/try0pgx1qw
-  constexpr double kSigmoidCapacity = 1.045;
+  // https://www.desmos.com/calculator/ha6fh9yw44
+  constexpr double kSigmoidCapacity = 1.065;
   // Stretch the sigmoid out correctly.
-  // Currently, good estimates have deviations of around 2 pixels.
-  constexpr double kSigmoidScalar = 0.04452;
-  constexpr double kSigmoidGrowthRate = -0.4021;
+  // Currently, good estimates have deviations of 1 or less pixels.
+  constexpr double kSigmoidScalar = 0.06496;
+  constexpr double kSigmoidGrowthRate = -0.6221;
   confidence_ =
       kSigmoidCapacity /
       (1.0 + kSigmoidScalar * std::exp(-kSigmoidGrowthRate * std_dev));
@@ -277,6 +317,7 @@
 }
 
 namespace {
+
 // Hacks to extract a double from a scalar, which is either a ceres jet or a
 // double. Only used for debugging and displaying.
 template <typename S>
@@ -289,6 +330,7 @@
 cv::Point2d ScalarPointToDouble(cv::Point_<S> p) {
   return cv::Point2d(ScalarToDouble(p.x), ScalarToDouble(p.y));
 }
+
 }  // namespace
 
 template <typename S>
@@ -297,20 +339,10 @@
                                  const S *const theta,
                                  const S *const camera_height,
                                  S *residual) const {
-  using Vector3s = Eigen::Matrix<S, 3, 1>;
-  using Affine3s = Eigen::Transform<S, 3, Eigen::Affine>;
+  const auto H_hub_camera = ComputeHubCameraTransform(
+      *roll, *pitch, *yaw, *distance, *theta, *camera_height);
 
-  Eigen::AngleAxis<S> roll_angle(*roll, Vector3s::UnitX());
-  Eigen::AngleAxis<S> pitch_angle(*pitch, Vector3s::UnitY());
-  Eigen::AngleAxis<S> yaw_angle(*yaw, Vector3s::UnitZ());
-  // Construct the rotation and translation of the camera in the hub's frame
-  Eigen::Quaternion<S> R_camera_hub = yaw_angle * pitch_angle * roll_angle;
-  Vector3s T_camera_hub(*distance * ceres::cos(*theta),
-                        *distance * ceres::sin(*theta), *camera_height);
-
-  Affine3s H_camera_hub = Eigen::Translation<S, 3>(T_camera_hub) * R_camera_hub;
-  Affine3s H_hub_camera = H_camera_hub.inverse();
-
+  // Project tape points
   std::vector<cv::Point_<S>> tape_points_proj;
   for (cv::Point3d tape_point_hub : kTapePoints) {
     tape_points_proj.emplace_back(ProjectToImage(tape_point_hub, H_hub_camera));
@@ -328,51 +360,141 @@
         ProjectToImage(*tape_piece_it, H_hub_camera);
   }
 
+  // Now, find the closest tape for each blob.
+  // We don't normally see tape without matching blobs in the center.  So we
+  // want to compress any gaps in the matched tape blobs.  This makes it so it
+  // doesn't want to make the goal super small and skip tape blobs.  The
+  // resulting accuracy is then pretty good.
+
+  // Mapping from tape index to blob index.
+  std::vector<std::pair<size_t, size_t>> tape_indices;
   for (size_t i = 0; i < blob_stats_.size(); i++) {
-    const auto distance = DistanceFromTape(i, tape_points_proj);
+    tape_indices.emplace_back(ClosestTape(i, tape_points_proj), i);
+    VLOG(2) << "Tape indices were " << tape_indices.back().first;
+  }
+
+  std::sort(
+      tape_indices.begin(), tape_indices.end(),
+      [](const std::pair<size_t, size_t> &a,
+         const std::pair<size_t, size_t> &b) { return a.first < b.first; });
+
+  size_t middle_tape_index = 1000;
+  for (size_t i = 0; i < tape_indices.size(); ++i) {
+    if (tape_indices[i].second == middle_blob_index_) {
+      middle_tape_index = i;
+    }
+  }
+  CHECK_NE(middle_tape_index, 1000) << "Failed to find middle tape";
+
+  if (VLOG_IS_ON(2)) {
+    LOG(INFO) << "Middle tape is " << middle_tape_index << ", blob "
+              << middle_blob_index_;
+    for (size_t i = 0; i < tape_indices.size(); ++i) {
+      const auto distance = DistanceFromTapeIndex(
+          tape_indices[i].second, tape_indices[i].first, tape_points_proj);
+      LOG(INFO) << "Blob index " << tape_indices[i].second << " maps to "
+                << tape_indices[i].first << " distance " << distance.x << " "
+                << distance.y;
+    }
+  }
+
+  {
+    size_t offset = 0;
+    for (size_t i = middle_tape_index + 1; i < tape_indices.size(); ++i) {
+      tape_indices[i].first -= offset;
+
+      if (tape_indices[i].first > tape_indices[i - 1].first + 1) {
+        offset += tape_indices[i].first - (tape_indices[i - 1].first + 1);
+        VLOG(2) << "Offset now " << offset;
+        tape_indices[i].first = tape_indices[i - 1].first + 1;
+      }
+    }
+  }
+
+  if (VLOG_IS_ON(2)) {
+    LOG(INFO) << "Middle tape is " << middle_tape_index << ", blob "
+              << middle_blob_index_;
+    for (size_t i = 0; i < tape_indices.size(); ++i) {
+      const auto distance = DistanceFromTapeIndex(
+          tape_indices[i].second, tape_indices[i].first, tape_points_proj);
+      LOG(INFO) << "Blob index " << tape_indices[i].second << " maps to "
+                << tape_indices[i].first << " distance " << distance.x << " "
+                << distance.y;
+    }
+  }
+
+  {
+    size_t offset = 0;
+    for (size_t i = middle_tape_index; i > 0; --i) {
+      tape_indices[i - 1].first -= offset;
+
+      if (tape_indices[i - 1].first + 1 < tape_indices[i].first) {
+        VLOG(2) << "Too big a gap. " << tape_indices[i].first << " and "
+                << tape_indices[i - 1].first;
+
+        offset += tape_indices[i].first - (tape_indices[i - 1].first + 1);
+        tape_indices[i - 1].first = tape_indices[i].first - 1;
+        VLOG(2) << "Offset now " << offset;
+      }
+    }
+  }
+
+  if (VLOG_IS_ON(2)) {
+    LOG(INFO) << "Middle tape is " << middle_tape_index << ", blob "
+              << middle_blob_index_;
+    for (size_t i = 0; i < tape_indices.size(); ++i) {
+      const auto distance = DistanceFromTapeIndex(
+          tape_indices[i].second, tape_indices[i].first, tape_points_proj);
+      LOG(INFO) << "Blob index " << tape_indices[i].second << " maps to "
+                << tape_indices[i].first << " distance " << distance.x << " "
+                << distance.y;
+    }
+  }
+
+  for (size_t i = 0; i < tape_indices.size(); ++i) {
+    const auto distance = DistanceFromTapeIndex(
+        tape_indices[i].second, tape_indices[i].first, tape_points_proj);
+    // Scale the distance based on the blob area: larger blobs have less noise.
+    const S distance_scalar =
+        S(blob_stats_[tape_indices[i].second].area / max_blob_area_);
+    VLOG(2) << "Blob index " << tape_indices[i].second << " maps to "
+            << tape_indices[i].first << " distance " << distance.x << " "
+            << distance.y << " distance scalar "
+            << ScalarToDouble(distance_scalar);
+
     // Set the residual to the (x, y) distance of the centroid from the
-    // nearest projected piece of tape
-    residual[i * 2] = distance.x;
-    residual[(i * 2) + 1] = distance.y;
+    // matched projected piece of tape
+    residual[i * 2] = distance_scalar * distance.x;
+    residual[(i * 2) + 1] = distance_scalar * distance.y;
   }
 
   // Penalize based on the difference between the size of the projected piece of
-  // tape and that of the detected blobs. Use the squared size to avoid taking a
-  // norm, which ceres can't handle well
-  const S middle_tape_piece_width_squared =
-      ceres::pow(middle_tape_piece_points_proj[2].x -
-                     middle_tape_piece_points_proj[3].x,
-                 2) +
-      ceres::pow(middle_tape_piece_points_proj[2].y -
-                     middle_tape_piece_points_proj[3].y,
-                 2);
-  const S middle_tape_piece_height_squared =
-      ceres::pow(middle_tape_piece_points_proj[1].x -
-                     middle_tape_piece_points_proj[2].x,
-                 2) +
-      ceres::pow(middle_tape_piece_points_proj[1].y -
-                     middle_tape_piece_points_proj[2].y,
-                 2);
+  // tape and that of the detected blobs.
+  const S middle_tape_piece_width = ceres::hypot(
+      middle_tape_piece_points_proj[2].x - middle_tape_piece_points_proj[3].x,
+      middle_tape_piece_points_proj[2].y - middle_tape_piece_points_proj[3].y);
+  const S middle_tape_piece_height = ceres::hypot(
+      middle_tape_piece_points_proj[1].x - middle_tape_piece_points_proj[2].x,
+      middle_tape_piece_points_proj[1].y - middle_tape_piece_points_proj[2].y);
 
+  constexpr double kCenterBlobSizeScalar = 0.1;
   residual[blob_stats_.size() * 2] =
-      middle_tape_piece_width_squared -
-      std::pow(blob_stats_[middle_blob_index_].size.width, 2);
+      kCenterBlobSizeScalar *
+      (middle_tape_piece_width -
+       static_cast<S>(blob_stats_[middle_blob_index_].size.width));
   residual[(blob_stats_.size() * 2) + 1] =
-      middle_tape_piece_height_squared -
-      std::pow(blob_stats_[middle_blob_index_].size.height, 2);
+      kCenterBlobSizeScalar *
+      (middle_tape_piece_height -
+       static_cast<S>(blob_stats_[middle_blob_index_].size.height));
 
   if (image_.has_value()) {
     // Draw the current stage of the solving
     cv::Mat image = image_->clone();
-    for (size_t i = 0; i < tape_points_proj.size() - 1; i++) {
-      cv::line(image, ScalarPointToDouble(tape_points_proj[i]),
-               ScalarPointToDouble(tape_points_proj[i + 1]),
-               cv::Scalar(255, 255, 255));
-      cv::circle(image, ScalarPointToDouble(tape_points_proj[i]), 2,
-                 cv::Scalar(255, 20, 147), cv::FILLED);
-      cv::circle(image, ScalarPointToDouble(tape_points_proj[i + 1]), 2,
-                 cv::Scalar(255, 20, 147), cv::FILLED);
+    std::vector<cv::Point2d> tape_points_proj_double;
+    for (auto point : tape_points_proj) {
+      tape_points_proj_double.emplace_back(ScalarPointToDouble(point));
     }
+    DrawProjectedHub(tape_points_proj_double, image);
     cv::imshow("image", image);
     cv::waitKey(10);
   }
@@ -381,9 +503,30 @@
 }
 
 template <typename S>
+Eigen::Transform<S, 3, Eigen::Affine>
+TargetEstimator::ComputeHubCameraTransform(S roll, S pitch, S yaw, S distance,
+                                           S theta, S camera_height) const {
+  using Vector3s = Eigen::Matrix<S, 3, 1>;
+  using Affine3s = Eigen::Transform<S, 3, Eigen::Affine>;
+
+  Eigen::AngleAxis<S> roll_angle(roll, Vector3s::UnitX());
+  Eigen::AngleAxis<S> pitch_angle(pitch, Vector3s::UnitY());
+  Eigen::AngleAxis<S> yaw_angle(yaw, Vector3s::UnitZ());
+  // Construct the rotation and translation of the camera in the hub's frame
+  Eigen::Quaternion<S> R_camera_hub = yaw_angle * pitch_angle * roll_angle;
+  Vector3s T_camera_hub(distance * ceres::cos(theta),
+                        distance * ceres::sin(theta), camera_height);
+
+  Affine3s H_camera_hub = Eigen::Translation<S, 3>(T_camera_hub) * R_camera_hub;
+  Affine3s H_hub_camera = H_camera_hub.inverse();
+
+  return H_hub_camera;
+}
+
+template <typename S>
 cv::Point_<S> TargetEstimator::ProjectToImage(
     cv::Point3d tape_point_hub,
-    Eigen::Transform<S, 3, Eigen::Affine> &H_hub_camera) const {
+    const Eigen::Transform<S, 3, Eigen::Affine> &H_hub_camera) const {
   using Vector3s = Eigen::Matrix<S, 3, 1>;
 
   const Vector3s tape_point_hub_eigen =
@@ -412,33 +555,70 @@
 }  // namespace
 
 template <typename S>
-cv::Point_<S> TargetEstimator::DistanceFromTape(
+cv::Point_<S> TargetEstimator::DistanceFromTapeIndex(
+    size_t blob_index, size_t tape_index,
+    const std::vector<cv::Point_<S>> &tape_points) const {
+  return Distance(blob_stats_[blob_index].centroid, tape_points[tape_index]);
+}
+
+template <typename S>
+size_t TargetEstimator::ClosestTape(
     size_t blob_index, const std::vector<cv::Point_<S>> &tape_points) const {
   auto distance = cv::Point_<S>(std::numeric_limits<S>::infinity(),
                                 std::numeric_limits<S>::infinity());
+  size_t final_match = 255;
   if (blob_index == middle_blob_index_) {
     // Fix the middle blob so the solver can't go too far off
-    distance = Distance(blob_stats_[middle_blob_index_].centroid,
-                        tape_points[tape_points.size() / 2]);
+    final_match = tape_points.size() / 2;
+    distance = DistanceFromTapeIndex(blob_index, final_match, tape_points);
   } else {
     // Give the other blob_stats some freedom in case some are split into pieces
     for (auto it = tape_points.begin(); it < tape_points.end(); it++) {
+      const size_t tape_index = std::distance(tape_points.begin(), it);
       const auto current_distance =
-          Distance(blob_stats_[blob_index].centroid, *it);
-      if ((it != tape_points.begin() + (tape_points.size() / 2)) &&
+          DistanceFromTapeIndex(blob_index, tape_index, tape_points);
+      if ((tape_index != (tape_points.size() / 2)) &&
           Less(current_distance, distance)) {
+        final_match = tape_index;
         distance = current_distance;
       }
     }
   }
 
-  return distance;
+  VLOG(2) << "Matched index " << blob_index << " to " << final_match
+          << " distance " << distance.x << " " << distance.y;
+  CHECK_NE(final_match, 255);
+
+  return final_match;
 }
 
-namespace {
-void DrawEstimateValues(double distance, double angle_to_target,
-                        double angle_to_camera, double roll, double pitch,
-                        double yaw, double confidence, cv::Mat view_image) {
+void TargetEstimator::DrawProjectedHub(
+    const std::vector<cv::Point2d> &tape_points_proj,
+    cv::Mat view_image) const {
+  for (size_t i = 0; i < tape_points_proj.size() - 1; i++) {
+    cv::line(view_image, ScalarPointToDouble(tape_points_proj[i]),
+             ScalarPointToDouble(tape_points_proj[i + 1]),
+             cv::Scalar(255, 255, 255));
+    cv::circle(view_image, ScalarPointToDouble(tape_points_proj[i]), 2,
+               cv::Scalar(255, 20, 147), cv::FILLED);
+    cv::circle(view_image, ScalarPointToDouble(tape_points_proj[i + 1]), 2,
+               cv::Scalar(255, 20, 147), cv::FILLED);
+  }
+}
+
+void TargetEstimator::DrawEstimate(cv::Mat view_image) const {
+  if (FLAGS_draw_projected_hub) {
+    // Draw projected hub
+    const auto H_hub_camera = ComputeHubCameraTransform(
+        roll_, pitch_, yaw_, distance_, angle_to_camera_, camera_height_);
+    std::vector<cv::Point2d> tape_points_proj;
+    for (cv::Point3d tape_point_hub : kTapePoints) {
+      tape_points_proj.emplace_back(
+          ProjectToImage(tape_point_hub, H_hub_camera));
+    }
+    DrawProjectedHub(tape_points_proj, view_image);
+  }
+
   constexpr int kTextX = 10;
   int text_y = 0;
   constexpr int kTextSpacing = 25;
@@ -446,44 +626,29 @@
   const auto kTextColor = cv::Scalar(0, 255, 255);
   constexpr double kFontScale = 0.6;
 
-  cv::putText(view_image, absl::StrFormat("Distance: %.3f", distance),
+  cv::putText(view_image,
+              absl::StrFormat("Distance: %.3f m (%.3f in)", distance_,
+                              distance_ / 0.0254),
               cv::Point(kTextX, text_y += kTextSpacing),
               cv::FONT_HERSHEY_DUPLEX, kFontScale, kTextColor, 2);
   cv::putText(view_image,
-              absl::StrFormat("Angle to target: %.3f", angle_to_target),
+              absl::StrFormat("Angle to target: %.3f", angle_to_target()),
               cv::Point(kTextX, text_y += kTextSpacing),
               cv::FONT_HERSHEY_DUPLEX, kFontScale, kTextColor, 2);
   cv::putText(view_image,
-              absl::StrFormat("Angle to camera: %.3f", angle_to_camera),
+              absl::StrFormat("Angle to camera: %.3f", angle_to_camera_),
               cv::Point(kTextX, text_y += kTextSpacing),
               cv::FONT_HERSHEY_DUPLEX, kFontScale, kTextColor, 2);
 
-  cv::putText(
-      view_image,
-      absl::StrFormat("Roll: %.3f, pitch: %.3f, yaw: %.3f", roll, pitch, yaw),
-      cv::Point(kTextX, text_y += kTextSpacing), cv::FONT_HERSHEY_DUPLEX,
-      kFontScale, kTextColor, 2);
-
-  cv::putText(view_image, absl::StrFormat("Confidence: %.3f", confidence),
+  cv::putText(view_image,
+              absl::StrFormat("Roll: %.3f, pitch: %.3f, yaw: %.3f", roll_,
+                              pitch_, yaw_),
               cv::Point(kTextX, text_y += kTextSpacing),
               cv::FONT_HERSHEY_DUPLEX, kFontScale, kTextColor, 2);
-}
-}  // namespace
 
-void TargetEstimator::DrawEstimate(const TargetEstimate &target_estimate,
-                                   cv::Mat view_image) {
-  DrawEstimateValues(target_estimate.distance(),
-                     target_estimate.angle_to_target(),
-                     target_estimate.angle_to_camera(),
-                     target_estimate.rotation_camera_hub()->roll(),
-                     target_estimate.rotation_camera_hub()->pitch(),
-                     target_estimate.rotation_camera_hub()->yaw(),
-                     target_estimate.confidence(), view_image);
-}
-
-void TargetEstimator::DrawEstimate(cv::Mat view_image) const {
-  DrawEstimateValues(distance_, angle_to_target(), angle_to_camera_, roll_,
-                     pitch_, yaw_, confidence_, view_image);
+  cv::putText(view_image, absl::StrFormat("Confidence: %.3f", confidence_),
+              cv::Point(kTextX, text_y += kTextSpacing),
+              cv::FONT_HERSHEY_DUPLEX, kFontScale, kTextColor, 2);
 }
 
 }  // namespace y2022::vision
diff --git a/y2022/vision/target_estimator.h b/y2022/vision/target_estimator.h
index b509a2e..ac170e8 100644
--- a/y2022/vision/target_estimator.h
+++ b/y2022/vision/target_estimator.h
@@ -47,9 +47,7 @@
 
   inline double confidence() const { return confidence_; }
 
-  // Draws the distance, angle, and rotation on the given image
-  static void DrawEstimate(const TargetEstimate &target_estimate,
-                           cv::Mat view_image);
+  // Draws the distance, angle, rotation, and projected tape on the given image
   void DrawEstimate(cv::Mat view_image) const;
 
  private:
@@ -59,18 +57,31 @@
   // clockwise around the rectangle
   static const std::array<cv::Point3d, 4> kMiddleTapePiecePoints;
 
+  // Computes matrix of hub in camera's frame
+  template <typename S>
+  Eigen::Transform<S, 3, Eigen::Affine> ComputeHubCameraTransform(
+      S roll, S pitch, S yaw, S distance, S theta, S camera_height) const;
+
   template <typename S>
   cv::Point_<S> ProjectToImage(
       cv::Point3d tape_point_hub,
-      Eigen::Transform<S, 3, Eigen::Affine> &H_hub_camera) const;
+      const Eigen::Transform<S, 3, Eigen::Affine> &H_hub_camera) const;
 
   template <typename S>
-  cv::Point_<S> DistanceFromTape(
-      size_t centroid_index,
+  size_t ClosestTape(size_t centroid_index,
+                     const std::vector<cv::Point_<S>> &tape_points) const;
+
+  template <typename S>
+  cv::Point_<S> DistanceFromTapeIndex(
+      size_t centroid_index, size_t tape_index,
       const std::vector<cv::Point_<S>> &tape_points) const;
 
+  void DrawProjectedHub(const std::vector<cv::Point2d> &tape_points_proj,
+                        cv::Mat view_image) const;
+
   std::vector<BlobDetector::BlobStats> blob_stats_;
   size_t middle_blob_index_;
+  double max_blob_area_;
   std::optional<cv::Mat> image_;
 
   Eigen::Matrix3d intrinsics_;
diff --git a/y2022/vision/test_ball_color_image.jpg b/y2022/vision/test_ball_color_image.jpg
new file mode 100644
index 0000000..8750460
--- /dev/null
+++ b/y2022/vision/test_ball_color_image.jpg
Binary files differ
diff --git a/y2022/vision/viewer.cc b/y2022/vision/viewer.cc
index 82a5e45..446f1f6 100644
--- a/y2022/vision/viewer.cc
+++ b/y2022/vision/viewer.cc
@@ -1,3 +1,4 @@
+#include <algorithm>
 #include <map>
 #include <opencv2/calib3d.hpp>
 #include <opencv2/features2d.hpp>
@@ -5,6 +6,7 @@
 #include <opencv2/imgproc.hpp>
 #include <random>
 
+#include "absl/strings/str_format.h"
 #include "aos/events/shm_event_loop.h"
 #include "aos/init.h"
 #include "aos/time/time.h"
@@ -19,6 +21,7 @@
 DEFINE_string(channel, "/camera", "Channel name for the image.");
 DEFINE_string(config, "aos_config.json", "Path to the config file to use.");
 DEFINE_string(png_dir, "", "Path to a set of images to display.");
+DEFINE_string(png_pattern, "*", R"(Pattern to match pngs using '*'/'?'.)");
 DEFINE_string(calibration_node, "",
               "If reading locally, use the calibration for this node");
 DEFINE_int32(
@@ -29,6 +32,7 @@
 DEFINE_bool(show_features, true, "Show the blobs.");
 DEFINE_bool(display_estimation, false,
             "If true, display the target estimation graphically");
+DEFINE_bool(sort_by_time, true, "If true, sort the images by time");
 
 namespace y2022 {
 namespace vision {
@@ -175,10 +179,36 @@
   event_loop.Run();
 }
 
-// TODO(milind): delete this when viewer can accumulate local images and results
+size_t FindImageTimestamp(std::string_view filename) {
+  // Find the first number in the string
+  const auto timestamp_start = std::find_if(
+      filename.begin(), filename.end(), [](char c) { return std::isdigit(c); });
+  CHECK_NE(timestamp_start, filename.end())
+      << "Expected a number in image filename, got " << filename;
+  const auto timestamp_end =
+      std::find_if_not(timestamp_start + 1, filename.end(),
+                       [](char c) { return std::isdigit(c); });
+
+  return static_cast<size_t>(
+      std::atoi(filename
+                    .substr(timestamp_start - filename.begin(),
+                            timestamp_end - timestamp_start)
+                    .data()));
+}
+
 void ViewerLocal() {
   std::vector<cv::String> file_list;
-  cv::glob(FLAGS_png_dir + "/*.png", file_list, false);
+  cv::glob(absl::StrFormat("%s/%s.png", FLAGS_png_dir, FLAGS_png_pattern),
+           file_list, false);
+
+  // Sort the images by timestamp
+  if (FLAGS_sort_by_time) {
+    std::sort(file_list.begin(), file_list.end(),
+              [](std::string_view filename_1, std::string_view filename_2) {
+                return (FindImageTimestamp(filename_1) <
+                        FindImageTimestamp(filename_2));
+              });
+  }
 
   const aos::FlatbufferSpan<calibration::CalibrationData> calibration_data(
       CalibrationData());
@@ -197,9 +227,10 @@
                      << FLAGS_calibration_node << "\" with team number "
                      << FLAGS_calibration_team_number;
 
-  auto intrinsics_float = cv::Mat(3, 3, CV_32F,
-                                  const_cast<void *>(static_cast<const void *>(
-                                      calibration->intrinsics()->data())));
+  const auto intrinsics_float = cv::Mat(
+      3, 3, CV_32F,
+      const_cast<void *>(
+          static_cast<const void *>(calibration->intrinsics()->data())));
   cv::Mat intrinsics;
   intrinsics_float.convertTo(intrinsics, CV_64F);
 
@@ -213,11 +244,21 @@
   cv::Mat extrinsics;
   extrinsics_float.convertTo(extrinsics, CV_64F);
 
+  const auto dist_coeffs_float = cv::Mat(
+      5, 1, CV_32F,
+      const_cast<void *>(
+          static_cast<const void *>(calibration->dist_coeffs()->data())));
+  cv::Mat dist_coeffs;
+  dist_coeffs_float.convertTo(dist_coeffs, CV_64F);
+
   TargetEstimator estimator(intrinsics, extrinsics);
 
   for (auto it = file_list.begin() + FLAGS_skip; it < file_list.end(); it++) {
     LOG(INFO) << "Reading file " << (it - file_list.begin()) << ": " << *it;
-    cv::Mat image_mat = cv::imread(it->c_str());
+    cv::Mat image_mat_distorted = cv::imread(it->c_str());
+    cv::Mat image_mat;
+    cv::undistort(image_mat_distorted, image_mat, intrinsics, dist_coeffs);
+
     BlobDetector::BlobResult blob_result;
     blob_result.binarized_image =
         cv::Mat::zeros(cv::Size(image_mat.cols, image_mat.rows), CV_8UC1);
@@ -233,19 +274,25 @@
                      blob_result.filtered_blobs.size()
               << ")";
 
+    estimator.Solve(blob_result.filtered_stats,
+                    FLAGS_display_estimation ? std::make_optional(ret_image)
+                                             : std::nullopt);
     if (blob_result.filtered_blobs.size() > 0) {
-      estimator.Solve(blob_result.filtered_stats,
-                      FLAGS_display_estimation ? std::make_optional(ret_image)
-                                               : std::nullopt);
       estimator.DrawEstimate(ret_image);
+      LOG(INFO) << "Read file " << (it - file_list.begin()) << ": " << *it;
     }
 
     cv::imshow("image", image_mat);
     cv::imshow("mask", blob_result.binarized_image);
     cv::imshow("blobs", ret_image);
 
-    int keystroke = cv::waitKey(0);
-    if ((keystroke & 0xFF) == static_cast<int>('q')) {
+    constexpr size_t kWaitKeyDelay = 0;  // ms
+    int keystroke = cv::waitKey(kWaitKeyDelay) & 0xFF;
+    // Ignore alt key
+    while (keystroke == 233) {
+      keystroke = cv::waitKey(kWaitKeyDelay);
+    }
+    if (keystroke == static_cast<int>('q')) {
       return;
     }
   }
diff --git a/y2022/vision/viewer_replay.cc b/y2022/vision/viewer_replay.cc
index c1bf88b..b2d3464 100644
--- a/y2022/vision/viewer_replay.cc
+++ b/y2022/vision/viewer_replay.cc
@@ -1,14 +1,17 @@
 #include "aos/events/logging/log_reader.h"
 #include "aos/events/simulated_event_loop.h"
 #include "aos/init.h"
+#include "frc971/control_loops/drivetrain/drivetrain_status_generated.h"
+#include "frc971/input/joystick_state_generated.h"
 #include "frc971/vision/vision_generated.h"
 #include "opencv2/calib3d.hpp"
 #include "opencv2/features2d.hpp"
 #include "opencv2/highgui/highgui.hpp"
 #include "opencv2/imgproc.hpp"
+#include "y2022/control_loops/superstructure/superstructure_status_generated.h"
 #include "y2022/vision/blob_detector.h"
 
-DEFINE_string(node, "pi1", "Node name to replay.");
+DEFINE_string(pi, "pi3", "Node name to replay.");
 DEFINE_string(image_save_prefix, "/tmp/img",
               "Prefix to use for saving images from the logfile.");
 DEFINE_bool(display, false, "If true, display the images with a timeout.");
@@ -16,29 +19,173 @@
             "If true, only write images which had blobs (unfiltered) detected");
 DEFINE_bool(filtered_only, false,
             "If true, only write images which had blobs (filtered) detected");
+DEFINE_bool(match_timestamps, false,
+            "If true, name the files based on the time since the robot was "
+            "enabled (match start). Only consider images during this time");
+DEFINE_string(logger_pi_log, "/tmp/logger_pi/", "Path to logger pi log");
+DEFINE_string(roborio_log, "/tmp/roborio/", "Path to roborio log");
 
 namespace y2022 {
 namespace vision {
 namespace {
 
-void ViewerMain(int argc, char *argv[]) {
+using aos::monotonic_clock;
+namespace superstructure = control_loops::superstructure;
+
+// Information to extract from the roborio log
+struct ReplayData {
+  monotonic_clock::time_point match_start;
+  monotonic_clock::time_point match_end;
+  std::map<monotonic_clock::time_point, bool> robot_moving;
+  std::map<monotonic_clock::time_point, superstructure::SuperstructureState>
+      superstructure_states;
+};
+
+// Extract the useful data from the roborio log to be used for naming images
+void ReplayRoborio(ReplayData *data) {
+  data->match_start = monotonic_clock::min_time;
+  data->match_end = monotonic_clock::min_time;
+
   std::vector<std::string> unsorted_logfiles =
-      aos::logger::FindLogs(argc, argv);
+      aos::logger::FindLogs(FLAGS_roborio_log);
+  // Open logfiles
+  aos::logger::LogReader reader(aos::logger::SortParts(unsorted_logfiles));
+  reader.Register();
+  const aos::Node *roborio =
+      aos::configuration::GetNode(reader.configuration(), "roborio");
+
+  std::unique_ptr<aos::EventLoop> event_loop =
+      reader.event_loop_factory()->MakeEventLoop("roborio", roborio);
+
+  auto joystick_state_fetcher =
+      event_loop->MakeFetcher<aos::JoystickState>("/roborio/aos");
+  auto drivetrain_status_fetcher =
+      event_loop->MakeFetcher<frc971::control_loops::drivetrain::Status>(
+          "/drivetrain");
+  auto superstructure_status_fetcher =
+      event_loop->MakeFetcher<superstructure::Status>("/superstructure");
+
+  // Periodically check if the robot state updated
+  event_loop->AddPhasedLoop(
+      [&](int) {
+        // Find the match start and end times
+        if (joystick_state_fetcher.Fetch()) {
+          if (data->match_start == monotonic_clock::min_time &&
+              joystick_state_fetcher->enabled()) {
+            data->match_start =
+                joystick_state_fetcher.context().monotonic_event_time;
+          } else {
+            if (data->match_end == monotonic_clock::min_time &&
+                data->match_start != monotonic_clock::min_time &&
+                !joystick_state_fetcher->autonomous() &&
+                !joystick_state_fetcher->enabled()) {
+              data->match_end =
+                  joystick_state_fetcher.context().monotonic_event_time;
+            }
+          }
+        }
+
+        // Add whether the robot was moving at a non-negligible speed to
+        // the image name for debugging.
+        drivetrain_status_fetcher.Fetch();
+        if (drivetrain_status_fetcher.get()) {
+          // If the robot speed was atleast this (m/s), it is
+          // considered moving.
+          constexpr double kMinMovingRobotSpeed = 0.5;
+          data->robot_moving[drivetrain_status_fetcher.context()
+                                 .monotonic_event_time] =
+              (std::abs(drivetrain_status_fetcher->robot_speed()) >=
+               kMinMovingRobotSpeed);
+        }
+
+        superstructure_status_fetcher.Fetch();
+        if (superstructure_status_fetcher.get()) {
+          data->superstructure_states[superstructure_status_fetcher.context()
+                                          .monotonic_event_time] =
+              superstructure_status_fetcher->state();
+        }
+      },
+      std::chrono::milliseconds(50));
+  reader.event_loop_factory()->Run();
+}
+
+template <typename T>
+T ClosestElement(const std::map<monotonic_clock::time_point, T> &map,
+                 monotonic_clock::time_point now) {
+  T closest;
+
+  // The closest element is either the one right above it, or the element before
+  // that one
+  auto closest_it = map.lower_bound(now);
+  // Handle the case where now is greater than all times in the map
+  if (closest_it == map.cend()) {
+    closest_it--;
+    closest = closest_it->second;
+  } else {
+    // Start off with the closest as the first after now
+    closest = closest_it->second;
+    const monotonic_clock::duration after_duration = closest_it->first - now;
+    closest_it--;
+
+    // If there is a time before, check if that's closer to now
+    if (closest_it != map.cbegin()) {
+      const monotonic_clock::duration before_duration = now - closest_it->first;
+      if (before_duration < after_duration) {
+        closest = closest_it->second;
+      }
+    }
+  }
+
+  return closest;
+}
+
+// Extract images from the pi logs
+void ReplayPi(const ReplayData &data) {
+  if (FLAGS_match_timestamps) {
+    CHECK_NE(data.match_start, monotonic_clock::min_time)
+        << "Can't use match timestamps if match never started";
+    CHECK_NE(data.match_end, monotonic_clock::min_time)
+        << "Can't use match timestamps if match never ended";
+  }
+
+  std::vector<std::string> unsorted_logfiles =
+      aos::logger::FindLogs(FLAGS_logger_pi_log);
 
   // Open logfiles
   aos::logger::LogReader reader(aos::logger::SortParts(unsorted_logfiles));
   reader.Register();
-  const aos::Node *node = nullptr;
-  if (aos::configuration::MultiNode(reader.configuration())) {
-    node = aos::configuration::GetNode(reader.configuration(), FLAGS_node);
-  }
-  std::unique_ptr<aos::EventLoop> event_loop =
-      reader.event_loop_factory()->MakeEventLoop("player", node);
+  const aos::Node *pi =
+      aos::configuration::GetNode(reader.configuration(), FLAGS_pi);
 
-  int image_count = 0;
+  std::unique_ptr<aos::EventLoop> event_loop =
+      reader.event_loop_factory()->MakeEventLoop("player", pi);
+
+  LOG(INFO) << "Match start: " << data.match_start
+            << ", match end: " << data.match_end;
+
+  size_t nonmatch_image_count = 0;
+
   event_loop->MakeWatcher(
-      "/camera/decimated",
-      [&image_count](const frc971::vision::CameraImage &image) {
+      "/camera/decimated", [&](const frc971::vision::CameraImage &image) {
+        const auto match_start = data.match_start;
+        // Find the closest robot moving and superstructure state to now
+        const bool robot_moving =
+            ClosestElement(data.robot_moving, event_loop->monotonic_now());
+        const auto superstructure_state = ClosestElement(
+            data.superstructure_states, event_loop->monotonic_now());
+
+        if (FLAGS_match_timestamps) {
+          if (event_loop->monotonic_now() < data.match_start) {
+            // Ignore prematch images if we only care about ones during the
+            // match
+            return;
+          } else if (event_loop->monotonic_now() >= data.match_end) {
+            // We're done if the match is over and we only wanted match images
+            reader.event_loop_factory()->Exit();
+            return;
+          }
+        }
+
         // Create color image:
         cv::Mat image_color_mat(cv::Size(image.cols(), image.rows()), CV_8UC2,
                                 (void *)image.data()->data());
@@ -54,11 +201,44 @@
               ((FLAGS_filtered_only ? blob_result.filtered_blobs.size()
                                     : blob_result.unfiltered_blobs.size()) > 0);
         }
+
         if (use_image) {
           if (!FLAGS_image_save_prefix.empty()) {
-            cv::imwrite(FLAGS_image_save_prefix +
-                            std::to_string(image_count++) + ".png",
-                        image_mat);
+            std::stringstream image_name;
+            image_name << FLAGS_image_save_prefix;
+
+            if (FLAGS_match_timestamps) {
+              // Add the time since match start into the image for debugging.
+              // We can match images with the game recording.
+              image_name << "match_"
+                         << std::chrono::duration_cast<std::chrono::seconds>(
+                                event_loop->monotonic_now() - match_start)
+                                .count()
+                         << 's';
+            } else {
+              image_name << nonmatch_image_count++;
+            }
+
+            // Add superstructure state to the filename
+            if (superstructure_state !=
+                superstructure::SuperstructureState::IDLE) {
+              std::string superstructure_state_name =
+                  superstructure::EnumNameSuperstructureState(
+                      superstructure_state);
+              std::transform(superstructure_state_name.begin(),
+                             superstructure_state_name.end(),
+                             superstructure_state_name.begin(),
+                             [](char c) { return std::tolower(c); });
+              image_name << '_' << superstructure_state_name;
+            }
+
+            if (robot_moving) {
+              image_name << "_moving";
+            }
+
+            image_name << ".png";
+
+            cv::imwrite(image_name.str(), image_mat);
           }
           if (FLAGS_display) {
             cv::imshow("Display", image_mat);
@@ -70,6 +250,12 @@
   reader.event_loop_factory()->Run();
 }
 
+void ViewerMain() {
+  ReplayData data;
+  ReplayRoborio(&data);
+  ReplayPi(data);
+}
+
 }  // namespace
 }  // namespace vision
 }  // namespace y2022
@@ -77,5 +263,5 @@
 // Quick and lightweight viewer for image logs
 int main(int argc, char **argv) {
   aos::InitGoogle(&argc, &argv);
-  y2022::vision::ViewerMain(argc, argv);
+  y2022::vision::ViewerMain();
 }
diff --git a/y2022/vision/vision_plotter.ts b/y2022/vision/vision_plotter.ts
index 13dd9e5..adbf900 100644
--- a/y2022/vision/vision_plotter.ts
+++ b/y2022/vision/vision_plotter.ts
@@ -4,13 +4,13 @@
 import {BLUE, BROWN, CYAN, GREEN, PINK, RED, WHITE} from 'org_frc971/aos/network/www/colors';
 import {Connection} from 'org_frc971/aos/network/www/proxy';
 import {Table} from 'org_frc971/aos/network/www/reflection';
-import {Schema} from 'org_frc971/external/com_github_google_flatbuffers/reflection/reflection_generated';
+import {Schema} from 'flatbuffers_reflection/reflection_generated';
 import {TargetEstimate} from 'org_frc971/y2022/vision/target_estimate_generated';
 
 
 const TIME = AosPlotter.TIME;
-// magenta, yellow, cyan, orange
-const PI_COLORS = [[255, 0, 255], [255, 255, 0], [0, 255, 255], [255, 165, 0]];
+// magenta, yellow, cyan, black
+const PI_COLORS = [[255, 0, 255], [255, 255, 0], [0, 255, 255], [0, 0, 0]];
 
 class VisionMessageHandler extends MessageHandler {
   constructor(private readonly schema: Schema) {
diff --git a/y2022/wpilib_interface.cc b/y2022/wpilib_interface.cc
index 2e33db3..56eba95 100644
--- a/y2022/wpilib_interface.cc
+++ b/y2022/wpilib_interface.cc
@@ -61,6 +61,8 @@
 namespace chrono = ::std::chrono;
 using std::make_unique;
 
+DEFINE_bool(can_catapult, false, "If true, use CAN to control the catapult.");
+
 namespace y2022 {
 namespace wpilib {
 namespace {
@@ -178,8 +180,15 @@
     imu_yaw_rate_input_ = ::std::move(sensor);
     imu_yaw_rate_reader_.set_input(imu_yaw_rate_input_.get());
   }
+  void set_catapult_falcon_1(
+      ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> t1,
+      ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> t2) {
+    catapult_falcon_1_can_ = ::std::move(t1);
+    catapult_falcon_2_can_ = ::std::move(t2);
+  }
 
   void RunIteration() override {
+    superstructure_reading_->Set(true);
     {
       auto builder = superstructure_position_sender_.MakeBuilder();
 
@@ -342,6 +351,13 @@
     flipper_arm_right_potentiometer_ = ::std::move(potentiometer);
   }
 
+  std::shared_ptr<frc::DigitalOutput> superstructure_reading_;
+
+  void set_superstructure_reading(
+      std::shared_ptr<frc::DigitalOutput> superstructure_reading) {
+    superstructure_reading_ = superstructure_reading;
+  }
+
   void set_intake_encoder_front(::std::unique_ptr<frc::Encoder> encoder) {
     fast_encoder_filter_.Add(encoder.get());
     intake_encoder_front_.set_encoder(::std::move(encoder));
@@ -418,6 +434,9 @@
       intake_encoder_back_, turret_encoder_, catapult_encoder_;
 
   frc971::wpilib::DMAPulseWidthReader imu_heading_reader_, imu_yaw_rate_reader_;
+
+  ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX>
+      catapult_falcon_1_can_, catapult_falcon_2_can_;
 };
 
 class SuperstructureWriter
@@ -425,7 +444,8 @@
  public:
   SuperstructureWriter(aos::EventLoop *event_loop)
       : frc971::wpilib::LoopOutputHandler<superstructure::Output>(
-            event_loop, "/superstructure") {}
+            event_loop, "/superstructure"),
+        catapult_reversal_(make_unique<frc::DigitalOutput>(0)) {}
 
   void set_climber_falcon(std::unique_ptr<frc::TalonFX> t) {
     climber_falcon_ = std::move(t);
@@ -439,6 +459,31 @@
     catapult_falcon_1_ = ::std::move(t);
   }
 
+  void set_catapult_falcon_1(
+      ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> t1,
+      ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> t2) {
+    catapult_falcon_1_can_ = ::std::move(t1);
+    catapult_falcon_2_can_ = ::std::move(t2);
+
+    for (auto &falcon : {catapult_falcon_1_can_, catapult_falcon_2_can_}) {
+      falcon->ConfigSupplyCurrentLimit(
+          {false, Values::kIntakeRollerSupplyCurrentLimit(),
+           Values::kIntakeRollerSupplyCurrentLimit(), 0});
+      falcon->ConfigStatorCurrentLimit(
+          {false, Values::kIntakeRollerStatorCurrentLimit(),
+           Values::kIntakeRollerStatorCurrentLimit(), 0});
+      falcon->SetStatusFramePeriod(
+          ctre::phoenix::motorcontrol::Status_1_General, 1);
+      falcon->SetControlFramePeriod(
+          ctre::phoenix::motorcontrol::Control_3_General, 1);
+      falcon->SetStatusFramePeriod(
+          ctre::phoenix::motorcontrol::Status_Brushless_Current, 50);
+      falcon->ConfigOpenloopRamp(0.0);
+      falcon->ConfigClosedloopRamp(0.0);
+      falcon->ConfigVoltageMeasurementFilter(1);
+    }
+  }
+
   void set_intake_falcon_front(::std::unique_ptr<frc::TalonFX> t) {
     intake_falcon_front_ = ::std::move(t);
   }
@@ -485,12 +530,15 @@
     return flipper_arms_falcon_;
   }
 
-  void set_transfer_roller_victor_front(::std::unique_ptr<::frc::VictorSP> t) {
-    transfer_roller_victor_front_ = ::std::move(t);
+  void set_transfer_roller_victor(::std::unique_ptr<::frc::VictorSP> t) {
+    transfer_roller_victor_ = ::std::move(t);
   }
 
-  void set_transfer_roller_victor_back(::std::unique_ptr<::frc::VictorSP> t) {
-    transfer_roller_victor_back_ = ::std::move(t);
+  std::shared_ptr<frc::DigitalOutput> superstructure_reading_;
+
+  void set_superstructure_reading(
+      std::shared_ptr<frc::DigitalOutput> superstructure_reading) {
+    superstructure_reading_ = superstructure_reading;
   }
 
  private:
@@ -505,9 +553,16 @@
         ctre::phoenix::motorcontrol::ControlMode::Disabled, 0);
     intake_falcon_front_->SetDisabled();
     intake_falcon_back_->SetDisabled();
-    transfer_roller_victor_front_->SetDisabled();
-    transfer_roller_victor_back_->SetDisabled();
-    catapult_falcon_1_->SetDisabled();
+    transfer_roller_victor_->SetDisabled();
+    if (catapult_falcon_1_) {
+      catapult_falcon_1_->SetDisabled();
+    }
+    if (catapult_falcon_1_can_) {
+      catapult_falcon_1_can_->Set(
+          ctre::phoenix::motorcontrol::ControlMode::Disabled, 0);
+      catapult_falcon_2_can_->Set(
+          ctre::phoenix::motorcontrol::ControlMode::Disabled, 0);
+    }
     turret_falcon_->SetDisabled();
   }
 
@@ -518,14 +573,23 @@
     WritePwm(output.intake_voltage_back(), intake_falcon_back_.get());
     WriteCan(output.roller_voltage_front(), roller_falcon_front_.get());
     WriteCan(output.roller_voltage_back(), roller_falcon_back_.get());
-    WritePwm(output.transfer_roller_voltage_front(),
-             transfer_roller_victor_front_.get());
-    WritePwm(-output.transfer_roller_voltage_back(),
-             transfer_roller_victor_back_.get());
+    WritePwm(output.transfer_roller_voltage(), transfer_roller_victor_.get());
 
     WriteCan(-output.flipper_arms_voltage(), flipper_arms_falcon_.get());
 
-    WritePwm(output.catapult_voltage(), catapult_falcon_1_.get());
+    if (catapult_falcon_1_) {
+      WritePwm(output.catapult_voltage(), catapult_falcon_1_.get());
+      superstructure_reading_->Set(false);
+      if (output.catapult_voltage() > 0) {
+        catapult_reversal_->Set(true);
+      } else {
+        catapult_reversal_->Set(false);
+      }
+    }
+    if (catapult_falcon_1_can_) {
+      WriteCan(output.catapult_voltage(), catapult_falcon_1_can_.get());
+      WriteCan(output.catapult_voltage(), catapult_falcon_2_can_.get());
+    }
 
     WritePwm(-output.turret_voltage(), turret_falcon_.get());
   }
@@ -551,10 +615,14 @@
   ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX>
       flipper_arms_falcon_;
 
+  ::std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX>
+      catapult_falcon_1_can_, catapult_falcon_2_can_;
+
   ::std::unique_ptr<::frc::TalonFX> turret_falcon_, catapult_falcon_1_,
       climber_falcon_;
-  ::std::unique_ptr<::frc::VictorSP> transfer_roller_victor_front_,
-      transfer_roller_victor_back_;
+  ::std::unique_ptr<::frc::VictorSP> transfer_roller_victor_;
+
+  std::unique_ptr<frc::DigitalOutput> catapult_reversal_;
 };
 
 class CANSensorReader {
@@ -626,11 +694,16 @@
     ::frc971::wpilib::PDPFetcher pdp_fetcher(&pdp_fetcher_event_loop);
     AddLoop(&pdp_fetcher_event_loop);
 
+    std::shared_ptr<frc::DigitalOutput> superstructure_reading =
+        make_unique<frc::DigitalOutput>(25);
+
     // Thread 3.
     ::aos::ShmEventLoop sensor_reader_event_loop(&config.message());
     SensorReader sensor_reader(&sensor_reader_event_loop, values);
+    sensor_reader.set_pwm_trigger(true);
     sensor_reader.set_drivetrain_left_encoder(make_encoder(1));
     sensor_reader.set_drivetrain_right_encoder(make_encoder(0));
+    sensor_reader.set_superstructure_reading(superstructure_reading);
 
     sensor_reader.set_intake_encoder_front(make_encoder(3));
     sensor_reader.set_intake_front_absolute_pwm(
@@ -688,9 +761,7 @@
     superstructure_writer.set_roller_falcon_back(
         make_unique<::ctre::phoenix::motorcontrol::can::TalonFX>(1));
 
-    superstructure_writer.set_transfer_roller_victor_front(
-        make_unique<::frc::VictorSP>(6));
-    superstructure_writer.set_transfer_roller_victor_back(
+    superstructure_writer.set_transfer_roller_victor(
         make_unique<::frc::VictorSP>(5));
 
     superstructure_writer.set_intake_falcon_front(make_unique<frc::TalonFX>(2));
@@ -698,8 +769,20 @@
     superstructure_writer.set_climber_falcon(make_unique<frc::TalonFX>(8));
     superstructure_writer.set_flipper_arms_falcon(
         make_unique<::ctre::phoenix::motorcontrol::can::TalonFX>(2));
+    superstructure_writer.set_superstructure_reading(superstructure_reading);
 
-    superstructure_writer.set_catapult_falcon_1(make_unique<::frc::TalonFX>(9));
+    if (!FLAGS_can_catapult) {
+      superstructure_writer.set_catapult_falcon_1(make_unique<frc::TalonFX>(9));
+    } else {
+      std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> catapult1 =
+          make_unique<::ctre::phoenix::motorcontrol::can::TalonFX>(3,
+                                                                   "Catapult");
+      std::shared_ptr<::ctre::phoenix::motorcontrol::can::TalonFX> catapult2 =
+          make_unique<::ctre::phoenix::motorcontrol::can::TalonFX>(4,
+                                                                   "Catapult");
+      superstructure_writer.set_catapult_falcon_1(catapult1, catapult2);
+      sensor_reader.set_catapult_falcon_1(catapult1, catapult2);
+    }
 
     AddLoop(&output_event_loop);
 
diff --git a/y2022/www/field_handler.ts b/y2022/www/field_handler.ts
index 63e73c7..6e0f08d 100644
--- a/y2022/www/field_handler.ts
+++ b/y2022/www/field_handler.ts
@@ -278,37 +278,13 @@
 
     // Draw the matches with debugging information from the localizer.
     const now = Date.now() / 1000.0;
-    for (const [time, value] of this.localizerImageMatches) {
-      const age = now - time;
-      const kRemovalAge = 2.0;
-      if (age > kRemovalAge) {
-        this.localizerImageMatches.delete(time);
-        continue;
-      }
-      const ageAlpha = (kRemovalAge - age) / kRemovalAge
-      for (let i = 0; i < value.targetsLength(); i++) {
-        const imageDebug = value.targets(i);
-        const x = imageDebug.impliedRobotX();
-        const y = imageDebug.impliedRobotY();
-        const theta = imageDebug.impliedRobotTheta();
-        const cameraX = imageDebug.cameraX();
-        const cameraY = imageDebug.cameraY();
-        const cameraTheta = imageDebug.cameraTheta();
-        const accepted = imageDebug.accepted();
-        // Make camera readings fade over time.
-        const alpha = Math.round(255 * ageAlpha).toString(16).padStart(2, '0');
-        const dashed = false;
-        const acceptedRgb = accepted ? '#00FF00' : '#FF0000';
-        const acceptedRgba = acceptedRgb + alpha;
-        const cameraRgb = PI_COLORS[imageDebug.camera()];
-        const cameraRgba = cameraRgb + alpha;
-        this.drawRobot(x, y, theta, null, acceptedRgba, dashed, false);
-        this.drawCamera(cameraX, cameraY, cameraTheta, cameraRgba, false);
-      }
-    }
     if (this.superstructureStatus) {
       this.shotDistance.innerHTML = this.superstructureStatus.aimer() ?
-          this.superstructureStatus.aimer().shotDistance().toFixed(2) :
+          (this.superstructureStatus.aimer().shotDistance() /
+           0.0254).toFixed(2) +
+              'in, ' +
+              this.superstructureStatus.aimer().shotDistance().toFixed(2) +
+              'm' :
           'NA';
 
       this.fire.innerHTML = this.superstructureStatus.fire() ? 'true' : 'false';
@@ -418,6 +394,36 @@
               null);
     }
 
+    for (const [time, value] of this.localizerImageMatches) {
+      const age = now - time;
+      const kRemovalAge = 1.0;
+      if (age > kRemovalAge) {
+        this.localizerImageMatches.delete(time);
+        continue;
+      }
+      const kMaxImageAlpha = 0.5;
+      const ageAlpha = kMaxImageAlpha * (kRemovalAge - age) / kRemovalAge
+      for (let i = 0; i < value.targetsLength(); i++) {
+        const imageDebug = value.targets(i);
+        const x = imageDebug.impliedRobotX();
+        const y = imageDebug.impliedRobotY();
+        const theta = imageDebug.impliedRobotTheta();
+        const cameraX = imageDebug.cameraX();
+        const cameraY = imageDebug.cameraY();
+        const cameraTheta = imageDebug.cameraTheta();
+        const accepted = imageDebug.accepted();
+        // Make camera readings fade over time.
+        const alpha = Math.round(255 * ageAlpha).toString(16).padStart(2, '0');
+        const dashed = false;
+        const acceptedRgb = accepted ? '#00FF00' : '#FF0000';
+        const acceptedRgba = acceptedRgb + alpha;
+        const cameraRgb = PI_COLORS[imageDebug.camera()];
+        const cameraRgba = cameraRgb + alpha;
+        this.drawRobot(x, y, theta, null, acceptedRgba, dashed, false);
+        this.drawCamera(cameraX, cameraY, cameraTheta, cameraRgba, false);
+      }
+    }
+
     window.requestAnimationFrame(() => this.draw());
   }
 
diff --git a/y2022/y2022_imu.json b/y2022/y2022_imu.json
index 06a7955..0e61210 100644
--- a/y2022/y2022_imu.json
+++ b/y2022/y2022_imu.json
@@ -19,41 +19,95 @@
       "name": "/imu/aos",
       "type": "aos.starter.Status",
       "source_node": "imu",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
       "frequency": 50,
       "num_senders": 20,
+      "logger_nodes": [
+        "roborio",
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "roborio",
           "priority": 5,
-          "time_to_live": 5000000
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
         },
         {
           "name": "logger",
           "priority": 5,
-          "time_to_live": 5000000
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
         }
       ]
     },
     {
+      "name": "/imu/aos/remote_timestamps/roborio/imu/aos/aos-starter-Status",
+      "type": "aos.message_bridge.RemoteMessage",
+      "frequency": 100,
+      "source_node": "imu",
+      "max_size": 208
+    },
+    {
+      "name": "/imu/aos/remote_timestamps/logger/imu/aos/aos-starter-Status",
+      "type": "aos.message_bridge.RemoteMessage",
+      "frequency": 100,
+      "source_node": "imu",
+      "max_size": 208
+    },
+    {
       "name": "/imu/aos",
       "type": "aos.starter.StarterRpc",
       "source_node": "imu",
       "frequency": 10,
       "num_senders": 2,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio",
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "roborio",
           "priority": 5,
-          "time_to_live": 5000000
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
         },
         {
           "name": "logger",
           "priority": 5,
-          "time_to_live": 5000000
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
         }
       ]
     },
     {
+      "name": "/imu/aos/remote_timestamps/roborio/imu/aos/aos-starter-StarterRpc",
+      "type": "aos.message_bridge.RemoteMessage",
+      "frequency": 20,
+      "source_node": "imu",
+      "max_size": 208
+    },
+    {
+      "name": "/imu/aos/remote_timestamps/logger/imu/aos/aos-starter-StarterRpc",
+      "type": "aos.message_bridge.RemoteMessage",
+      "frequency": 20,
+      "source_node": "imu",
+      "max_size": 208
+    },
+    {
       "name": "/imu/aos",
       "type": "aos.message_bridge.ServerStatistics",
       "source_node": "imu",
@@ -64,7 +118,7 @@
       "name": "/imu/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "imu",
-      "frequency": 10,
+      "frequency": 20,
       "num_senders": 2
     },
     {
@@ -118,6 +172,10 @@
       "name": "/logger/aos",
       "type": "aos.starter.StarterRpc",
       "source_node": "logger",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "imu"
+      ],
       "destination_nodes": [
         {
           "name": "imu",
@@ -143,6 +201,10 @@
       "name": "/logger/aos",
       "type": "aos.starter.Status",
       "source_node": "logger",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "imu"
+      ],
       "destination_nodes": [
         {
           "name": "imu",
@@ -168,6 +230,10 @@
       "name": "/roborio/aos",
       "type": "aos.starter.StarterRpc",
       "source_node": "roborio",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "imu"
+      ],
       "destination_nodes": [
         {
           "name": "imu",
@@ -226,50 +292,17 @@
       "max_size": 2000
     },
     {
-      "name": "/imu/aos/remote_timestamps/logger/localizer/frc971-controls-LocalizerStatus",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "imu",
-      "logger": "NOT_LOGGED",
-      "frequency": 2200,
-      "num_senders": 2,
-      "max_size": 200
-    },
-    {
       "name": "/localizer",
       "type": "frc971.controls.LocalizerVisualization",
       "source_node": "imu",
       "frequency": 200,
-      "max_size": 2000,
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_nodes": [
-        "logger"
-      ],
-      "destination_nodes": [
-        {
-          "name": "logger",
-          "priority": 5,
-          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
-          "timestamp_logger_nodes": [
-            "imu"
-          ],
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/imu/aos/remote_timestamps/logger/localizer/frc971-controls-LocalizerVisualization",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "imu",
-      "logger": "NOT_LOGGED",
-      "frequency": 200,
-      "num_senders": 2,
-      "max_size": 200
+      "max_size": 2000
     },
     {
       "name": "/localizer",
       "type": "frc971.controls.LocalizerOutput",
       "source_node": "imu",
-      "frequency": 200,
+      "frequency": 400,
       "max_size": 200,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
@@ -302,7 +335,7 @@
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "imu",
       "logger": "NOT_LOGGED",
-      "frequency": 200,
+      "frequency": 400,
       "num_senders": 2,
       "max_size": 200
     },
@@ -321,31 +354,7 @@
       "source_node": "imu",
       "frequency": 2200,
       "max_size": 1600,
-      "num_senders": 2,
-      "logger": "LOCAL_AND_REMOTE_LOGGER",
-      "logger_nodes": [
-        "logger"
-      ],
-      "destination_nodes": [
-        {
-          "name": "logger",
-          "priority": 5,
-          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
-          "timestamp_logger_nodes": [
-            "imu"
-          ],
-          "time_to_live": 5000000
-        }
-      ]
-    },
-    {
-      "name": "/imu/aos/remote_timestamps/logger/localizer/frc971-IMUValuesBatch",
-      "type": "aos.message_bridge.RemoteMessage",
-      "source_node": "imu",
-      "logger": "NOT_LOGGED",
-      "frequency": 2200,
-      "num_senders": 2,
-      "max_size": 200
+      "num_senders": 2
     }
   ],
   "applications": [
diff --git a/y2022/y2022_logger.json b/y2022/y2022_logger.json
index df1d56e..f54ccd7 100644
--- a/y2022/y2022_logger.json
+++ b/y2022/y2022_logger.json
@@ -19,6 +19,38 @@
       ]
    },
     {
+      "name": "/superstructure",
+      "type": "y2022.vision.BallColor",
+      "source_node": "logger",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio"
+      ],
+      "frequency": 200,
+      "num_senders": 2,
+      "max_size": 72,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 2,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ],
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/aos/remote_timestamps/roborio/superstructure/y2022-vision-BallColor",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "logger",
+      "logger": "NOT_LOGGED",
+      "frequency": 20,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Position",
       "source_node": "roborio",
@@ -30,11 +62,24 @@
         {
           "name": "logger",
           "priority": 2,
-          "time_to_live": 500000000
+          "time_to_live": 500000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ]
         }
       ]
     },
     {
+      "name": "/roborio/aos/remote_timestamps/logger/drivetrain/frc971-control_loops-drivetrain-Position",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "roborio",
+      "logger": "NOT_LOGGED",
+      "frequency": 400,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Output",
       "source_node": "roborio",
@@ -46,11 +91,24 @@
         {
           "name": "logger",
           "priority": 2,
-          "time_to_live": 500000000
+          "time_to_live": 500000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ]
         }
       ]
     },
     {
+      "name": "/roborio/aos/remote_timestamps/logger/drivetrain/frc971-control_loops-drivetrain-Output",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "roborio",
+      "logger": "NOT_LOGGED",
+      "frequency": 400,
+      "num_senders": 2,
+      "max_size": 400
+    },
+    {
       "name": "/pi1/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "pi1",
@@ -140,7 +198,7 @@
       "name": "/logger/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "logger",
-      "frequency": 10,
+      "frequency": 20,
       "max_size": 2000,
       "num_senders": 2
     },
@@ -148,8 +206,13 @@
       "name": "/logger/aos",
       "type": "aos.starter.Status",
       "source_node": "logger",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
       "frequency": 50,
       "num_senders": 20,
+      "max_size": 2000,
+      "logger_nodes": [
+        "roborio"
+      ],
       "destination_nodes": [
         {
           "name": "roborio",
@@ -175,6 +238,10 @@
       "name": "/logger/aos",
       "type": "aos.starter.StarterRpc",
       "source_node": "logger",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio"
+      ],
       "frequency": 10,
       "num_senders": 2,
       "destination_nodes": [
@@ -322,6 +389,14 @@
       "max_size": 200
     },
     {
+      "name": "/logger/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "logger",
+      "frequency": 100,
+      "max_size": 620000,
+      "num_senders": 1
+    },
+    {
       "name": "/pi1/camera/decimated",
       "type": "frc971.vision.CameraImage",
       "source_node": "pi1",
@@ -395,7 +470,17 @@
       "rename": {
         "name": "/logger/aos"
       }
+    },
+    {
+      "match": {
+        "name": "/camera",
+        "source_node": "logger"
+      },
+      "rename": {
+        "name": "/logger/camera"
+      }
     }
+
   ],
   "applications": [
     {
@@ -421,6 +506,20 @@
       "nodes": [
         "logger"
       ]
+    },
+    {
+      "name": "image_streamer",
+      "executable_name": "image_streamer_start.sh",
+      "nodes": [
+        "logger"
+      ]
+    },
+    {
+      "name": "ball_color_detector",
+      "executable_name": "ball_color_detector",
+      "nodes": [
+        "logger"
+      ]
     }
   ],
   "nodes": [
diff --git a/y2022/y2022_pi_template.json b/y2022/y2022_pi_template.json
index 3dff81e..bcd3f6b 100644
--- a/y2022/y2022_pi_template.json
+++ b/y2022/y2022_pi_template.json
@@ -21,6 +21,12 @@
       "source_node": "pi{{ NUM }}",
       "frequency": 50,
       "num_senders": 20,
+      "max_size": 2000,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio",
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "roborio",
@@ -40,6 +46,11 @@
       "source_node": "pi{{ NUM }}",
       "frequency": 10,
       "num_senders": 2,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "roborio",
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "roborio",
@@ -64,7 +75,7 @@
       "name": "/pi{{ NUM }}/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "pi{{ NUM }}",
-      "frequency": 10,
+      "frequency": 20,
       "num_senders": 2
     },
     {
@@ -118,6 +129,10 @@
       "name": "/imu/aos",
       "type": "aos.message_bridge.Timestamp",
       "source_node": "imu",
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "pi{{ NUM }}"
+      ],
       "destination_nodes": [
         {
           "name": "pi{{ NUM }}",
@@ -166,7 +181,7 @@
       "source_node": "pi{{ NUM }}",
       "frequency": 25,
       "num_senders": 2,
-      "max_size": 20000,
+      "max_size": 40000,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
         "imu",
@@ -196,14 +211,14 @@
     {
       "name": "/pi{{ NUM }}/aos/remote_timestamps/imu/pi{{ NUM }}/camera/y2022-vision-TargetEstimate",
       "type": "aos.message_bridge.RemoteMessage",
-      "frequency": 20,
+      "frequency": 40,
       "source_node": "pi{{ NUM }}",
       "max_size": 208
     },
     {
       "name": "/pi{{ NUM }}/aos/remote_timestamps/logger/pi{{ NUM }}/camera/y2022-vision-TargetEstimate",
       "type": "aos.message_bridge.RemoteMessage",
-      "frequency": 20,
+      "frequency": 40,
       "source_node": "pi{{ NUM }}",
       "max_size": 208
     },
@@ -215,11 +230,24 @@
         {
           "name": "pi{{ NUM }}",
           "priority": 5,
-          "time_to_live": 5000000
+          "time_to_live": 5000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "imu"
+          ]
         }
       ]
     },
     {
+      "name": "/imu/aos/remote_timestamps/pi{{ NUM }}/localizer/frc971-controls-LocalizerOutput",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "imu",
+      "logger": "NOT_LOGGED",
+      "frequency": 400,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
       "name": "/logger/aos",
       "type": "aos.starter.StarterRpc",
       "source_node": "logger",
@@ -324,6 +352,7 @@
     {
       "name": "message_bridge_client",
       "executable_name": "message_bridge_client",
+      "args": ["--rt_priority=16"],
       "nodes": [
         "pi{{ NUM }}"
       ]
diff --git a/y2022/y2022_roborio.json b/y2022/y2022_roborio.json
index 6e14bae..8651923 100644
--- a/y2022/y2022_roborio.json
+++ b/y2022/y2022_roborio.json
@@ -4,7 +4,8 @@
       "name": "/roborio/aos",
       "type": "aos.JoystickState",
       "source_node": "roborio",
-      "frequency": 75,
+      "frequency": 100,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes" : [
         "imu"
       ],
@@ -12,11 +13,24 @@
         {
           "name": "imu",
           "priority": 5,
-          "time_to_live": 50000000
+          "time_to_live": 50000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ]
         }
       ]
     },
     {
+      "name": "/roborio/aos/remote_timestamps/imu/roborio/aos/aos-JoystickState",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "roborio",
+      "logger": "NOT_LOGGED",
+      "frequency": 200,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
       "name": "/roborio/aos",
       "type": "aos.RobotState",
       "source_node": "roborio",
@@ -44,6 +58,11 @@
       "source_node": "roborio",
       "frequency": 50,
       "num_senders": 20,
+      "max_size": 2000,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "logger",
@@ -72,6 +91,10 @@
       "frequency": 10,
       "max_size": 400,
       "num_senders": 2,
+      "logger": "LOCAL_AND_REMOTE_LOGGER",
+      "logger_nodes": [
+        "logger"
+      ],
       "destination_nodes": [
         {
           "name": "logger",
@@ -104,7 +127,7 @@
       "name": "/roborio/aos",
       "type": "aos.message_bridge.ClientStatistics",
       "source_node": "roborio",
-      "frequency": 15,
+      "frequency": 20,
       "max_size": 2000,
       "num_senders": 2
     },
@@ -217,7 +240,7 @@
       "name": "/superstructure",
       "type": "y2022.control_loops.superstructure.Status",
       "source_node": "roborio",
-      "frequency": 200,
+      "frequency": 400,
       "num_senders": 2,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
       "logger_nodes": [
@@ -228,16 +251,42 @@
         {
           "name": "imu",
           "priority": 5,
-          "time_to_live": 50000000
+          "time_to_live": 50000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ]
         },
         {
           "name": "logger",
           "priority": 5,
-          "time_to_live": 50000000
+          "time_to_live": 50000000,
+          "timestamp_logger": "LOCAL_AND_REMOTE_LOGGER",
+          "timestamp_logger_nodes": [
+            "roborio"
+          ]
         }
       ]
     },
     {
+      "name": "/roborio/aos/remote_timestamps/imu/superstructure/y2022-control_loops-superstructure-Status",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "roborio",
+      "logger": "NOT_LOGGED",
+      "frequency": 400,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
+      "name": "/roborio/aos/remote_timestamps/logger/superstructure/y2022-control_loops-superstructure-Status",
+      "type": "aos.message_bridge.RemoteMessage",
+      "source_node": "roborio",
+      "logger": "NOT_LOGGED",
+      "frequency": 400,
+      "num_senders": 2,
+      "max_size": 200
+    },
+    {
       "name": "/superstructure",
       "type": "y2022.control_loops.superstructure.Output",
       "source_node": "roborio",
@@ -268,6 +317,21 @@
       "num_senders": 2
     },
     {
+      "name": "/superstructure",
+      "type": "y2022.vision.BallColor",
+      "source_node": "logger",
+      "frequency": 200,
+      "num_senders": 2,
+      "max_size": 72,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 2,
+          "time_to_live": 500000000
+        }
+      ]
+    },
+    {
       "name": "/drivetrain",
       "type": "frc971.sensors.GyroReading",
       "source_node": "roborio",
@@ -309,7 +373,7 @@
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Position",
       "source_node": "roborio",
-      "frequency": 200,
+      "frequency": 400,
       "max_size": 112,
       "num_senders": 2
     },
@@ -317,7 +381,7 @@
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Output",
       "source_node": "roborio",
-      "frequency": 200,
+      "frequency": 400,
       "max_size": 80,
       "num_senders": 2,
       "logger": "LOCAL_AND_REMOTE_LOGGER",
@@ -341,7 +405,7 @@
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "roborio",
       "logger": "NOT_LOGGED",
-      "frequency": 200,
+      "frequency": 400,
       "num_senders": 2,
       "max_size": 200
     },
@@ -349,7 +413,7 @@
       "name": "/drivetrain",
       "type": "frc971.control_loops.drivetrain.Status",
       "source_node": "roborio",
-      "frequency": 200,
+      "frequency": 400,
       "max_size": 1616,
       "num_senders": 2
     },
@@ -380,7 +444,7 @@
       "type": "aos.message_bridge.RemoteMessage",
       "source_node": "roborio",
       "logger": "NOT_LOGGED",
-      "frequency": 200,
+      "frequency": 400,
       "num_senders": 2,
       "max_size": 200
     },
@@ -471,8 +535,8 @@
       ]
     },
     {
-      "name": "message_bridge_client",
-      "executable_name": "message_bridge_client",
+      "name": "roborio_message_bridge_client",
+      "executable_name": "message_bridge_client.sh",
       "args": ["--rt_priority=16"],
       "nodes": [
         "roborio"