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/aos/network/BUILD b/aos/network/BUILD
index bf2cb00..6053260 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -389,11 +389,11 @@
hdrs = ["web_proxy.h"],
copts = [
"-DWEBRTC_POSIX",
- "-Wno-unused-parameter",
],
target_compatible_with = ["@platforms//os:linux"],
deps = [
":connect_fbs",
+ ":gen_embedded",
":web_proxy_fbs",
":web_proxy_utils",
"//aos/events:shm_event_loop",
@@ -419,7 +419,6 @@
srcs = ["web_proxy_main.cc"],
copts = [
"-DWEBRTC_POSIX",
- "-Wno-unused-parameter",
],
data = [
"//aos/network/www:files",
@@ -428,12 +427,9 @@
],
target_compatible_with = ["@platforms//os:linux"],
deps = [
- ":gen_embedded",
":web_proxy",
"//aos:init",
"//aos/events:shm_event_loop",
- "//aos/seasocks:seasocks_logger",
- "//third_party/seasocks",
"@com_github_google_flatbuffers//:flatbuffers",
],
)
@@ -446,16 +442,12 @@
],
copts = [
"-DWEBRTC_POSIX",
- "-Wno-unused-parameter",
],
deps = [
- ":gen_embedded",
":web_proxy",
"//aos:init",
"//aos/events:simulated_event_loop",
"//aos/events/logging:logger",
- "//aos/seasocks:seasocks_logger",
- "//third_party/seasocks",
"@com_github_google_flatbuffers//:flatbuffers",
],
)
diff --git a/aos/network/log_web_proxy_main.cc b/aos/network/log_web_proxy_main.cc
index dda9ae4..1942c57 100644
--- a/aos/network/log_web_proxy_main.cc
+++ b/aos/network/log_web_proxy_main.cc
@@ -9,21 +9,14 @@
#include "aos/flatbuffer_merge.h"
#include "aos/init.h"
#include "aos/network/web_proxy.h"
-#include "aos/seasocks/seasocks_logger.h"
#include "gflags/gflags.h"
-#include "internal/Embedded.h"
-#include "seasocks/Server.h"
-#include "seasocks/WebSocket.h"
-
DEFINE_string(data_dir, "www", "Directory to serve data files from");
DEFINE_string(node, "", "Directory to serve data files from");
DEFINE_int32(buffer_size, -1, "-1 if infinite, in # of messages / channel.");
int main(int argc, char **argv) {
aos::InitGoogle(&argc, &argv);
- // Make sure to reference this to force the linker to include it.
- findEmbeddedContent("");
const std::vector<std::string> unsorted_logfiles =
aos::logger::FindLogs(argc, argv);
diff --git a/aos/network/web_proxy.cc b/aos/network/web_proxy.cc
index af1b646..5902d30 100644
--- a/aos/network/web_proxy.cc
+++ b/aos/network/web_proxy.cc
@@ -7,6 +7,7 @@
#include "aos/seasocks/seasocks_logger.h"
#include "api/create_peerconnection_factory.h"
#include "glog/logging.h"
+#include "internal/Embedded.h"
namespace aos {
namespace web_proxy {
@@ -22,7 +23,7 @@
return new rtc::RefCountedObject<DummySetSessionDescriptionObserver>();
}
virtual void OnSuccess() {}
- virtual void OnFailure(webrtc::RTCError error) {}
+ virtual void OnFailure(webrtc::RTCError /*error*/) {}
};
} // namespace
@@ -32,6 +33,8 @@
: server_(server),
config_(aos::CopyFlatBuffer(event_loop->configuration())),
event_loop_(event_loop) {
+ // We need to reference findEmbeddedContent() to make the linker happy...
+ findEmbeddedContent("");
const aos::Node *self = event_loop->node();
for (uint i = 0; i < event_loop->configuration()->channels()->size(); ++i) {
@@ -276,11 +279,14 @@
// Function called for web socket data. Parses the flatbuffer and
// handles it appropriately.
void Connection::HandleWebSocketData(const uint8_t *data, size_t size) {
- const WebSocketMessage *message =
- flatbuffers::GetRoot<WebSocketMessage>(data);
- switch (message->payload_type()) {
+ const FlatbufferSpan<WebSocketMessage> message({data, size});
+ if (!message.Verify()) {
+ LOG(ERROR) << "Invalid WebsocketMessage received from browser.";
+ return;
+ }
+ switch (message.message().payload_type()) {
case Payload::WebSocketSdp: {
- const WebSocketSdp *offer = message->payload_as_WebSocketSdp();
+ const WebSocketSdp *offer = message.message().payload_as_WebSocketSdp();
if (offer->type() != SdpType::OFFER) {
LOG(WARNING) << "Got the wrong sdp type from client";
break;
@@ -325,7 +331,7 @@
break;
}
case Payload::WebSocketIce: {
- const WebSocketIce *ice = message->payload_as_WebSocketIce();
+ const WebSocketIce *ice = message.message().payload_as_WebSocketIce();
std::string candidate = ice->candidate()->str();
std::string sdpMid = ice->sdpMid()->str();
int sdpMLineIndex = ice->sdpMLineIndex();
diff --git a/aos/network/web_proxy.h b/aos/network/web_proxy.h
index ab524de..e6d47c4 100644
--- a/aos/network/web_proxy.h
+++ b/aos/network/web_proxy.h
@@ -178,7 +178,7 @@
rtc::scoped_refptr<webrtc::DataChannelInterface> channel) override;
void OnRenegotiationNeeded() override {}
void OnIceConnectionChange(
- webrtc::PeerConnectionInterface::IceConnectionState state) override {}
+ webrtc::PeerConnectionInterface::IceConnectionState /*state*/) override {}
void OnIceGatheringChange(
webrtc::PeerConnectionInterface::IceGatheringState) override {}
void OnIceCandidate(const webrtc::IceCandidateInterface *candidate) override;
@@ -186,7 +186,7 @@
// CreateSessionDescriptionObserver implementation
void OnSuccess(webrtc::SessionDescriptionInterface *desc) override;
- void OnFailure(webrtc::RTCError error) override {}
+ void OnFailure(webrtc::RTCError /*error*/) override {}
// CreateSessionDescriptionObserver is a refcounted object
void AddRef() const override {}
// We handle ownership with a unique_ptr so don't worry about actually
@@ -198,7 +198,7 @@
// DataChannelObserver implementation
void OnStateChange() override;
void OnMessage(const webrtc::DataBuffer &buffer) override;
- void OnBufferedAmountChange(uint64_t sent_data_size) override {}
+ void OnBufferedAmountChange(uint64_t /*sent_data_size*/) override {}
private:
::seasocks::WebSocket *sock_;
diff --git a/aos/network/web_proxy_main.cc b/aos/network/web_proxy_main.cc
index ddab5dc..06fe942 100644
--- a/aos/network/web_proxy_main.cc
+++ b/aos/network/web_proxy_main.cc
@@ -2,21 +2,14 @@
#include "aos/flatbuffer_merge.h"
#include "aos/init.h"
#include "aos/network/web_proxy.h"
-#include "aos/seasocks/seasocks_logger.h"
#include "gflags/gflags.h"
-#include "internal/Embedded.h"
-#include "seasocks/Server.h"
-#include "seasocks/WebSocket.h"
-
DEFINE_string(config, "./config.json", "File path of aos configuration");
DEFINE_string(data_dir, "www", "Directory to serve data files from");
DEFINE_int32(buffer_size, 0, "-1 if infinite, in # of messages / channel.");
int main(int argc, char **argv) {
aos::InitGoogle(&argc, &argv);
- // Make sure to reference this to force the linker to include it.
- findEmbeddedContent("");
aos::FlatbufferDetachedBuffer<aos::Configuration> config =
aos::configuration::ReadConfig(FLAGS_config);
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"
+ ]
+}