blob: 0145c075856fc86d4f7b52b1e6c78ce4c471858d [file] [log] [blame]
James Kuszmaul5f5e1232020-12-22 20:58:00 -08001// This library provides a wrapper around our WebGL plotter that makes it
2// easy to plot AOS messages/channels as time series.
3//
4// This is works by subscribing to each channel that we want to plot, storing
5// all the messages for that channel, and then periodically running through
6// every message and extracting the fields to plot.
7// It is also possible to insert code to make modifications to the messages
8// as we read/process them, as is the case for the IMU processing code (see
9// //frc971/wpilib:imu*.ts) where each message is actually a batch of several
10// individual messages that need to be plotted as separate points.
11//
12// The basic flow for using the AosPlotter is:
13// // 1) Construct the plotter
14// const aosPlotter = new AosPlotter(connection);
15// // 2) Add messages sources that we'll want to subscribe to.
16// const source = aosPlotter.addMessageSource('/aos', 'aos.timing.Report');
17// // 3) Create figures at defined positions within a given HTML element..
18// const timingPlot = aosPlotter.addPlot(parentDiv, [0, 0], [width, height]);
19// // 4) Add specific signals to each figure, using the message sources you
20// defined at the start.
21// timingPlot.addMessageLine(source, ['pid']);
22//
23// The demo_plot.ts script has a basic example of using this library, with all
24// the required boilerplate, as well as some extra examples about how to
25// add axis labels and the such.
26import * as configuration from 'org_frc971/aos/configuration_generated';
James Kuszmaul0d7df892021-04-09 22:19:49 -070027import {Line, Plot, Point} from 'org_frc971/aos/network/www/plotter';
James Kuszmaul5f5e1232020-12-22 20:58:00 -080028import * as proxy from 'org_frc971/aos/network/www/proxy';
29import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
30import * as reflection from 'org_frc971/aos/network/www/reflection'
31import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
32import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
33
34import Channel = configuration.aos.Channel;
35import Connection = proxy.Connection;
36import Configuration = configuration.aos.Configuration;
37import Schema = configuration.reflection.Schema;
38import Parser = reflection.Parser;
39import Table = reflection.Table;
40import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
41import ChannelRequest = web_proxy.aos.web_proxy.ChannelRequest;
42import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
43
44export class TimestampedMessage {
45 constructor(
46 public readonly message: Table, public readonly time: number) {}
47}
48
49// The MessageHandler stores an array of every single message on a given channel
50// and then supplies individual fields as arrays when requested. Currently this
51// is very much unoptimized and re-processes the entire array of messages on
52// every call to getField().
53export class MessageHandler {
54 protected parser: Parser;
55 protected messages: TimestampedMessage[] = [];
56 constructor(schema: Schema) {
57 this.parser = new Parser(schema);
58 }
59 addMessage(data: Uint8Array, time: number): void {
60 this.messages.push(
61 new TimestampedMessage(Table.getRootTable(new ByteBuffer(data)), time));
62 }
James Kuszmaul8073c0d2021-03-07 20:14:41 -080063 private readField<T>(
64 message: Table, fieldName: string,
65 normalReader: (message: Table, name: string) => T | null,
66 vectorReader: (message: Table, name: string) => T[] | null): T[]|null {
67 // Typescript handles bindings in non-obvious ways that aren't caught well
68 // by the compiler.
69 normalReader = normalReader.bind(this.parser);
70 vectorReader = vectorReader.bind(this.parser);
71 const regex = /(.*)\[([0-9]*)\]/;
72 const match = fieldName.match(regex);
73 if (match) {
74 const name = match[1];
75 const vector = vectorReader(message, name);
76 if (vector === null) {
77 return null;
78 }
79 if (match[2] === "") {
80 return vector;
81 } else {
82 const index = parseInt(match[2]);
83 return (index < vector.length) ? [vector[index]] : null;
84 }
85 }
86 const singleResult = normalReader(message, fieldName);
87 return singleResult ? [singleResult] : null;
88 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -080089 // Returns a time-series of every single instance of the given field. Format
90 // of the return value is [time0, value0, time1, value1,... timeN, valueN],
91 // to match with the Line.setPoint() interface.
92 // By convention, NaN is used to indicate that a message existed at a given
93 // timestamp but the requested field was not populated.
James Kuszmaulf3727e82021-03-06 16:52:51 -080094 // If you want to retrieve a single signal from a vector, you can specify it
95 // as "field_name[index]".
James Kuszmaul0d7df892021-04-09 22:19:49 -070096 getField(field: string[]): Point[] {
James Kuszmaul5f5e1232020-12-22 20:58:00 -080097 const fieldName = field[field.length - 1];
98 const subMessage = field.slice(0, field.length - 1);
James Kuszmaul8073c0d2021-03-07 20:14:41 -080099 const results = [];
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800100 for (let ii = 0; ii < this.messages.length; ++ii) {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800101 let tables = [this.messages[ii].message];
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800102 for (const subMessageName of subMessage) {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800103 let nextTables = [];
104 for (const table of tables) {
105 const nextTable = this.readField(
106 table, subMessageName, Parser.prototype.readTable,
107 Parser.prototype.readVectorOfTables);
108 if (nextTable === null) {
109 continue;
110 }
111 nextTables = nextTables.concat(nextTable);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800112 }
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800113 tables = nextTables;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800114 }
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800115 const time = this.messages[ii].time;
116 if (tables.length === 0) {
James Kuszmaul0d7df892021-04-09 22:19:49 -0700117 results.push(new Point(time, NaN));
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800118 } else {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800119 for (const table of tables) {
120 const values = this.readField(
121 table, fieldName, Parser.prototype.readScalar,
122 Parser.prototype.readVectorOfScalars);
123 if (values === null) {
James Kuszmaul0d7df892021-04-09 22:19:49 -0700124 results.push(new Point(time, NaN));
James Kuszmaulf3727e82021-03-06 16:52:51 -0800125 } else {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800126 for (const value of values) {
James Kuszmaul0d7df892021-04-09 22:19:49 -0700127 results.push(new Point(time, (value === null) ? NaN : value));
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800128 }
James Kuszmaulf3727e82021-03-06 16:52:51 -0800129 }
130 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800131 }
132 }
James Kuszmaul0d7df892021-04-09 22:19:49 -0700133 return results;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800134 }
135 numMessages(): number {
136 return this.messages.length;
137 }
138}
139
140class MessageLine {
James Kuszmaul3e382c02021-04-09 22:34:36 -0700141 private _lastNumMessages: number = 0;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800142 constructor(
143 public readonly messages: MessageHandler, public readonly line: Line,
144 public readonly field: string[]) {}
James Kuszmaul3e382c02021-04-09 22:34:36 -0700145 hasUpdate(): boolean {
146 const updated = this._lastNumMessages != this.messages.numMessages();
147 this._lastNumMessages = this.messages.numMessages();
148 return updated;
149 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800150}
151
152class AosPlot {
153 private lines: MessageLine[] = [];
154 constructor(
155 private readonly plotter: AosPlotter, public readonly plot: Plot) {}
156
157 // Adds a line to the figure.
158 // message specifies what channel/data source to pull from, and field
159 // specifies the field within that channel. field is an array specifying
160 // the full path to the field within the message. For instance, to
161 // plot whether the drivetrain is currently zeroed based on the drivetrain
162 // status message, you would specify the ['zeroing', 'zeroed'] field to
163 // get the DrivetrainStatus.zeroing().zeroed() member.
164 // Currently, this interface does not provide any support for non-numeric
165 // fields or for repeated fields (or sub-messages) of any sort.
166 addMessageLine(message: MessageHandler, field: string[]): Line {
167 const line = this.plot.getDrawer().addLine();
168 line.setLabel(field.join('.'));
169 this.lines.push(new MessageLine(message, line, field));
170 return line;
171 }
172
173 draw(): void {
174 // Only redraw lines if the number of points has changed--because getField()
175 // is a relatively expensive call, we don't want to do it any more than
176 // necessary.
177 for (const line of this.lines) {
James Kuszmaul3e382c02021-04-09 22:34:36 -0700178 if (line.hasUpdate()) {
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800179 line.line.setPoints(line.messages.getField(line.field));
180 }
181 }
182 }
183}
184
185export class AosPlotter {
milind upadhyay9bd381d2021-01-23 13:44:13 -0800186 public static readonly TIME: string = "Monotonic Time (sec)";
187
188 public static readonly DEFAULT_WIDTH: number = 900;
189 public static readonly DEFAULT_HEIGHT: number = 400;
190
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800191 private plots: AosPlot[] = [];
192 private messages = new Set<MessageHandler>();
193 constructor(private readonly connection: Connection) {
194 // Set up to redraw at some regular interval. The exact rate is unimportant.
195 setInterval(() => {
196 this.draw();
197 }, 100);
198 }
199
200 // Sets up an AOS channel as a message source. Returns a handler that can
201 // be passed to addMessageLine().
202 addMessageSource(name: string, type: string): MessageHandler {
203 return this.addRawMessageSource(
204 name, type, new MessageHandler(this.connection.getSchema(type)));
205 }
206
207 // Same as addMessageSource, but allows you to specify a custom MessageHandler
208 // that does some processing on the requested message. This allows you to
209 // create post-processed versions of individual channels.
210 addRawMessageSource(
211 name: string, type: string,
212 messageHandler: MessageHandler): MessageHandler {
213 this.messages.add(messageHandler);
214 // Use a "reliable" handler so that we get *all* the data when we are
215 // plotting from a logfile.
216 this.connection.addReliableHandler(
217 name, type, (data: Uint8Array, time: number) => {
218 messageHandler.addMessage(data, time);
219 });
220 return messageHandler;
221 }
222 // Add a new figure at the provided position with the provided size within
223 // parentElement.
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800224 addPlot(
Austin Schuhc2e9c502021-11-25 21:23:24 -0800225 parentElement: Element,
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800226 size: number[] = [AosPlotter.DEFAULT_WIDTH, AosPlotter.DEFAULT_HEIGHT]):
227 AosPlot {
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800228 const div = document.createElement("div");
Austin Schuhc2e9c502021-11-25 21:23:24 -0800229 div.style.position = 'relative';
230 div.style.width = size[0].toString() + "px";
231 div.style.height = size[1].toString() + "px";
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800232 parentElement.appendChild(div);
Austin Schuhc2e9c502021-11-25 21:23:24 -0800233 const newPlot = new Plot(div);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800234 for (let plot of this.plots.values()) {
235 newPlot.linkXAxis(plot.plot);
236 }
237 this.plots.push(new AosPlot(this, newPlot));
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800238 return this.plots[this.plots.length - 1];
239 }
240 private draw(): void {
241 for (const plot of this.plots) {
242 plot.draw();
243 }
244 }
245}