Move generic packages from frc971/analysis to aos

To eliminate a dependency of aos on frc971.

Signed-off-by: Stephan Pleines <pleines.stephan@gmail.com>
Change-Id: Ic4a8f75da29f8e8c6675d96f02824cd08a9b99be
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index 93f50f4..b179ad9 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -1,37 +1,7 @@
-load("//aos/flatbuffers:generate.bzl", "static_flatbuffer")
 load("//tools/build_rules:js.bzl", "rollup_bundle", "ts_project")
-load("@com_github_google_flatbuffers//:typescript.bzl", "flatbuffer_ts_library")
-load("//aos:config.bzl", "aos_config")
 
 package(default_visibility = ["//visibility:public"])
 
-cc_binary(
-    name = "py_log_reader.so",
-    srcs = ["py_log_reader.cc"],
-    linkshared = True,
-    target_compatible_with = ["@platforms//os:linux"],
-    deps = [
-        "//aos:configuration",
-        "//aos:json_to_flatbuffer",
-        "//aos/events:shm_event_loop",
-        "//aos/events:simulated_event_loop",
-        "//aos/events/logging:log_reader",
-        "//third_party/python",
-        "@com_github_google_glog//:glog",
-    ],
-)
-
-py_test(
-    name = "log_reader_test",
-    srcs = ["log_reader_test.py"],
-    data = [
-        ":py_log_reader.so",
-        "@sample_logfile//file",
-    ],
-    target_compatible_with = ["@platforms//os:linux"],
-    deps = ["//aos:configuration_fbs_python"],
-)
-
 ts_project(
     name = "plot_index",
     srcs = ["plot_index.ts"],
@@ -73,15 +43,6 @@
     ],
 )
 
-genrule(
-    name = "copy_css",
-    srcs = [
-        "//aos/network/www:styles.css",
-    ],
-    outs = ["styles.css"],
-    cmd = "cp $< $@",
-)
-
 filegroup(
     name = "plotter_files",
     srcs = [
@@ -112,67 +73,13 @@
     target_compatible_with = ["@platforms//os:linux"],
 )
 
-static_flatbuffer(
-    name = "plot_data_fbs",
+genrule(
+    name = "copy_css",
     srcs = [
-        "plot_data.fbs",
+        "//aos/network/www:styles.css",
     ],
-    target_compatible_with = ["@platforms//os:linux"],
-)
-
-flatbuffer_ts_library(
-    name = "plot_data_ts_fbs",
-    srcs = [
-        "plot_data.fbs",
-    ],
-    target_compatible_with = ["@platforms//os:linux"],
-)
-
-ts_project(
-    name = "plot_data_utils",
-    srcs = ["plot_data_utils.ts"],
-    visibility = ["//visibility:public"],
-    deps = [
-        ":plot_data_ts_fbs",
-        "//aos:configuration_ts_fbs",
-        "//aos/network/www:aos_plotter",
-        "//aos/network/www:plotter",
-        "//aos/network/www:proxy",
-        "@com_github_google_flatbuffers//reflection:reflection_ts_fbs",
-        "@com_github_google_flatbuffers//ts:flatbuffers_ts",
-    ],
-)
-
-aos_config(
-    name = "plotter",
-    src = "plotter_config.json",
-    flatbuffers = [":plot_data_fbs"],
-    target_compatible_with = ["@platforms//os:linux"],
-    deps = ["//aos/events:aos_config"],
-)
-
-cc_library(
-    name = "in_process_plotter",
-    srcs = ["in_process_plotter.cc"],
-    hdrs = ["in_process_plotter.h"],
-    data = [
-        ":plotter",
-        "//frc971/analysis/cpp_plot:cpp_plot_files",
-    ],
-    deps = [
-        ":plot_data_fbs",
-        "//aos/events:simulated_event_loop",
-        "//aos/network:web_proxy",
-    ],
-)
-
-cc_binary(
-    name = "in_process_plotter_demo",
-    srcs = ["in_process_plotter_demo.cc"],
-    deps = [
-        ":in_process_plotter",
-        "//aos:init",
-    ],
+    outs = ["styles.css"],
+    cmd = "cp $< $@",
 )
 
 cc_binary(
@@ -204,18 +111,6 @@
     ],
 )
 
-cc_binary(
-    name = "local_foxglove",
-    srcs = ["local_foxglove.cc"],
-    data = ["@foxglove_studio"],
-    deps = [
-        "//aos:init",
-        "//aos/network:gen_embedded",
-        "//aos/seasocks:seasocks_logger",
-        "//third_party/seasocks",
-    ],
-)
-
 py_binary(
     name = "trim_and_plot_foxglove",
     srcs = ["trim_and_plot_foxglove.py"],
diff --git a/frc971/analysis/cpp_plot/BUILD b/frc971/analysis/cpp_plot/BUILD
deleted file mode 100644
index 7f34afe..0000000
--- a/frc971/analysis/cpp_plot/BUILD
+++ /dev/null
@@ -1,42 +0,0 @@
-load("//tools/build_rules:js.bzl", "rollup_bundle", "ts_project")
-
-package(default_visibility = ["//visibility:public"])
-
-ts_project(
-    name = "cpp_plot",
-    srcs = ["cpp_plot.ts"],
-    target_compatible_with = ["@platforms//os:linux"],
-    deps = [
-        "//aos:configuration_ts_fbs",
-        "//aos/network/www:proxy",
-        "//frc971/analysis:plot_data_utils",
-    ],
-)
-
-rollup_bundle(
-    name = "cpp_plot_bundle",
-    entry_point = "cpp_plot.ts",
-    target_compatible_with = ["@platforms//os:linux"],
-    deps = [
-        ":cpp_plot",
-    ],
-)
-
-genrule(
-    name = "copy_css",
-    srcs = [
-        "//aos/network/www:styles.css",
-    ],
-    outs = ["styles.css"],
-    cmd = "cp $< $@",
-)
-
-filegroup(
-    name = "cpp_plot_files",
-    srcs = [
-        "cpp_plot_bundle.js",
-        "cpp_plot_bundle.min.js",
-        "index.html",
-        "styles.css",
-    ],
-)
diff --git a/frc971/analysis/cpp_plot/cpp_plot.ts b/frc971/analysis/cpp_plot/cpp_plot.ts
deleted file mode 100644
index 08e2b1c..0000000
--- a/frc971/analysis/cpp_plot/cpp_plot.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Plotter for the C++ in-process plotter.
-import {Configuration} from '../../../aos/configuration_generated';
-import {Connection} from '../../../aos/network/www/proxy';
-import {plotData} from '../plot_data_utils';
-
-const rootDiv = document.createElement('div');
-rootDiv.classList.add('aos_cpp_plot');
-document.body.appendChild(rootDiv);
-
-const conn = new Connection();
-conn.connect();
-
-conn.addConfigHandler((config: Configuration) => {
-  plotData(conn, rootDiv);
-});
diff --git a/frc971/analysis/cpp_plot/index.html b/frc971/analysis/cpp_plot/index.html
deleted file mode 100644
index fbb5199..0000000
--- a/frc971/analysis/cpp_plot/index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<html>
-  <head>
-    <script src="cpp_plot_bundle.min.js" defer></script>
-    <link rel="stylesheet" href="styles.css">
-  </head>
-  <body>
-  </body>
-</html>
-
diff --git a/frc971/analysis/in_process_plotter.cc b/frc971/analysis/in_process_plotter.cc
deleted file mode 100644
index b537aa4..0000000
--- a/frc971/analysis/in_process_plotter.cc
+++ /dev/null
@@ -1,170 +0,0 @@
-#include "frc971/analysis/in_process_plotter.h"
-
-#include "aos/configuration.h"
-
-namespace frc971::analysis {
-
-namespace {
-const char *kDataPath = "frc971/analysis/cpp_plot";
-const char *kConfigPath = "frc971/analysis/plotter.json";
-}  // namespace
-
-Plotter::Plotter()
-    : config_(aos::configuration::ReadConfig(kConfigPath)),
-      event_loop_factory_(&config_.message()),
-      event_loop_(event_loop_factory_.MakeEventLoop("plotter")),
-      plot_sender_(event_loop_->MakeSender<Plot>("/analysis")),
-      web_proxy_(event_loop_.get(), event_loop_factory_.scheduler_epoll(),
-                 aos::web_proxy::StoreHistory::kYes, -1),
-      builder_(plot_sender_.MakeBuilder()) {
-  web_proxy_.SetDataPath(kDataPath);
-  event_loop_->SkipTimingReport();
-
-  color_wheel_.emplace_back(ColorWheelColor{.name = "red", .color = {1, 0, 0}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "green", .color = {0, 1, 0}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "purple", .color = {0.54, 0.3, 0.75}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "blue", .color = {0, 0, 1}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "yellow", .color = {1, 1, 0}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "teal", .color = {0, 1, 1}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "pink", .color = {1, 0, 1}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "orange", .color = {1, 0.6, 0}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "brown", .color = {0.6, 0.3, 0}});
-  color_wheel_.emplace_back(
-      ColorWheelColor{.name = "white", .color = {1, 1, 1}});
-}
-
-void Plotter::Spin() {
-  // Set non-infinite replay rate to avoid pegging a full CPU.
-  event_loop_factory_.SetRealtimeReplayRate(1.0);
-  event_loop_factory_.Run();
-}
-
-void Plotter::Title(std::string_view title) {
-  title_ = builder_.fbb()->CreateString(title);
-}
-
-void Plotter::AddFigure(std::string_view title, double width, double height) {
-  MaybeFinishFigure();
-
-  if (!title.empty()) {
-    figure_title_ = builder_.fbb()->CreateString(title);
-  }
-
-  // For positioning, just stack figures vertically.
-  auto position_builder = builder_.MakeBuilder<Position>();
-  position_builder.add_top(next_top_);
-  position_builder.add_left(0);
-  position_builder.add_width(width);
-  position_builder.add_height(height);
-  position_ = position_builder.Finish();
-
-  next_top_ += height;
-}
-
-void Plotter::XLabel(std::string_view label) {
-  xlabel_ = builder_.fbb()->CreateString(label);
-}
-
-void Plotter::YLabel(std::string_view label) {
-  ylabel_ = builder_.fbb()->CreateString(label);
-}
-
-void Plotter::AddLine(const std::vector<double> &x,
-                      const std::vector<double> &y, LineOptions options) {
-  CHECK_EQ(x.size(), y.size()) << ": " << options.label;
-  CHECK(!position_.IsNull())
-      << "You must call AddFigure() before calling AddLine().";
-
-  flatbuffers::Offset<flatbuffers::String> label_offset;
-  if (!options.label.empty()) {
-    label_offset = builder_.fbb()->CreateString(options.label);
-  }
-
-  std::vector<Point> points;
-  for (size_t ii = 0; ii < x.size(); ++ii) {
-    points.emplace_back(x[ii], y[ii]);
-  }
-  const flatbuffers::Offset<flatbuffers::Vector<const Point *>> points_offset =
-      builder_.fbb()->CreateVectorOfStructs(points);
-
-  const Color *color;
-  if (options.color.empty()) {
-    color = &color_wheel_.at(color_wheel_position_).color;
-    color_wheel_position_ = (color_wheel_position_ + 1) % color_wheel_.size();
-  } else {
-    auto it = std::find_if(
-        color_wheel_.begin(), color_wheel_.end(),
-        [options_color = options.color](const ColorWheelColor &color) {
-          return color.name == options_color;
-        });
-    CHECK(it != color_wheel_.end()) << ": Failed to find " << options.color;
-    color = &(it->color);
-  }
-
-  LineStyle::Builder style_builder = builder_.MakeBuilder<LineStyle>();
-  if (options.line_style.find('*') != options.line_style.npos) {
-    style_builder.add_point_size(options.point_size);
-  } else {
-    style_builder.add_point_size(0.0);
-  }
-  style_builder.add_draw_line(options.line_style.find('-') !=
-                              options.line_style.npos);
-  const flatbuffers::Offset<LineStyle> style_offset = style_builder.Finish();
-
-  auto line_builder = builder_.MakeBuilder<Line>();
-  line_builder.add_label(label_offset);
-  line_builder.add_points(points_offset);
-  line_builder.add_color(color);
-  line_builder.add_style(style_offset);
-  lines_.push_back(line_builder.Finish());
-}
-
-void Plotter::MaybeFinishFigure() {
-  if (!lines_.empty()) {
-    const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Line>>>
-        lines_offset = builder_.fbb()->CreateVector(lines_);
-    auto figure_builder = builder_.MakeBuilder<Figure>();
-    figure_builder.add_title(figure_title_);
-    figure_builder.add_position(position_);
-    figure_builder.add_lines(lines_offset);
-    figure_builder.add_xlabel(xlabel_);
-    figure_builder.add_share_x_axis(share_x_axis_);
-    figure_builder.add_ylabel(ylabel_);
-    figures_.push_back(figure_builder.Finish());
-  }
-  lines_.clear();
-  figure_title_.o = 0;
-  xlabel_.o = 0;
-  ylabel_.o = 0;
-  position_.o = 0;
-  share_x_axis_ = false;
-  color_wheel_position_ = 0;
-}
-
-void Plotter::Publish() {
-  MaybeFinishFigure();
-  const flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<Figure>>>
-      figures_offset = builder_.fbb()->CreateVector(figures_);
-
-  auto plot_builder = builder_.MakeBuilder<Plot>();
-  plot_builder.add_title(title_);
-  plot_builder.add_figures(figures_offset);
-
-  CHECK_EQ(builder_.Send(plot_builder.Finish()), aos::RawSender::Error::kOk);
-
-  builder_ = plot_sender_.MakeBuilder();
-
-  title_.o = 0;
-  figures_.clear();
-  next_top_ = 0;
-}
-
-}  // namespace frc971::analysis
diff --git a/frc971/analysis/in_process_plotter.h b/frc971/analysis/in_process_plotter.h
deleted file mode 100644
index df81041..0000000
--- a/frc971/analysis/in_process_plotter.h
+++ /dev/null
@@ -1,99 +0,0 @@
-#ifndef FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
-#define FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
-
-#include <vector>
-
-#include "aos/events/simulated_event_loop.h"
-#include "aos/network/web_proxy.h"
-#include "frc971/analysis/plot_data_generated.h"
-
-namespace frc971::analysis {
-
-// This class wraps the WebProxy class to provide a convenient C++ interface to
-// dynamically generate plots.
-// Currently, the main useful interface that this provides is a matplotlib-like
-// interface--see in_process_plotter_demo.cc for sample usage. It doesn't
-// precisely follow matplotlib's conventions, but the basic style does mimic
-// matplotlib. Future iterations may ditch this in favor of a more modern
-// interface where we actually return handles for plots and lines and the such.
-//
-// Note that currently the port for the seb server is hard-coded to 8080, so
-// only one instance of the Plotter can be present at once.
-//
-// You must call Spin() for the web server to actually do anything helpful.
-class Plotter {
- public:
-  Plotter();
-
-  // matplotlib-like interface
-  // The basic pattern is:
-  // 1) Call Figure()
-  // 2) Set up the lines, labels, etc. for the figure.
-  // 3) Repeat 1-2 however many times.
-  // 4) Call Publish().
-  // 5) Repeat 1-5 however many times.
-  //
-  // Publish() actually pushes the figures that you setup to the web-page,
-  // either with an autogenerated title or the title set by Title(). All state
-  // is cleared (or should be cleared) by the call to Publish().
-
-  // Sets the title for the current set of plots; if you
-  void Title(std::string_view title);
-  void AddFigure(std::string_view title = "", double width = 0,
-                 double height = 0);
-  struct LineOptions {
-    std::string_view label = "";
-    std::string_view line_style = "*-";
-    std::string_view color = "";
-    double point_size = 3.0;
-  };
-
-  void AddLine(const std::vector<double> &x, const std::vector<double> &y,
-               std::string_view label) {
-    AddLine(x, y, LineOptions{.label = label});
-  }
-  void AddLine(const std::vector<double> &x, const std::vector<double> &y,
-               std::string_view label, std::string_view line_style) {
-    AddLine(x, y, LineOptions{.label = label, .line_style = line_style});
-  }
-  void AddLine(const std::vector<double> &x, const std::vector<double> &y,
-               LineOptions options);
-
-  void ShareXAxis(bool share) { share_x_axis_ = share; }
-  void XLabel(std::string_view label);
-  void YLabel(std::string_view label);
-  void Publish();
-
-  void Spin();
-
- private:
-  void MaybeFinishFigure();
-
-  aos::FlatbufferDetachedBuffer<aos::Configuration> config_;
-  aos::SimulatedEventLoopFactory event_loop_factory_;
-  std::unique_ptr<aos::EventLoop> event_loop_;
-  aos::Sender<Plot> plot_sender_;
-  aos::web_proxy::WebProxy web_proxy_;
-
-  aos::Sender<Plot>::Builder builder_;
-  flatbuffers::Offset<flatbuffers::String> title_;
-  flatbuffers::Offset<flatbuffers::String> figure_title_;
-  flatbuffers::Offset<flatbuffers::String> xlabel_;
-  flatbuffers::Offset<flatbuffers::String> ylabel_;
-  bool share_x_axis_ = false;
-  float next_top_ = 0;
-  flatbuffers::Offset<Position> position_;
-  std::vector<flatbuffers::Offset<Figure>> figures_;
-  std::vector<flatbuffers::Offset<Line>> lines_;
-
-  struct ColorWheelColor {
-    std::string name;
-    Color color;
-  };
-
-  size_t color_wheel_position_ = 0;
-  std::vector<ColorWheelColor> color_wheel_;
-};
-
-}  // namespace frc971::analysis
-#endif  // FRC971_ANALYSIS_IN_PROCESS_PLOTTER_H_
diff --git a/frc971/analysis/in_process_plotter_demo.cc b/frc971/analysis/in_process_plotter_demo.cc
deleted file mode 100644
index 99e0c1d..0000000
--- a/frc971/analysis/in_process_plotter_demo.cc
+++ /dev/null
@@ -1,41 +0,0 @@
-#include "aos/init.h"
-#include "frc971/analysis/in_process_plotter.h"
-
-// To run this example, do:
-// bazel run -c opt //frc971/analysis:in_process_plotter_demo
-// And then open localhost:8080, select "C++ Plotter" from the drop-down, and
-// then select "TITLE!" or "Trig Functions" from the second drop-down to see
-// each plot.
-int main(int argc, char *argv[]) {
-  aos::InitGoogle(&argc, &argv);
-  frc971::analysis::Plotter plotter;
-  plotter.Title("TITLE!");
-  plotter.AddFigure("Fig Foo");
-  plotter.ShareXAxis(true);
-  plotter.AddLine({1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, "y = x");
-  plotter.AddLine({5, 4, 3, 2, 1}, {1, 2, 3, 4, 5}, "y = -x");
-  plotter.YLabel("Y Axis");
-  plotter.AddFigure("Fig Bar");
-  plotter.ShareXAxis(true);
-  plotter.AddLine({1, 2, 3}, {3, 4, 5}, "y = x + 2");
-  plotter.XLabel("X Axis (Linked to both above plots)");
-  plotter.Publish();
-
-  plotter.Title("Trig Functions");
-
-  plotter.AddFigure("Sin & Cos");
-  std::vector<double> x;
-  std::vector<double> sinx;
-  std::vector<double> cosx;
-  constexpr int kNumPoints = 100000;
-  for (int ii = 0; ii < kNumPoints; ++ii) {
-    x.push_back(ii * 2 * M_PI / kNumPoints);
-    sinx.push_back(std::sin(x.back()));
-    cosx.push_back(std::cos(x.back()));
-  }
-  plotter.AddLine(x, sinx, "sin(x)");
-  plotter.AddLine(x, cosx, "cos(x)");
-  plotter.Publish();
-
-  plotter.Spin();
-}
diff --git a/frc971/analysis/local_foxglove.cc b/frc971/analysis/local_foxglove.cc
deleted file mode 100644
index ade56a5..0000000
--- a/frc971/analysis/local_foxglove.cc
+++ /dev/null
@@ -1,19 +0,0 @@
-#include "glog/logging.h"
-
-#include "aos/init.h"
-#include "aos/seasocks/seasocks_logger.h"
-#include "internal/Embedded.h"
-#include "seasocks/Server.h"
-
-DEFINE_string(data_path, "external/foxglove_studio",
-              "Path to foxglove studio files to serve.");
-DEFINE_uint32(port, 8000, "Port to serve files at.");
-
-int main(int argc, char *argv[]) {
-  aos::InitGoogle(&argc, &argv);
-  // Magic for seasocks.
-  findEmbeddedContent("");
-  ::seasocks::Server server(std::make_shared<aos::seasocks::SeasocksLogger>(
-      ::seasocks::Logger::Level::Info));
-  server.serve(FLAGS_data_path.c_str(), FLAGS_port);
-}
diff --git a/frc971/analysis/log_reader_test.py b/frc971/analysis/log_reader_test.py
deleted file mode 100644
index 7af08e6..0000000
--- a/frc971/analysis/log_reader_test.py
+++ /dev/null
@@ -1,115 +0,0 @@
-#!/usr/bin/python3
-import json
-import unittest
-
-from frc971.analysis.py_log_reader import LogReader
-
-
-class LogReaderTest(unittest.TestCase):
-
-    def setUp(self):
-        self.reader = LogReader("external/sample_logfile/file/log.fbs")
-        # A list of all the channels in the logfile--this is used to confirm that
-        # we did indeed read the config correctly.
-        self.all_channels = [
-            ("/aos", "aos.JoystickState"), ("/aos", "aos.RobotState"),
-            ("/aos", "aos.timing.Report"), ("/aos", "frc971.PDPValues"),
-            ("/aos", "frc971.wpilib.PneumaticsToLog"),
-            ("/autonomous", "aos.common.actions.Status"),
-            ("/autonomous", "frc971.autonomous.AutonomousMode"),
-            ("/autonomous", "frc971.autonomous.Goal"),
-            ("/camera", "y2019.CameraLog"),
-            ("/camera", "y2019.control_loops.drivetrain.CameraFrame"),
-            ("/drivetrain", "frc971.IMUValues"),
-            ("/drivetrain", "frc971.control_loops.drivetrain.Goal"),
-            ("/drivetrain",
-             "frc971.control_loops.drivetrain.LocalizerControl"),
-            ("/drivetrain", "frc971.control_loops.drivetrain.Output"),
-            ("/drivetrain", "frc971.control_loops.drivetrain.Position"),
-            ("/drivetrain", "frc971.control_loops.drivetrain.Status"),
-            ("/drivetrain", "frc971.sensors.GyroReading"),
-            ("/drivetrain",
-             "y2019.control_loops.drivetrain.TargetSelectorHint"),
-            ("/superstructure", "y2019.StatusLight"),
-            ("/superstructure", "y2019.control_loops.superstructure.Goal"),
-            ("/superstructure", "y2019.control_loops.superstructure.Output"),
-            ("/superstructure", "y2019.control_loops.superstructure.Position"),
-            ("/superstructure", "y2019.control_loops.superstructure.Status")
-        ]
-        # A channel that is known to have data on it which we will use for testing.
-        self.test_channel = ("/aos", "aos.timing.Report")
-        # A non-existent channel
-        self.bad_channel = ("/aos", "aos.timing.FooBar")
-
-    def test_do_nothing(self):
-        """Tests that we sanely handle doing nothing.
-
-        A previous iteration of the log reader seg faulted when doing this."""
-        pass
-
-    @unittest.skip("broken by flatbuffer upgrade")
-    def test_read_config(self):
-        """Tests that we can read the configuration from the logfile."""
-        config_bytes = self.reader.configuration()
-        config = Configuration.GetRootAsConfiguration(config_bytes, 0)
-
-        channel_set = set(self.all_channels)
-        for ii in range(config.ChannelsLength()):
-            channel = config.Channels(ii)
-            # Will raise KeyError if the channel does not exist
-            channel_set.remove((channel.Name().decode("utf-8"),
-                                channel.Type().decode("utf-8")))
-
-        self.assertEqual(0, len(channel_set))
-
-    def test_empty_process(self):
-        """Tests running process() without subscribing to anything succeeds."""
-        self.reader.process()
-        for channel in self.all_channels:
-            with self.assertRaises(ValueError) as context:
-                self.reader.get_data_for_channel(channel[0], channel[1])
-
-    def test_subscribe(self):
-        """Tests that we can subscribe to a channel and get data out."""
-        name = self.test_channel[0]
-        message_type = self.test_channel[1]
-        self.assertTrue(self.reader.subscribe(name, message_type))
-        self.reader.process()
-        data = self.reader.get_data_for_channel(name, message_type)
-        self.assertLess(100, len(data))
-        last_monotonic_time = 0
-        for entry in data:
-            monotonic_time = entry[0]
-            realtime_time = entry[1]
-            json_data = entry[2].replace('nan', '\"nan\"')
-            self.assertLess(last_monotonic_time, monotonic_time)
-            # Sanity check that the realtime times are in the correct range.
-            self.assertLess(1500000000e9, realtime_time)
-            self.assertGreater(2000000000e9, realtime_time)
-            parsed_json = json.loads(json_data)
-            self.assertIn("name", parsed_json)
-
-            last_monotonic_time = monotonic_time
-
-    def test_bad_subscribe(self):
-        """Tests that we return false when subscribing to a non-existent channel."""
-        self.assertFalse(
-            self.reader.subscribe(self.bad_channel[0], self.bad_channel[1]),
-            self.bad_channel)
-
-    def test_subscribe_after_process(self):
-        """Tests that an exception is thrown if we subscribe after calling process()."""
-        self.reader.process()
-        for channel in self.all_channels:
-            with self.assertRaises(RuntimeError) as context:
-                self.reader.subscribe(channel[0], channel[1])
-
-    def test_get_data_before_processj(self):
-        """Tests that an exception is thrown if we retrieve data before calling process()."""
-        for channel in self.all_channels:
-            with self.assertRaises(RuntimeError) as context:
-                self.reader.get_data_for_channel(channel[0], channel[1])
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/frc971/analysis/plot_data.fbs b/frc971/analysis/plot_data.fbs
deleted file mode 100644
index 641cd6e..0000000
--- a/frc971/analysis/plot_data.fbs
+++ /dev/null
@@ -1,59 +0,0 @@
-// This flatbuffer defines the interface that is used by the in-process
-// web plotter to plot data dynamically. Both the structure of the plot and
-// the data to plot is all packaged within a single Plot message. Each Plot
-// message will correspond to a single view/tab on the web-page, and can have
-// multiple figures, each of which can have multiple lines.
-namespace frc971.analysis;
-
-// Position within the web-page to plot a figure at. [0, 0] will be the upper
-// left corner of the allowable places where plots can be put, and should
-// generally be the default location. All values in pixels.
-table Position {
-  top:float (id: 0);
-  left:float (id: 1);
-  width:float (id: 2);
-  height:float (id: 3);
-}
-
-struct Point {
-  x:double (id: 0);
-  y:double (id: 1);
-}
-
-// RGB values are in the range [0, 1].
-struct Color {
-  r:float (id: 0);
-  g:float (id: 1);
-  b:float (id: 2);
-}
-
-table LineStyle {
-  point_size:float (id: 0);
-  draw_line:bool (id: 1);
-}
-
-table Line {
-  label:string (id: 0);
-  points:[Point] (id: 1);
-  color:Color (id: 2);
-  style:LineStyle (id: 3);
-}
-
-table Figure {
-  position:Position (id: 0);
-  lines:[Line] (id: 1);
-  // Specifies whether to link the x-axis of this Figure with that of other
-  // figures in this Plot. Only the axes of Figure's with this flag set will
-  // be linked.
-  share_x_axis:bool (id: 2);
-  title:string (id: 3);
-  xlabel:string (id: 4);
-  ylabel:string (id: 5);
-}
-
-table Plot {
-  figures:[Figure] (id: 0);
-  title:string (id: 1);
-}
-
-root_type Plot;
diff --git a/frc971/analysis/plot_data_utils.ts b/frc971/analysis/plot_data_utils.ts
deleted file mode 100644
index 25131fb..0000000
--- a/frc971/analysis/plot_data_utils.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-// Provides a plot which handles plotting the plot defined by a
-// frc971.analysis.Plot message.
-import {Plot as PlotFb} from './plot_data_generated';
-import {MessageHandler, TimestampedMessage} from '../../aos/network/www/aos_plotter';
-import {ByteBuffer} from 'flatbuffers';
-import {Plot, Point} from '../../aos/network/www/plotter';
-import {Connection} from '../../aos/network/www/proxy';
-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.
-  const plotSelect = document.createElement('select');
-  parentDiv.appendChild(plotSelect);
-  const plots = new Map<string, HTMLElement>();
-  const invalidSelectValue = 'null';
-  plotSelect.addEventListener('input', () => {
-    for (const plot of plots.values()) {
-      plot.style.display = 'none';
-    }
-    if (plotSelect.value == invalidSelectValue) {
-      return;
-    }
-    plots.get(plotSelect.value).style.display = 'block';
-  });
-  plotSelect.add(new Option('Select Plot', invalidSelectValue));
-
-  const plotDiv = document.createElement('div');
-  parentDiv.appendChild(plotDiv);
-
-  conn.addReliableHandler(
-      '/analysis', 'frc971.analysis.Plot', (data: Uint8Array, time: number) => {
-        const plotFb = PlotFb.getRootAsPlot(new ByteBuffer(data));
-        const name = (!plotFb.title()) ? 'Plot ' + plots.size : plotFb.title();
-        const div = document.createElement('div');
-        div.style.display = 'none';
-        plots.set(name, div);
-        plotDiv.appendChild(div);
-        plotSelect.add(new Option(name, name));
-
-        const linkedXAxes: Plot[] = [];
-
-        for (let ii = 0; ii < plotFb.figuresLength(); ++ii) {
-          const figure = plotFb.figures(ii);
-          const figureDiv = document.createElement('div');
-          if (figure.position().width() == 0) {
-            figureDiv.style.width = '100%';
-          } else {
-            figureDiv.style.width = figure.position().width().toString() + 'px';
-          }
-          if (figure.position().height() == 0) {
-            figureDiv.style.height = '100%';
-          } else {
-            figureDiv.style.height =
-                figure.position().height().toString() + 'px';
-          }
-          figureDiv.style.position = 'relative';
-          div.appendChild(figureDiv);
-          const plot = new Plot(figureDiv);
-
-          if (figure.title()) {
-            plot.getAxisLabels().setTitle(figure.title());
-          }
-          if (figure.xlabel()) {
-            plot.getAxisLabels().setXLabel(figure.xlabel());
-          }
-          if (figure.ylabel()) {
-            plot.getAxisLabels().setYLabel(figure.ylabel());
-          }
-          if (figure.shareXAxis()) {
-            for (const other of linkedXAxes) {
-              plot.linkXAxis(other);
-            }
-            linkedXAxes.push(plot);
-          }
-
-          for (let jj = 0; jj < figure.linesLength(); ++jj) {
-            const lineFb = figure.lines(jj);
-            const line = plot.getDrawer().addLine();
-            if (lineFb.label()) {
-              line.setLabel(lineFb.label());
-            }
-            const points = [];
-            for (let kk = 0; kk < lineFb.pointsLength(); ++kk) {
-              const point = lineFb.points(kk);
-              points.push(new Point(point.x(), point.y()));
-            }
-            if (lineFb.color()) {
-              line.setColor(
-                  [lineFb.color().r(), lineFb.color().g(), lineFb.color().b()]);
-            }
-            if (lineFb.style()) {
-              if (lineFb.style().pointSize() !== null) {
-                line.setPointSize(lineFb.style().pointSize());
-              }
-              if (lineFb.style().drawLine() !== null) {
-                line.setDrawLine(lineFb.style().drawLine());
-              }
-            }
-            line.setPoints(points);
-          }
-        }
-
-        // If this is the first new element (ignoring the placeholder up top),
-        // select it by default.
-        if (plotSelect.length == 2) {
-          plotSelect.value = name;
-          plotSelect.dispatchEvent(new Event('input'));
-        }
-      });
-}
diff --git a/frc971/analysis/plotter_config.json b/frc971/analysis/plotter_config.json
deleted file mode 100644
index fef4a50..0000000
--- a/frc971/analysis/plotter_config.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "channels": [
-    {
-      "name": "/analysis",
-      "type": "frc971.analysis.Plot",
-      "max_size": 1000000000
-    }
-  ],
-  "imports": [
-    "../../aos/events/aos.json"
-  ]
-}
diff --git a/frc971/analysis/py_log_reader.cc b/frc971/analysis/py_log_reader.cc
deleted file mode 100644
index 57d57d0..0000000
--- a/frc971/analysis/py_log_reader.cc
+++ /dev/null
@@ -1,306 +0,0 @@
-// This file provides a Python module for reading logfiles. See
-// log_reader_test.py for usage.
-//
-// NOTE: This code has not been maintained recently, and so is missing key
-// features to support reading multi-node logfiles (namely, it assumes the the
-// logfile is just a single file). Updating this code should not be difficult,
-// but hasn't been needed thus far.
-//
-// This reader works by having the user specify exactly what channels they want
-// data for. We then process the logfile and store all the data on that channel
-// into a list of timestamps + JSON message data. The user can then use an
-// accessor method (get_data_for_channel) to retrieve the cached data.
-
-// Defining PY_SSIZE_T_CLEAN seems to be suggested by most of the Python
-// documentation.
-#define PY_SSIZE_T_CLEAN
-// Note that Python.h needs to be included before anything else.
-#include <Python.h>
-
-#include <cerrno>
-#include <memory>
-
-#include "aos/configuration.h"
-#include "aos/events/logging/log_reader.h"
-#include "aos/events/simulated_event_loop.h"
-#include "aos/flatbuffer_merge.h"
-#include "aos/init.h"
-#include "aos/json_to_flatbuffer.h"
-
-namespace frc971::analysis {
-namespace {
-
-// All the data corresponding to a single message.
-struct MessageData {
-  aos::monotonic_clock::time_point monotonic_sent_time;
-  aos::realtime_clock::time_point realtime_sent_time;
-  // JSON representation of the message.
-  std::string json_data;
-};
-
-// Data corresponding to an entire channel.
-struct ChannelData {
-  std::string name;
-  std::string type;
-  // Each message published on the channel, in order by monotonic time.
-  std::vector<MessageData> messages;
-};
-
-// All the objects that we need for managing reading a logfile.
-struct LogReaderTools {
-  std::unique_ptr<aos::logger::LogReader> reader;
-  // Event loop to use for subscribing to buses.
-  std::unique_ptr<aos::EventLoop> event_loop;
-  std::vector<ChannelData> channel_data;
-  // Whether we have called process() on the reader yet.
-  bool processed = false;
-};
-
-struct LogReaderType {
-  PyObject_HEAD;
-  LogReaderTools *tools = nullptr;
-};
-
-void LogReader_dealloc(LogReaderType *self) {
-  LogReaderTools *tools = self->tools;
-  delete tools;
-  Py_TYPE(self)->tp_free((PyObject *)self);
-}
-
-PyObject *LogReader_new(PyTypeObject *type, PyObject * /*args*/,
-                        PyObject * /*kwds*/) {
-  LogReaderType *self;
-  self = (LogReaderType *)type->tp_alloc(type, 0);
-  if (self != nullptr) {
-    self->tools = new LogReaderTools();
-    if (self->tools == nullptr) {
-      return nullptr;
-    }
-  }
-  return (PyObject *)self;
-}
-
-int LogReader_init(LogReaderType *self, PyObject *args, PyObject *kwds) {
-  int count = 1;
-  if (!aos::IsInitialized()) {
-    // Fake out argc and argv to let InitGoogle run properly to instrument
-    // malloc, setup glog, and such.
-    char *name = program_invocation_name;
-    char **argv = &name;
-    aos::InitGoogle(&count, &argv);
-  }
-
-  const char *kwlist[] = {"log_file_name", nullptr};
-
-  const char *log_file_name;
-  if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char **>(kwlist),
-                                   &log_file_name)) {
-    return -1;
-  }
-
-  LogReaderTools *tools = CHECK_NOTNULL(self->tools);
-  tools->reader = std::make_unique<aos::logger::LogReader>(log_file_name);
-  tools->reader->Register();
-
-  if (aos::configuration::MultiNode(tools->reader->configuration())) {
-    tools->event_loop = tools->reader->event_loop_factory()->MakeEventLoop(
-        "data_fetcher",
-        aos::configuration::GetNode(tools->reader->configuration(), "roborio"));
-  } else {
-    tools->event_loop =
-        tools->reader->event_loop_factory()->MakeEventLoop("data_fetcher");
-  }
-  tools->event_loop->SkipTimingReport();
-  tools->event_loop->SkipAosLog();
-
-  return 0;
-}
-
-PyObject *LogReader_get_data_for_channel(LogReaderType *self, PyObject *args,
-                                         PyObject *kwds) {
-  const char *kwlist[] = {"name", "type", nullptr};
-
-  const char *name;
-  const char *type;
-  if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
-                                   const_cast<char **>(kwlist), &name, &type)) {
-    return nullptr;
-  }
-
-  LogReaderTools *tools = CHECK_NOTNULL(self->tools);
-
-  if (!tools->processed) {
-    PyErr_SetString(PyExc_RuntimeError,
-                    "Called get_data_for_bus before calling process().");
-    return nullptr;
-  }
-
-  for (const auto &channel : tools->channel_data) {
-    if (channel.name == name && channel.type == type) {
-      PyObject *list = PyList_New(channel.messages.size());
-      for (size_t ii = 0; ii < channel.messages.size(); ++ii) {
-        const auto &message = channel.messages[ii];
-        PyObject *monotonic_time = PyLong_FromLongLong(
-            std::chrono::duration_cast<std::chrono::nanoseconds>(
-                message.monotonic_sent_time.time_since_epoch())
-                .count());
-        PyObject *realtime_time = PyLong_FromLongLong(
-            std::chrono::duration_cast<std::chrono::nanoseconds>(
-                message.realtime_sent_time.time_since_epoch())
-                .count());
-        PyObject *json_data = PyUnicode_FromStringAndSize(
-            message.json_data.data(), message.json_data.size());
-        PyObject *entry =
-            PyTuple_Pack(3, monotonic_time, realtime_time, json_data);
-        if (PyList_SetItem(list, ii, entry) != 0) {
-          return nullptr;
-        }
-      }
-      return list;
-    }
-  }
-  PyErr_SetString(PyExc_ValueError,
-                  "The provided channel was never subscribed to.");
-  return nullptr;
-}
-
-PyObject *LogReader_subscribe(LogReaderType *self, PyObject *args,
-                              PyObject *kwds) {
-  const char *kwlist[] = {"name", "type", nullptr};
-
-  const char *name;
-  const char *type;
-  if (!PyArg_ParseTupleAndKeywords(args, kwds, "ss",
-                                   const_cast<char **>(kwlist), &name, &type)) {
-    return nullptr;
-  }
-
-  LogReaderTools *tools = CHECK_NOTNULL(self->tools);
-
-  if (tools->processed) {
-    PyErr_SetString(PyExc_RuntimeError,
-                    "Called subscribe after calling process().");
-    return nullptr;
-  }
-
-  const aos::Channel *const channel = aos::configuration::GetChannel(
-      tools->reader->configuration(), name, type, "", nullptr);
-  if (channel == nullptr) {
-    return Py_False;
-  }
-  const int index = tools->channel_data.size();
-  tools->channel_data.push_back({.name = name, .type = type, .messages = {}});
-  tools->event_loop->MakeRawWatcher(
-      channel, [channel, index, tools](const aos::Context &context,
-                                       const void *message) {
-        tools->channel_data[index].messages.push_back(
-            {.monotonic_sent_time = context.monotonic_event_time,
-             .realtime_sent_time = context.realtime_event_time,
-             .json_data = aos::FlatbufferToJson(
-                 channel->schema(), static_cast<const uint8_t *>(message))});
-      });
-  return Py_True;
-}
-
-static PyObject *LogReader_process(LogReaderType *self,
-                                   PyObject *Py_UNUSED(ignored)) {
-  LogReaderTools *tools = CHECK_NOTNULL(self->tools);
-
-  if (tools->processed) {
-    PyErr_SetString(PyExc_RuntimeError, "process() may only be called once.");
-    return nullptr;
-  }
-
-  tools->processed = true;
-
-  tools->reader->event_loop_factory()->Run();
-
-  Py_RETURN_NONE;
-}
-
-static PyObject *LogReader_configuration(LogReaderType *self,
-                                         PyObject *Py_UNUSED(ignored)) {
-  LogReaderTools *tools = CHECK_NOTNULL(self->tools);
-
-  // I have no clue if the Configuration that we get from the log reader is in a
-  // contiguous chunk of memory, and I'm too lazy to either figure it out or
-  // figure out how to extract the actual data buffer + offset.
-  // Instead, copy the flatbuffer and return a copy of the new buffer.
-  aos::FlatbufferDetachedBuffer<aos::Configuration> buffer =
-      aos::CopyFlatBuffer(tools->reader->configuration());
-
-  return PyBytes_FromStringAndSize(
-      reinterpret_cast<const char *>(buffer.span().data()),
-      buffer.span().size());
-}
-
-static PyMethodDef LogReader_methods[] = {
-    {"configuration", (PyCFunction)LogReader_configuration, METH_NOARGS,
-     "Return a bytes buffer for the Configuration of the logfile."},
-    {"process", (PyCFunction)LogReader_process, METH_NOARGS,
-     "Processes the logfile and all the subscribed to channels."},
-    {"subscribe", (PyCFunction)LogReader_subscribe,
-     METH_VARARGS | METH_KEYWORDS,
-     "Attempts to subscribe to the provided channel name + type. Returns True "
-     "if successful."},
-    {"get_data_for_channel", (PyCFunction)LogReader_get_data_for_channel,
-     METH_VARARGS | METH_KEYWORDS,
-     "Returns the logged data for a given channel. Raises an exception if you "
-     "did not subscribe to the provided channel. Returned data is a list of "
-     "tuples where each tuple is of the form (monotonic_nsec, realtime_nsec, "
-     "json_message_data)."},
-    {nullptr, 0, 0, nullptr} /* Sentinel */
-};
-
-#ifdef __clang__
-// These extensions to C++ syntax do surprising things in C++, but for these
-// uses none of them really matter I think, and the alternatives are really
-// annoying.
-#pragma clang diagnostic ignored "-Wc99-designator"
-#endif
-
-static PyTypeObject LogReaderType = {
-    PyVarObject_HEAD_INIT(NULL, 0)
-        // The previous macro initializes some fields, leave a comment to help
-        // clang-format not make this uglier.
-        .tp_name = "py_log_reader.LogReader",
-    .tp_basicsize = sizeof(LogReaderType),
-    .tp_itemsize = 0,
-    .tp_dealloc = (destructor)LogReader_dealloc,
-    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-    .tp_doc = "LogReader objects",
-    .tp_methods = LogReader_methods,
-    .tp_init = (initproc)LogReader_init,
-    .tp_new = LogReader_new,
-};
-
-static PyModuleDef log_reader_module = {
-    PyModuleDef_HEAD_INIT,
-    .m_name = "py_log_reader",
-    .m_doc = "Example module that creates an extension type.",
-    .m_size = -1,
-};
-
-PyObject *InitModule() {
-  PyObject *m;
-  if (PyType_Ready(&LogReaderType) < 0) return nullptr;
-
-  m = PyModule_Create(&log_reader_module);
-  if (m == nullptr) return nullptr;
-
-  Py_INCREF(&LogReaderType);
-  if (PyModule_AddObject(m, "LogReader", (PyObject *)&LogReaderType) < 0) {
-    Py_DECREF(&LogReaderType);
-    Py_DECREF(m);
-    return nullptr;
-  }
-
-  return m;
-}
-
-}  // namespace
-}  // namespace frc971::analysis
-
-PyMODINIT_FUNC PyInit_py_log_reader(void) {
-  return frc971::analysis::InitModule();
-}
diff --git a/frc971/control_loops/drivetrain/BUILD b/frc971/control_loops/drivetrain/BUILD
index 6f98839..afbce9b 100644
--- a/frc971/control_loops/drivetrain/BUILD
+++ b/frc971/control_loops/drivetrain/BUILD
@@ -606,8 +606,8 @@
     target_compatible_with = ["@platforms//os:linux"],
     deps = [
         ":spline",
+        "//aos/analysis:in_process_plotter",
         "//aos/testing:googletest",
-        "//frc971/analysis:in_process_plotter",
         "@com_github_gflags_gflags//:gflags",
     ],
 )
diff --git a/frc971/control_loops/drivetrain/spline_test.cc b/frc971/control_loops/drivetrain/spline_test.cc
index 6d84d53..fcd0030 100644
--- a/frc971/control_loops/drivetrain/spline_test.cc
+++ b/frc971/control_loops/drivetrain/spline_test.cc
@@ -5,7 +5,7 @@
 #include "gflags/gflags.h"
 #include "gtest/gtest.h"
 
-#include "frc971/analysis/in_process_plotter.h"
+#include "aos/analysis/in_process_plotter.h"
 
 DEFINE_bool(plot, false, "If true, plot");
 
diff --git a/frc971/vision/BUILD b/frc971/vision/BUILD
index 5d8bf7c..6cee780 100644
--- a/frc971/vision/BUILD
+++ b/frc971/vision/BUILD
@@ -131,7 +131,6 @@
         ":foxglove_image_converter_lib",
         "//aos:init",
         "//aos/events/logging:log_reader",
-        "//frc971/analysis:in_process_plotter",
         "//frc971/control_loops/drivetrain:improved_down_estimator",
         "//frc971/vision:visualize_robot",
         "//frc971/wpilib:imu_batch_fbs",
@@ -142,6 +141,12 @@
         "@com_google_absl//absl/strings:str_format",
         "@com_google_ceres_solver//:ceres",
         "@org_tuxfamily_eigen//:eigen",
+    ] + [
+        # TODO(Stephan):  This is a whacky hack.  If we include this
+        # in the proper spot above (alphabetically), then we get a
+        # linker error: duplicate symbol: crc32.
+        # It's part of both @zlib and @com_github_rawrtc_re.
+        "//aos/analysis:in_process_plotter",
     ],
 )
 
diff --git a/frc971/vision/extrinsics_calibration.cc b/frc971/vision/extrinsics_calibration.cc
index 6017258..5c4ae31 100644
--- a/frc971/vision/extrinsics_calibration.cc
+++ b/frc971/vision/extrinsics_calibration.cc
@@ -7,8 +7,8 @@
 #include <opencv2/highgui/highgui.hpp>
 #include <opencv2/imgproc.hpp>
 
+#include "aos/analysis/in_process_plotter.h"
 #include "aos/time/time.h"
-#include "frc971/analysis/in_process_plotter.h"
 #include "frc971/control_loops/runge_kutta.h"
 #include "frc971/vision/calibration_accumulator.h"
 #include "frc971/vision/charuco_lib.h"