Add support for plotting arbitrary flatbuffer vectors

Adds support for both:
-Plotting every element in a vector.
-Plotting repeated sub-messages.

Change-Id: I52de3dbcf7bcf345a7a21d9de6d04692926a9590
diff --git a/aos/network/www/aos_plotter.ts b/aos/network/www/aos_plotter.ts
index b84bfc4..35c27a7 100644
--- a/aos/network/www/aos_plotter.ts
+++ b/aos/network/www/aos_plotter.ts
@@ -60,6 +60,32 @@
     this.messages.push(
         new TimestampedMessage(Table.getRootTable(new ByteBuffer(data)), time));
   }
+  private readField<T>(
+      message: Table, fieldName: string,
+      normalReader: (message: Table, name: string) => T | null,
+      vectorReader: (message: Table, name: string) => T[] | null): T[]|null {
+    // Typescript handles bindings in non-obvious ways that aren't caught well
+    // by the compiler.
+    normalReader = normalReader.bind(this.parser);
+    vectorReader = vectorReader.bind(this.parser);
+    const regex = /(.*)\[([0-9]*)\]/;
+    const match = fieldName.match(regex);
+    if (match) {
+      const name = match[1];
+      const vector = vectorReader(message, name);
+      if (vector === null) {
+        return null;
+      }
+      if (match[2] === "") {
+        return vector;
+      } else {
+        const index = parseInt(match[2]);
+        return (index < vector.length) ? [vector[index]] : null;
+      }
+    }
+    const singleResult = normalReader(message, fieldName);
+    return singleResult ? [singleResult] : null;
+  }
   // 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.
@@ -70,41 +96,44 @@
   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);
+    const results = [];
     for (let ii = 0; ii < this.messages.length; ++ii) {
-      let message = this.messages[ii].message;
+      let tables = [this.messages[ii].message];
       for (const subMessageName of subMessage) {
-        message = this.parser.readTable(message, subMessageName);
-        if (message === undefined) {
-          break;
+        let nextTables = [];
+        for (const table of tables) {
+          const nextTable = this.readField(
+              table, subMessageName, Parser.prototype.readTable,
+              Parser.prototype.readVectorOfTables);
+          if (nextTable === null) {
+            continue;
+          }
+          nextTables = nextTables.concat(nextTable);
         }
+        tables = nextTables;
       }
-      results[ii * 2] = this.messages[ii].time;
-      const regex = /(.*)\[([0-9]*)\]/;
-      let name = fieldName;
-      let match = fieldName.match(regex);
-      let index = undefined;
-      if (match) {
-        name = match[1]
-        index = parseInt(match[2])
-      }
-      if (message === undefined) {
-        results[ii * 2 + 1] = NaN;
+      const time = this.messages[ii].time;
+      if (tables.length === 0) {
+        results.push(time);
+        results.push(NaN);
       } else {
-        if (index === undefined) {
-          results[ii * 2 + 1] = this.parser.readScalar(message, name);
-        } else {
-          const vector =
-              this.parser.readVectorOfScalars(message, name);
-          if (index < vector.length) {
-            results[ii * 2 + 1] = vector[index];
+        for (const table of tables) {
+          const values = this.readField(
+              table, fieldName, Parser.prototype.readScalar,
+              Parser.prototype.readVectorOfScalars);
+          if (values === null) {
+            results.push(time);
+            results.push(NaN);
           } else {
-            results[ii * 2 + 1] = NaN;
+            for (const value of values) {
+              results.push(time);
+              results.push((value === null) ? NaN : value);
+            }
           }
         }
       }
     }
-    return results;
+    return new Float32Array(results);
   }
   numMessages(): number {
     return this.messages.length;
diff --git a/aos/network/www/demo_plot.ts b/aos/network/www/demo_plot.ts
index d223757..ebdff7c 100644
--- a/aos/network/www/demo_plot.ts
+++ b/aos/network/www/demo_plot.ts
@@ -19,11 +19,11 @@
 import Connection = proxy.Connection;
 
 export function plotDemo(conn: Connection, parentDiv: Element): void {
-  const width = 900;
-  const height = 400;
+  const width = AosPlotter.DEFAULT_WIDTH;
+  const height = AosPlotter.DEFAULT_HEIGHT;
 
   const benchmarkDiv = document.createElement('div');
-  benchmarkDiv.style.top = height.toString();
+  benchmarkDiv.style.top = (height * 2).toString();
   benchmarkDiv.style.left = '0';
   benchmarkDiv.style.position = 'absolute';
   parentDiv.appendChild(benchmarkDiv);
@@ -33,20 +33,45 @@
   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.
+    // Setup a plot that shows some arbitrary PDP current values.
+    const pdpValues =
+        aosPlotter.addMessageSource('/aos', 'frc971.PDPValues');
+    const timingPlot = aosPlotter.addPlot(parentDiv);
+    timingPlot.plot.getAxisLabels().setTitle('Current Values');
+    timingPlot.plot.getAxisLabels().setYLabel('Current (Amps)');
+    timingPlot.plot.getAxisLabels().setXLabel('Monotonic Send Time (sec)');
+    // Displays points for every single current sample at each time-point.
+    const allValuesLine = timingPlot.addMessageLine(pdpValues, ['currents[]']);
+    allValuesLine.setDrawLine(false);
+    allValuesLine.setPointSize(5);
+    // Displays a line for the current along channel 1.
+    const singleValueLine = timingPlot.addMessageLine(pdpValues, ['currents[1]']);
+    singleValueLine.setDrawLine(true);
+    singleValueLine.setPointSize(0);
+    const voltageLine = timingPlot.addMessageLine(pdpValues, ['voltage']);
+    voltageLine.setPointSize(0);
+  }
+
+  {
     const timingReport =
         aosPlotter.addMessageSource('/aos', 'aos.timing.Report');
-    const timingPlot =
-        aosPlotter.addPlot(parentDiv, [0, 0], [width, height]);
-    timingPlot.plot.getAxisLabels().setTitle('Timing Report PID');
+    // Setup a plot that just shows some arbitrary timing data.
+    const timingPlot = aosPlotter.addPlot(parentDiv);
+    timingPlot.plot.getAxisLabels().setTitle('Timing Report Wakeups');
     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);
+    // Show *all* the wakeup latencies for all timers.
+    const allValuesLine = timingPlot.addMessageLine(
+        timingReport, ['timers[]', 'wakeup_latency', 'average']);
+    allValuesLine.setDrawLine(false);
+    allValuesLine.setPointSize(5);
+    // Show *all* the wakeup latencies for the first timer in each timing report
+    // (this is not actually all that helpful unless you were to also filter by
+    // PID).
+    const singleValueLine = timingPlot.addMessageLine(
+        timingReport, ['timers[0]', 'wakeup_latency', 'average']);
+    singleValueLine.setDrawLine(true);
+    singleValueLine.setPointSize(0);
   }
 
   // Set up and draw the benchmarking plot.