blob: 4b1d8bfd67cd946bce69de652cf1bd2fcf7f4c0a [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';
27import {Line, Plot} from 'org_frc971/aos/network/www/plotter';
28import * 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 }
63 // Returns a time-series of every single instance of the given field. Format
64 // of the return value is [time0, value0, time1, value1,... timeN, valueN],
65 // to match with the Line.setPoint() interface.
66 // By convention, NaN is used to indicate that a message existed at a given
67 // timestamp but the requested field was not populated.
James Kuszmaulf3727e82021-03-06 16:52:51 -080068 // If you want to retrieve a single signal from a vector, you can specify it
69 // as "field_name[index]".
James Kuszmaul5f5e1232020-12-22 20:58:00 -080070 getField(field: string[]): Float32Array {
71 const fieldName = field[field.length - 1];
72 const subMessage = field.slice(0, field.length - 1);
73 const results = new Float32Array(this.messages.length * 2);
74 for (let ii = 0; ii < this.messages.length; ++ii) {
75 let message = this.messages[ii].message;
76 for (const subMessageName of subMessage) {
77 message = this.parser.readTable(message, subMessageName);
78 if (message === undefined) {
79 break;
80 }
81 }
82 results[ii * 2] = this.messages[ii].time;
James Kuszmaulf3727e82021-03-06 16:52:51 -080083 const regex = /(.*)\[([0-9]*)\]/;
84 let name = fieldName;
85 let match = fieldName.match(regex);
86 let index = undefined;
87 if (match) {
88 name = match[1]
89 index = parseInt(match[2])
90 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -080091 if (message === undefined) {
92 results[ii * 2 + 1] = NaN;
93 } else {
James Kuszmaulf3727e82021-03-06 16:52:51 -080094 if (index === undefined) {
95 results[ii * 2 + 1] = this.parser.readScalar(message, name);
96 } else {
97 const vector =
98 this.parser.readVectorOfScalars(message, name);
99 if (index < vector.length) {
100 results[ii * 2 + 1] = vector[index];
101 } else {
102 results[ii * 2 + 1] = NaN;
103 }
104 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800105 }
106 }
107 return results;
108 }
109 numMessages(): number {
110 return this.messages.length;
111 }
112}
113
114class MessageLine {
115 constructor(
116 public readonly messages: MessageHandler, public readonly line: Line,
117 public readonly field: string[]) {}
118}
119
120class AosPlot {
121 private lines: MessageLine[] = [];
122 constructor(
123 private readonly plotter: AosPlotter, public readonly plot: Plot) {}
124
125 // Adds a line to the figure.
126 // message specifies what channel/data source to pull from, and field
127 // specifies the field within that channel. field is an array specifying
128 // the full path to the field within the message. For instance, to
129 // plot whether the drivetrain is currently zeroed based on the drivetrain
130 // status message, you would specify the ['zeroing', 'zeroed'] field to
131 // get the DrivetrainStatus.zeroing().zeroed() member.
132 // Currently, this interface does not provide any support for non-numeric
133 // fields or for repeated fields (or sub-messages) of any sort.
134 addMessageLine(message: MessageHandler, field: string[]): Line {
135 const line = this.plot.getDrawer().addLine();
136 line.setLabel(field.join('.'));
137 this.lines.push(new MessageLine(message, line, field));
138 return line;
139 }
140
141 draw(): void {
142 // Only redraw lines if the number of points has changed--because getField()
143 // is a relatively expensive call, we don't want to do it any more than
144 // necessary.
145 for (const line of this.lines) {
146 if (line.messages.numMessages() * 2 != line.line.getPoints().length) {
147 line.line.setPoints(line.messages.getField(line.field));
148 }
149 }
150 }
151}
152
153export class AosPlotter {
milind upadhyay9bd381d2021-01-23 13:44:13 -0800154
155 public static readonly TIME: string = "Monotonic Time (sec)";
156
157 public static readonly DEFAULT_WIDTH: number = 900;
158 public static readonly DEFAULT_HEIGHT: number = 400;
159
160 public static readonly RED: number[] = [1, 0, 0];
161 public static readonly GREEN: number[] = [0, 1, 0];
162 public static readonly BLUE: number[] = [0, 0, 1];
163 public static readonly BROWN: number[] = [0.6, 0.3, 0];
164 public static readonly PINK: number[] = [1, 0.3, 0.6];
165 public static readonly CYAN: number[] = [0.3, 1, 1];
166 public static readonly WHITE: number[] = [1, 1, 1];
167
168
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800169 private plots: AosPlot[] = [];
170 private messages = new Set<MessageHandler>();
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800171 private lowestHeight = 0;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800172 constructor(private readonly connection: Connection) {
173 // Set up to redraw at some regular interval. The exact rate is unimportant.
174 setInterval(() => {
175 this.draw();
176 }, 100);
177 }
178
179 // Sets up an AOS channel as a message source. Returns a handler that can
180 // be passed to addMessageLine().
181 addMessageSource(name: string, type: string): MessageHandler {
182 return this.addRawMessageSource(
183 name, type, new MessageHandler(this.connection.getSchema(type)));
184 }
185
186 // Same as addMessageSource, but allows you to specify a custom MessageHandler
187 // that does some processing on the requested message. This allows you to
188 // create post-processed versions of individual channels.
189 addRawMessageSource(
190 name: string, type: string,
191 messageHandler: MessageHandler): MessageHandler {
192 this.messages.add(messageHandler);
193 // Use a "reliable" handler so that we get *all* the data when we are
194 // plotting from a logfile.
195 this.connection.addReliableHandler(
196 name, type, (data: Uint8Array, time: number) => {
197 messageHandler.addMessage(data, time);
198 });
199 return messageHandler;
200 }
201 // Add a new figure at the provided position with the provided size within
202 // parentElement.
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800203 addPlot(
204 parentElement: Element, topLeft: number[]|null = null,
205 size: number[] = [AosPlotter.DEFAULT_WIDTH, AosPlotter.DEFAULT_HEIGHT]):
206 AosPlot {
207 if (topLeft === null) {
208 topLeft = [0, this.lowestHeight];
209 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800210 const div = document.createElement("div");
211 div.style.top = topLeft[1].toString();
212 div.style.left = topLeft[0].toString();
213 div.style.position = 'absolute';
214 parentElement.appendChild(div);
215 const newPlot = new Plot(div, size[0], size[1]);
216 for (let plot of this.plots.values()) {
217 newPlot.linkXAxis(plot.plot);
218 }
219 this.plots.push(new AosPlot(this, newPlot));
James Kuszmaul9dbb99a2021-03-07 20:01:27 -0800220 // Height goes up as you go down.
221 this.lowestHeight = Math.max(topLeft[1] + size[1], this.lowestHeight);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800222 return this.plots[this.plots.length - 1];
223 }
224 private draw(): void {
225 for (const plot of this.plots) {
226 plot.draw();
227 }
228 }
229}