Provide utilities for convenient plotting of channels
Implement a set of utilities (along with basic examples) to
make it so that we can readily make plots of individual channels
from logfiles (or live on the robot).
Change-Id: Ic648c9ccb9dcb73419dc2c8c4c395fdea0536110
diff --git a/aos/network/BUILD b/aos/network/BUILD
index c68f3cc..fa5cacc 100644
--- a/aos/network/BUILD
+++ b/aos/network/BUILD
@@ -441,18 +441,12 @@
name = "log_web_proxy_main",
srcs = ["log_web_proxy_main.cc"],
args = [
- "--config=aos/network/www/test_config.json",
"--data_dir=aos/network/www",
],
copts = [
"-DWEBRTC_POSIX",
"-Wno-unused-parameter",
],
- data = [
- "//aos/network/www:plotting_sample",
- "//y2020:config",
- "@com_github_google_flatbuffers//:flatjs",
- ],
deps = [
":gen_embedded",
":web_proxy",
diff --git a/aos/network/log_web_proxy_main.cc b/aos/network/log_web_proxy_main.cc
index 525eb29..dda9ae4 100644
--- a/aos/network/log_web_proxy_main.cc
+++ b/aos/network/log_web_proxy_main.cc
@@ -16,7 +16,6 @@
#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_string(node, "", "Directory to serve data files from");
DEFINE_int32(buffer_size, -1, "-1 if infinite, in # of messages / channel.");
diff --git a/aos/network/www/BUILD b/aos/network/www/BUILD
index 8f5bd49..a153ff3 100644
--- a/aos/network/www/BUILD
+++ b/aos/network/www/BUILD
@@ -102,11 +102,12 @@
)
ts_library(
- name = "graph_main",
+ name = "aos_plotter",
srcs = [
- "graph_main.ts",
+ "aos_plotter.ts",
],
target_compatible_with = ["@platforms//os:linux"],
+ visibility = ["//visibility:public"],
deps = [
":plotter",
":proxy",
@@ -118,13 +119,23 @@
],
)
-rollup_bundle(
- name = "graph_main_bundle",
- enable_code_splitting = False,
- entry_point = "graph_main.ts",
+ts_library(
+ name = "demo_plot",
+ srcs = [
+ "demo_plot.ts",
+ ],
target_compatible_with = ["@platforms//os:linux"],
+ visibility = ["//visibility:public"],
deps = [
- ":graph_main",
+ ":aos_plotter",
+ ":plotter",
+ ":proxy",
+ ":reflection_ts",
+ "//aos:configuration_ts_fbs",
+ "//aos/network:connect_ts_fbs",
+ "//aos/network:web_proxy_ts_fbs",
+ "//frc971/wpilib:imu_batch_ts_fbs",
+ "@com_github_google_flatbuffers//ts:flatbuffers_ts",
],
)
@@ -136,6 +147,7 @@
"//aos:json_to_flatbuffer_flatbuffer",
],
target_compatible_with = ["@platforms//os:linux"],
+ visibility = ["//visibility:public"],
deps = [
"//aos/events:config",
],
@@ -155,8 +167,6 @@
name = "web_proxy_demo",
srcs = ["web_proxy_demo.sh"],
data = [
- ":graph.html",
- ":graph_main_bundle.min.js",
":reflection_test.html",
":reflection_test_bundle.min.js",
":test_config",
@@ -165,14 +175,3 @@
],
target_compatible_with = ["@platforms//os:linux"],
)
-
-filegroup(
- name = "plotting_sample",
- srcs = [
- "graph.html",
- "graph_main_bundle.min.js",
- "styles.css",
- "test_config",
- ],
- visibility = ["//visibility:public"],
-)
diff --git a/aos/network/www/aos_plotter.ts b/aos/network/www/aos_plotter.ts
new file mode 100644
index 0000000..33a3de1
--- /dev/null
+++ b/aos/network/www/aos_plotter.ts
@@ -0,0 +1,185 @@
+// This library provides a wrapper around our WebGL plotter that makes it
+// easy to plot AOS messages/channels as time series.
+//
+// This is works by subscribing to each channel that we want to plot, storing
+// all the messages for that channel, and then periodically running through
+// every message and extracting the fields to plot.
+// It is also possible to insert code to make modifications to the messages
+// as we read/process them, as is the case for the IMU processing code (see
+// //frc971/wpilib:imu*.ts) where each message is actually a batch of several
+// individual messages that need to be plotted as separate points.
+//
+// The basic flow for using the AosPlotter is:
+// // 1) Construct the plotter
+// const aosPlotter = new AosPlotter(connection);
+// // 2) Add messages sources that we'll want to subscribe to.
+// const source = aosPlotter.addMessageSource('/aos', 'aos.timing.Report');
+// // 3) Create figures at defined positions within a given HTML element..
+// const timingPlot = aosPlotter.addPlot(parentDiv, [0, 0], [width, height]);
+// // 4) Add specific signals to each figure, using the message sources you
+// defined at the start.
+// timingPlot.addMessageLine(source, ['pid']);
+//
+// The demo_plot.ts script has a basic example of using this library, with all
+// the required boilerplate, as well as some extra examples about how to
+// add axis labels and the such.
+import * as configuration from 'org_frc971/aos/configuration_generated';
+import {Line, Plot} from 'org_frc971/aos/network/www/plotter';
+import * as proxy from 'org_frc971/aos/network/www/proxy';
+import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
+import * as reflection from 'org_frc971/aos/network/www/reflection'
+import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
+
+import Channel = configuration.aos.Channel;
+import Connection = proxy.Connection;
+import Configuration = configuration.aos.Configuration;
+import Schema = configuration.reflection.Schema;
+import Parser = reflection.Parser;
+import Table = reflection.Table;
+import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
+import ChannelRequest = web_proxy.aos.web_proxy.ChannelRequest;
+import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
+
+export class TimestampedMessage {
+ constructor(
+ public readonly message: Table, public readonly time: number) {}
+}
+
+// The MessageHandler stores an array of every single message on a given channel
+// and then supplies individual fields as arrays when requested. Currently this
+// is very much unoptimized and re-processes the entire array of messages on
+// every call to getField().
+export class MessageHandler {
+ protected parser: Parser;
+ protected messages: TimestampedMessage[] = [];
+ constructor(schema: Schema) {
+ this.parser = new Parser(schema);
+ }
+ addMessage(data: Uint8Array, time: number): void {
+ this.messages.push(
+ new TimestampedMessage(Table.getRootTable(new ByteBuffer(data)), time));
+ }
+ // Returns a time-series of every single instance of the given field. Format
+ // of the return value is [time0, value0, time1, value1,... timeN, valueN],
+ // to match with the Line.setPoint() interface.
+ // By convention, NaN is used to indicate that a message existed at a given
+ // timestamp but the requested field was not populated.
+ getField(field: string[]): Float32Array {
+ const fieldName = field[field.length - 1];
+ const subMessage = field.slice(0, field.length - 1);
+ const results = new Float32Array(this.messages.length * 2);
+ for (let ii = 0; ii < this.messages.length; ++ii) {
+ let message = this.messages[ii].message;
+ for (const subMessageName of subMessage) {
+ message = this.parser.readTable(message, subMessageName);
+ if (message === undefined) {
+ break;
+ }
+ }
+ results[ii * 2] = this.messages[ii].time;
+ if (message === undefined) {
+ results[ii * 2 + 1] = NaN;
+ } else {
+ results[ii * 2 + 1] = this.parser.readScalar(message, fieldName);
+ }
+ }
+ return results;
+ }
+ numMessages(): number {
+ return this.messages.length;
+ }
+}
+
+class MessageLine {
+ constructor(
+ public readonly messages: MessageHandler, public readonly line: Line,
+ public readonly field: string[]) {}
+}
+
+class AosPlot {
+ private lines: MessageLine[] = [];
+ constructor(
+ private readonly plotter: AosPlotter, public readonly plot: Plot) {}
+
+ // Adds a line to the figure.
+ // message specifies what channel/data source to pull from, and field
+ // specifies the field within that channel. field is an array specifying
+ // the full path to the field within the message. For instance, to
+ // plot whether the drivetrain is currently zeroed based on the drivetrain
+ // status message, you would specify the ['zeroing', 'zeroed'] field to
+ // get the DrivetrainStatus.zeroing().zeroed() member.
+ // Currently, this interface does not provide any support for non-numeric
+ // fields or for repeated fields (or sub-messages) of any sort.
+ addMessageLine(message: MessageHandler, field: string[]): Line {
+ const line = this.plot.getDrawer().addLine();
+ line.setLabel(field.join('.'));
+ this.lines.push(new MessageLine(message, line, field));
+ return line;
+ }
+
+ draw(): void {
+ // Only redraw lines if the number of points has changed--because getField()
+ // is a relatively expensive call, we don't want to do it any more than
+ // necessary.
+ for (const line of this.lines) {
+ if (line.messages.numMessages() * 2 != line.line.getPoints().length) {
+ line.line.setPoints(line.messages.getField(line.field));
+ }
+ }
+ }
+}
+
+export class AosPlotter {
+ private plots: AosPlot[] = [];
+ private messages = new Set<MessageHandler>();
+ constructor(private readonly connection: Connection) {
+ // Set up to redraw at some regular interval. The exact rate is unimportant.
+ setInterval(() => {
+ this.draw();
+ }, 100);
+ }
+
+ // Sets up an AOS channel as a message source. Returns a handler that can
+ // be passed to addMessageLine().
+ addMessageSource(name: string, type: string): MessageHandler {
+ return this.addRawMessageSource(
+ name, type, new MessageHandler(this.connection.getSchema(type)));
+ }
+
+ // Same as addMessageSource, but allows you to specify a custom MessageHandler
+ // that does some processing on the requested message. This allows you to
+ // create post-processed versions of individual channels.
+ addRawMessageSource(
+ name: string, type: string,
+ messageHandler: MessageHandler): MessageHandler {
+ this.messages.add(messageHandler);
+ // Use a "reliable" handler so that we get *all* the data when we are
+ // plotting from a logfile.
+ this.connection.addReliableHandler(
+ name, type, (data: Uint8Array, time: number) => {
+ messageHandler.addMessage(data, time);
+ });
+ return messageHandler;
+ }
+ // Add a new figure at the provided position with the provided size within
+ // parentElement.
+ addPlot(parentElement: Element, topLeft: number[], size: number[]): AosPlot {
+ const div = document.createElement("div");
+ div.style.top = topLeft[1].toString();
+ div.style.left = topLeft[0].toString();
+ div.style.position = 'absolute';
+ parentElement.appendChild(div);
+ const newPlot = new Plot(div, size[0], size[1]);
+ for (let plot of this.plots.values()) {
+ newPlot.linkXAxis(plot.plot);
+ }
+ this.plots.push(new AosPlot(this, newPlot));
+ return this.plots[this.plots.length - 1];
+ }
+ private draw(): void {
+ for (const plot of this.plots) {
+ plot.draw();
+ }
+ }
+}
diff --git a/aos/network/www/config_handler.ts b/aos/network/www/config_handler.ts
index df2d4f8..73b6b89 100644
--- a/aos/network/www/config_handler.ts
+++ b/aos/network/www/config_handler.ts
@@ -72,14 +72,8 @@
const channel = this.config.channels(Number(index));
this.connection.addHandler(
channel.name(), channel.type(), (data, time) => {
- const config = this.connection.getConfig();
- let schema = null;
- for (let ii = 0; ii < config.channelsLength(); ++ii) {
- if (config.channels(ii).type() === channel.type()) {
- schema = config.channels(ii).schema();
- }
- }
- const parser = new Parser(schema);
+ const parser =
+ new Parser(this.connection.getSchema(channel.type()));
console.log(
parser.toObject(Table.getRootTable(new ByteBuffer(data))));
});
diff --git a/aos/network/www/demo_plot.ts b/aos/network/www/demo_plot.ts
new file mode 100644
index 0000000..d223757
--- /dev/null
+++ b/aos/network/www/demo_plot.ts
@@ -0,0 +1,74 @@
+// This file provides a basic demonstration of the plotting functionality.
+// The plotDemo() function provided here is called by
+// //frc971/analysis:plot_index.ts
+// To view the demo plot, run
+// bazel run -c opt //frc971/analysis:live_web_plotter_demo
+// And then navigate to
+// http://localhost:8080/?plot=Demo
+// The plot=Demo isn't structly necessary, but ensures that this plot is
+// selected by default--otherwise, you'll need to select "Demo" from the
+// drop-down menu.
+//
+// This example shows how to:
+// (a) Make use of the AosPlotter to plot a shmem message as a time-series.
+// (b) Define your own custom plot with whatever data you want.
+import {AosPlotter} from 'org_frc971/aos/network/www/aos_plotter';
+import {Plot} from 'org_frc971/aos/network/www/plotter';
+import * as proxy from 'org_frc971/aos/network/www/proxy';
+
+import Connection = proxy.Connection;
+
+export function plotDemo(conn: Connection, parentDiv: Element): void {
+ const width = 900;
+ const height = 400;
+
+ const benchmarkDiv = document.createElement('div');
+ benchmarkDiv.style.top = height.toString();
+ benchmarkDiv.style.left = '0';
+ benchmarkDiv.style.position = 'absolute';
+ parentDiv.appendChild(benchmarkDiv);
+
+ const benchmarkPlot = new Plot(benchmarkDiv, width, height);
+
+ const aosPlotter = new AosPlotter(conn);
+
+ {
+ // Setup a plot that just shows the PID of each timing report message.
+ // For the basic live_web_plotter_demo, this will be a boring line showing
+ // just the PID of the proxy process. On a real system, or against a logfile,
+ // this would show the PIDs of all active processes.
+ const timingReport =
+ aosPlotter.addMessageSource('/aos', 'aos.timing.Report');
+ const timingPlot =
+ aosPlotter.addPlot(parentDiv, [0, 0], [width, height]);
+ timingPlot.plot.getAxisLabels().setTitle('Timing Report PID');
+ timingPlot.plot.getAxisLabels().setYLabel('PID');
+ timingPlot.plot.getAxisLabels().setXLabel('Monotonic Send Time (sec)');
+ const msgLine = timingPlot.addMessageLine(timingReport, ['pid']);
+ msgLine.setDrawLine(false);
+ msgLine.setPointSize(5);
+ }
+
+ // Set up and draw the benchmarking plot.
+ benchmarkPlot.getAxisLabels().setTitle(
+ 'Benchmarking plot (1M points per line)');
+ const line1 = benchmarkPlot.getDrawer().addLine();
+ // For demonstration purposes, make line1 only have points and line2 only have
+ // lines.
+ line1.setDrawLine(false);
+ line1.setLabel('LINE ONE');
+ const line2 = benchmarkPlot.getDrawer().addLine();
+ line2.setPointSize(0);
+ line2.setLabel('LINE TWO');
+ const NUM_POINTS = 1000000;
+ const points1 = [];
+ const points2 = [];
+ for (let ii = 0; ii < NUM_POINTS; ++ii) {
+ points1.push(ii);
+ points2.push(ii);
+ points1.push(Math.sin(ii * 10 / NUM_POINTS));
+ points2.push(Math.cos(ii * 10 / NUM_POINTS));
+ }
+ line1.setPoints(new Float32Array(points1));
+ line2.setPoints(new Float32Array(points2));
+}
diff --git a/aos/network/www/graph.html b/aos/network/www/graph.html
deleted file mode 100644
index 0f66bb0..0000000
--- a/aos/network/www/graph.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<html>
- <head>
- <script src="graph_main_bundle.min.js" defer></script>
- <link rel="stylesheet" href="styles.css">
- </head>
- <body>
- </body>
-</html>
-
diff --git a/aos/network/www/graph_main.ts b/aos/network/www/graph_main.ts
deleted file mode 100644
index 2548e1d..0000000
--- a/aos/network/www/graph_main.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-// This file creates a set of two demonstration plots for testing out the
-// plotting utility.
-// To run this and see a demo, run:
-// bazel run -c opt //aos/network/www:web_proxy_demo
-// And then open localhost:8080/graph.html
-//
-// The plots are just the plots of the handler latency / run times of the first
-// timer in the the /aos timing report message (this message was chosen because
-// it is already being published by the web proxy process, so the demo requires
-// very little setup).
-import * as configuration from 'org_frc971/aos/configuration_generated';
-import {Line, Plot} from 'org_frc971/aos/network/www/plotter';
-import * as proxy from 'org_frc971/aos/network/www/proxy';
-import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
-import * as reflection from 'org_frc971/aos/network/www/reflection'
-import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
-import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
-
-import Channel = configuration.aos.Channel;
-import Connection = proxy.Connection;
-import Configuration = configuration.aos.Configuration;
-import Parser = reflection.Parser;
-import Table = reflection.Table;
-import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
-import ChannelRequest = web_proxy.aos.web_proxy.ChannelRequest;
-import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
-
-const width = 900;
-const height = 400;
-
-const helpDiv = document.createElement('div');
-helpDiv.style.top = '0';
-helpDiv.style.left = '0';
-helpDiv.style.position = 'absolute';
-document.body.appendChild(helpDiv);
-helpDiv.innerHTML =
- 'Help: click + drag to pan, double click to reset, scroll to zoom. ' +
- 'Hold the x/y keys to only pan/zoom along the x/y axes.<br>' +
- 'X-axes of the top two plots are linked together.';
-
-const div = document.createElement('div');
-div.style.top = '40';
-div.style.left = '0';
-div.style.position = 'absolute';
-document.body.appendChild(div);
-
-const div2 = document.createElement('div');
-div2.style.top = (parseFloat(div.style.top) + height).toString();
-div2.style.left = '0';
-div2.style.position = 'absolute';
-document.body.appendChild(div2);
-
-const benchmarkDiv = document.createElement('div');
-benchmarkDiv.style.top = (parseFloat(div2.style.top) + height).toString();
-benchmarkDiv.style.left = '0';
-benchmarkDiv.style.position = 'absolute';
-document.body.appendChild(benchmarkDiv);
-
-const handlerTimePlot = new Plot(div, width, height);
-const latencyPlot = new Plot(div2, width, height);
-const benchmarkPlot = new Plot(benchmarkDiv, width, height);
-
-// Class to store the lines for plotting a Statistics message.
-class StatLines {
- private max: Line;
- private min: Line;
- private average: Line;
- private maxPoints: number[] = [];
- private minPoints: number[] = [];
- private averagePoints: number[] = [];
- constructor(plotter: Plot) {
- this.max = plotter.getDrawer().addLine();
- this.min = plotter.getDrawer().addLine();
- this.average = plotter.getDrawer().addLine();
-
- this.max.setLabel("Max");
- this.min.setLabel("Min");
- this.average.setLabel("Average");
-
- this.max.setColor([1, 0, 0]);
- this.min.setColor([0, 1, 0]);
- this.average.setColor([0, 0, 1]);
- }
- addPoints(parser: Parser, statsTable: Table, time: number) {
- this.maxPoints.push(time);
- this.minPoints.push(time);
- this.averagePoints.push(time);
- this.maxPoints.push(parser.readScalar(statsTable, "max") * 1000);
- this.minPoints.push(parser.readScalar(statsTable, "min") * 1000);
- this.averagePoints.push(parser.readScalar(statsTable, "average") * 1000);
-
-
- // TODO: These memory allocations absolutely kill performance.
- this.max.setPoints(new Float32Array(this.maxPoints));
- this.min.setPoints(new Float32Array(this.minPoints));
- this.average.setPoints(new Float32Array(this.averagePoints));
- }
-}
-
-function setupPlot(plotter: Plot, title: string): StatLines {
- plotter.getAxisLabels().setXLabel("Monotonic send time since start (sec)");
- plotter.getAxisLabels().setYLabel("Time (msec)");
- plotter.getAxisLabels().setTitle(title);
- return new StatLines(plotter);
-}
-
-// Sets of the two x-axes to be shared; remove this to be able to zoom/pan the
-// x-axes independently.
-handlerTimePlot.linkXAxis(latencyPlot);
-
-const handlerLines = setupPlot(handlerTimePlot, "Handler Run Times");
-const latencyLines = setupPlot(latencyPlot, "Handler Latencies");
-
-const conn = new Connection();
-
-conn.connect();
-
-const timingReport = {
- name: '/aos',
- type: 'aos.timing.Report',
-};
-
-let reportParser = null;
-let startTime = null;
-
-conn.addConfigHandler((config: Configuration) => {
- // Locate the timing report schema so that we can read the received messages.
- let reportSchema = null;
- for (let ii = 0; ii < config.channelsLength(); ++ii) {
- if (config.channels(ii).type() === timingReport.type) {
- reportSchema = config.channels(ii).schema();
- }
- }
- if (reportSchema === null) {
- throw new Error('Couldn\'t find timing report schema in config.');
- }
- reportParser = new Parser(reportSchema);
-
- conn.addReliableHandler(
- timingReport.name, timingReport.type,
- (data: Uint8Array, time: number) => {
- if (startTime === null) {
- startTime = time;
- }
- time = time - startTime;
- const table = Table.getRootTable(new ByteBuffer(data));
-
- const timer = reportParser.readVectorOfTables(table, 'timers')[0];
- handlerLines.addPoints(
- reportParser, reportParser.readTable(timer, 'handler_time'), time);
- latencyLines.addPoints(
- reportParser, reportParser.readTable(timer, 'wakeup_latency'),
- time);
- });
-});
-
-// Set up and draw the benchmarking plot
-benchmarkPlot.getAxisLabels().setTitle(
- 'Benchmarkping plot (1M points per line)');
-const line1 = benchmarkPlot.getDrawer().addLine();
-// For demonstration purposes, make line1 only have points and line2 only have
-// lines.
-line1.setDrawLine(false);
-line1.setLabel('LINE ONE');
-const line2 = benchmarkPlot.getDrawer().addLine();
-line2.setPointSize(0);
-line2.setLabel('LINE TWO');
-const NUM_POINTS = 1000000;
-const points1 = [];
-const points2 = [];
-for (let ii = 0; ii < NUM_POINTS; ++ii) {
- points1.push(ii);
- points2.push(ii);
- points1.push(Math.sin(ii * 10 / NUM_POINTS));
- points2.push(Math.cos(ii * 10 / NUM_POINTS));
-}
-line1.setPoints(new Float32Array(points1));
-line2.setPoints(new Float32Array(points2));
diff --git a/aos/network/www/plotter.ts b/aos/network/www/plotter.ts
index d33953c..bbf5205 100644
--- a/aos/network/www/plotter.ts
+++ b/aos/network/www/plotter.ts
@@ -130,6 +130,10 @@
}
}
+ getPoints(): Float32Array {
+ return this.points;
+ }
+
// Get/set the label to use for the line when drawing the legend.
setLabel(label: string) {
this._label = label;
@@ -231,7 +235,7 @@
// Location, in pixels, of the legend in the text canvas.
private location: number[] = [0, 0];
constructor(private ctx: CanvasRenderingContext2D, private lines: Line[]) {
- this.location = [this.ctx.canvas.width - 100, 30];
+ this.location = [80, 30];
}
setPosition(location: number[]): void {
@@ -501,9 +505,6 @@
for (let line of this.lines) {
minValues = cwiseOp(minValues, line.minValues(), Math.min);
}
- if (!isFinite(minValues[0]) || !isFinite(minValues[1])) {
- return [0, 0];
- }
return minValues;
}
@@ -512,9 +513,6 @@
for (let line of this.lines) {
maxValues = cwiseOp(maxValues, line.maxValues(), Math.max);
}
- if (!isFinite(maxValues[0]) || !isFinite(maxValues[1])) {
- return [0, 0];
- }
return maxValues;
}
@@ -740,6 +738,7 @@
private autoFollow: boolean = true;
private linkedXAxes: Plot[] = [];
private lastTimeMs: number = 0;
+ private defaultYRange: number[]|null = null;
constructor(wrapperDiv: HTMLDivElement, width: number, height: number) {
wrapperDiv.appendChild(this.canvas);
@@ -889,9 +888,35 @@
this.setZoom(scale, offset);
}
+ setDefaultYRange(range: number[]|null) {
+ if (range == null) {
+ this.defaultYRange = null;
+ return;
+ }
+ if (range.length != 2) {
+ throw new Error('Range should contain exactly two values.');
+ }
+ this.defaultYRange = range;
+ }
+
resetZoom() {
const minValues = this.drawer.minValues();
const maxValues = this.drawer.maxValues();
+ for (const plot of this.linkedXAxes) {
+ const otherMin = plot.drawer.minValues();
+ const otherMax = plot.drawer.maxValues();
+ // For linked x-axes, only adjust the x limits.
+ minValues[0] = Math.min(minValues[0], otherMin[0]);
+ maxValues[0] = Math.max(maxValues[0], otherMax[0]);
+ }
+ if (!isFinite(minValues[0]) || !isFinite(maxValues[0])) {
+ minValues[0] = 0;
+ maxValues[0] = 0;
+ }
+ if (!isFinite(minValues[1]) || !isFinite(maxValues[1])) {
+ minValues[1] = 0;
+ maxValues[1] = 0;
+ }
if (minValues[0] == maxValues[0]) {
minValues[0] -= 1;
maxValues[0] += 1;
@@ -900,6 +925,10 @@
minValues[1] -= 1;
maxValues[1] += 1;
}
+ if (this.defaultYRange != null) {
+ minValues[1] = this.defaultYRange[0];
+ maxValues[1] = this.defaultYRange[1];
+ }
this.setZoomCorners(minValues, maxValues);
this.autoFollow = true;
for (let plot of this.linkedXAxes) {
diff --git a/aos/network/www/proxy.ts b/aos/network/www/proxy.ts
index 4fbba85..948f8db 100644
--- a/aos/network/www/proxy.ts
+++ b/aos/network/www/proxy.ts
@@ -5,6 +5,7 @@
import ChannelFb = configuration.aos.Channel;
import Configuration = configuration.aos.Configuration;
+import Schema = configuration.reflection.Schema;
import MessageHeader = web_proxy.aos.web_proxy.MessageHeader;
import WebSocketIce = web_proxy.aos.web_proxy.WebSocketIce;
import WebSocketMessage = web_proxy.aos.web_proxy.WebSocketMessage;
@@ -83,7 +84,7 @@
private readonly configHandlers = new Set<(config: Configuration) => void>();
private readonly handlerFuncs =
- new Map<string, (data: Uint8Array, sentTime: number) => void>();
+ new Map<string, ((data: Uint8Array, sentTime: number) => void)[]>();
private readonly handlers = new Set<Handler>();
private subscribedChannels: ChannelRequest[] = [];
@@ -122,10 +123,27 @@
handler: (data: Uint8Array, sentTime: number) => void): void {
const channel = new Channel(name, type);
const request = new ChannelRequest(channel, method);
- this.handlerFuncs.set(channel.key(), handler);
+ if (!this.handlerFuncs.has(channel.key())) {
+ this.handlerFuncs.set(channel.key(), []);
+ }
+ this.handlerFuncs.get(channel.key()).push(handler);
this.subscribeToChannel(request);
}
+ getSchema(typeName: string): Schema {
+ let schema = null;
+ const config = this.getConfig();
+ for (let ii = 0; ii < config.channelsLength(); ++ii) {
+ if (config.channels(ii).type() === typeName) {
+ schema = config.channels(ii).schema();
+ }
+ }
+ if (schema === null) {
+ throw new Error('Unable to find schema for ' + typeName);
+ }
+ return schema;
+ }
+
subscribeToChannel(channel: ChannelRequest): void {
this.subscribedChannels.push(channel);
if (this.configInternal === null) {
@@ -183,8 +201,10 @@
onDataChannel(ev: RTCDataChannelEvent): void {
const channel = ev.channel;
const name = channel.label;
- const handlerFunc = this.handlerFuncs.get(name);
- this.handlers.add(new Handler(handlerFunc, channel));
+ const handlers = this.handlerFuncs.get(name);
+ for (const handler of handlers) {
+ this.handlers.add(new Handler(handler, channel));
+ }
}
onIceCandidate(e: RTCPeerConnectionIceEvent): void {
diff --git a/aos/network/www/reflection.ts b/aos/network/www/reflection.ts
index 9c21a17..2e1ed8f 100644
--- a/aos/network/www/reflection.ts
+++ b/aos/network/www/reflection.ts
@@ -120,6 +120,16 @@
static getRootTable(bb: ByteBuffer): Table {
return new Table(bb, -1, bb.readInt32(bb.position()) + bb.position());
}
+ static getNamedTable(
+ bb: ByteBuffer, schema: reflection.Schema, type: string,
+ offset: number): Table {
+ for (let ii = 0; ii < schema.objectsLength(); ++ii) {
+ if (schema.objects(ii).name() == type) {
+ return new Table(bb, ii, offset);
+ }
+ }
+ throw new Error('Unable to find type ' + type + ' in schema.');
+ }
// Reads a scalar of a given type at a given offset.
readScalar(fieldType: reflection.BaseType, offset: number) {
switch (fieldType) {