blob: cd2e1319075e50aa962977691d6e3ca1b4b46d96 [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.
James Kuszmauldac091f2022-03-22 09:35:06 -070026import {Channel, 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 Kuszmauldac091f2022-03-22 09:35:06 -070028import {Connection} from 'org_frc971/aos/network/www/proxy';
29import {SubscriberRequest, ChannelRequest, TransferMethod} from 'org_frc971/aos/network/web_proxy_generated';
30import {Parser, Table} from 'org_frc971/aos/network/www/reflection'
James Kuszmaul136aa2b2022-04-02 14:50:56 -070031import {Schema} from 'flatbuffers_reflection/reflection_generated';
James Kuszmauldac091f2022-03-22 09:35:06 -070032import {ByteBuffer} from 'flatbuffers';
James Kuszmaul5f5e1232020-12-22 20:58:00 -080033
34export class TimestampedMessage {
35 constructor(
36 public readonly message: Table, public readonly time: number) {}
37}
38
39// The MessageHandler stores an array of every single message on a given channel
40// and then supplies individual fields as arrays when requested. Currently this
41// is very much unoptimized and re-processes the entire array of messages on
42// every call to getField().
43export class MessageHandler {
44 protected parser: Parser;
45 protected messages: TimestampedMessage[] = [];
46 constructor(schema: Schema) {
47 this.parser = new Parser(schema);
48 }
49 addMessage(data: Uint8Array, time: number): void {
50 this.messages.push(
51 new TimestampedMessage(Table.getRootTable(new ByteBuffer(data)), time));
52 }
James Kuszmaulf8355ff2021-12-19 22:08:45 -080053 private parseFieldName(rawName: string): [string, boolean, number|null] {
54 // If the fieldName includes an array index at the end, attempt to read a
55 // vector.
56 // The indices can either be in the form [X] for some index X, or be an
57 // empty [], which is a request to return all values.
James Kuszmaul8073c0d2021-03-07 20:14:41 -080058 const regex = /(.*)\[([0-9]*)\]/;
James Kuszmaulf8355ff2021-12-19 22:08:45 -080059 const match = rawName.match(regex);
James Kuszmaul8073c0d2021-03-07 20:14:41 -080060 if (match) {
61 const name = match[1];
James Kuszmaulf8355ff2021-12-19 22:08:45 -080062 const requestFullVector = match[2] === '';
63 const requestedIndex = requestFullVector ? null : parseInt(match[2]);
64 return [name, true, requestedIndex];
65 } else {
66 return [rawName, false, null];
James Kuszmaul8073c0d2021-03-07 20:14:41 -080067 }
James Kuszmaulf8355ff2021-12-19 22:08:45 -080068 }
69 private readField<T>(
70 typeIndex: number, fieldName: string,
71 normalReader: (typeIndex: number, name: string) => (t: Table) => T | null,
72 vectorReader: (typeIndex: number, name: string) => (t: Table) => T[] |
73 null): (t: Table) => T[] | null {
74 const [name, isVector, vectorIndex] = this.parseFieldName(fieldName);
75 if (isVector) {
76 const vectorLambda = vectorReader(typeIndex, name);
77 const requestFullVector = vectorIndex === null;
78 return (message: Table) => {
79 const vector = vectorLambda(message);
80 if (vector === null) {
81 return null;
82 }
83 if (requestFullVector) {
84 return vector;
85 } else {
86 return (vectorIndex < vector.length) ? [vector[vectorIndex]] : null;
87 }
88 };
89 }
90 const singleLambda = normalReader(typeIndex, fieldName);
91 return (message: Table) => {
92 // For single values, return as a 1-vector so that the type is
93 // consistent with that used for vectors.
94 const singleValue = singleLambda(message);
95 return (singleValue === null) ? null : [singleValue];
96 };
James Kuszmaul8073c0d2021-03-07 20:14:41 -080097 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -080098 // Returns a time-series of every single instance of the given field. Format
99 // of the return value is [time0, value0, time1, value1,... timeN, valueN],
100 // to match with the Line.setPoint() interface.
101 // By convention, NaN is used to indicate that a message existed at a given
102 // timestamp but the requested field was not populated.
James Kuszmaulf3727e82021-03-06 16:52:51 -0800103 // If you want to retrieve a single signal from a vector, you can specify it
104 // as "field_name[index]".
James Kuszmaul0d7df892021-04-09 22:19:49 -0700105 getField(field: string[]): Point[] {
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800106 if (this.messages.length == 0) {
107 return [];
108 }
109 const rootType = this.messages[0].message.typeIndex;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800110 const fieldName = field[field.length - 1];
111 const subMessage = field.slice(0, field.length - 1);
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800112
113 const lambdas = [];
114 let currentType = rootType;
115 for (const subMessageName of subMessage) {
116 lambdas.push(this.readField(
117 currentType, subMessageName,
118 (typeIndex: number, name: string) =>
119 this.parser.readTableLambda(typeIndex, name),
120 (typeIndex: number, name: string) =>
121 this.parser.readVectorOfTablesLambda(typeIndex, name)));
122 const [name, isVector, vectorIndex] = this.parseFieldName(subMessageName);
123 currentType =
124 this.parser.getField(name, currentType).type().index();
125 }
126 const fieldLambda = this.readField(
127 currentType, fieldName,
128 (typeIndex: number, name: string) =>
129 this.parser.readScalarLambda(typeIndex, name),
130 (typeIndex: number, name: string) =>
131 this.parser.readVectorOfScalarsLambda(typeIndex, name));
132
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800133 const results = [];
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800134 for (let ii = 0; ii < this.messages.length; ++ii) {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800135 let tables = [this.messages[ii].message];
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800136 for (const lambda of lambdas) {
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800137 let nextTables = [];
138 for (const table of tables) {
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800139 const nextTable = lambda(table);
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800140 if (nextTable === null) {
141 continue;
142 }
143 nextTables = nextTables.concat(nextTable);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800144 }
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800145 tables = nextTables;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800146 }
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800147
148 const values = [];
149 for (const table of tables) {
150 const value = fieldLambda(table);
151 if (value !== null) {
152 values.push(value);
153 }
154 }
James Kuszmaul8073c0d2021-03-07 20:14:41 -0800155 const time = this.messages[ii].time;
156 if (tables.length === 0) {
James Kuszmaul0d7df892021-04-09 22:19:49 -0700157 results.push(new Point(time, NaN));
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800158 } else {
James Kuszmaulf8355ff2021-12-19 22:08:45 -0800159 for (const valueVector of values) {
160 for (const value of valueVector) {
161 results.push(new Point(time, (value === null) ? NaN : value));
James Kuszmaulf3727e82021-03-06 16:52:51 -0800162 }
163 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800164 }
165 }
James Kuszmaul0d7df892021-04-09 22:19:49 -0700166 return results;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800167 }
168 numMessages(): number {
169 return this.messages.length;
170 }
171}
172
173class MessageLine {
James Kuszmaul3e382c02021-04-09 22:34:36 -0700174 private _lastNumMessages: number = 0;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800175 constructor(
176 public readonly messages: MessageHandler, public readonly line: Line,
177 public readonly field: string[]) {}
James Kuszmaul3e382c02021-04-09 22:34:36 -0700178 hasUpdate(): boolean {
179 const updated = this._lastNumMessages != this.messages.numMessages();
180 this._lastNumMessages = this.messages.numMessages();
181 return updated;
182 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800183}
184
185class AosPlot {
186 private lines: MessageLine[] = [];
187 constructor(
188 private readonly plotter: AosPlotter, public readonly plot: Plot) {}
189
190 // Adds a line to the figure.
191 // message specifies what channel/data source to pull from, and field
192 // specifies the field within that channel. field is an array specifying
193 // the full path to the field within the message. For instance, to
194 // plot whether the drivetrain is currently zeroed based on the drivetrain
195 // status message, you would specify the ['zeroing', 'zeroed'] field to
196 // get the DrivetrainStatus.zeroing().zeroed() member.
197 // Currently, this interface does not provide any support for non-numeric
198 // fields or for repeated fields (or sub-messages) of any sort.
James Kuszmaulb26abe62022-01-30 16:38:07 -0800199 addMessageLine(message: MessageHandler|null, field: string[]): Line {
200 // Construct the line regardless so that we have something to return to the
201 // user.
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800202 const line = this.plot.getDrawer().addLine();
James Kuszmaulb26abe62022-01-30 16:38:07 -0800203 if (message === null) {
204 console.warn(
205 'Not plotting field ' + field.join('.') +
206 ' because of an invalid MessageHandler.');
207 return line;
208 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800209 line.setLabel(field.join('.'));
210 this.lines.push(new MessageLine(message, line, field));
211 return line;
212 }
213
214 draw(): void {
215 // Only redraw lines if the number of points has changed--because getField()
216 // is a relatively expensive call, we don't want to do it any more than
217 // necessary.
218 for (const line of this.lines) {
James Kuszmaul3e382c02021-04-09 22:34:36 -0700219 if (line.hasUpdate()) {
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800220 line.line.setPoints(line.messages.getField(line.field));
221 }
222 }
223 }
224}
225
226export class AosPlotter {
milind upadhyay9bd381d2021-01-23 13:44:13 -0800227 public static readonly TIME: string = "Monotonic Time (sec)";
228
229 public static readonly DEFAULT_WIDTH: number = 900;
230 public static readonly DEFAULT_HEIGHT: number = 400;
231
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800232 private plots: AosPlot[] = [];
233 private messages = new Set<MessageHandler>();
234 constructor(private readonly connection: Connection) {
235 // Set up to redraw at some regular interval. The exact rate is unimportant.
236 setInterval(() => {
237 this.draw();
238 }, 100);
239 }
240
241 // Sets up an AOS channel as a message source. Returns a handler that can
242 // be passed to addMessageLine().
James Kuszmaulb26abe62022-01-30 16:38:07 -0800243 addMessageSource(name: string, type: string): MessageHandler|null {
244 let schema = null;
245 try {
246 schema = this.connection.getSchema(type);
247 } catch (e) {
248 console.error(e);
249 return null;
250 }
251 return this.addRawMessageSource(name, type, new MessageHandler(schema));
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800252 }
253
254 // Same as addMessageSource, but allows you to specify a custom MessageHandler
255 // that does some processing on the requested message. This allows you to
256 // create post-processed versions of individual channels.
257 addRawMessageSource(
258 name: string, type: string,
James Kuszmaulb26abe62022-01-30 16:38:07 -0800259 messageHandler: MessageHandler|null): MessageHandler {
260 if (messageHandler === null) {
261 return null;
262 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800263 this.messages.add(messageHandler);
264 // Use a "reliable" handler so that we get *all* the data when we are
265 // plotting from a logfile.
266 this.connection.addReliableHandler(
267 name, type, (data: Uint8Array, time: number) => {
268 messageHandler.addMessage(data, time);
269 });
270 return messageHandler;
271 }
272 // Add a new figure at the provided position with the provided size within
273 // parentElement.
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800274 addPlot(
Austin Schuhc2e9c502021-11-25 21:23:24 -0800275 parentElement: Element,
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800276 size: number[] = [AosPlotter.DEFAULT_WIDTH, AosPlotter.DEFAULT_HEIGHT]):
277 AosPlot {
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800278 const div = document.createElement("div");
Austin Schuhc2e9c502021-11-25 21:23:24 -0800279 div.style.position = 'relative';
280 div.style.width = size[0].toString() + "px";
281 div.style.height = size[1].toString() + "px";
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800282 parentElement.appendChild(div);
Austin Schuhc2e9c502021-11-25 21:23:24 -0800283 const newPlot = new Plot(div);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800284 for (let plot of this.plots.values()) {
285 newPlot.linkXAxis(plot.plot);
286 }
287 this.plots.push(new AosPlot(this, newPlot));
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800288 return this.plots[this.plots.length - 1];
289 }
290 private draw(): void {
291 for (const plot of this.plots) {
292 plot.draw();
293 }
294 }
295}