Implement interface for using web plotter from C++
It's not actually usable yet due to ODR violations created by
abseil being compiled into libwebrtc_full.a, but it does work based
on testing I've done with using websockets for data transfer.
Change-Id: I574570c7b5c85df9e53321bfb971a608d20b9803
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index 465df0d..0ef9bca 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -3,6 +3,8 @@
load("@npm_bazel_typescript//:defs.bzl", "ts_library")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary", "rollup_bundle")
+load("@com_github_google_flatbuffers//:build_defs.bzl", "flatbuffer_cc_library", "flatbuffer_ts_library")
+load("//aos:config.bzl", "aos_config")
py_binary(
name = "plot_action",
@@ -89,6 +91,7 @@
srcs = ["plot_index.ts"],
target_compatible_with = ["@platforms//os:linux"],
deps = [
+ ":plot_data_utils",
"//aos:configuration_ts_fbs",
"//aos/network/www:demo_plot",
"//aos/network/www:proxy",
@@ -134,3 +137,75 @@
],
target_compatible_with = ["@platforms//os:linux"],
)
+
+flatbuffer_cc_library(
+ name = "plot_data_fbs",
+ srcs = [
+ "plot_data.fbs",
+ ],
+ gen_reflections = 1,
+ 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_library(
+ 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//ts:flatbuffers_ts",
+ ],
+)
+
+aos_config(
+ name = "plotter",
+ src = "plotter_config.json",
+ flatbuffers = [":plot_data_fbs"],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = ["//aos/events:config"],
+)
+
+cc_library(
+ name = "in_process_plotter",
+ srcs = ["in_process_plotter.cc"],
+ hdrs = ["in_process_plotter.h"],
+ copts = [
+ "-DWEBRTC_POSIX",
+ ],
+ 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"],
+ copts = [
+ "-DWEBRTC_POSIX",
+ ],
+ data = [
+ ":plotter",
+ ":plotter_files",
+ ],
+ # Tagged manual until we either get the linker working with the current
+ # WebRTC implementation or we get a new implementation.
+ tags = ["manual"],
+ deps = [
+ ":in_process_plotter",
+ "//aos:init",
+ ],
+)
diff --git a/frc971/analysis/in_process_plotter.cc b/frc971/analysis/in_process_plotter.cc
new file mode 100644
index 0000000..e82cb2f
--- /dev/null
+++ b/frc971/analysis/in_process_plotter.cc
@@ -0,0 +1,131 @@
+#include "frc971/analysis/in_process_plotter.h"
+
+#include "aos/configuration.h"
+
+namespace frc971 {
+namespace analysis {
+
+namespace {
+const char *kDataPath = "frc971/analysis";
+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(), -1),
+ builder_(plot_sender_.MakeBuilder()) {
+ web_proxy_.SetDataPath(kDataPath);
+ event_loop_->SkipTimingReport();
+ color_wheel_.push_back(Color(1, 0, 0));
+ color_wheel_.push_back(Color(0, 1, 0));
+ color_wheel_.push_back(Color(0, 0, 1));
+ color_wheel_.push_back(Color(1, 1, 0));
+ color_wheel_.push_back(Color(0, 1, 1));
+ color_wheel_.push_back(Color(1, 0, 1));
+}
+
+void Plotter::Spin() { 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, std::string_view label) {
+ CHECK_EQ(x.size(), y.size());
+ CHECK(!position_.IsNull())
+ << "You must call AddFigure() before calling AddLine().";
+
+ flatbuffers::Offset<flatbuffers::String> label_offset;
+ if (!label.empty()) {
+ label_offset = builder_.fbb()->CreateString(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 = &color_wheel_.at(color_wheel_position_);
+ color_wheel_position_ = (color_wheel_position_ + 1) % color_wheel_.size();
+
+ auto line_builder = builder_.MakeBuilder<Line>();
+ line_builder.add_label(label_offset);
+ line_builder.add_points(points_offset);
+ line_builder.add_color(color);
+ 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);
+
+ builder_.Send(plot_builder.Finish());
+
+ builder_ = plot_sender_.MakeBuilder();
+
+ title_.o = 0;
+ figures_.clear();
+ next_top_ = 0;
+}
+
+} // namespace analysis
+} // namespace frc971
diff --git a/frc971/analysis/in_process_plotter.h b/frc971/analysis/in_process_plotter.h
new file mode 100644
index 0000000..3d7a037
--- /dev/null
+++ b/frc971/analysis/in_process_plotter.h
@@ -0,0 +1,79 @@
+#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 {
+namespace 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) Setup 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 = 900,
+ double height = 400);
+ void AddLine(const std::vector<double> &x, const std::vector<double> &y,
+ std::string_view label = "");
+ 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_;
+
+ size_t color_wheel_position_ = 0;
+ std::vector<Color> color_wheel_;
+};
+
+} // namespace analysis
+} // namespace frc971
+#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
new file mode 100644
index 0000000..492ddfa
--- /dev/null
+++ b/frc971/analysis/in_process_plotter_demo.cc
@@ -0,0 +1,42 @@
+#include "frc971/analysis/in_process_plotter.h"
+
+#include "aos/init.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/plot_data.fbs b/frc971/analysis/plot_data.fbs
new file mode 100644
index 0000000..c21add1
--- /dev/null
+++ b/frc971/analysis/plot_data.fbs
@@ -0,0 +1,53 @@
+// 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 Line {
+ label:string (id: 0);
+ points:[Point] (id: 1);
+ color:Color (id: 2);
+}
+
+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
new file mode 100644
index 0000000..8d42a4a
--- /dev/null
+++ b/frc971/analysis/plot_data_utils.ts
@@ -0,0 +1,95 @@
+// Provides a plot which handles plotting the plot defined by a
+// frc971.analysis.Plot message.
+import * as configuration from 'org_frc971/aos/configuration_generated';
+import * as plot_data from 'org_frc971/frc971/analysis/plot_data_generated';
+import {MessageHandler, TimestampedMessage} from 'org_frc971/aos/network/www/aos_plotter';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+import {Plot} from 'org_frc971/aos/network/www/plotter';
+import * as proxy from 'org_frc971/aos/network/www/proxy';
+
+import Connection = proxy.Connection;
+import Schema = configuration.reflection.Schema;
+import PlotFb = plot_data.frc971.analysis.Plot;
+
+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');
+ plotDiv.style.position = 'absolute';
+ plotDiv.style.top = '30';
+ plotDiv.style.left = '0';
+ parentDiv.appendChild(plotDiv);
+
+ conn.addReliableHandler(
+ '/analysis', 'frc971.analysis.Plot', (data: Uint8Array, time: number) => {
+ const plotFb = PlotFb.getRootAsPlot(
+ new ByteBuffer(data) as unknown as flatbuffers.ByteBuffer);
+ 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');
+ figureDiv.style.top = figure.position().top().toString();
+ figureDiv.style.left = figure.position().left().toString();
+ figureDiv.style.position = 'absolute';
+ div.appendChild(figureDiv);
+ const plot = new Plot(
+ figureDiv, figure.position().width(), figure.position().height());
+
+ if (figure.title()) {
+ plot.getAxisLabels().setTitle(figure.title());
+ }
+ if (figure.xlabel()) {
+ plot.getAxisLabels().setXLabel(figure.xlabel());
+ }
+ if (figure.ylabel()) {
+ plot.getAxisLabels().setYLabel(figure.xlabel());
+ }
+ 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 = new Float32Array(lineFb.pointsLength() * 2);
+ for (let kk = 0; kk < lineFb.pointsLength(); ++kk) {
+ points[kk * 2] = lineFb.points(kk).x();
+ points[kk * 2 + 1] = lineFb.points(kk).y();
+ }
+ if (lineFb.color()) {
+ line.setColor(
+ [lineFb.color().r(), lineFb.color().g(), lineFb.color().b()]);
+ }
+ line.setPoints(points);
+ }
+ }
+ });
+}
diff --git a/frc971/analysis/plot_index.ts b/frc971/analysis/plot_index.ts
index 8fbdb7c..6b0e0e2 100644
--- a/frc971/analysis/plot_index.ts
+++ b/frc971/analysis/plot_index.ts
@@ -24,6 +24,7 @@
import * as proxy from 'org_frc971/aos/network/www/proxy';
import {plotImu} from 'org_frc971/frc971/wpilib/imu_plotter';
import {plotDemo} from 'org_frc971/aos/network/www/demo_plot';
+import {plotData} from 'org_frc971/frc971/analysis/plot_data_utils';
import Connection = proxy.Connection;
import Configuration = configuration.aos.Configuration;
@@ -77,7 +78,8 @@
// presence of certain channels.
const plotIndex = new Map<string, PlotState>([
['Demo', new PlotState(plotDiv, plotDemo)],
- ['IMU', new PlotState(plotDiv, plotImu)]
+ ['IMU', new PlotState(plotDiv, plotImu)],
+ ['C++ Plotter', new PlotState(plotDiv, plotData)],
]);
const invalidSelectValue = 'null';
diff --git a/frc971/analysis/plotter_config.json b/frc971/analysis/plotter_config.json
new file mode 100644
index 0000000..49266ee
--- /dev/null
+++ b/frc971/analysis/plotter_config.json
@@ -0,0 +1,12 @@
+{
+ "channels": [
+ {
+ "name": "/analysis",
+ "type": "frc971.analysis.Plot",
+ "max_size": 10000000
+ }
+ ],
+ "imports": [
+ "../../aos/events/aos.json"
+ ]
+}