Refactor typescript message handler

This makes it so that, to register a handler on a given topic, you just
need to call a single function, rather than assembling the entire
flatbuffer request message by yourself.

Change-Id: I540f475d9650ab27efe715a05d7586719045aeed
diff --git a/aos/network/www/BUILD b/aos/network/www/BUILD
index 03e269d..8f5bd49 100644
--- a/aos/network/www/BUILD
+++ b/aos/network/www/BUILD
@@ -20,6 +20,7 @@
     target_compatible_with = ["@platforms//os:linux"],
     visibility = ["//visibility:public"],
     deps = [
+        ":reflection_ts",
         "//aos:configuration_ts_fbs",
         "//aos/network:connect_ts_fbs",
         "//aos/network:web_proxy_ts_fbs",
diff --git a/aos/network/www/config_handler.ts b/aos/network/www/config_handler.ts
index 81f61b4..df2d4f8 100644
--- a/aos/network/www/config_handler.ts
+++ b/aos/network/www/config_handler.ts
@@ -2,6 +2,8 @@
 import {Connection} from 'org_frc971/aos/network/www/proxy';
 import * as flatbuffers_builder from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
 import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
+import {Parser, Table} from './reflection'
+import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
 
 
 import Configuration = configuration.aos.Configuration;
@@ -62,35 +64,27 @@
 
   handleChange() {
     const toggles = this.root_div.getElementsByTagName('input');
-    const builder =
-        new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
-
-    const channels: flatbuffers.Offset[] = [];
     for (const toggle of toggles) {
       if (!toggle.checked) {
         continue;
       }
       const index = toggle.getAttribute('data-index');
       const channel = this.config.channels(Number(index));
-      const namefb = builder.createString(channel.name());
-      const typefb = builder.createString(channel.type());
-      Channel.startChannel(builder);
-      Channel.addName(builder, namefb);
-      Channel.addType(builder, typefb);
-      const channelfb = Channel.endChannel(builder);
-      ChannelRequest.startChannelRequest(builder);
-      ChannelRequest.addChannel(builder, channelfb);
-      ChannelRequest.addMethod(builder, TransferMethod.SUBSAMPLE);
-      channels.push(ChannelRequest.endChannelRequest(builder));
+      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);
+            console.log(
+                parser.toObject(Table.getRootTable(new ByteBuffer(data))));
+          });
     }
 
-    const channelsfb =
-        SubscriberRequest.createChannelsToTransferVector(builder, channels);
-    SubscriberRequest.startSubscriberRequest(builder);
-    SubscriberRequest.addChannelsToTransfer(builder, channelsfb);
-    const request = SubscriberRequest.endSubscriberRequest(builder);
-    builder.finish(request);
-    this.connection.sendConnectMessage(builder);
   }
 
   toggleConfig() {
diff --git a/aos/network/www/graph_main.ts b/aos/network/www/graph_main.ts
index 9135cf4..2548e1d 100644
--- a/aos/network/www/graph_main.ts
+++ b/aos/network/www/graph_main.ts
@@ -121,6 +121,7 @@
 };
 
 let reportParser = null;
+let startTime = null;
 
 conn.addConfigHandler((config: Configuration) => {
   // Locate the timing report schema so that we can read the received messages.
@@ -135,44 +136,22 @@
   }
   reportParser = new Parser(reportSchema);
 
-  // Subscribe to the timing report message.
-  const builder =
-      new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
-  const channels: flatbuffers.Offset[] = [];
-  for (const channel of [timingReport]) {
-    const nameFb = builder.createString(channel.name);
-    const typeFb = builder.createString(channel.type);
-    Channel.startChannel(builder);
-    Channel.addName(builder, nameFb);
-    Channel.addType(builder, typeFb);
-    const channelFb = Channel.endChannel(builder);
-    ChannelRequest.startChannelRequest(builder);
-    ChannelRequest.addChannel(builder, channelFb);
-    ChannelRequest.addMethod(builder, TransferMethod.EVERYTHING_WITH_HISTORY);
-    channels.push(ChannelRequest.endChannelRequest(builder));
-  }
+  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 channelsFb = SubscriberRequest.createChannelsToTransferVector(builder, channels);
-  SubscriberRequest.startSubscriberRequest(builder);
-  SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
-  const connect = SubscriberRequest.endSubscriberRequest(builder);
-  builder.finish(connect);
-  conn.sendConnectMessage(builder);
-});
-
-let startTime = null;
-conn.addHandler(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);
+        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
diff --git a/aos/network/www/main.ts b/aos/network/www/main.ts
index 1840ffb..79a39d8 100644
--- a/aos/network/www/main.ts
+++ b/aos/network/www/main.ts
@@ -7,4 +7,4 @@
 
 PingHandler.SetupDom();
 
-conn.addHandler(PingHandler.GetId(), PingHandler.HandlePing);
+conn.addHandler('/aos', PingHandler.GetId(), PingHandler.HandlePing);
diff --git a/aos/network/www/proxy.ts b/aos/network/www/proxy.ts
index 5660cef..4fbba85 100644
--- a/aos/network/www/proxy.ts
+++ b/aos/network/www/proxy.ts
@@ -3,6 +3,7 @@
 import {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 ChannelFb = configuration.aos.Channel;
 import Configuration = configuration.aos.Configuration;
 import MessageHeader = web_proxy.aos.web_proxy.MessageHeader;
 import WebSocketIce = web_proxy.aos.web_proxy.WebSocketIce;
@@ -10,6 +11,9 @@
 import Payload = web_proxy.aos.web_proxy.Payload;
 import WebSocketSdp = web_proxy.aos.web_proxy.WebSocketSdp;
 import SdpType = web_proxy.aos.web_proxy.SdpType;
+import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
+import ChannelRequestFb = web_proxy.aos.web_proxy.ChannelRequest;
+import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
 
 // There is one handler for each DataChannel, it maintains the state of
 // multi-part messages and delegates to a callback when the message is fully
@@ -53,6 +57,19 @@
   }
 }
 
+class Channel {
+  constructor(public readonly name: string, public readonly type: string) {}
+  key(): string {
+    return this.name + "/" + this.type;
+  }
+}
+
+class ChannelRequest {
+  constructor(
+      public readonly channel: Channel,
+      public readonly transferMethod: TransferMethod) {}
+}
+
 // Analogous to the Connection class in //aos/network/web_proxy.h. Because most
 // of the apis are native in JS, it is much simpler.
 export class Connection {
@@ -69,6 +86,8 @@
       new Map<string, (data: Uint8Array, sentTime: number) => void>();
   private readonly handlers = new Set<Handler>();
 
+  private subscribedChannels: ChannelRequest[] = [];
+
   constructor() {
     const server = location.host;
     this.webSocketUrl = `ws://${server}/ws`;
@@ -79,12 +98,62 @@
   }
 
   /**
-   * Add a handler for a specific message type. Until we need to handle
-   * different channel names with the same type differently, this is good
-   * enough.
+   * Add a handler for a specific message type, with reliable delivery of all
+   * messages.
    */
-  addHandler(id: string, handler: (data: Uint8Array, sentTime: number) => void): void {
-    this.handlerFuncs.set(id, handler);
+  addReliableHandler(
+      name: string, type: string,
+      handler: (data: Uint8Array, sentTime: number) => void): void {
+    this.addHandlerImpl(
+        name, type, TransferMethod.EVERYTHING_WITH_HISTORY, handler);
+  }
+
+  /**
+   * Add a handler for a specific message type.
+   */
+  addHandler(
+      name: string, type: string,
+      handler: (data: Uint8Array, sentTime: number) => void): void {
+    this.addHandlerImpl(name, type, TransferMethod.SUBSAMPLE, handler);
+  }
+
+  addHandlerImpl(
+      name: string, type: string, method: TransferMethod,
+      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);
+    this.subscribeToChannel(request);
+  }
+
+  subscribeToChannel(channel: ChannelRequest): void {
+    this.subscribedChannels.push(channel);
+    if (this.configInternal === null) {
+      throw new Error(
+          'Must call subscribeToChannel after we\'ve received the config.');
+    }
+    const builder = new Builder(512) as unknown as flatbuffers.Builder;
+    const channels: flatbuffers.Offset[] = [];
+    for (const channel of this.subscribedChannels) {
+      const nameFb = builder.createString(channel.channel.name);
+      const typeFb = builder.createString(channel.channel.type);
+      ChannelFb.startChannel(builder);
+      ChannelFb.addName(builder, nameFb);
+      ChannelFb.addType(builder, typeFb);
+      const channelFb = ChannelFb.endChannel(builder);
+      ChannelRequestFb.startChannelRequest(builder);
+      ChannelRequestFb.addChannel(builder, channelFb);
+      ChannelRequestFb.addMethod(builder, channel.transferMethod);
+      channels.push(ChannelRequestFb.endChannelRequest(builder));
+    }
+
+    const channelsFb =
+        SubscriberRequest.createChannelsToTransferVector(builder, channels);
+    SubscriberRequest.startSubscriberRequest(builder);
+    SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
+    const connect = SubscriberRequest.endSubscriberRequest(builder);
+    builder.finish(connect);
+    this.sendConnectMessage(builder);
   }
 
   connect(): void {
@@ -114,8 +183,7 @@
   onDataChannel(ev: RTCDataChannelEvent): void {
     const channel = ev.channel;
     const name = channel.label;
-    const channelType = name.split('/').pop();
-    const handlerFunc = this.handlerFuncs.get(channelType);
+    const handlerFunc = this.handlerFuncs.get(name);
     this.handlers.add(new Handler(handlerFunc, channel));
   }
 
diff --git a/y2020/www/camera_main.ts b/y2020/www/camera_main.ts
index 8c7ae55..58e5bed 100644
--- a/y2020/www/camera_main.ts
+++ b/y2020/www/camera_main.ts
@@ -1,9 +1,11 @@
 import {Connection} from 'org_frc971/aos/network/www/proxy';
 
 import {ImageHandler} from './image_handler';
+import {ConfigHandler} from 'org_frc971/aos/network/www/config_handler';
 
 const conn = new Connection();
 
 conn.connect();
 
 const iHandler = new ImageHandler(conn);
+const configHandler = new ConfigHandler(conn);
diff --git a/y2020/www/field_handler.ts b/y2020/www/field_handler.ts
index d3bafa2..b2daea1 100644
--- a/y2020/www/field_handler.ts
+++ b/y2020/www/field_handler.ts
@@ -59,37 +59,6 @@
 const ROBOT_WIDTH = 28 * IN_TO_M;
 const ROBOT_LENGTH = 30 * IN_TO_M;
 
-/**
- * All the messages that are required to display camera information on the field.
- * Messages not readable on the server node are ignored.
- */
-const REQUIRED_CHANNELS = [
-  {
-    name: '/pi1/camera',
-    type: 'frc971.vision.sift.ImageMatchResult',
-  },
-  {
-    name: '/pi2/camera',
-    type: 'frc971.vision.sift.ImageMatchResult',
-  },
-  {
-    name: '/pi3/camera',
-    type: 'frc971.vision.sift.ImageMatchResult',
-  },
-  {
-    name: '/pi4/camera',
-    type: 'frc971.vision.sift.ImageMatchResult',
-  },
-  {
-    name: '/pi5/camera',
-    type: 'frc971.vision.sift.ImageMatchResult',
-  },
-  {
-    name: '/drivetrain',
-    type: 'frc971.control_loops.drivetrain.Status',
-  },
-];
-
 export class FieldHandler {
   private canvas = document.createElement('canvas');
   private imageMatchResult: ImageMatchResult|null = null;
@@ -99,16 +68,15 @@
     document.body.appendChild(this.canvas);
 
     this.connection.addConfigHandler(() => {
-      this.sendConnect();
+      this.connection.addHandler(
+          '/camera', ImageMatchResult.getFullyQualifiedName(), (res) => {
+            this.handleImageMatchResult(res);
+          });
+      this.connection.addHandler(
+          '/drivetrain', DrivetrainStatus.getFullyQualifiedName(), (data) => {
+            this.handleDrivetrainStatus(data);
+          });
     });
-    this.connection.addHandler(
-        ImageMatchResult.getFullyQualifiedName(), (res) => {
-          this.handleImageMatchResult(res);
-        });
-    this.connection.addHandler(
-        DrivetrainStatus.getFullyQualifiedName(), (data) => {
-          this.handleDrivetrainStatus(data);
-        });
   }
 
   private handleImageMatchResult(data: Uint8Array): void {
@@ -123,32 +91,6 @@
         fbBuffer as unknown as flatbuffers.ByteBuffer);
   }
 
-  private sendConnect(): void {
-    const builder =
-        new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
-    const channels: flatbuffers.Offset[] = [];
-    for (const channel of REQUIRED_CHANNELS) {
-      const nameFb = builder.createString(channel.name);
-      const typeFb = builder.createString(channel.type);
-      Channel.startChannel(builder);
-      Channel.addName(builder, nameFb);
-      Channel.addType(builder, typeFb);
-      const channelFb = Channel.endChannel(builder);
-      ChannelRequest.startChannelRequest(builder);
-      ChannelRequest.addChannel(builder, channelFb);
-      ChannelRequest.addMethod(builder, TransferMethod.SUBSAMPLE);
-      channels.push(ChannelRequest.endChannelRequest(builder));
-    }
-
-    const channelsFb =
-        SubscriberRequest.createChannelsToTransferVector(builder, channels);
-    SubscriberRequest.startSubscriberRequest(builder);
-    SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
-    const connect = SubscriberRequest.endSubscriberRequest(builder);
-    builder.finish(connect);
-    this.connection.sendConnectMessage(builder);
-  }
-
   drawField(): void {
     const MY_COLOR = 'red';
     const OTHER_COLOR = 'blue';
diff --git a/y2020/www/image_handler.ts b/y2020/www/image_handler.ts
index 79ce74e..b254f2c 100644
--- a/y2020/www/image_handler.ts
+++ b/y2020/www/image_handler.ts
@@ -16,53 +16,6 @@
 import ChannelRequest = web_proxy.aos.web_proxy.ChannelRequest;
 import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
 
-/*
- * All the messages that are required to show an image with metadata.
- * Messages not readable on the server node are ignored.
- */
-const REQUIRED_CHANNELS = [
-  {
-    name: '/pi1/camera',
-    type: CameraImage.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi2/camera',
-    type: CameraImage.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi3/camera',
-    type: CameraImage.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi4/camera',
-    type: CameraImage.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi5/camera',
-    type: CameraImage.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi1/camera/detailed',
-    type: ImageMatchResult.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi2/camera/detailed',
-    type: ImageMatchResult.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi3/camera/detailed',
-    type: ImageMatchResult.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi4/camera/detailed',
-    type: ImageMatchResult.getFullyQualifiedName(),
-  },
-  {
-    name: '/pi5/camera/detailed',
-    type: ImageMatchResult.getFullyQualifiedName(),
-  },
-];
-
 export class ImageHandler {
   private canvas = document.createElement('canvas');
   private select = document.createElement('select');
@@ -86,41 +39,15 @@
     document.body.appendChild(this.canvas);
 
     this.connection.addConfigHandler(() => {
-      this.sendConnect();
+      this.connection.addHandler(
+          '/camera', ImageMatchResult.getFullyQualifiedName(), (data) => {
+            this.handleImageMetadata(data);
+          });
+      this.connection.addHandler(
+          '/camera', CameraImage.getFullyQualifiedName(), (data) => {
+            this.handleImage(data);
+          });
     });
-    this.connection.addHandler(
-        ImageMatchResult.getFullyQualifiedName(), (data) => {
-          this.handleImageMetadata(data);
-        });
-    this.connection.addHandler(CameraImage.getFullyQualifiedName(), (data) => {
-      this.handleImage(data);
-    });
-  }
-
-  private sendConnect(): void {
-    const builder =
-        new flatbuffers_builder.Builder(512) as unknown as flatbuffers.Builder;
-    const channels: flatbuffers.Offset[] = [];
-    for (const channel of REQUIRED_CHANNELS) {
-      const nameFb = builder.createString(channel.name);
-      const typeFb = builder.createString(channel.type);
-      Channel.startChannel(builder);
-      Channel.addName(builder, nameFb);
-      Channel.addType(builder, typeFb);
-      const channelFb = Channel.endChannel(builder);
-      ChannelRequest.startChannelRequest(builder);
-      ChannelRequest.addChannel(builder, channelFb);
-      ChannelRequest.addMethod(builder, TransferMethod.SUBSAMPLE);
-      channels.push(ChannelRequest.endChannelRequest(builder));
-    }
-
-    const channelsFb =
-        SubscriberRequest.createChannelsToTransferVector(builder, channels);
-    SubscriberRequest.startSubscriberRequest(builder);
-    SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
-    const connect = SubscriberRequest.endSubscriberRequest(builder);
-    builder.finish(connect);
-    this.connection.sendConnectMessage(builder);
   }
 
   handleSelect(ev: Event) {