Merge "Tune hood"
diff --git a/aos/network/www/config_handler.ts b/aos/network/www/config_handler.ts
index 72b496e..3d20f0d 100644
--- a/aos/network/www/config_handler.ts
+++ b/aos/network/www/config_handler.ts
@@ -1,18 +1,35 @@
 import {Configuration, Channel} from 'aos/configuration_generated';
 import {Connect} from 'aos/network/connect_generated';
+import {Connection} from './proxy';
 
 export class ConfigHandler {
   private readonly root_div = document.getElementById('config');
+  private readonly tree_div;
+  private config: Configuration|null = null
 
-  constructor(
-      private readonly config: Configuration,
-      private readonly dataChannel: RTCDataChannel) {}
+  constructor(private readonly connection: Connection) {
+    this.connection.addConfigHandler((config) => this.handleConfig(config));
+
+    const show_button = document.createElement('button');
+    show_button.addEventListener('click', () => this.toggleConfig());
+    const show_text = document.createTextNode('Show/Hide Config');
+    show_button.appendChild(show_text);
+    this.tree_div = document.createElement('div');
+    this.tree_div.hidden = true;
+    this.root_div.appendChild(show_button);
+    this.root_div.appendChild(this.tree_div);
+  }
+
+  handleConfig(config: Configuration) {
+    this.config = config;
+    this.printConfig();
+  }
 
   printConfig() {
     for (const i = 0; i < this.config.channelsLength(); i++) {
       const channel_div = document.createElement('div');
       channel_div.classList.add('channel');
-      this.root_div.appendChild(channel_div);
+      this.tree_div.appendChild(channel_div);
 
       const input_el = document.createElement('input');
       input_el.setAttribute('data-index', i);
@@ -60,8 +77,10 @@
     Connect.addChannelsToTransfer(builder, channelsfb);
     const connect = Connect.endConnect(builder);
     builder.finish(connect);
-    const array = builder.asUint8Array();
-    console.log('connect', array);
-    this.dataChannel.send(array.buffer.slice(array.byteOffset));
+    this.connection.sendConnectMessage(builder);
+  }
+
+  toggleConfig() {
+    this.tree_div.hidden = !this.tree_div.hidden;
   }
 }
diff --git a/aos/network/www/proxy.ts b/aos/network/www/proxy.ts
index 704fc85..9eb0bb6 100644
--- a/aos/network/www/proxy.ts
+++ b/aos/network/www/proxy.ts
@@ -49,8 +49,11 @@
   private rtcPeerConnection: RTCPeerConnection|null = null;
   private dataChannel: DataChannel|null = null;
   private webSocketUrl: string;
-  private configHandler: ConfigHandler|null = null;
-  private config: Configuration|null = null;
+
+  private configInternal: Configuration|null = null;
+  // A set of functions that accept the config to handle.
+  private readonly configHandlers = new Set<(config: Configuration) => void>();
+
   private readonly handlerFuncs = new Map<string, (data: Uint8Array) => void>();
   private readonly handlers = new Set<Handler>();
 
@@ -59,6 +62,10 @@
     this.webSocketUrl = `ws://${server}/ws`;
   }
 
+  addConfigHandler(handler: (config: Configuration) => void): void {
+    this.configHandlers.add(handler);
+  }
+
   addHandler(id: string, handler: (data: Uint8Array) => void): void {
     this.handlerFuncs.set(id, handler);
   }
@@ -72,17 +79,17 @@
         'message', (e) => this.onWebSocketMessage(e));
   }
 
+  get config() {
+    return this.config_internal;
+  }
+
   // Handle messages on the DataChannel. Handles the Configuration message as
   // all other messages are sent on specific DataChannels.
   onDataChannelMessage(e: MessageEvent): void {
     const fbBuffer = new flatbuffers.ByteBuffer(new Uint8Array(e.data));
-    // TODO(alex): handle config updates if/when required
-    if (!this.configHandler) {
-      const config = Configuration.getRootAsConfiguration(fbBuffer);
-      this.config = config;
-      this.configHandler = new ConfigHandler(config, this.dataChannel);
-      this.configHandler.printConfig();
-      return;
+    this.configInternal = Configuration.getRootAsConfiguration(fbBuffer);
+    for (handler of this.configHandlers) {
+      handler(this.configInternal);
     }
   }
 
@@ -174,4 +181,13 @@
         break;
     }
   }
+
+  /**
+   * Subscribes to messages.
+   * @param a Finished flatbuffer.Builder containing a Connect message to send.
+   */
+  sendConnectMessage(builder: any) {
+    const array = builder.assUint8Array();
+    this.dataChannel.send(array.buffer.slice(array.byteOffset));
+  }
 }
diff --git a/frc971/downloader/downloader.py b/frc971/downloader/downloader.py
index 5ebf417..42d4a7c 100644
--- a/frc971/downloader/downloader.py
+++ b/frc971/downloader/downloader.py
@@ -82,6 +82,8 @@
     ssh_path = "external/ssh/ssh"
     scp_path = "external/ssh/scp"
 
+    subprocess.check_call([ssh_path, ssh_target, "mkdir", "-p", target_dir])
+
     rsync_cmd = ([
         "external/rsync/usr/bin/rsync", "-e", ssh_path, "-c", "-v", "-z",
         "--copy-links"
diff --git a/y2020/BUILD b/y2020/BUILD
index f3ea4d6..4aa851f 100644
--- a/y2020/BUILD
+++ b/y2020/BUILD
@@ -166,6 +166,7 @@
     srcs = ["web_proxy.sh"],
     data = [
         ":config.json",
+        "//y2020/www:field_main_bundle",
         "//aos/network:web_proxy_main",
         "//y2020/www:files",
         "//y2020/www:flatbuffers",
diff --git a/y2020/control_loops/drivetrain/localizer.cc b/y2020/control_loops/drivetrain/localizer.cc
index 4b164b5..8330289 100644
--- a/y2020/control_loops/drivetrain/localizer.cc
+++ b/y2020/control_loops/drivetrain/localizer.cc
@@ -23,6 +23,9 @@
   return result;
 }
 
+// Indices of the pis to use.
+const std::array<std::string, 3> kPisToUse{"pi1", "pi2", "pi3"};
+
 }  // namespace
 
 Localizer::Localizer(
@@ -51,9 +54,11 @@
                            ekf_.P());
   });
 
-  image_fetchers_.emplace_back(
-      event_loop_->MakeFetcher<frc971::vision::sift::ImageMatchResult>(
-          "/pi1/camera"));
+  for (const auto &pi : kPisToUse) {
+    image_fetchers_.emplace_back(
+        event_loop_->MakeFetcher<frc971::vision::sift::ImageMatchResult>(
+            "/" + pi + "/camera"));
+  }
 
   target_selector_.set_has_target(false);
 }
@@ -89,9 +94,10 @@
                        aos::monotonic_clock::time_point now,
                        double left_encoder, double right_encoder,
                        double gyro_rate, const Eigen::Vector3d &accel) {
-  for (auto &image_fetcher : image_fetchers_) {
+  for (size_t ii = 0; ii < kPisToUse.size(); ++ii) {
+    auto &image_fetcher = image_fetchers_[ii];
     while (image_fetcher.FetchNext()) {
-      HandleImageMatch(*image_fetcher);
+      HandleImageMatch(kPisToUse[ii], *image_fetcher);
     }
   }
   ekf_.UpdateEncodersAndGyro(left_encoder, right_encoder, gyro_rate, U, accel,
@@ -99,13 +105,13 @@
 }
 
 void Localizer::HandleImageMatch(
-    const frc971::vision::sift::ImageMatchResult &result) {
+    std::string_view pi, const frc971::vision::sift::ImageMatchResult &result) {
   std::chrono::nanoseconds monotonic_offset(0);
   clock_offset_fetcher_.Fetch();
   if (clock_offset_fetcher_.get() != nullptr) {
     for (const auto connection : *clock_offset_fetcher_->connections()) {
       if (connection->has_node() && connection->node()->has_name() &&
-          connection->node()->name()->string_view() == "pi1") {
+          connection->node()->name()->string_view() == pi) {
         monotonic_offset =
             std::chrono::nanoseconds(connection->monotonic_offset());
         break;
diff --git a/y2020/control_loops/drivetrain/localizer.h b/y2020/control_loops/drivetrain/localizer.h
index 8636a7a..1d6f816 100644
--- a/y2020/control_loops/drivetrain/localizer.h
+++ b/y2020/control_loops/drivetrain/localizer.h
@@ -1,6 +1,8 @@
 #ifndef Y2020_CONTROL_LOOPS_DRIVETRAIN_LOCALIZER_H_
 #define Y2020_CONTROL_LOOPS_DRIVETRAIN_LOCALIZER_H_
 
+#include <string_view>
+
 #include "aos/containers/ring_buffer.h"
 #include "aos/events/event_loop.h"
 #include "aos/network/message_bridge_server_generated.h"
@@ -69,8 +71,9 @@
     double velocity = 0.0;  // rad/sec
   };
 
-  // Processes new image data and updates the EKF.
-  void HandleImageMatch(const frc971::vision::sift::ImageMatchResult &result);
+  // Processes new image data from the given pi and updates the EKF.
+  void HandleImageMatch(std::string_view pi,
+                        const frc971::vision::sift::ImageMatchResult &result);
 
   // Processes the most recent turret position and stores it in the turret_data_
   // buffer.
diff --git a/y2020/www/BUILD b/y2020/www/BUILD
index 292618b..e8c227d 100644
--- a/y2020/www/BUILD
+++ b/y2020/www/BUILD
@@ -16,6 +16,18 @@
     ],
 )
 
+ts_library(
+    name = "field_main",
+    srcs = [
+        "field_main.ts",
+        "field_handler.ts",
+        "constants.ts",
+    ],
+    deps = [
+        "//aos/network/www:proxy",
+    ],
+)
+
 rollup_bundle(
     name = "main_bundle",
     entry_point = "y2020/www/main",
@@ -25,6 +37,15 @@
     ],
 )
 
+rollup_bundle(
+    name = "field_main_bundle",
+    entry_point = "y2020/www/field_main",
+    deps = [
+        "field_main",
+    ],
+    visibility = ["//y2020:__subpackages__"],
+)
+
 filegroup(
     name = "files",
     srcs = glob([
diff --git a/y2020/www/constants.ts b/y2020/www/constants.ts
new file mode 100644
index 0000000..b94d7a7
--- /dev/null
+++ b/y2020/www/constants.ts
@@ -0,0 +1,7 @@
+// Conversion constants to meters
+export const IN_TO_M = 0.0254;
+export const FT_TO_M = 0.3048;
+// Dimensions of the field in meters
+export const FIELD_WIDTH = 26 * FT_TO_M + 11.25 * IN_TO_M;
+export const FIELD_LENGTH = 52 * FT_TO_M + 5.25 * IN_TO_M;
+
diff --git a/y2020/www/field.html b/y2020/www/field.html
new file mode 100644
index 0000000..37452a3
--- /dev/null
+++ b/y2020/www/field.html
@@ -0,0 +1,12 @@
+<html>
+  <head>
+    <script src="flatbuffers.js"></script>
+    <script src="field_main_bundle.min.js" defer></script>
+    <link rel="stylesheet" href="styles.css">
+  </head>
+  <body>
+    <div id="config">
+    </div>
+  </body>
+</html>
+
diff --git a/y2020/www/field_handler.ts b/y2020/www/field_handler.ts
new file mode 100644
index 0000000..a960a63
--- /dev/null
+++ b/y2020/www/field_handler.ts
@@ -0,0 +1,167 @@
+import {FIELD_LENGTH, FIELD_WIDTH, FT_TO_M, IN_TO_M} from './constants';
+
+const FIELD_SIDE_Y = FIELD_WIDTH / 2;
+const FIELD_CENTER_X = (198.75 + 116) * IN_TO_M;
+
+const DS_WIDTH = 69 * IN_TO_M;
+const DS_ANGLE = 20 * Math.PI / 180;
+const DS_END_X = DS_WIDTH * Math.sin(DS_ANGLE);
+const OTHER_DS_X = FIELD_LENGTH - DS_END_X;
+const DS_INSIDE_Y = FIELD_SIDE_Y - DS_WIDTH * Math.cos(DS_ANGLE);
+
+const TRENCH_START_X = 206.57 * IN_TO_M;
+const TRENCH_END_X = FIELD_LENGTH - TRENCH_START_X;
+const TRENCH_WIDTH = 55.5 * IN_TO_M;
+const TRENCH_INSIDE = FIELD_SIDE_Y - TRENCH_WIDTH;
+
+const SPINNER_LENGTH = 30 * IN_TO_M;
+const SPINNER_TOP_X = 374.59 * IN_TO_M;
+const SPINNER_BOTTOM_X = SPINNER_TOP_X - SPINNER_LENGTH;
+
+const SHIELD_BOTTOM_X = FIELD_CENTER_X - 116 * IN_TO_M;
+const SHIELD_BOTTOM_Y = 43.75 * IN_TO_M;
+
+const SHIELD_TOP_X = FIELD_CENTER_X + 116 * IN_TO_M;
+const SHIELD_TOP_Y = -43.75 * IN_TO_M;
+
+const SHIELD_RIGHT_X = FIELD_CENTER_X - 51.06 * IN_TO_M;
+const SHIELD_RIGHT_Y = -112.88 * IN_TO_M;
+
+const SHIELD_LEFT_X = FIELD_CENTER_X + 51.06 * IN_TO_M;
+const SHIELD_LEFT_Y = 112.88 * IN_TO_M;
+
+const SHIELD_CENTER_TOP_X = (SHIELD_TOP_X + SHIELD_LEFT_X) / 2
+const SHIELD_CENTER_TOP_Y = (SHIELD_TOP_Y + SHIELD_LEFT_Y) / 2
+
+const SHIELD_CENTER_BOTTOM_X = (SHIELD_BOTTOM_X + SHIELD_RIGHT_X) / 2
+const SHIELD_CENTER_BOTTOM_Y = (SHIELD_BOTTOM_Y + SHIELD_RIGHT_Y) / 2
+
+const INITIATION_X = 120 * IN_TO_M;
+const FAR_INITIATION_X = FIELD_LENGTH - 120 * IN_TO_M;
+
+const TARGET_ZONE_TIP_X = 30 * IN_TO_M;
+const TARGET_ZONE_WIDTH = 48 * IN_TO_M;
+const LOADING_ZONE_WIDTH = 60 * IN_TO_M;
+
+export class FieldHandler {
+  private canvas = document.createElement('canvas');
+
+  constructor() {
+    document.body.appendChild(this.canvas);
+  }
+
+  drawField(): void {
+    const MY_COLOR = 'red';
+    const OTHER_COLOR = 'blue';
+    const ctx = this.canvas.getContext('2d');
+    // draw perimiter
+    ctx.beginPath();
+    ctx.moveTo(0, DS_INSIDE_Y);
+    ctx.lineTo(DS_END_X, FIELD_SIDE_Y);
+    ctx.lineTo(OTHER_DS_X, FIELD_SIDE_Y);
+    ctx.lineTo(FIELD_LENGTH, DS_INSIDE_Y);
+    ctx.lineTo(FIELD_LENGTH, -DS_INSIDE_Y);
+    ctx.lineTo(OTHER_DS_X, -FIELD_SIDE_Y);
+    ctx.lineTo(DS_END_X, -FIELD_SIDE_Y);
+    ctx.lineTo(0, -DS_INSIDE_Y);
+    ctx.lineTo(0, DS_INSIDE_Y);
+    ctx.stroke();
+
+    // draw shield generator
+    ctx.beginPath();
+    ctx.moveTo(SHIELD_BOTTOM_X, SHIELD_BOTTOM_Y);
+    ctx.lineTo(SHIELD_RIGHT_X, SHIELD_RIGHT_Y);
+    ctx.lineTo(SHIELD_TOP_X, SHIELD_TOP_Y);
+    ctx.lineTo(SHIELD_LEFT_X, SHIELD_LEFT_Y);
+    ctx.lineTo(SHIELD_BOTTOM_X, SHIELD_BOTTOM_Y);
+    ctx.moveTo(SHIELD_CENTER_TOP_X, SHIELD_CENTER_TOP_Y);
+    ctx.lineTo(SHIELD_CENTER_BOTTOM_X, SHIELD_CENTER_BOTTOM_Y);
+    ctx.stroke();
+
+    // draw trenches
+    ctx.strokeStyle = MY_COLOR;
+    ctx.beginPath();
+    ctx.moveTo(TRENCH_START_X, FIELD_SIDE_Y);
+    ctx.lineTo(TRENCH_START_X, TRENCH_INSIDE);
+    ctx.lineTo(TRENCH_END_X, TRENCH_INSIDE);
+    ctx.lineTo(TRENCH_END_X, FIELD_SIDE_Y);
+    ctx.stroke();
+
+    ctx.strokeStyle = OTHER_COLOR;
+    ctx.beginPath();
+    ctx.moveTo(TRENCH_START_X, -FIELD_SIDE_Y);
+    ctx.lineTo(TRENCH_START_X, -TRENCH_INSIDE);
+    ctx.lineTo(TRENCH_END_X, -TRENCH_INSIDE);
+    ctx.lineTo(TRENCH_END_X, -FIELD_SIDE_Y);
+    ctx.stroke();
+
+    ctx.strokeStyle = 'black';
+    ctx.beginPath();
+    ctx.moveTo(SPINNER_TOP_X, FIELD_SIDE_Y);
+    ctx.lineTo(SPINNER_TOP_X, TRENCH_INSIDE);
+    ctx.lineTo(SPINNER_BOTTOM_X, TRENCH_INSIDE);
+    ctx.lineTo(SPINNER_BOTTOM_X, FIELD_SIDE_Y);
+    ctx.moveTo(FIELD_LENGTH - SPINNER_TOP_X, -FIELD_SIDE_Y);
+    ctx.lineTo(FIELD_LENGTH - SPINNER_TOP_X, -TRENCH_INSIDE);
+    ctx.lineTo(FIELD_LENGTH - SPINNER_BOTTOM_X, -TRENCH_INSIDE);
+    ctx.lineTo(FIELD_LENGTH - SPINNER_BOTTOM_X, -FIELD_SIDE_Y);
+    ctx.stroke();
+
+    // draw initiation lines
+    ctx.beginPath();
+    ctx.moveTo(INITIATION_X, FIELD_SIDE_Y);
+    ctx.lineTo(INITIATION_X, -FIELD_SIDE_Y);
+    ctx.moveTo(FAR_INITIATION_X, FIELD_SIDE_Y);
+    ctx.lineTo(FAR_INITIATION_X, -FIELD_SIDE_Y);
+    ctx.stroke();
+
+    // draw target/loading zones
+    ctx.strokeStyle = MY_COLOR;
+    ctx.beginPath();
+    ctx.moveTo(0, DS_INSIDE_Y);
+    ctx.lineTo(TARGET_ZONE_TIP_X, DS_INSIDE_Y - 0.5 * TARGET_ZONE_WIDTH);
+    ctx.lineTo(0, DS_INSIDE_Y - TARGET_ZONE_WIDTH);
+
+    ctx.moveTo(FIELD_LENGTH, DS_INSIDE_Y);
+    ctx.lineTo(
+        FIELD_LENGTH - TARGET_ZONE_TIP_X,
+        DS_INSIDE_Y - 0.5 * LOADING_ZONE_WIDTH);
+    ctx.lineTo(FIELD_LENGTH, DS_INSIDE_Y - LOADING_ZONE_WIDTH);
+    ctx.stroke();
+
+    ctx.strokeStyle = OTHER_COLOR;
+    ctx.beginPath();
+    ctx.moveTo(0, -DS_INSIDE_Y);
+    ctx.lineTo(TARGET_ZONE_TIP_X, -(DS_INSIDE_Y - 0.5 * LOADING_ZONE_WIDTH));
+    ctx.lineTo(0, -(DS_INSIDE_Y - LOADING_ZONE_WIDTH));
+
+    ctx.moveTo(FIELD_LENGTH, -DS_INSIDE_Y);
+    ctx.lineTo(
+        FIELD_LENGTH - TARGET_ZONE_TIP_X,
+        -(DS_INSIDE_Y - 0.5 * TARGET_ZONE_WIDTH));
+    ctx.lineTo(FIELD_LENGTH, -(DS_INSIDE_Y - TARGET_ZONE_WIDTH));
+    ctx.stroke();
+  }
+
+  reset(): void {
+    const ctx = this.canvas.getContext('2d');
+    ctx.setTransform(1, 0, 0, 1, 0, 0);
+    const size = window.innerHeight * 0.9;
+    ctx.canvas.height = size;
+    ctx.canvas.width = size / 2 + 10;
+    ctx.clearRect(0, 0, size, size / 2 + 10);
+
+    // Translate to center of bottom of display.
+    ctx.translate(size / 4, size);
+    // Coordinate system is:
+    // x -> forward.
+    // y -> to the left.
+    ctx.rotate(-Math.PI / 2);
+    ctx.scale(1, -1);
+    ctx.translate(5, 0);
+
+    const M_TO_PX = (size - 10) / FIELD_LENGTH;
+    ctx.scale(M_TO_PX, M_TO_PX);
+    ctx.lineWidth = 1 / M_TO_PX;
+  }
+}
diff --git a/y2020/www/field_main.ts b/y2020/www/field_main.ts
new file mode 100644
index 0000000..adcaa27
--- /dev/null
+++ b/y2020/www/field_main.ts
@@ -0,0 +1,13 @@
+import {Connection} from 'aos/network/www/proxy';
+
+import {FieldHandler} from './field_handler';
+
+const conn = new Connection();
+
+conn.connect();
+
+const fieldHandler = new FieldHandler();
+
+fieldHandler.reset();
+fieldHandler.drawField();
+
diff --git a/y2020/www/main.ts b/y2020/www/main.ts
index d414eac..46a0e8a 100644
--- a/y2020/www/main.ts
+++ b/y2020/www/main.ts
@@ -1,9 +1,12 @@
 import {Connection} from 'aos/network/www/proxy';
 
 import {ImageHandler} from './image_handler';
+import {ConfigHandler} from 'aos/network/www/config_handler';
 
 const conn = new Connection();
 
+const configPrinter = new ConfigHandler(conn);
+
 conn.connect();
 
 const iHandler = new ImageHandler();
diff --git a/y2020/y2020.json b/y2020/y2020.json
index 0e1a6ba..3d966b8 100644
--- a/y2020/y2020.json
+++ b/y2020/y2020.json
@@ -380,6 +380,42 @@
       "max_size": 2000000
     },
     {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.CameraImage",
+      "source_node": "pi3",
+      "frequency": 25,
+      "max_size": 620000,
+      "num_senders": 18
+    },
+    {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi3",
+      "frequency": 25,
+      "max_size": 10000,
+      "destination_nodes": [
+        {
+          "name": "roborio",
+          "priority": 1,
+          "time_to_live": 5000000
+        }
+      ]
+    },
+    {
+      "name": "/pi3/camera/detailed",
+      "type": "frc971.vision.sift.ImageMatchResult",
+      "source_node": "pi3",
+      "frequency": 25,
+      "max_size": 300000
+    },
+    {
+      "name": "/pi3/camera",
+      "type": "frc971.vision.sift.TrainingData",
+      "source_node": "pi3",
+      "frequency": 2,
+      "max_size": 2000000
+    },
+    {
       "name": "/autonomous",
       "type": "aos.common.actions.Status",
       "source_node": "roborio"
@@ -491,6 +527,24 @@
     },
     {
       "match": {
+        "name": "/camera",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/pi3/camera"
+      }
+    },
+    {
+      "match": {
+        "name": "/camera/detailed",
+        "source_node": "pi3"
+      },
+      "rename": {
+        "name": "/pi3/camera/detailed"
+      }
+    },
+    {
+      "match": {
         "name": "/aos",
         "type": "aos.RobotState"
       },