Initial commit

Change-Id: I8286d1153d298956233bbbb7ae750837e5b7c6f9
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f4958c5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.distdir
+*.zip
+site/interpreter.*
+dfu-util
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..452b66b
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "webdfu"]
+	path = webdfu
+	url = https://github.com/devanlai/webdfu.git
+[submodule "libedgetpu"]
+	path = libedgetpu
+	url = https://coral.googlesource.com/libedgetpu
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4e05c54
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,106 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+MAKEFILE_DIR := $(realpath $(dir $(lastword $(MAKEFILE_LIST))))
+
+.PHONY: wasm zip server dfu-util
+
+COMPILATION_MODE ?= dbg
+ifeq ($(filter $(COMPILATION_MODE),opt dbg),)
+$(error COMPILATION_MODE must be opt or dbg)
+endif
+
+ifneq (,$(wildcard /.dockerenv))
+  BAZEL_OPTIONS += --output_base=/output/base --output_user_root=/output/user_root
+endif
+
+wasm:
+	bazel $(BAZEL_OPTIONS) build \
+  --distdir=$(MAKEFILE_DIR)/.distdir \
+  --verbose_failures \
+  --sandbox_debug \
+  --subcommands \
+  --experimental_repo_remote_exec \
+  --compilation_mode=$(COMPILATION_MODE) \
+  --define darwinn_portable=1 \
+  --action_env PYTHON_BIN_PATH=$(shell which python3) \
+  --linkopt=--preload-file=$(MAKEFILE_DIR)/mobilenet_v1_1.0_224_quant_edgetpu.tflite@model.tflite \
+  --linkopt=--preload-file=$(MAKEFILE_DIR)/cat.rgb@cat.rgb \
+  --features=use_pthreads \
+  --linkopt=-sPTHREAD_POOL_SIZE=8 \
+  --linkopt=-sINITIAL_MEMORY=67108864 \
+  --linkopt="-sEXTRA_EXPORTED_RUNTIME_METHODS=['cwrap']" \
+  --linkopt=-sASYNCIFY \
+  --linkopt=-sASYNCIFY_STACK_SIZE=16384 \
+  --linkopt="-sASYNCIFY_IMPORTS=['emscripten_receive_on_main_thread_js','emscripten_asm_const_int_sync_on_main_thread']" \
+  //tflite:interpreter-wasm && \
+  cp -f "$(MAKEFILE_DIR)/bazel-bin/tflite/interpreter-wasm/interpreter.data" \
+        "$(MAKEFILE_DIR)/bazel-bin/tflite/interpreter-wasm/interpreter.js" \
+        "$(MAKEFILE_DIR)/bazel-bin/tflite/interpreter-wasm/interpreter.wasm" \
+        "$(MAKEFILE_DIR)/bazel-bin/tflite/interpreter-wasm/interpreter.worker.js" \
+        "$(MAKEFILE_DIR)/site"
+
+dfu-util:
+	git clone git://git.code.sf.net/p/dfu-util/dfu-util || true
+	cd $(MAKEFILE_DIR)/dfu-util && ./autogen.sh && ./configure && make
+
+reset:
+	(lsusb -d 1a6e:089a && \
+    $(MAKEFILE_DIR)/dfu-util/src/dfu-util -D $(MAKEFILE_DIR)/libedgetpu/driver/usb/apex_latest_single_ep.bin -d 1a6e:089a -R) || true
+
+zip:
+	zip -r site.zip site
+
+server:
+	cd "$(MAKEFILE_DIR)/site" && python3 -m http.server
+
+################################################################################
+# Docker commands
+################################################################################
+DOCKER_CONTEXT_DIR := $(MAKEFILE_DIR)/docker
+DOCKER_WORKSPACE := $(MAKEFILE_DIR)
+DOCKER_CONTAINER_WORKSPACE := /workspace
+DOCKER_CPUS ?= k8 armv7a armv6 aarch64
+DOCKER_TARGETS ?=
+DOCKER_IMAGE ?= debian:buster
+DOCKER_TAG_BASE ?= webcoral
+DOCKER_TAG := "$(DOCKER_TAG_BASE)-$(subst :,-,$(DOCKER_IMAGE))"
+DOCKER_SHELL_COMMAND ?=
+
+DOCKER_MAKE_COMMAND := \
+for cpu in $(DOCKER_CPUS); do \
+    make CPU=\$${cpu} -C $(DOCKER_CONTAINER_WORKSPACE) $(DOCKER_TARGETS) || exit 1; \
+done
+
+define docker_run_command
+chmod a+w /; \
+groupadd --gid $(shell id -g) $(shell id -g -n); \
+useradd -m -e '' -s /bin/bash --gid $(shell id -g) --uid $(shell id -u) $(shell id -u -n); \
+echo '$(shell id -u -n) ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers; \
+su $(shell id -u -n) $(if $(1),-c '$(1)',)
+endef
+
+docker-image:
+	docker build $(DOCKER_IMAGE_OPTIONS) -t $(DOCKER_TAG) \
+      --build-arg IMAGE=$(DOCKER_IMAGE) $(DOCKER_CONTEXT_DIR)
+
+docker-shell: docker-image
+	docker run --rm -i --tty --workdir $(DOCKER_CONTAINER_WORKSPACE) \
+      --tmpfs /output:rw,exec,nr_inodes=0,size=20G \
+      -v $(DOCKER_WORKSPACE):$(DOCKER_CONTAINER_WORKSPACE) \
+      $(DOCKER_TAG) /bin/bash -c "$(call docker_run_command,$(DOCKER_SHELL_COMMAND))"
+
+docker-build: docker-image
+	docker run --rm -i $(shell tty -s && echo --tty) \
+      -v $(DOCKER_WORKSPACE):$(DOCKER_CONTAINER_WORKSPACE) \
+      $(DOCKER_TAG) /bin/bash -c "$(call docker_run_command,$(DOCKER_MAKE_COMMAND))"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..654584f
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+WebCoral
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..53c4695
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,77 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+workspace(name = "coral-web")
+
+# Configure libedgetpu and downstream libraries (TF and Crosstool).
+new_local_repository(
+    name = "libedgetpu",
+    path = "libedgetpu",
+    build_file = "libedgetpu/BUILD"
+)
+
+TENSORFLOW_COMMIT = "ee728bcc18abef50b968c4b4cb06523f64abf7ac"
+TENSORFLOW_SHA256 = "d429e78426a08ddfe121514aa132b3e90051055e3a379676ade8058a266b726e"
+
+load("@libedgetpu//:workspace.bzl", "libedgetpu_dependencies")
+libedgetpu_dependencies(TENSORFLOW_COMMIT, TENSORFLOW_SHA256)
+#libedgetpu_dependencies()
+
+load("@org_tensorflow//tensorflow:workspace3.bzl", "tf_workspace3")
+tf_workspace3()
+
+load("@org_tensorflow//tensorflow:workspace2.bzl", "tf_workspace2")
+tf_workspace2()
+
+load("@org_tensorflow//tensorflow:workspace1.bzl", "tf_workspace1")
+tf_workspace1()
+
+load("@org_tensorflow//tensorflow:workspace0.bzl", "tf_workspace0")
+tf_workspace0()
+
+#
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name = "build_bazel_rules_nodejs",
+    sha256 = "0f2de53628e848c1691e5729b515022f5a77369c76a09fbe55611e12731c90e3",
+    urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/2.0.1/rules_nodejs-2.0.1.tar.gz"],
+)
+
+load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
+
+# emscripten 2.0.15
+# http_archive(
+#     name = "emscripten",
+#     sha256 = "e35cced1514ad0da40584f8dd6f76aabf847ce0fa82c6dc8dd9442fb74ed6d0d",
+#     strip_prefix = "install",
+#     url = "https://storage.googleapis.com/webassembly/emscripten-releases-builds/mac/89202930a98fe7f9ed59b574469a9471b0bda7dd/wasm-binaries.tbz2",
+#     build_file = "//emscripten_toolchain:emscripten.BUILD",
+#     type = "tar.bz2",
+# )
+
+# emscripten 2.0.15
+http_archive(
+    name = "emscripten",
+    sha256 = "7ff49fc63adf29970f6e7af1df445d7f554bdbbb2606db1cb5d3567ce69df1db",
+    strip_prefix = "install",
+    url = "https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/89202930a98fe7f9ed59b574469a9471b0bda7dd/wasm-binaries.tbz2",
+    build_file = "//emscripten_toolchain:emscripten.BUILD",
+    type = "tar.bz2",
+)
+
+npm_install(
+    name = "npm",
+    package_json = "@emscripten//:emscripten/package.json",
+    package_lock_json = "@emscripten//:emscripten/package-lock.json",
+)
diff --git a/coral_device_rules.sh b/coral_device_rules.sh
new file mode 100755
index 0000000..b853bf6
--- /dev/null
+++ b/coral_device_rules.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+#
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+set -e
+
+readonly RULES_FILE=/etc/udev/rules.d/99-edgetpu-accelerator.rules
+
+if [[ ! -x "$(command -v udevadm)" ]]; then
+  echo "Your system does not support device rules"
+  exit 1
+fi
+
+if [[ "${EUID}" != 0 ]]; then
+  echo "Please use sudo to run as root"
+  exit 1
+fi
+
+if [[ "$1" == "install" ]]; then
+  cat << EOF > "${RULES_FILE}"
+SUBSYSTEM=="usb",ATTRS{idVendor}=="1a6e",ATTRS{idProduct}=="089a",GROUP="plugdev"
+SUBSYSTEM=="usb",ATTRS{idVendor}=="18d1",ATTRS{idProduct}=="9302",GROUP="plugdev"
+EOF
+  udevadm control --reload-rules && udevadm trigger
+  cat "${RULES_FILE}"
+elif [[ "$1" == "uninstall" ]]; then
+  rm -f "${RULES_FILE}"
+  udevadm control --reload-rules && udevadm trigger
+else
+  echo "$0 install|uninstall"
+  exit 1
+fi
+
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..f44e4eb
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,36 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ARG IMAGE
+FROM ${IMAGE}
+
+RUN apt-get update && apt-get install -y \
+  sudo \
+  curl \
+  wget \
+  vim \
+  zip \
+  unzip \
+  make \
+  git \
+  python3-all \
+  python3-numpy \
+  build-essential \
+  libusb-1.0-0-dev
+
+RUN ln -s /usr/bin/python3 /usr/bin/python
+
+ARG BAZEL_VERSION=4.0.0
+RUN wget -O /bazel https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh && \
+    bash /bazel && \
+    rm -f /bazel
diff --git a/emscripten_toolchain/BUILD.bazel b/emscripten_toolchain/BUILD.bazel
new file mode 100644
index 0000000..89fc320
--- /dev/null
+++ b/emscripten_toolchain/BUILD.bazel
@@ -0,0 +1,80 @@
+load(":crosstool.bzl", "emscripten_cc_toolchain_config_rule")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "common-script-includes",
+    srcs = [
+        "emar.sh",
+        "emcc.sh",
+        "emscripten_config",
+        "env.sh",
+        "@emscripten//:all",
+        "@nodejs//:node_files",
+        "@npm//:node_modules",
+    ],
+)
+
+filegroup(
+    name = "compile-emscripten",
+    srcs = [":common-script-includes"],
+)
+
+filegroup(
+    name = "link-emscripten",
+    srcs = [
+        "emcc_link.sh",
+        "link_wrapper.py",
+        ":common-script-includes",
+        "@emscripten//:all",
+        "@nodejs//:node_files",
+    ],
+)
+
+filegroup(
+    name = "every-file",
+    srcs = [
+        ":compile-emscripten",
+        ":link-emscripten",
+        "@emscripten//:all",
+        "@nodejs//:node_files",
+    ],
+)
+
+filegroup(name = "empty")
+
+# dlmalloc.bc is implictly added by the emscripten toolchain
+cc_library(name = "malloc")
+
+emscripten_cc_toolchain_config_rule(
+    name = "wasm",
+    cpu = "wasm",
+    emscripten_version = "emscripten",
+)
+
+cc_toolchain(
+    name = "cc-compiler-wasm",
+    all_files = ":every-file",
+    ar_files = ":common-script-includes",
+    as_files = ":empty",
+    compiler_files = ":compile-emscripten",
+    dwp_files = ":empty",
+    linker_files = ":link-emscripten",
+    objcopy_files = ":empty",
+    strip_files = ":empty",
+    toolchain_config = "wasm",
+    toolchain_identifier = "emscripten-wasm",
+)
+
+cc_toolchain_suite(
+    name = "everything",
+    toolchains = {
+        "wasm": ":cc-compiler-wasm",
+        "wasm|emscripten": ":cc-compiler-wasm",
+    },
+)
+
+py_binary(
+    name = "wasm_binary",
+    srcs = ["wasm_binary.py"],
+)
diff --git a/emscripten_toolchain/crosstool.bzl b/emscripten_toolchain/crosstool.bzl
new file mode 100644
index 0000000..10c15cb
--- /dev/null
+++ b/emscripten_toolchain/crosstool.bzl
@@ -0,0 +1,1100 @@
+"""This module encapsulates logic to create emscripten_cc_toolchain_config rule."""
+
+load(
+    "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+    "action_config",
+    "env_entry",
+    "env_set",
+    "feature",
+    "feature_set",
+    "flag_group",
+    "tool",
+    "tool_path",
+    "variable_with_value",
+    "with_feature_set",
+    _flag_set = "flag_set",
+)
+load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+
+def flag_set(flags = None, features = None, not_features = None, **kwargs):
+    """Extension to flag_set which allows for a "simple" form.
+
+    The simple form allows specifying flags as a simple list instead of a flag_group
+    if enable_if or expand_if semantics are not required.
+
+    Similarly, the simple form allows passing features/not_features if they are a simple
+    list of semantically "and" features.
+    (i.e. "asan" and "dbg", rather than "asan" or "dbg")
+
+    Args:
+      flags: list, set of flags
+      features: list, set of features required to be enabled.
+      not_features: list, set of features required to not be enabled.
+      **kwargs: The rest of the args for flag_set.
+
+    Returns:
+      flag_set
+    """
+    if flags:
+        if kwargs.get("flag_groups"):
+            fail("Cannot set flags and flag_groups")
+        else:
+            kwargs["flag_groups"] = [flag_group(flags = flags)]
+
+    if features or not_features:
+        if kwargs.get("with_features"):
+            fail("Cannot set features/not_feature and with_features")
+        kwargs["with_features"] = [with_feature_set(
+            features = features or [],
+            not_features = not_features or [],
+        )]
+    return _flag_set(**kwargs)
+
+CROSSTOOL_DEFAULT_WARNINGS = [
+    "-Wall",
+]
+
+def _impl(ctx):
+    target_cpu = ctx.attr.cpu
+    toolchain_identifier = "emscripten-" + target_cpu
+    target_system_name = target_cpu + "-unknown-emscripten"
+
+    host_system_name = "i686-unknown-linux-gnu"
+
+    target_libc = "musl/js"
+
+    abi_version = "emscripten_syscalls"
+
+    compiler = "emscripten"
+    abi_libc_version = "default"
+
+    cc_target_os = "emscripten"
+    emscripten_version = ctx.attr.emscripten_version
+
+    builtin_sysroot = "external/emscripten/emscripten/cache/sysroot"
+
+    ################################################################
+    # Tools
+    ################################################################
+    clang_tool = tool(path = "emcc.sh")
+    clif_match_tool = tool(path = "dummy_clif_matcher")
+    link_tool = tool(path = "emcc_link.sh")
+    archive_tool = tool(path = "emar.sh")
+    strip_tool = tool(path = "NOT_USED_STRIP_TOOL")
+
+    #### Legacy tool paths (much of this is redundant with action_configs, but
+    #### these are still used for some things)
+    tool_paths = [
+        tool_path(name = "ar", path = "emar.sh"),
+        tool_path(name = "cpp", path = "/bin/false"),
+        tool_path(name = "gcc", path = "emcc.sh"),
+        tool_path(name = "gcov", path = "/bin/false"),
+        tool_path(name = "ld", path = "emcc_link.sh"),
+        tool_path(name = "nm", path = "NOT_USED"),
+        tool_path(name = "objdump", path = "/bin/false"),
+        tool_path(name = "strip", path = "NOT_USED"),
+    ]
+
+    ################################################################
+    # Action Configs
+    ################################################################
+
+    cpp_compile_action = action_config(
+        action_name = ACTION_NAMES.cpp_compile,
+        tools = [clang_tool],
+    )
+
+    cpp_module_compile_action = action_config(
+        action_name = ACTION_NAMES.cpp_module_compile,
+        tools = [clang_tool],
+    )
+
+    cpp_module_codegen_action = action_config(
+        action_name = ACTION_NAMES.cpp_module_codegen,
+        tools = [clang_tool],
+    )
+
+    clif_match_action = action_config(
+        action_name = ACTION_NAMES.clif_match,
+        tools = [clif_match_tool],
+    )
+
+    cpp_link_dynamic_library_action = action_config(
+        action_name = ACTION_NAMES.cpp_link_dynamic_library,
+        tools = [link_tool],
+    )
+
+    strip_action = action_config(
+        action_name = ACTION_NAMES.strip,
+        tools = [strip_tool],
+    )
+
+    preprocess_assemble_action = action_config(
+        action_name = ACTION_NAMES.preprocess_assemble,
+        tools = [clang_tool],
+    )
+
+    cpp_header_parsing_action = action_config(
+        action_name = ACTION_NAMES.cpp_header_parsing,
+        tools = [clang_tool],
+    )
+
+    cpp_link_static_library_action = action_config(
+        action_name = ACTION_NAMES.cpp_link_static_library,
+        enabled = True,
+        flag_sets = [
+            flag_set(
+                flag_groups = [
+                    flag_group(
+                        flags = ["rcsD", "%{output_execpath}"],
+                        expand_if_available = "output_execpath",
+                    ),
+                ],
+            ),
+            flag_set(
+                flag_groups = [
+                    flag_group(
+                        iterate_over = "libraries_to_link",
+                        flag_groups = [
+                            flag_group(
+                                flags = ["%{libraries_to_link.name}"],
+                                expand_if_equal = variable_with_value(
+                                    name = "libraries_to_link.type",
+                                    value = "object_file",
+                                ),
+                            ),
+                            flag_group(
+                                flags = ["%{libraries_to_link.object_files}"],
+                                iterate_over = "libraries_to_link.object_files",
+                                expand_if_equal = variable_with_value(
+                                    name = "libraries_to_link.type",
+                                    value = "object_file_group",
+                                ),
+                            ),
+                        ],
+                        expand_if_available = "libraries_to_link",
+                    ),
+                ],
+            ),
+            flag_set(
+                flag_groups = [
+                    flag_group(
+                        flags = ["@%{linker_param_file}"],
+                        expand_if_available = "linker_param_file",
+                    ),
+                ],
+            ),
+        ],
+        tools = [archive_tool],
+    )
+
+    c_compile_action = action_config(
+        action_name = ACTION_NAMES.c_compile,
+        tools = [clang_tool],
+    )
+
+    linkstamp_compile_action = action_config(
+        action_name = ACTION_NAMES.linkstamp_compile,
+        tools = [clang_tool],
+    )
+
+    assemble_action = action_config(
+        action_name = ACTION_NAMES.assemble,
+        tools = [clang_tool],
+    )
+
+    cpp_link_executable_action = action_config(
+        action_name = ACTION_NAMES.cpp_link_executable,
+        tools = [link_tool],
+    )
+
+    cpp_link_nodeps_dynamic_library_action = action_config(
+        action_name = ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+        tools = [link_tool],
+    )
+
+    action_configs = [
+        strip_action,
+        c_compile_action,
+        cpp_compile_action,
+        linkstamp_compile_action,
+        assemble_action,
+        preprocess_assemble_action,
+        cpp_header_parsing_action,
+        cpp_module_compile_action,
+        cpp_module_codegen_action,
+        cpp_link_executable_action,
+        cpp_link_dynamic_library_action,
+        cpp_link_nodeps_dynamic_library_action,
+        cpp_link_static_library_action,
+        clif_match_action,
+    ]
+
+    all_compile_actions = [
+        ACTION_NAMES.c_compile,
+        ACTION_NAMES.cpp_compile,
+        ACTION_NAMES.linkstamp_compile,
+        ACTION_NAMES.assemble,
+        ACTION_NAMES.preprocess_assemble,
+        ACTION_NAMES.cpp_header_parsing,
+        ACTION_NAMES.cpp_module_compile,
+        ACTION_NAMES.cpp_module_codegen,
+        ACTION_NAMES.clif_match,
+        ACTION_NAMES.lto_backend,
+    ]
+
+    all_cpp_compile_actions = [
+        ACTION_NAMES.cpp_compile,
+        ACTION_NAMES.linkstamp_compile,
+        ACTION_NAMES.cpp_header_parsing,
+        ACTION_NAMES.cpp_module_compile,
+        ACTION_NAMES.cpp_module_codegen,
+        ACTION_NAMES.clif_match,
+    ]
+
+    preprocessor_compile_actions = [
+        ACTION_NAMES.c_compile,
+        ACTION_NAMES.cpp_compile,
+        ACTION_NAMES.linkstamp_compile,
+        ACTION_NAMES.preprocess_assemble,
+        ACTION_NAMES.cpp_header_parsing,
+        ACTION_NAMES.cpp_module_compile,
+        ACTION_NAMES.clif_match,
+    ]
+
+    all_link_actions = [
+        ACTION_NAMES.cpp_link_executable,
+        ACTION_NAMES.cpp_link_dynamic_library,
+        ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+    ]
+
+    ################################################################
+    # Features
+    ################################################################
+
+    features = [
+        # This set of magic "feature"s are important configuration information for blaze.
+        feature(name = "no_legacy_features", enabled = True),
+        feature(
+            name = "has_configured_linker_path",
+            enabled = True,
+        ),
+
+        # Blaze requests this feature by default, but we don't care.
+        feature(name = "dependency_file"),
+
+        # Blaze requests this feature by default, but we don't care.
+        feature(name = "random_seed"),
+
+        # Formerly "needsPic" attribute
+        feature(name = "supports_pic", enabled = False),
+
+        # Blaze requests this feature by default.
+        # Blaze also tests if this feature is supported, before setting the "pic" build-variable.
+        feature(name = "pic"),
+
+        # Blaze requests this feature if fission is requested
+        # Blaze also tests if it's supported to see if we support fission.
+        feature(name = "per_object_debug_info"),
+
+        # Blaze requests this feature by default.
+        # Blaze also tests if this feature is supported before setting preprocessor_defines
+        # (...but why?)
+        feature(name = "preprocessor_defines"),
+
+        # Blaze requests this feature by default.
+        # Blaze also tests if this feature is supported before setting includes. (...but why?)
+        feature(name = "include_paths"),
+
+        # Blaze tests if this feature is enabled in order to create implicit
+        # "nodeps" .so outputs from cc_library rules.
+        feature(name = "supports_dynamic_linker", enabled = False),
+
+        # Blaze requests this feature when linking a cc_binary which is
+        # "dynamic" aka linked against nodeps-dynamic-library cc_library
+        # outputs.
+        feature(name = "dynamic_linking_mode"),
+
+        #### Configuration features
+        feature(
+            name = "crosstool_cpu",
+            enabled = True,
+            implies = ["crosstool_cpu_" + target_cpu],
+        ),
+        feature(
+            name = "crosstool_cpu_asmjs",
+            provides = ["variant:crosstool_cpu"],
+        ),
+        feature(
+            name = "crosstool_cpu_wasm",
+            provides = ["variant:crosstool_cpu"],
+        ),
+
+        # These 3 features will be automatically enabled by blaze in the
+        # corresponding build mode.
+        feature(
+            name = "opt",
+            provides = ["variant:crosstool_build_mode"],
+        ),
+        feature(
+            name = "dbg",
+            provides = ["variant:crosstool_build_mode"],
+        ),
+        feature(
+            name = "fastbuild",
+            provides = ["variant:crosstool_build_mode"],
+        ),
+
+        #### User-settable features
+
+        # Set if enabling exceptions.
+        feature(name = "exceptions"),
+
+        # This feature overrides the default optimization to prefer execution speed
+        # over binary size (like clang -O3).
+        feature(
+            name = "optimized_for_speed",
+            provides = ["variant:crosstool_optimization_mode"],
+        ),
+
+        # This feature overrides the default optimization to prefer binary size over
+        # execution speed (like clang -Oz).
+        feature(
+            name = "optimized_for_size",
+            provides = ["variant:crosstool_optimization_mode"],
+        ),
+
+        # Convenience aliases / alt-spellings.
+        feature(
+            name = "optimize_for_speed",
+            implies = ["optimized_for_speed"],
+        ),
+        feature(
+            name = "optimize_for_size",
+            implies = ["optimized_for_size"],
+        ),
+
+        # This feature allows easier use of profiling tools by preserving mangled
+        # C++ names. This does everything profiling_funcs does and more.
+        feature(name = "profiling"),
+
+        # This feature emits only enough debug info for function names to appear
+        # in profiles.
+        feature(name = "profiling_funcs"),
+
+        # This feature allows source maps to be generated.
+        feature(
+            name = "source_maps",
+            implies = ["full_debug_info"],
+        ),
+        feature(
+            name = "dwarf_debug_info",
+            implies = ["profiling"],
+        ),
+
+        # Turns on full debug info (-g4).
+        feature(name = "full_debug_info"),
+
+        # Enables the use of "Emscripten" Pthread implementation.
+        # https://kripken.github.io/emscripten-site/docs/porting/pthreads.html
+        # https://github.com/kripken/emscripten/wiki/Pthreads-with-WebAssembly
+        feature(name = "use_pthreads"),
+
+        # If enabled, the runtime will exit when main() completes.
+        feature(name = "exit_runtime"),
+
+        # Primarily for toolchain maintainers:
+        feature(name = "emcc_debug"),
+        feature(name = "emcc_debug_link"),
+        feature(
+            name = "llvm_backend",
+            requires = [feature_set(features = ["crosstool_cpu_wasm"])],
+            enabled = True,
+        ),
+
+        # Remove once flag is flipped.
+        # See https://github.com/bazelbuild/bazel/issues/7687
+        feature(
+            name = "do_not_split_linking_cmdline",
+        ),
+
+        # Adds simd support, only available with the llvm backend.
+        feature(
+            name = "wasm_simd",
+            requires = [feature_set(features = ["llvm_backend"])],
+        ),
+        feature(
+            name = "precise_long_double_printf",
+            enabled = True,
+        ),
+        feature(
+            name = "wasm_warnings_as_errors",
+            enabled = True,
+        ),
+
+        # ASan and UBSan. See also:
+        # https://emscripten.org/docs/debugging/Sanitizers.html
+        feature(name = "wasm_asan"),
+        feature(name = "wasm_ubsan"),
+
+        feature(
+            name = "output_format_js",
+            enabled = True,
+        ),
+    ]
+
+    crosstool_default_flag_sets = [
+        # Compile, Link, and CC_FLAGS make variable
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+                ACTION_NAMES.cpp_link_executable,
+                ACTION_NAMES.cpp_link_dynamic_library,
+                ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+            ],
+            flag_groups = [
+                flag_group(
+                    flags = ["--sysroot=%{sysroot}"],
+                    expand_if_available = "sysroot",
+                ),
+            ],
+        ),
+        # Compile + Link
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+                ACTION_NAMES.cpp_link_executable,
+                ACTION_NAMES.cpp_link_dynamic_library,
+                ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+            ],
+            # This forces color diagnostics even on Forge (where we don't have an
+            # attached terminal).
+            flags = [
+                "-fdiagnostics-color",
+            ],
+        ),
+        # C++ compiles (and implicitly link)
+        flag_set(
+            actions = all_cpp_compile_actions,
+            flags = [
+                "-fno-exceptions",
+            ],
+            not_features = ["exceptions"],
+        ),
+        flag_set(
+            actions = all_cpp_compile_actions,
+            flags = [
+                "-fexceptions",
+            ],
+            features = ["exceptions"],
+        ),
+        # All compiles (and implicitly link)
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = [
+                "-fno-strict-aliasing",
+                "-funsigned-char",
+                "-no-canonical-prefixes",
+            ],
+        ),
+        # Language Features
+        flag_set(
+            actions = all_cpp_compile_actions,
+            flags = ["-std=gnu++17", "-nostdinc", "-nostdinc++",],
+        ),
+
+        # Emscripten-specific settings:
+        flag_set(
+            actions = all_compile_actions + all_link_actions,
+            flags = ["-s", "WASM=0"],
+            features = ["crosstool_cpu_asmjs"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-s", "USE_PTHREADS=1"],
+            features = ["use_pthreads"],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flags = ["-s", "EXIT_RUNTIME=1"],
+            features = ["exit_runtime"],
+        ),
+        flag_set(
+            actions = all_compile_actions + all_link_actions,
+            flags = ["-pthread"],
+            features = ["llvm_backend", "use_pthreads"],
+        ),
+        flag_set(
+            actions = all_compile_actions + all_link_actions,
+            flags = ["-msimd128"],
+            features = ["wasm_simd"],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flags = ["-s", "PRINTF_LONG_DOUBLE=1"],
+            features = ["precise_long_double_printf"],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flags = ["--oformat=js"],
+            features = ["output_format_js"],
+        ),
+
+        # Opt
+        flag_set(
+            actions = preprocessor_compile_actions,
+            flags = ["-DNDEBUG"],
+            features = ["opt"],
+        ),
+        flag_set(
+            actions = all_compile_actions,
+            flags = ["-fomit-frame-pointer"],
+            features = ["opt"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-O3"],
+            features = ["opt"],
+        ),
+        # Users can override opt-level with semantic names...
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-Oz"],
+            features = ["optimized_for_size", "opt"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-O3"],
+            features = ["optimized_for_speed", "opt"],
+        ),
+
+        # Fastbuild
+        flag_set(
+            actions = all_compile_actions,
+            flags = ["-fomit-frame-pointer"],
+            features = ["fastbuild"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-O2"],
+            features = ["fastbuild"],
+        ),
+
+        # Dbg
+        flag_set(
+            actions = all_compile_actions,
+            flags = ["-fno-omit-frame-pointer"],
+            features = ["dbg"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-g", "-O0"],
+            features = ["dbg"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = [
+                "-g4",
+                "-fsanitize=address",
+                "-O1",
+                "-DADDRESS_SANITIZER=1",
+                "-fno-omit-frame-pointer",
+            ],
+            features = ["wasm_asan"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = [
+                "-g4",
+                "-fsanitize=undefined",
+                "-O1",
+                "-DUNDEFINED_BEHAVIOR_SANITIZER=1",
+                "-fno-omit-frame-pointer",
+                "-fno-sanitize=vptr",
+            ],
+            features = ["wasm_ubsan"],
+        ),
+
+        # Profiling provides full debug info and a special --profiling flag
+        # to control name mangling
+        flag_set(
+            actions = all_link_actions,
+            flags = ["--profiling"],
+            features = ["profiling"],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flags = ["--profiling_funcs"],
+            features = ["profiling_funcs"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-g4"],
+            features = ["full_debug_info"],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flags = ["-gseparate-dwarf"],
+            features = ["dwarf_debug_info"],
+        ),
+        flag_set(
+            actions = all_compile_actions +
+                      all_link_actions,
+            flags = ["-fdebug-compilation-dir=."],
+            features = ["dwarf_debug_info"],
+        ),
+        # Generic warning flag list
+        flag_set(
+            actions = all_compile_actions,
+            flags = CROSSTOOL_DEFAULT_WARNINGS,
+        ),
+
+        # Defines and Includes and Paths and such
+        flag_set(
+            actions = all_compile_actions,
+            flag_groups = [
+                flag_group(flags = ["-fPIC"], expand_if_available = "pic"),
+            ],
+        ),
+        flag_set(
+            actions = preprocessor_compile_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["-D%{preprocessor_defines}"],
+                    iterate_over = "preprocessor_defines",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = preprocessor_compile_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["-include", "%{includes}"],
+                    iterate_over = "includes",
+                    expand_if_available = "includes",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = preprocessor_compile_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["-iquote", "%{quote_include_paths}"],
+                    iterate_over = "quote_include_paths",
+                ),
+                flag_group(
+                    flags = ["-I%{include_paths}"],
+                    iterate_over = "include_paths",
+                ),
+                flag_group(
+                    flags = ["-isystem", "%{system_include_paths}"],
+                    iterate_over = "system_include_paths",
+                ),
+            ],
+        ),
+
+        ## Linking options (not libs -- those go last)
+
+        # Generic link options
+        flag_set(
+            actions = [
+                ACTION_NAMES.cpp_link_dynamic_library,
+                ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+            ],
+            flags = ["-shared"],
+        ),
+
+        # Linker search paths and objects:
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    iterate_over = "runtime_library_search_directories",
+                    flag_groups = [
+                        flag_group(
+                            flags = [
+                                "-Wl,-rpath,$EXEC_ORIGIN/%{runtime_library_search_directories}",
+                            ],
+                            expand_if_true = "is_cc_test",
+                        ),
+                        flag_group(
+                            flags = [
+                                "-Wl,-rpath,$ORIGIN/%{runtime_library_search_directories}",
+                            ],
+                            expand_if_false = "is_cc_test",
+                        ),
+                    ],
+                    expand_if_available = "runtime_library_search_directories",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["-L%{library_search_directories}"],
+                    iterate_over = "library_search_directories",
+                    expand_if_available = "library_search_directories",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    # This is actually a list of object files from the linkstamp steps
+                    flags = ["%{linkstamp_paths}"],
+                    iterate_over = "linkstamp_paths",
+                    expand_if_available = "linkstamp_paths",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["@%{thinlto_param_file}"],
+                    expand_if_available = "libraries_to_link",
+                    expand_if_true = "thinlto_param_file",
+                ),
+                flag_group(
+                    iterate_over = "libraries_to_link",
+                    flag_groups = [
+                        flag_group(
+                            flags = ["-Wl,--start-lib"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "object_file_group",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["-Wl,-whole-archive"],
+                            expand_if_true = "libraries_to_link.is_whole_archive",
+                        ),
+                        flag_group(
+                            flags = ["%{libraries_to_link.object_files}"],
+                            iterate_over = "libraries_to_link.object_files",
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "object_file_group",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["%{libraries_to_link.name}"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "object_file",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["%{libraries_to_link.name}"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "interface_library",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["%{libraries_to_link.name}"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "static_library",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["-l%{libraries_to_link.name}"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "dynamic_library",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["-l:%{libraries_to_link.name}"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "versioned_dynamic_library",
+                            ),
+                        ),
+                        flag_group(
+                            flags = ["-Wl,-no-whole-archive"],
+                            expand_if_true = "libraries_to_link.is_whole_archive",
+                        ),
+                        flag_group(
+                            flags = ["-Wl,--end-lib"],
+                            expand_if_equal = variable_with_value(
+                                name = "libraries_to_link.type",
+                                value = "object_file_group",
+                            ),
+                        ),
+                    ],
+                    expand_if_available = "libraries_to_link",
+                ),
+            ],
+        ),
+
+        # Configure the header parsing and preprocessing.
+        flag_set(
+            actions = [ACTION_NAMES.cpp_header_parsing],
+            flags = ["-xc++-header", "-fsyntax-only"],
+            features = ["parse_headers"],
+        ),
+
+        # Note: user compile flags should be nearly last -- you probably
+        # don't want to put any more features after this!
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+            ],
+            flag_groups = [
+                flag_group(
+                    flags = ["%{user_compile_flags}"],
+                    iterate_over = "user_compile_flags",
+                    expand_if_available = "user_compile_flags",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["%{user_link_flags}"],
+                    iterate_over = "user_link_flags",
+                    expand_if_available = "user_link_flags",
+                ),
+            ],
+        ),
+        ## Options which need to go late -- after all the user options -- go here.
+        flag_set(
+            # One might hope that these options would only be needed for C++
+            # compiles. But, sadly, users compile ".c" files with custom
+            # copts=["-x", "c++"], and expect that to be able to find C++ stdlib
+            # headers. It might be worth pondering how blaze could support this sort
+            # of use-case better.
+            actions = preprocessor_compile_actions +
+                      [ACTION_NAMES.cc_flags_make_variable],
+            flags = [
+                "-iwithsysroot" + "/include/c++/v1",
+                "-iwithsysroot" + "/include/compat",
+                "-iwithsysroot" + "/include",
+                "-isystem", "external/emscripten/lib/clang/13.0.0/include",
+            ],
+        ),
+        # Inputs and outputs
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+            ],
+            flag_groups = [
+                flag_group(
+                    flags = ["-MD", "-MF", "%{dependency_file}"],
+                    expand_if_available = "dependency_file",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+            ],
+            flag_groups = [
+                flag_group(
+                    flags = ["-c", "%{source_file}"],
+                    expand_if_available = "source_file",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = [
+                ACTION_NAMES.c_compile,
+                ACTION_NAMES.cpp_compile,
+                ACTION_NAMES.linkstamp_compile,
+                ACTION_NAMES.assemble,
+                ACTION_NAMES.preprocess_assemble,
+                ACTION_NAMES.cpp_header_parsing,
+                ACTION_NAMES.cpp_module_compile,
+                ACTION_NAMES.cpp_module_codegen,
+                ACTION_NAMES.clif_match,
+            ],
+            flag_groups = [
+                flag_group(
+                    flags = ["-S"],
+                    expand_if_available = "output_assembly_file",
+                ),
+                flag_group(
+                    flags = ["-E"],
+                    expand_if_available = "output_preprocess_file",
+                ),
+                flag_group(
+                    flags = ["-o", "%{output_file}"],
+                    expand_if_available = "output_file",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["-o", "%{output_execpath}"],
+                    expand_if_available = "output_execpath",
+                ),
+            ],
+        ),
+        # And finally, the params file!
+        flag_set(
+            actions = all_link_actions,
+            flag_groups = [
+                flag_group(
+                    flags = ["@%{linker_param_file}"],
+                    expand_if_available = "linker_param_file",
+                ),
+            ],
+        ),
+        flag_set(
+            actions = all_compile_actions,
+            flags = [
+                "-Wno-builtin-macro-redefined",
+                # Genrules may not escape quotes enough for these, so
+                # don't put them into $(CC_FLAGS):
+                '-D__DATE__="redacted"',
+                '-D__TIMESTAMP__="redacted"',
+                '-D__TIME__="redacted"',
+            ],
+        ),
+        flag_set(
+            actions = all_compile_actions,
+            flags = ["-Werror"],
+            features = ["wasm_warnings_as_errors"],
+        ),
+    ]
+
+    crosstool_default_env_sets = [
+        # Use llvm backend.  Off by default, enabled via --features=llvm_backend
+        env_set(
+            actions = all_compile_actions +
+                      all_link_actions +
+                      [ACTION_NAMES.cpp_link_static_library],
+            env_entries = [env_entry(key = "EMCC_WASM_BACKEND", value = "1")],
+            with_features = [with_feature_set(features = ["llvm_backend"])],
+        ),
+        # Debug compile and link. Off by default, enabled via --features=emcc_debug
+        env_set(
+            actions = all_compile_actions,
+            env_entries = [env_entry(key = "EMCC_DEBUG", value = "1")],
+            with_features = [with_feature_set(features = ["emcc_debug"])],
+        ),
+
+        # Debug only link step. Off by default, enabled via --features=emcc_debug_link
+        env_set(
+            actions = all_link_actions,
+            env_entries = [env_entry(key = "EMCC_DEBUG", value = "1")],
+            with_features = [
+                with_feature_set(features = ["emcc_debug"]),
+                with_feature_set(features = ["emcc_debug_link"]),
+            ],
+        ),
+    ]
+
+    crosstool_default_flags_feature = feature(
+        name = "crosstool_default_flags",
+        enabled = True,
+        flag_sets = crosstool_default_flag_sets,
+        env_sets = crosstool_default_env_sets,
+    )
+
+    features.append(crosstool_default_flags_feature)
+
+    cxx_builtin_include_directories = [
+        "external/emscripten/emscripten/cache/sysroot/include/c++/v1",
+        "external/emscripten/emscripten/cache/sysroot/include/compat",
+        "external/emscripten/emscripten/cache/sysroot/include",
+        "external/emscripten/lib/clang/13.0.0/include",
+    ]
+
+    artifact_name_patterns = []
+
+    make_variables = []
+
+    out = ctx.actions.declare_file(ctx.label.name)
+    ctx.actions.write(out, "Fake executable")
+    return [
+        cc_common.create_cc_toolchain_config_info(
+            ctx = ctx,
+            features = features,
+            action_configs = action_configs,
+            artifact_name_patterns = artifact_name_patterns,
+            cxx_builtin_include_directories = cxx_builtin_include_directories,
+            toolchain_identifier = toolchain_identifier,
+            host_system_name = host_system_name,
+            target_system_name = target_system_name,
+            target_cpu = target_cpu,
+            target_libc = target_libc,
+            compiler = compiler,
+            abi_version = abi_version,
+            abi_libc_version = abi_libc_version,
+            tool_paths = tool_paths,
+            make_variables = make_variables,
+            builtin_sysroot = builtin_sysroot,
+            cc_target_os = cc_target_os,
+        ),
+        DefaultInfo(
+            executable = out,
+        ),
+    ]
+
+emscripten_cc_toolchain_config_rule = rule(
+    implementation = _impl,
+    attrs = {
+        "cpu": attr.string(mandatory = True, values = ["asmjs", "wasm"]),
+        "emscripten_version": attr.string(mandatory = True),
+    },
+    provides = [CcToolchainConfigInfo],
+    executable = True,
+)
diff --git a/emscripten_toolchain/emar.sh b/emscripten_toolchain/emar.sh
new file mode 100755
index 0000000..965442e
--- /dev/null
+++ b/emscripten_toolchain/emar.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source emscripten_toolchain/env.sh
+
+exec python3 $EMSCRIPTEN/emar.py "$@"
diff --git a/emscripten_toolchain/emcc.sh b/emscripten_toolchain/emcc.sh
new file mode 100755
index 0000000..66d00b6
--- /dev/null
+++ b/emscripten_toolchain/emcc.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source emscripten_toolchain/env.sh
+
+exec python3 external/emscripten/emscripten/emcc.py "$@"
diff --git a/emscripten_toolchain/emcc_link.sh b/emscripten_toolchain/emcc_link.sh
new file mode 100755
index 0000000..9d0f8e2
--- /dev/null
+++ b/emscripten_toolchain/emcc_link.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+source emscripten_toolchain/env.sh
+
+exec python3 emscripten_toolchain/link_wrapper.py "$@"
diff --git a/emscripten_toolchain/emscripten.BUILD b/emscripten_toolchain/emscripten.BUILD
new file mode 100644
index 0000000..6f11852
--- /dev/null
+++ b/emscripten_toolchain/emscripten.BUILD
@@ -0,0 +1,6 @@
+package(default_visibility = ['//visibility:public'])
+
+filegroup(
+    name = "all",
+    srcs = glob(["**"]),
+)
diff --git a/emscripten_toolchain/emscripten_config b/emscripten_toolchain/emscripten_config
new file mode 100644
index 0000000..b7948ac
--- /dev/null
+++ b/emscripten_toolchain/emscripten_config
@@ -0,0 +1,12 @@
+import os
+import platform
+
+ROOT_DIR = os.environ["ROOT_DIR"]
+EMSCRIPTEN_ROOT = os.environ["EMSCRIPTEN"]
+LLVM_ROOT = ROOT_DIR + "/external/emscripten/bin"
+BINARYEN_ROOT = ROOT_DIR + "/external/emscripten"
+FROZEN_CACHE = True
+
+system = platform.system()
+nodejs_binary = "node.exe" if(system =="Windows") else "bin/node"
+NODE_JS = ROOT_DIR + "/external/nodejs_{}_amd64/{}".format(system.lower(), nodejs_binary)
diff --git a/emscripten_toolchain/env.sh b/emscripten_toolchain/env.sh
new file mode 100755
index 0000000..dfb4ddc
--- /dev/null
+++ b/emscripten_toolchain/env.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+export ROOT_DIR=`(pwd -P)`
+export EMSCRIPTEN=${ROOT_DIR}/external/emscripten/emscripten
+export EM_CONFIG=${ROOT_DIR}/emscripten_toolchain/emscripten_config
diff --git a/emscripten_toolchain/link_wrapper.py b/emscripten_toolchain/link_wrapper.py
new file mode 100644
index 0000000..1e26bde
--- /dev/null
+++ b/emscripten_toolchain/link_wrapper.py
@@ -0,0 +1,169 @@
+#!/usr/bin/env python
+"""wrapper around emcc link step.
+
+This wrapper currently serves the following purposes.
+
+1. When building with --config=wasm the final output is multiple files, usually
+   at least one .js and one .wasm file. Since the cc_binary link step only
+   allows a single output, we must tar up the outputs into a single file.
+
+2. Add quotes around arguments that need them in the response file to work
+   around a bazel quirk.
+
+3. Ensure the external_debug_info section of the wasm points at the correct
+   bazel path.
+"""
+
+from __future__ import print_function
+
+import argparse
+import os
+import subprocess
+import sys
+
+# Only argument should be @path/to/parameter/file
+assert sys.argv[1][0] == '@'
+param_filename = sys.argv[1][1:]
+param_file_args = [l.strip() for l in open(param_filename, 'r').readlines()]
+
+# Re-write response file if needed.
+if any(' ' in a for a in param_file_args):
+  new_param_filename = param_filename + '.modified'
+  with open(new_param_filename, 'w') as f:
+    for param in param_file_args:
+      if ' ' in param:
+        f.write('"%s"' % param)
+      else:
+        f.write(param)
+      f.write('\n')
+  sys.argv[1] = '@' + new_param_filename
+
+emcc_py = os.path.join(os.environ['EMSCRIPTEN'], 'emcc.py')
+rtn = subprocess.call(['python3', emcc_py] + sys.argv[1:])
+if rtn != 0:
+  sys.exit(1)
+
+# Parse the arguments that we gave to the linker to determine what the output
+# file is named and what the output format is.
+parser = argparse.ArgumentParser(add_help=False)
+parser.add_argument('-o')
+parser.add_argument('--oformat')
+options = parser.parse_known_args(param_file_args)[0]
+output_file = options.o
+oformat = options.oformat
+outdir = os.path.dirname(output_file)
+base_name = os.path.basename(output_file)
+
+# The output file name is the name of the build rule that was built.
+# Add an appropriate file extension based on --oformat.
+if oformat is not None:
+  base_name_split = os.path.splitext(base_name)
+
+  # If the output name has no extension, give it the appropriate extension.
+  if not base_name_split[1]:
+    os.rename(output_file, output_file + '.' + oformat)
+
+  # If the output name does have an extension and it matches the output format,
+  # change the base_name so it doesn't have an extension.
+  elif base_name_split[1] == '.' + oformat:
+    base_name = base_name_split[0]
+
+  # If the output name does have an extension and it does not match the output
+  # format, change the base_name so it doesn't have an extension and rename
+  # the output_file so it has the proper extension.
+  # Note that if you do something like name your build rule "foo.js" and pass
+  # "--oformat=html", emscripten will write to the same file for both the js and
+  # html output, overwriting the js output entirely with the html.
+  # Please don't do that.
+  else:
+    base_name = base_name_split[0]
+    os.rename(output_file, os.path.join(outdir, base_name + '.' + oformat))
+
+files = []
+extensions = [
+    '.js',
+    '.wasm',
+    '.wasm.map',
+    '.js.mem',
+    '.fetch.js',
+    '.worker.js',
+    '.data',
+    '.js.symbols',
+    '.wasm.debug.wasm',
+    '.html'
+]
+
+for ext in extensions:
+  filename = base_name + ext
+  if os.path.exists(os.path.join(outdir, filename)):
+    files.append(filename)
+
+wasm_base = os.path.join(outdir, base_name + '.wasm')
+if os.path.exists(wasm_base + '.debug.wasm') and os.path.exists(wasm_base):
+  # If we have a .wasm.debug.wasm file and a .wasm file, we need to rewrite the
+  # section in the .wasm file that refers to it. The path that's in there
+  # is the blaze output path; we want it to be just the filename.
+
+  llvm_objcopy = os.path.join(
+      os.environ['EMSCRIPTEN'], 'llvm-bin/llvm-objcopy')
+  # First, check to make sure the .wasm file has the header that needs to be
+  # rewritten.
+  rtn = subprocess.call([
+      llvm_objcopy,
+      '--dump-section=external_debug_info=/dev/null',
+      wasm_base], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  if rtn == 0:
+    # If llvm-objcopy did not return an error, the external_debug_info section
+    # must exist, so we're good to continue.
+
+    # Next we need to convert length of the filename to LEB128.
+    # Start by converting the length of the filename to a bit string.
+    bit_string = '{0:b}'.format(len(base_name + '.wasm.debug.wasm'))
+
+    # Pad the bit string with 0s so that its length is a multiple of 7.
+    while len(bit_string) % 7 != 0:
+      bit_string = '0' + bit_string
+
+    # Break up our bit string into chunks of 7.
+    # We do this backwards because the final format is little-endian.
+    final_bytes = bytearray()
+    for i in reversed(range(0, len(bit_string), 7)):
+      binary_part = bit_string[i:i + 7]
+      if i != 0:
+        # Every chunk except the last one needs to be prepended with '1'.
+        # The length of each chunk is 7, so that one has an implicit '0'.
+        binary_part = '1' + binary_part
+      final_bytes.append(int(binary_part, 2))
+    # Finally, add the actual filename.
+    final_bytes.extend((base_name + '.wasm.debug.wasm').encode())
+
+    # Write our length + filename bytes to a temp file.
+    with open('debugsection.tmp', 'wb+') as f:
+      f.write(final_bytes)
+      f.close()
+
+    # First delete the old section.
+    subprocess.check_call([
+        llvm_objcopy,
+        wasm_base,
+        '--remove-section=external_debug_info'])
+    # Rewrite section with the new size and filename from the temp file.
+    subprocess.check_call([
+        llvm_objcopy,
+        wasm_base,
+        '--add-section=external_debug_info=debugsection.tmp'])
+
+# If we have more than one output file then create tarball
+if len(files) > 1:
+  cmd = ['tar', 'cf', 'tmp.tar'] + files
+  subprocess.check_call(cmd, cwd=outdir)
+  os.rename(os.path.join(outdir, 'tmp.tar'), output_file)
+elif len(files) == 1:
+  # Otherwise, if only have a single output than move it to the expected name
+  if files[0] != os.path.basename(output_file):
+    os.rename(os.path.join(outdir, files[0]), output_file)
+else:
+  print('emcc.py did not appear to output any known files!')
+  sys.exit(1)
+
+sys.exit(0)
diff --git a/emscripten_toolchain/wasm_binary.py b/emscripten_toolchain/wasm_binary.py
new file mode 100644
index 0000000..641c0d6
--- /dev/null
+++ b/emscripten_toolchain/wasm_binary.py
@@ -0,0 +1,84 @@
+"""Unpackages a bazel emscripten archive for use in a bazel BUILD rule.
+
+This script will take a tar archive containing the output of the emscripten
+toolchain. This file contains any output files produced by a wasm_cc_binary or a
+cc_binary built with --config=wasm. The files are extracted into the given
+output path.
+
+The name of archive is expected to be of the format `foo` or `foo.XXX` and
+the contents are expected to be foo.js and foo.wasm.
+
+Several optional files may also be in the archive, including but not limited to
+foo.js.mem, pthread-main.js, and foo.wasm.map.
+
+If the file is not a tar archive, the passed file will simply be copied to its
+destination.
+
+This script and its accompanying Bazel rule should allow you to extract a
+WebAssembly binary into a larger web application.
+"""
+
+import argparse
+import os
+import subprocess
+import sys
+
+
+def ensure(f):
+  if not os.path.exists(f):
+    with open(f, 'w'):
+      pass
+
+
+def check(f):
+  if not os.path.exists(f):
+    raise Exception('Expected file in archive: %s' % f)
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--archive', help='The archive to extract from.')
+  parser.add_argument('--output_path', help='The path to extract into.')
+  args = parser.parse_args()
+
+  basename = os.path.basename(args.archive)
+  stem = basename.split('.')[0]
+
+  # Check the type of the input file
+  mimetype_bytes = subprocess.check_output(['file', '-Lb', '--mime-type', '--mime-encoding', args.archive])
+  mimetype = mimetype_bytes.decode(sys.stdout.encoding)
+
+  # If we have a tar, extract all files. If we have just a single file, copy it.
+  if 'tar' in mimetype:
+    subprocess.check_call(
+        ['tar', 'xf', args.archive, '-C', args.output_path])
+  elif 'binary' in mimetype:
+    subprocess.check_call([
+        'cp',
+        args.archive,
+        os.path.join(args.output_path, stem + '.wasm')])
+  elif 'text' in mimetype:
+    subprocess.check_call([
+        'cp',
+        args.archive,
+        os.path.join(args.output_path, stem + '.js')])
+  else:
+    subprocess.check_call(['cp', args.archive, args.output_path])
+
+  # At least one of these two files should exist at this point.
+  ensure(os.path.join(args.output_path, stem + '.js'))
+  ensure(os.path.join(args.output_path, stem + '.wasm'))
+
+  # And can optionally contain these extra files.
+  ensure(os.path.join(args.output_path, stem + '.wasm.map'))
+  ensure(os.path.join(args.output_path, stem + '.worker.js'))
+  ensure(os.path.join(args.output_path, stem + '.js.mem'))
+  ensure(os.path.join(args.output_path, stem + '.data'))
+  ensure(os.path.join(args.output_path, stem + '.fetch.js'))
+  ensure(os.path.join(args.output_path, stem + '.js.symbols'))
+  ensure(os.path.join(args.output_path, stem + '.wasm.debug.wasm'))
+  ensure(os.path.join(args.output_path, stem + '.html'))
+
+
+if __name__ == '__main__':
+  main()
diff --git a/emscripten_toolchain/wasm_cc_binary.bzl b/emscripten_toolchain/wasm_cc_binary.bzl
new file mode 100644
index 0000000..de758b0
--- /dev/null
+++ b/emscripten_toolchain/wasm_cc_binary.bzl
@@ -0,0 +1,152 @@
+"""wasm_cc_binary rule for compiling C++ targets to WebAssembly.
+"""
+
+def _wasm_transition_impl(settings, attr):
+    _ignore = (settings, attr)
+
+    features = list(settings["//command_line_option:features"])
+    linkopts = list(settings["//command_line_option:linkopt"])
+
+    if attr.threads == "emscripten":
+        # threads enabled
+        features.append("use_pthreads")
+    elif attr.threads == "off":
+        # threads disabled
+        features.append("-use_pthreads")
+
+    if attr.exit_runtime == True:
+        features.append("exit_runtime")
+
+    if attr.backend == "llvm":
+        features.append("llvm_backend")
+    elif attr.backend == "emscripten":
+        features.append("-llvm_backend")
+
+    if attr.simd:
+        features.append("wasm_simd")
+
+    return {
+        "//command_line_option:compiler": "emscripten",
+        "//command_line_option:crosstool_top": "//emscripten_toolchain:everything",
+        "//command_line_option:cpu": "wasm",
+        "//command_line_option:features": features,
+        "//command_line_option:dynamic_mode": "off",
+        "//command_line_option:linkopt": linkopts,
+#        "//command_line_option:platforms": [],
+        "//command_line_option:custom_malloc": "//emscripten_toolchain:malloc",
+    }
+
+_wasm_transition = transition(
+    implementation = _wasm_transition_impl,
+    inputs = [
+        "//command_line_option:features",
+        "//command_line_option:linkopt",
+    ],
+    outputs = [
+        "//command_line_option:compiler",
+        "//command_line_option:cpu",
+        "//command_line_option:crosstool_top",
+        "//command_line_option:features",
+        "//command_line_option:dynamic_mode",
+        "//command_line_option:linkopt",
+#        "//command_line_option:platforms",
+        "//command_line_option:custom_malloc",
+    ],
+)
+
+def _wasm_binary_impl(ctx):
+    cc_target = ctx.attr.cc_target[0]
+
+    args = [
+        "--output_path={}".format(ctx.outputs.loader.dirname),
+    ] + [
+        ctx.expand_location("--archive=$(location {})".format(
+            cc_target.label,
+        ), [cc_target]),
+    ]
+    outputs = [
+        ctx.outputs.loader,
+        ctx.outputs.wasm,
+        ctx.outputs.map,
+        ctx.outputs.mem,
+        ctx.outputs.fetch,
+        ctx.outputs.worker,
+        ctx.outputs.data,
+        ctx.outputs.symbols,
+        ctx.outputs.dwarf,
+        ctx.outputs.html,
+    ]
+
+    ctx.actions.run(
+        inputs = ctx.files.cc_target,
+        outputs = outputs,
+        arguments = args,
+        executable = ctx.executable._wasm_binary_extractor,
+    )
+
+    return DefaultInfo(
+        files = depset(outputs),
+        # This is needed since rules like web_test usually have a data
+        # dependency on this target.
+        data_runfiles = ctx.runfiles(transitive_files = depset(outputs)),
+    )
+
+def _wasm_binary_outputs(name, cc_target):
+    basename = cc_target.name
+    basename = basename.split(".")[0]
+    outputs = {
+        "loader": "{}/{}.js".format(name, basename),
+        "wasm": "{}/{}.wasm".format(name, basename),
+        "map": "{}/{}.wasm.map".format(name, basename),
+        "mem": "{}/{}.js.mem".format(name, basename),
+        "fetch": "{}/{}.fetch.js".format(name, basename),
+        "worker": "{}/{}.worker.js".format(name, basename),
+        "data": "{}/{}.data".format(name, basename),
+        "symbols": "{}/{}.js.symbols".format(name, basename),
+        "dwarf": "{}/{}.wasm.debug.wasm".format(name, basename),
+        "html": "{}/{}.html".format(name, basename),
+    }
+
+    return outputs
+
+# Wraps a C++ Blaze target, extracting the appropriate files.
+#
+# This rule will transition to the emscripten toolchain in order
+# to build the the cc_target as a WebAssembly binary.
+#
+# Args:
+#   name: The name of the rule.
+#   cc_target: The cc_binary or cc_library to extract files from.
+wasm_cc_binary = rule(
+    implementation = _wasm_binary_impl,
+    attrs = {
+        "backend": attr.string(
+            default = "_default",
+            values = ["_default", "emscripten", "llvm"],
+        ),
+        "cc_target": attr.label(
+            cfg = _wasm_transition,
+            mandatory = True,
+        ),
+        "exit_runtime": attr.bool(
+            default = False,
+        ),
+        "threads": attr.string(
+            default = "_default",
+            values = ["_default", "emscripten", "off"],
+        ),
+        "simd": attr.bool(
+            default = False,
+        ),
+        "_allowlist_function_transition": attr.label(
+            default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+        ),
+        "_wasm_binary_extractor": attr.label(
+            executable = True,
+            allow_files = True,
+            cfg = "exec",
+            default = Label("//emscripten_toolchain:wasm_binary"),
+        ),
+    },
+    outputs = _wasm_binary_outputs,
+)
diff --git a/emscripten_toolchain/wasm_rules.bzl b/emscripten_toolchain/wasm_rules.bzl
new file mode 100644
index 0000000..1c1c409
--- /dev/null
+++ b/emscripten_toolchain/wasm_rules.bzl
@@ -0,0 +1,6 @@
+"""Rules related to C++ and WebAssembly.
+"""
+
+load("//emscripten_toolchain:wasm_cc_binary.bzl", _wasm_cc_binary = "wasm_cc_binary")
+
+wasm_cc_binary = _wasm_cc_binary
diff --git a/libedgetpu b/libedgetpu
new file mode 160000
index 0000000..ed7b3bf
--- /dev/null
+++ b/libedgetpu
@@ -0,0 +1 @@
+Subproject commit ed7b3bff11555e7126c4cacd19a2cd60f3c4567f
diff --git a/site/dfu.js b/site/dfu.js
new file mode 120000
index 0000000..bfebfec
--- /dev/null
+++ b/site/dfu.js
@@ -0,0 +1 @@
+../webdfu/dfu-util/dfu.js
\ No newline at end of file
diff --git a/site/dfuse.js b/site/dfuse.js
new file mode 120000
index 0000000..90f3af9
--- /dev/null
+++ b/site/dfuse.js
@@ -0,0 +1 @@
+../webdfu/dfu-util/dfuse.js
\ No newline at end of file
diff --git a/site/firmware.bin b/site/firmware.bin
new file mode 120000
index 0000000..fe4459a
--- /dev/null
+++ b/site/firmware.bin
@@ -0,0 +1 @@
+../libedgetpu/driver/usb/apex_latest_single_ep.bin
\ No newline at end of file
diff --git a/site/imagenet_labels.js b/site/imagenet_labels.js
new file mode 100644
index 0000000..351d8e2
--- /dev/null
+++ b/site/imagenet_labels.js
@@ -0,0 +1,1003 @@
+let imagenet_labels = [
+  "background",
+  "tench, Tinca tinca",
+  "goldfish, Carassius auratus",
+  "great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias",
+  "tiger shark, Galeocerdo cuvieri",
+  "hammerhead, hammerhead shark",
+  "electric ray, crampfish, numbfish, torpedo",
+  "stingray",
+  "cock",
+  "hen",
+  "ostrich, Struthio camelus",
+  "brambling, Fringilla montifringilla",
+  "goldfinch, Carduelis carduelis",
+  "house finch, linnet, Carpodacus mexicanus",
+  "junco, snowbird",
+  "indigo bunting, indigo finch, indigo bird, Passerina cyanea",
+  "robin, American robin, Turdus migratorius",
+  "bulbul",
+  "jay",
+  "magpie",
+  "chickadee",
+  "water ouzel, dipper",
+  "kite",
+  "bald eagle, American eagle, Haliaeetus leucocephalus",
+  "vulture",
+  "great grey owl, great gray owl, Strix nebulosa",
+  "European fire salamander, Salamandra salamandra",
+  "common newt, Triturus vulgaris",
+  "eft",
+  "spotted salamander, Ambystoma maculatum",
+  "axolotl, mud puppy, Ambystoma mexicanum",
+  "bullfrog, Rana catesbeiana",
+  "tree frog, tree-frog",
+  "tailed frog, bell toad, ribbed toad, tailed toad, Ascaphus trui",
+  "loggerhead, loggerhead turtle, Caretta caretta",
+  "leatherback turtle, leatherback, leathery turtle, Dermochelys coriacea",
+  "mud turtle",
+  "terrapin",
+  "box turtle, box tortoise",
+  "banded gecko",
+  "common iguana, iguana, Iguana iguana",
+  "American chameleon, anole, Anolis carolinensis",
+  "whiptail, whiptail lizard",
+  "agama",
+  "frilled lizard, Chlamydosaurus kingi",
+  "alligator lizard",
+  "Gila monster, Heloderma suspectum",
+  "green lizard, Lacerta viridis",
+  "African chameleon, Chamaeleo chamaeleon",
+  "Komodo dragon, Komodo lizard, dragon lizard, giant lizard, Varanus komodoensis",
+  "African crocodile, Nile crocodile, Crocodylus niloticus",
+  "American alligator, Alligator mississipiensis",
+  "triceratops",
+  "thunder snake, worm snake, Carphophis amoenus",
+  "ringneck snake, ring-necked snake, ring snake",
+  "hognose snake, puff adder, sand viper",
+  "green snake, grass snake",
+  "king snake, kingsnake",
+  "garter snake, grass snake",
+  "water snake",
+  "vine snake",
+  "night snake, Hypsiglena torquata",
+  "boa constrictor, Constrictor constrictor",
+  "rock python, rock snake, Python sebae",
+  "Indian cobra, Naja naja",
+  "green mamba",
+  "sea snake",
+  "horned viper, cerastes, sand viper, horned asp, Cerastes cornutus",
+  "diamondback, diamondback rattlesnake, Crotalus adamanteus",
+  "sidewinder, horned rattlesnake, Crotalus cerastes",
+  "trilobite",
+  "harvestman, daddy longlegs, Phalangium opilio",
+  "scorpion",
+  "black and gold garden spider, Argiope aurantia",
+  "barn spider, Araneus cavaticus",
+  "garden spider, Aranea diademata",
+  "black widow, Latrodectus mactans",
+  "tarantula",
+  "wolf spider, hunting spider",
+  "tick",
+  "centipede",
+  "black grouse",
+  "ptarmigan",
+  "ruffed grouse, partridge, Bonasa umbellus",
+  "prairie chicken, prairie grouse, prairie fowl",
+  "peacock",
+  "quail",
+  "partridge",
+  "African grey, African gray, Psittacus erithacus",
+  "macaw",
+  "sulphur-crested cockatoo, Kakatoe galerita, Cacatua galerita",
+  "lorikeet",
+  "coucal",
+  "bee eater",
+  "hornbill",
+  "hummingbird",
+  "jacamar",
+  "toucan",
+  "drake",
+  "red-breasted merganser, Mergus serrator",
+  "goose",
+  "black swan, Cygnus atratus",
+  "tusker",
+  "echidna, spiny anteater, anteater",
+  "platypus, duckbill, duckbilled platypus, duck-billed platypus, Ornithorhynchus anatinus",
+  "wallaby, brush kangaroo",
+  "koala, koala bear, kangaroo bear, native bear, Phascolarctos cinereus",
+  "wombat",
+  "jellyfish",
+  "sea anemone, anemone",
+  "brain coral",
+  "flatworm, platyhelminth",
+  "nematode, nematode worm, roundworm",
+  "conch",
+  "snail",
+  "slug",
+  "sea slug, nudibranch",
+  "chiton, coat-of-mail shell, sea cradle, polyplacophore",
+  "chambered nautilus, pearly nautilus, nautilus",
+  "Dungeness crab, Cancer magister",
+  "rock crab, Cancer irroratus",
+  "fiddler crab",
+  "king crab, Alaska crab, Alaskan king crab, Alaska king crab, Paralithodes camtschatica",
+  "American lobster, Northern lobster, Maine lobster, Homarus americanus",
+  "spiny lobster, langouste, rock lobster, crawfish, crayfish, sea crawfish",
+  "crayfish, crawfish, crawdad, crawdaddy",
+  "hermit crab",
+  "isopod",
+  "white stork, Ciconia ciconia",
+  "black stork, Ciconia nigra",
+  "spoonbill",
+  "flamingo",
+  "little blue heron, Egretta caerulea",
+  "American egret, great white heron, Egretta albus",
+  "bittern",
+  "crane",
+  "limpkin, Aramus pictus",
+  "European gallinule, Porphyrio porphyrio",
+  "American coot, marsh hen, mud hen, water hen, Fulica americana",
+  "bustard",
+  "ruddy turnstone, Arenaria interpres",
+  "red-backed sandpiper, dunlin, Erolia alpina",
+  "redshank, Tringa totanus",
+  "dowitcher",
+  "oystercatcher, oyster catcher",
+  "pelican",
+  "king penguin, Aptenodytes patagonica",
+  "albatross, mollymawk",
+  "grey whale, gray whale, devilfish, Eschrichtius gibbosus, Eschrichtius robustus",
+  "killer whale, killer, orca, grampus, sea wolf, Orcinus orca",
+  "dugong, Dugong dugon",
+  "sea lion",
+  "Chihuahua",
+  "Japanese spaniel",
+  "Maltese dog, Maltese terrier, Maltese",
+  "Pekinese, Pekingese, Peke",
+  "Shih-Tzu",
+  "Blenheim spaniel",
+  "papillon",
+  "toy terrier",
+  "Rhodesian ridgeback",
+  "Afghan hound, Afghan",
+  "basset, basset hound",
+  "beagle",
+  "bloodhound, sleuthhound",
+  "bluetick",
+  "black-and-tan coonhound",
+  "Walker hound, Walker foxhound",
+  "English foxhound",
+  "redbone",
+  "borzoi, Russian wolfhound",
+  "Irish wolfhound",
+  "Italian greyhound",
+  "whippet",
+  "Ibizan hound, Ibizan Podenco",
+  "Norwegian elkhound, elkhound",
+  "otterhound, otter hound",
+  "Saluki, gazelle hound",
+  "Scottish deerhound, deerhound",
+  "Weimaraner",
+  "Staffordshire bullterrier, Staffordshire bull terrier",
+  "American Staffordshire terrier, Staffordshire terrier, American pit bull terrier, pit bull terrier",
+  "Bedlington terrier",
+  "Border terrier",
+  "Kerry blue terrier",
+  "Irish terrier",
+  "Norfolk terrier",
+  "Norwich terrier",
+  "Yorkshire terrier",
+  "wire-haired fox terrier",
+  "Lakeland terrier",
+  "Sealyham terrier, Sealyham",
+  "Airedale, Airedale terrier",
+  "cairn, cairn terrier",
+  "Australian terrier",
+  "Dandie Dinmont, Dandie Dinmont terrier",
+  "Boston bull, Boston terrier",
+  "miniature schnauzer",
+  "giant schnauzer",
+  "standard schnauzer",
+  "Scotch terrier, Scottish terrier, Scottie",
+  "Tibetan terrier, chrysanthemum dog",
+  "silky terrier, Sydney silky",
+  "soft-coated wheaten terrier",
+  "West Highland white terrier",
+  "Lhasa, Lhasa apso",
+  "flat-coated retriever",
+  "curly-coated retriever",
+  "golden retriever",
+  "Labrador retriever",
+  "Chesapeake Bay retriever",
+  "German short-haired pointer",
+  "vizsla, Hungarian pointer",
+  "English setter",
+  "Irish setter, red setter",
+  "Gordon setter",
+  "Brittany spaniel",
+  "clumber, clumber spaniel",
+  "English springer, English springer spaniel",
+  "Welsh springer spaniel",
+  "cocker spaniel, English cocker spaniel, cocker",
+  "Sussex spaniel",
+  "Irish water spaniel",
+  "kuvasz",
+  "schipperke",
+  "groenendael",
+  "malinois",
+  "briard",
+  "kelpie",
+  "komondor",
+  "Old English sheepdog, bobtail",
+  "Shetland sheepdog, Shetland sheep dog, Shetland",
+  "collie",
+  "Border collie",
+  "Bouvier des Flandres, Bouviers des Flandres",
+  "Rottweiler",
+  "German shepherd, German shepherd dog, German police dog, alsatian",
+  "Doberman, Doberman pinscher",
+  "miniature pinscher",
+  "Greater Swiss Mountain dog",
+  "Bernese mountain dog",
+  "Appenzeller",
+  "EntleBucher",
+  "boxer",
+  "bull mastiff",
+  "Tibetan mastiff",
+  "French bulldog",
+  "Great Dane",
+  "Saint Bernard, St Bernard",
+  "Eskimo dog, husky",
+  "malamute, malemute, Alaskan malamute",
+  "Siberian husky",
+  "dalmatian, coach dog, carriage dog",
+  "affenpinscher, monkey pinscher, monkey dog",
+  "basenji",
+  "pug, pug-dog",
+  "Leonberg",
+  "Newfoundland, Newfoundland dog",
+  "Great Pyrenees",
+  "Samoyed, Samoyede",
+  "Pomeranian",
+  "chow, chow chow",
+  "keeshond",
+  "Brabancon griffon",
+  "Pembroke, Pembroke Welsh corgi",
+  "Cardigan, Cardigan Welsh corgi",
+  "toy poodle",
+  "miniature poodle",
+  "standard poodle",
+  "Mexican hairless",
+  "timber wolf, grey wolf, gray wolf, Canis lupus",
+  "white wolf, Arctic wolf, Canis lupus tundrarum",
+  "red wolf, maned wolf, Canis rufus, Canis niger",
+  "coyote, prairie wolf, brush wolf, Canis latrans",
+  "dingo, warrigal, warragal, Canis dingo",
+  "dhole, Cuon alpinus",
+  "African hunting dog, hyena dog, Cape hunting dog, Lycaon pictus",
+  "hyena, hyaena",
+  "red fox, Vulpes vulpes",
+  "kit fox, Vulpes macrotis",
+  "Arctic fox, white fox, Alopex lagopus",
+  "grey fox, gray fox, Urocyon cinereoargenteus",
+  "tabby, tabby cat",
+  "tiger cat",
+  "Persian cat",
+  "Siamese cat, Siamese",
+  "Egyptian cat",
+  "cougar, puma, catamount, mountain lion, painter, panther, Felis concolor",
+  "lynx, catamount",
+  "leopard, Panthera pardus",
+  "snow leopard, ounce, Panthera uncia",
+  "jaguar, panther, Panthera onca, Felis onca",
+  "lion, king of beasts, Panthera leo",
+  "tiger, Panthera tigris",
+  "cheetah, chetah, Acinonyx jubatus",
+  "brown bear, bruin, Ursus arctos",
+  "American black bear, black bear, Ursus americanus, Euarctos americanus",
+  "ice bear, polar bear, Ursus Maritimus, Thalarctos maritimus",
+  "sloth bear, Melursus ursinus, Ursus ursinus",
+  "mongoose",
+  "meerkat, mierkat",
+  "tiger beetle",
+  "ladybug, ladybeetle, lady beetle, ladybird, ladybird beetle",
+  "ground beetle, carabid beetle",
+  "long-horned beetle, longicorn, longicorn beetle",
+  "leaf beetle, chrysomelid",
+  "dung beetle",
+  "rhinoceros beetle",
+  "weevil",
+  "fly",
+  "bee",
+  "ant, emmet, pismire",
+  "grasshopper, hopper",
+  "cricket",
+  "walking stick, walkingstick, stick insect",
+  "cockroach, roach",
+  "mantis, mantid",
+  "cicada, cicala",
+  "leafhopper",
+  "lacewing, lacewing fly",
+  "dragonfly, darning needle, devil's darning needle, sewing needle, snake feeder, snake doctor, mosquito hawk, skeeter hawk",
+  "damselfly",
+  "admiral",
+  "ringlet, ringlet butterfly",
+  "monarch, monarch butterfly, milkweed butterfly, Danaus plexippus",
+  "cabbage butterfly",
+  "sulphur butterfly, sulfur butterfly",
+  "lycaenid, lycaenid butterfly",
+  "starfish, sea star",
+  "sea urchin",
+  "sea cucumber, holothurian",
+  "wood rabbit, cottontail, cottontail rabbit",
+  "hare",
+  "Angora, Angora rabbit",
+  "hamster",
+  "porcupine, hedgehog",
+  "fox squirrel, eastern fox squirrel, Sciurus niger",
+  "marmot",
+  "beaver",
+  "guinea pig, Cavia cobaya",
+  "sorrel",
+  "zebra",
+  "hog, pig, grunter, squealer, Sus scrofa",
+  "wild boar, boar, Sus scrofa",
+  "warthog",
+  "hippopotamus, hippo, river horse, Hippopotamus amphibius",
+  "ox",
+  "water buffalo, water ox, Asiatic buffalo, Bubalus bubalis",
+  "bison",
+  "ram, tup",
+  "bighorn, bighorn sheep, cimarron, Rocky Mountain bighorn, Rocky Mountain sheep, Ovis canadensis",
+  "ibex, Capra ibex",
+  "hartebeest",
+  "impala, Aepyceros melampus",
+  "gazelle",
+  "Arabian camel, dromedary, Camelus dromedarius",
+  "llama",
+  "weasel",
+  "mink",
+  "polecat, fitch, foulmart, foumart, Mustela putorius",
+  "black-footed ferret, ferret, Mustela nigripes",
+  "otter",
+  "skunk, polecat, wood pussy",
+  "badger",
+  "armadillo",
+  "three-toed sloth, ai, Bradypus tridactylus",
+  "orangutan, orang, orangutang, Pongo pygmaeus",
+  "gorilla, Gorilla gorilla",
+  "chimpanzee, chimp, Pan troglodytes",
+  "gibbon, Hylobates lar",
+  "siamang, Hylobates syndactylus, Symphalangus syndactylus",
+  "guenon, guenon monkey",
+  "patas, hussar monkey, Erythrocebus patas",
+  "baboon",
+  "macaque",
+  "langur",
+  "colobus, colobus monkey",
+  "proboscis monkey, Nasalis larvatus",
+  "marmoset",
+  "capuchin, ringtail, Cebus capucinus",
+  "howler monkey, howler",
+  "titi, titi monkey",
+  "spider monkey, Ateles geoffroyi",
+  "squirrel monkey, Saimiri sciureus",
+  "Madagascar cat, ring-tailed lemur, Lemur catta",
+  "indri, indris, Indri indri, Indri brevicaudatus",
+  "Indian elephant, Elephas maximus",
+  "African elephant, Loxodonta africana",
+  "lesser panda, red panda, panda, bear cat, cat bear, Ailurus fulgens",
+  "giant panda, panda, panda bear, coon bear, Ailuropoda melanoleuca",
+  "barracouta, snoek",
+  "eel",
+  "coho, cohoe, coho salmon, blue jack, silver salmon, Oncorhynchus kisutch",
+  "rock beauty, Holocanthus tricolor",
+  "anemone fish",
+  "sturgeon",
+  "gar, garfish, garpike, billfish, Lepisosteus osseus",
+  "lionfish",
+  "puffer, pufferfish, blowfish, globefish",
+  "abacus",
+  "abaya",
+  "academic gown, academic robe, judge's robe",
+  "accordion, piano accordion, squeeze box",
+  "acoustic guitar",
+  "aircraft carrier, carrier, flattop, attack aircraft carrier",
+  "airliner",
+  "airship, dirigible",
+  "altar",
+  "ambulance",
+  "amphibian, amphibious vehicle",
+  "analog clock",
+  "apiary, bee house",
+  "apron",
+  "ashcan, trash can, garbage can, wastebin, ash bin, ash-bin, ashbin, dustbin, trash barrel, trash bin",
+  "assault rifle, assault gun",
+  "backpack, back pack, knapsack, packsack, rucksack, haversack",
+  "bakery, bakeshop, bakehouse",
+  "balance beam, beam",
+  "balloon",
+  "ballpoint, ballpoint pen, ballpen, Biro",
+  "Band Aid",
+  "banjo",
+  "bannister, banister, balustrade, balusters, handrail",
+  "barbell",
+  "barber chair",
+  "barbershop",
+  "barn",
+  "barometer",
+  "barrel, cask",
+  "barrow, garden cart, lawn cart, wheelbarrow",
+  "baseball",
+  "basketball",
+  "bassinet",
+  "bassoon",
+  "bathing cap, swimming cap",
+  "bath towel",
+  "bathtub, bathing tub, bath, tub",
+  "beach wagon, station wagon, wagon, estate car, beach waggon, station waggon, waggon",
+  "beacon, lighthouse, beacon light, pharos",
+  "beaker",
+  "bearskin, busby, shako",
+  "beer bottle",
+  "beer glass",
+  "bell cote, bell cot",
+  "bib",
+  "bicycle-built-for-two, tandem bicycle, tandem",
+  "bikini, two-piece",
+  "binder, ring-binder",
+  "binoculars, field glasses, opera glasses",
+  "birdhouse",
+  "boathouse",
+  "bobsled, bobsleigh, bob",
+  "bolo tie, bolo, bola tie, bola",
+  "bonnet, poke bonnet",
+  "bookcase",
+  "bookshop, bookstore, bookstall",
+  "bottlecap",
+  "bow",
+  "bow tie, bow-tie, bowtie",
+  "brass, memorial tablet, plaque",
+  "brassiere, bra, bandeau",
+  "breakwater, groin, groyne, mole, bulwark, seawall, jetty",
+  "breastplate, aegis, egis",
+  "broom",
+  "bucket, pail",
+  "buckle",
+  "bulletproof vest",
+  "bullet train, bullet",
+  "butcher shop, meat market",
+  "cab, hack, taxi, taxicab",
+  "caldron, cauldron",
+  "candle, taper, wax light",
+  "cannon",
+  "canoe",
+  "can opener, tin opener",
+  "cardigan",
+  "car mirror",
+  "carousel, carrousel, merry-go-round, roundabout, whirligig",
+  "carpenter's kit, tool kit",
+  "carton",
+  "car wheel",
+  "cash machine, cash dispenser, automated teller machine, automatic teller machine, automated teller, automatic teller, ATM",
+  "cassette",
+  "cassette player",
+  "castle",
+  "catamaran",
+  "CD player",
+  "cello, violoncello",
+  "cellular telephone, cellular phone, cellphone, cell, mobile phone",
+  "chain",
+  "chainlink fence",
+  "chain mail, ring mail, mail, chain armor, chain armour, ring armor, ring armour",
+  "chain saw, chainsaw",
+  "chest",
+  "chiffonier, commode",
+  "chime, bell, gong",
+  "china cabinet, china closet",
+  "Christmas stocking",
+  "church, church building",
+  "cinema, movie theater, movie theatre, movie house, picture palace",
+  "cleaver, meat cleaver, chopper",
+  "cliff dwelling",
+  "cloak",
+  "clog, geta, patten, sabot",
+  "cocktail shaker",
+  "coffee mug",
+  "coffeepot",
+  "coil, spiral, volute, whorl, helix",
+  "combination lock",
+  "computer keyboard, keypad",
+  "confectionery, confectionary, candy store",
+  "container ship, containership, container vessel",
+  "convertible",
+  "corkscrew, bottle screw",
+  "cornet, horn, trumpet, trump",
+  "cowboy boot",
+  "cowboy hat, ten-gallon hat",
+  "cradle",
+  "crane",
+  "crash helmet",
+  "crate",
+  "crib, cot",
+  "Crock Pot",
+  "croquet ball",
+  "crutch",
+  "cuirass",
+  "dam, dike, dyke",
+  "desk",
+  "desktop computer",
+  "dial telephone, dial phone",
+  "diaper, nappy, napkin",
+  "digital clock",
+  "digital watch",
+  "dining table, board",
+  "dishrag, dishcloth",
+  "dishwasher, dish washer, dishwashing machine",
+  "disk brake, disc brake",
+  "dock, dockage, docking facility",
+  "dogsled, dog sled, dog sleigh",
+  "dome",
+  "doormat, welcome mat",
+  "drilling platform, offshore rig",
+  "drum, membranophone, tympan",
+  "drumstick",
+  "dumbbell",
+  "Dutch oven",
+  "electric fan, blower",
+  "electric guitar",
+  "electric locomotive",
+  "entertainment center",
+  "envelope",
+  "espresso maker",
+  "face powder",
+  "feather boa, boa",
+  "file, file cabinet, filing cabinet",
+  "fireboat",
+  "fire engine, fire truck",
+  "fire screen, fireguard",
+  "flagpole, flagstaff",
+  "flute, transverse flute",
+  "folding chair",
+  "football helmet",
+  "forklift",
+  "fountain",
+  "fountain pen",
+  "four-poster",
+  "freight car",
+  "French horn, horn",
+  "frying pan, frypan, skillet",
+  "fur coat",
+  "garbage truck, dustcart",
+  "gasmask, respirator, gas helmet",
+  "gas pump, gasoline pump, petrol pump, island dispenser",
+  "goblet",
+  "go-kart",
+  "golf ball",
+  "golfcart, golf cart",
+  "gondola",
+  "gong, tam-tam",
+  "gown",
+  "grand piano, grand",
+  "greenhouse, nursery, glasshouse",
+  "grille, radiator grille",
+  "grocery store, grocery, food market, market",
+  "guillotine",
+  "hair slide",
+  "hair spray",
+  "half track",
+  "hammer",
+  "hamper",
+  "hand blower, blow dryer, blow drier, hair dryer, hair drier",
+  "hand-held computer, hand-held microcomputer",
+  "handkerchief, hankie, hanky, hankey",
+  "hard disc, hard disk, fixed disk",
+  "harmonica, mouth organ, harp, mouth harp",
+  "harp",
+  "harvester, reaper",
+  "hatchet",
+  "holster",
+  "home theater, home theatre",
+  "honeycomb",
+  "hook, claw",
+  "hoopskirt, crinoline",
+  "horizontal bar, high bar",
+  "horse cart, horse-cart",
+  "hourglass",
+  "iPod",
+  "iron, smoothing iron",
+  "jack-o'-lantern",
+  "jean, blue jean, denim",
+  "jeep, landrover",
+  "jersey, T-shirt, tee shirt",
+  "jigsaw puzzle",
+  "jinrikisha, ricksha, rickshaw",
+  "joystick",
+  "kimono",
+  "knee pad",
+  "knot",
+  "lab coat, laboratory coat",
+  "ladle",
+  "lampshade, lamp shade",
+  "laptop, laptop computer",
+  "lawn mower, mower",
+  "lens cap, lens cover",
+  "letter opener, paper knife, paperknife",
+  "library",
+  "lifeboat",
+  "lighter, light, igniter, ignitor",
+  "limousine, limo",
+  "liner, ocean liner",
+  "lipstick, lip rouge",
+  "Loafer",
+  "lotion",
+  "loudspeaker, speaker, speaker unit, loudspeaker system, speaker system",
+  "loupe, jeweler's loupe",
+  "lumbermill, sawmill",
+  "magnetic compass",
+  "mailbag, postbag",
+  "mailbox, letter box",
+  "maillot",
+  "maillot, tank suit",
+  "manhole cover",
+  "maraca",
+  "marimba, xylophone",
+  "mask",
+  "matchstick",
+  "maypole",
+  "maze, labyrinth",
+  "measuring cup",
+  "medicine chest, medicine cabinet",
+  "megalith, megalithic structure",
+  "microphone, mike",
+  "microwave, microwave oven",
+  "military uniform",
+  "milk can",
+  "minibus",
+  "miniskirt, mini",
+  "minivan",
+  "missile",
+  "mitten",
+  "mixing bowl",
+  "mobile home, manufactured home",
+  "Model T",
+  "modem",
+  "monastery",
+  "monitor",
+  "moped",
+  "mortar",
+  "mortarboard",
+  "mosque",
+  "mosquito net",
+  "motor scooter, scooter",
+  "mountain bike, all-terrain bike, off-roader",
+  "mountain tent",
+  "mouse, computer mouse",
+  "mousetrap",
+  "moving van",
+  "muzzle",
+  "nail",
+  "neck brace",
+  "necklace",
+  "nipple",
+  "notebook, notebook computer",
+  "obelisk",
+  "oboe, hautboy, hautbois",
+  "ocarina, sweet potato",
+  "odometer, hodometer, mileometer, milometer",
+  "oil filter",
+  "organ, pipe organ",
+  "oscilloscope, scope, cathode-ray oscilloscope, CRO",
+  "overskirt",
+  "oxcart",
+  "oxygen mask",
+  "packet",
+  "paddle, boat paddle",
+  "paddlewheel, paddle wheel",
+  "padlock",
+  "paintbrush",
+  "pajama, pyjama, pj's, jammies",
+  "palace",
+  "panpipe, pandean pipe, syrinx",
+  "paper towel",
+  "parachute, chute",
+  "parallel bars, bars",
+  "park bench",
+  "parking meter",
+  "passenger car, coach, carriage",
+  "patio, terrace",
+  "pay-phone, pay-station",
+  "pedestal, plinth, footstall",
+  "pencil box, pencil case",
+  "pencil sharpener",
+  "perfume, essence",
+  "Petri dish",
+  "photocopier",
+  "pick, plectrum, plectron",
+  "pickelhaube",
+  "picket fence, paling",
+  "pickup, pickup truck",
+  "pier",
+  "piggy bank, penny bank",
+  "pill bottle",
+  "pillow",
+  "ping-pong ball",
+  "pinwheel",
+  "pirate, pirate ship",
+  "pitcher, ewer",
+  "plane, carpenter's plane, woodworking plane",
+  "planetarium",
+  "plastic bag",
+  "plate rack",
+  "plow, plough",
+  "plunger, plumber's helper",
+  "Polaroid camera, Polaroid Land camera",
+  "pole",
+  "police van, police wagon, paddy wagon, patrol wagon, wagon, black Maria",
+  "poncho",
+  "pool table, billiard table, snooker table",
+  "pop bottle, soda bottle",
+  "pot, flowerpot",
+  "potter's wheel",
+  "power drill",
+  "prayer rug, prayer mat",
+  "printer",
+  "prison, prison house",
+  "projectile, missile",
+  "projector",
+  "puck, hockey puck",
+  "punching bag, punch bag, punching ball, punchball",
+  "purse",
+  "quill, quill pen",
+  "quilt, comforter, comfort, puff",
+  "racer, race car, racing car",
+  "racket, racquet",
+  "radiator",
+  "radio, wireless",
+  "radio telescope, radio reflector",
+  "rain barrel",
+  "recreational vehicle, RV, R.V.",
+  "reel",
+  "reflex camera",
+  "refrigerator, icebox",
+  "remote control, remote",
+  "restaurant, eating house, eating place, eatery",
+  "revolver, six-gun, six-shooter",
+  "rifle",
+  "rocking chair, rocker",
+  "rotisserie",
+  "rubber eraser, rubber, pencil eraser",
+  "rugby ball",
+  "rule, ruler",
+  "running shoe",
+  "safe",
+  "safety pin",
+  "saltshaker, salt shaker",
+  "sandal",
+  "sarong",
+  "sax, saxophone",
+  "scabbard",
+  "scale, weighing machine",
+  "school bus",
+  "schooner",
+  "scoreboard",
+  "screen, CRT screen",
+  "screw",
+  "screwdriver",
+  "seat belt, seatbelt",
+  "sewing machine",
+  "shield, buckler",
+  "shoe shop, shoe-shop, shoe store",
+  "shoji",
+  "shopping basket",
+  "shopping cart",
+  "shovel",
+  "shower cap",
+  "shower curtain",
+  "ski",
+  "ski mask",
+  "sleeping bag",
+  "slide rule, slipstick",
+  "sliding door",
+  "slot, one-armed bandit",
+  "snorkel",
+  "snowmobile",
+  "snowplow, snowplough",
+  "soap dispenser",
+  "soccer ball",
+  "sock",
+  "solar dish, solar collector, solar furnace",
+  "sombrero",
+  "soup bowl",
+  "space bar",
+  "space heater",
+  "space shuttle",
+  "spatula",
+  "speedboat",
+  "spider web, spider's web",
+  "spindle",
+  "sports car, sport car",
+  "spotlight, spot",
+  "stage",
+  "steam locomotive",
+  "steel arch bridge",
+  "steel drum",
+  "stethoscope",
+  "stole",
+  "stone wall",
+  "stopwatch, stop watch",
+  "stove",
+  "strainer",
+  "streetcar, tram, tramcar, trolley, trolley car",
+  "stretcher",
+  "studio couch, day bed",
+  "stupa, tope",
+  "submarine, pigboat, sub, U-boat",
+  "suit, suit of clothes",
+  "sundial",
+  "sunglass",
+  "sunglasses, dark glasses, shades",
+  "sunscreen, sunblock, sun blocker",
+  "suspension bridge",
+  "swab, swob, mop",
+  "sweatshirt",
+  "swimming trunks, bathing trunks",
+  "swing",
+  "switch, electric switch, electrical switch",
+  "syringe",
+  "table lamp",
+  "tank, army tank, armored combat vehicle, armoured combat vehicle",
+  "tape player",
+  "teapot",
+  "teddy, teddy bear",
+  "television, television system",
+  "tennis ball",
+  "thatch, thatched roof",
+  "theater curtain, theatre curtain",
+  "thimble",
+  "thresher, thrasher, threshing machine",
+  "throne",
+  "tile roof",
+  "toaster",
+  "tobacco shop, tobacconist shop, tobacconist",
+  "toilet seat",
+  "torch",
+  "totem pole",
+  "tow truck, tow car, wrecker",
+  "toyshop",
+  "tractor",
+  "trailer truck, tractor trailer, trucking rig, rig, articulated lorry, semi",
+  "tray",
+  "trench coat",
+  "tricycle, trike, velocipede",
+  "trimaran",
+  "tripod",
+  "triumphal arch",
+  "trolleybus, trolley coach, trackless trolley",
+  "trombone",
+  "tub, vat",
+  "turnstile",
+  "typewriter keyboard",
+  "umbrella",
+  "unicycle, monocycle",
+  "upright, upright piano",
+  "vacuum, vacuum cleaner",
+  "vase",
+  "vault",
+  "velvet",
+  "vending machine",
+  "vestment",
+  "viaduct",
+  "violin, fiddle",
+  "volleyball",
+  "waffle iron",
+  "wall clock",
+  "wallet, billfold, notecase, pocketbook",
+  "wardrobe, closet, press",
+  "warplane, military plane",
+  "washbasin, handbasin, washbowl, lavabo, wash-hand basin",
+  "washer, automatic washer, washing machine",
+  "water bottle",
+  "water jug",
+  "water tower",
+  "whiskey jug",
+  "whistle",
+  "wig",
+  "window screen",
+  "window shade",
+  "Windsor tie",
+  "wine bottle",
+  "wing",
+  "wok",
+  "wooden spoon",
+  "wool, woolen, woollen",
+  "worm fence, snake fence, snake-rail fence, Virginia fence",
+  "wreck",
+  "yawl",
+  "yurt",
+  "web site, website, internet site, site",
+  "comic book",
+  "crossword puzzle, crossword",
+  "street sign",
+  "traffic light, traffic signal, stoplight",
+  "book jacket, dust cover, dust jacket, dust wrapper",
+  "menu",
+  "plate",
+  "guacamole",
+  "consomme",
+  "hot pot, hotpot",
+  "trifle",
+  "ice cream, icecream",
+  "ice lolly, lolly, lollipop, popsicle",
+  "French loaf",
+  "bagel, beigel",
+  "pretzel",
+  "cheeseburger",
+  "hotdog, hot dog, red hot",
+  "mashed potato",
+  "head cabbage",
+  "broccoli",
+  "cauliflower",
+  "zucchini, courgette",
+  "spaghetti squash",
+  "acorn squash",
+  "butternut squash",
+  "cucumber, cuke",
+  "artichoke, globe artichoke",
+  "bell pepper",
+  "cardoon",
+  "mushroom",
+  "Granny Smith",
+  "strawberry",
+  "orange",
+  "lemon",
+  "fig",
+  "pineapple, ananas",
+  "banana",
+  "jackfruit, jak, jack",
+  "custard apple",
+  "pomegranate",
+  "hay",
+  "carbonara",
+  "chocolate sauce, chocolate syrup",
+  "dough",
+  "meat loaf, meatloaf",
+  "pizza, pizza pie",
+  "potpie",
+  "burrito",
+  "red wine",
+  "espresso",
+  "cup",
+  "eggnog",
+  "alp",
+  "bubble",
+  "cliff, drop, drop-off",
+  "coral reef",
+  "geyser",
+  "lakeside, lakeshore",
+  "promontory, headland, head, foreland",
+  "sandbar, sand bar",
+  "seashore, coast, seacoast, sea-coast",
+  "valley, vale",
+  "volcano",
+  "ballplayer, baseball player",
+  "groom, bridegroom",
+  "scuba diver",
+  "rapeseed",
+  "daisy",
+  "yellow lady's slipper, yellow lady-slipper, Cypripedium calceolus, Cypripedium parviflorum",
+  "corn",
+  "acorn",
+  "hip, rose hip, rosehip",
+  "buckeye, horse chestnut, conker",
+  "coral fungus",
+  "agaric",
+  "gyromitra",
+  "stinkhorn, carrion fungus",
+  "earthstar",
+  "hen-of-the-woods, hen of the woods, Polyporus frondosus, Grifola frondosa",
+  "bolete",
+  "ear, spike, capitulum",
+  "toilet tissue, toilet paper, bathroom tissue"
+];
diff --git a/site/index.html b/site/index.html
new file mode 100644
index 0000000..8b5675d
--- /dev/null
+++ b/site/index.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Coral USB Accelerator Demo</title>
+    <script src="dfu.js"></script>
+    <script src="dfuse.js"></script>
+    <script src="imagenet_labels.js"></script>
+    <script src="tflite.js"></script>
+    <script src="interpreter.js"></script>
+  </head>
+  <body>
+    <h1>Coral USB Accelerator Demo</h1>
+    <button id="button-firmware">1. Flash Device Firmware</button>&nbsp;<span id="firmware-status"></span><br/><br/>
+    <button id="button-init">2. Initialize Device</button><br/><br/>
+    <button id="button-image" onclick="document.getElementById('file').click();" disabled>3. Choose Image File</button><br/><br/>
+    <input type="file" id="file" style="display:none;"/><br/>
+    <canvas id="canvas"></canvas>
+    <h2 id="result"></h2>
+    <br/><br/>
+
+    <!--button id="test">Inference Test</button><br/><br/-->
+
+    <script>
+      async function loadLocalImage(file) {
+        return new Promise(resolve => {
+          var reader = new FileReader();
+          reader.onload = function() {
+            var img = new Image();
+            img.onload = function() {
+              resolve(img);
+            };
+            img.src = reader.result;
+          };
+          reader.readAsDataURL(file);
+        });
+      }
+
+      async function getDFUDescriptorProperties(device) {
+        let data = await device.readConfigurationDescriptor(0);
+        let configDesc = dfu.parseConfigurationDescriptor(data);
+        let funcDesc = null;
+        if (configDesc.bConfigurationValue == device.settings.configuration.configurationValue) {
+            for (let desc of configDesc.descriptors) {
+                if (desc.bDescriptorType == 0x21 && desc.hasOwnProperty("bcdDFUVersion")) {
+                    funcDesc = desc;
+                    break;
+                }
+            }
+        }
+
+        if (!funcDesc) return {};
+
+        return {
+            WillDetach:            ((funcDesc.bmAttributes & 0x08) != 0),
+            ManifestationTolerant: ((funcDesc.bmAttributes & 0x04) != 0),
+            CanUpload:             ((funcDesc.bmAttributes & 0x02) != 0),
+            CanDnload:             ((funcDesc.bmAttributes & 0x01) != 0),
+            TransferSize:          funcDesc.wTransferSize,
+            DetachTimeOut:         funcDesc.wDetachTimeOut,
+            DFUVersion:            funcDesc.bcdDFUVersion
+        };
+      }
+
+      async function connect(device) {
+          await device.open();
+
+          let desc = await getDFUDescriptorProperties(device);
+          if (desc && Object.keys(desc).length > 0)
+            device.properties = desc;
+
+          device.logDebug = console.log;
+          device.logInfo = console.log;
+          device.logWarning = console.log;
+          device.logError = console.log;
+          device.logProgress = console.log;
+
+          return device;
+      }
+
+      async function openDevice() {
+        try {
+          let device = await navigator.usb.requestDevice({ 'filters': [{'vendorId': 0x1a6e, 'productId': 0x089a}] });
+          let interfaces = dfu.findDeviceDfuInterfaces(device);
+          if (interfaces.length != 1) return null;
+          return await connect(new dfu.Device(device, interfaces[0]));
+        } catch (error) {
+          return null;
+        }
+      }
+
+      async function loadFile(url) {
+        return new Promise(resolve => {
+          let req = new XMLHttpRequest();
+          req.open('GET', url, true);
+          req.responseType = 'arraybuffer';
+          req.onload = function (event) { resolve(req.response); };
+          req.send(null);
+        });
+      }
+
+      document.addEventListener('DOMContentLoaded', event => {
+        let downloadButton = document.querySelector("#button-firmware");
+        downloadButton.addEventListener('click', async function(event) {
+          let firmwareFile = await loadFile('/firmware.bin');
+          let device = await openDevice();
+          if (device) {
+            console.log('Device: ', device.properties);
+          } else {
+            console.log('Cannot open device.');
+          }
+
+          if (device && firmwareFile) {
+            document.getElementById("firmware-status").textContent = "Flashing...";
+
+            try {
+              let status = await device.getStatus();
+              if (status.state == dfu.dfuERROR) {
+                await device.clearStatus();
+              }
+            } catch (error) {
+              device.logWarning("Failed to clear status");
+            }
+
+            try {
+              let manifestationTolerant = device.properties.ManifestationTolerant;
+              await device.do_download(device.properties.TransferSize, firmwareFile, manifestationTolerant);
+              if (!manifestationTolerant)
+                await device.waitDisconnected(5000);
+              document.getElementById("firmware-status").textContent = "Ready :)";
+            } catch (error) {
+              document.getElementById("firmware-status").textContent = "Failed :(";
+            }
+          }
+        });
+      });
+
+      Module['onRuntimeInitialized'] = () => {
+        let inference_test = Module.cwrap("inference_test",  null, [], { async: true });
+        let testButton = document.querySelector("#test");
+        if (testButton) {
+          testButton.addEventListener('click', async () => {
+            inference_test();
+          });
+        }
+
+        let interpreter;
+
+        document.querySelector("#button-init").addEventListener('click', async () => {
+          let model = new Uint8Array(await loadFile('/model.tflite'));
+          let modelBufferSize = model.length * model.BYTES_PER_ELEMENT;
+          let modelBuffer = Module._malloc(modelBufferSize);
+          Module.HEAPU8.set(model, modelBuffer);
+
+          interpreter = new tflite.Interpreter();
+          if (await interpreter.create(modelBuffer, modelBufferSize)) {
+            document.getElementById("button-firmware").disabled = true;
+            document.getElementById("button-init").disabled = true;
+            document.getElementById("button-image").disabled = false;
+          }
+        });
+
+        document.querySelector("#file").addEventListener('change', async () => {
+          var input = document.getElementById("file");
+          var file = input.files[0];
+          if (!file) return;
+
+          [_depth, height, width, _channels] = interpreter.inputShape();
+
+          let img = await loadLocalImage(file);
+          var c = document.getElementById("canvas");
+          c.width = width;
+          c.height = height;
+          var ctx = c.getContext('2d');
+          ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, width, height);
+
+          var imageData = ctx.getImageData(0, 0, width, height);
+          interpreter.setRgbaInput(0, imageData.data);
+          document.getElementById("result").textContent = "Recognizing...";
+          let inference_start = Date.now();
+          interpreter.invoke(() => {
+            let inference_time = Date.now() - inference_start;
+            let maxIndex = interpreter.getClassificationOutput(0);
+            document.getElementById("result").textContent = `${imagenet_labels[maxIndex]} (${inference_time} ms)`;
+          });
+        });
+      };
+      Module['print'] = txt => console.log(txt);
+    </script>
+  </body>
+</html>
diff --git a/site/tflite.js b/site/tflite.js
new file mode 100644
index 0000000..d90bd47
--- /dev/null
+++ b/site/tflite.js
@@ -0,0 +1,112 @@
+let tflite = {};
+
+(function() {
+  'use strict';
+
+  let nextId = 0;
+
+  function rgbaArrayToRgbArray(rgbaArray) {
+    var rgbArray = new Uint8Array(new ArrayBuffer(3 * rgbaArray.length / 4));
+    for (var i = 0, j = 0; i < rgbaArray.length; i += 4, j += 3) {
+      rgbArray[j + 0] = rgbaArray[i + 0];
+      rgbArray[j + 1] = rgbaArray[i + 1];
+      rgbArray[j + 2] = rgbaArray[i + 2];
+    }
+    return rgbArray;
+  }
+
+  function callbackKey(id) {
+    return 'interpreter_' + id;
+  }
+
+  tflite.Interpreter = function() {
+    this.interpreter_create       = Module.cwrap('interpreter_create',  'number', ['number'], { async: true });
+    this.interpreter_destroy      = Module.cwrap('interpreter_destroy', null,     ['number']);
+
+    this.interpreter_num_inputs     = Module.cwrap('interpreter_num_inputs',     'number',   ['number']);
+    this.interpreter_input_buffer   = Module.cwrap('interpreter_input_buffer',   'number',   ['number', 'number']);
+    this.interpreter_num_input_dims = Module.cwrap('interpreter_num_input_dims', 'number',   ['number', 'number']);
+    this.interpreter_input_dim      = Module.cwrap('interpreter_input_dim',      'number',   ['number', 'number', 'number']);
+
+    this.interpreter_num_outputs     = Module.cwrap('interpreter_num_outputs',     'number',   ['number']);
+    this.interpreter_output_buffer   = Module.cwrap('interpreter_output_buffer',   'number',   ['number', 'number']);
+    this.interpreter_num_output_dims = Module.cwrap('interpreter_num_output_dims', 'number',   ['number', 'number']);
+    this.interpreter_output_dim      = Module.cwrap('interpreter_output_dim',      'number',   ['number', 'number', 'number']);
+
+    this.interpreter_invoke_async = Module.cwrap('interpreter_invoke_async', null, ['number', 'number']);
+
+    this.id = nextId++;
+
+    Module['invokeDone'] = function(id) {
+      let key = callbackKey(id);
+      let callback = Module[key];
+      if (callback) callback();
+      delete Module[key];
+    };
+  }
+
+  tflite.Interpreter.prototype.create = async function(modelBuffer, modelBufferSize) {
+    this.interpreter = await this.interpreter_create(modelBuffer, modelBufferSize, 0);
+    return this.interpreter != null;
+  }
+
+  tflite.Interpreter.prototype.destroy = function() {
+    this.interpreter_destroy(this.interpreter);
+  }
+
+  tflite.Interpreter.prototype.numInputs = function() {
+    return this.interpreter_num_inputs(this.interpreter);
+  }
+
+  tflite.Interpreter.prototype.inputBuffer = function(index) {
+    return this.interpreter_input_buffer(this.interpreter, index);
+  }
+
+  tflite.Interpreter.prototype.inputShape = function(index) {
+    let dims = this.interpreter_num_input_dims(this.interpreter, index);
+    let shape = [];
+    for (let i = 0; i < dims; ++i)
+      shape.push(this.interpreter_input_dim(this.interpreter, index, i));
+    return shape;
+  }
+
+  tflite.Interpreter.prototype.numOutputs = function() {
+    return this.interpreter_num_outputs(this.interpreter);
+  }
+
+  tflite.Interpreter.prototype.outputBuffer = function(index) {
+    return this.interpreter_output_buffer(this.interpreter, index);
+  }
+
+  tflite.Interpreter.prototype.outputShape = function(index) {
+    let dims = this.interpreter_num_output_dims(this.interpreter, index);
+    let shape = [];
+    for (let i = 0; i < dims; ++i)
+      shape.push(this.interpreter_output_dim(this.interpreter, index, i));
+    return shape;
+  }
+
+  tflite.Interpreter.prototype.invoke = function(callback) {
+    Module[callbackKey(this.id)] = callback;
+    this.interpreter_invoke_async(this.interpreter, this.id);
+  }
+
+  tflite.Interpreter.prototype.setRgbInput = function(index, rgbArray) {
+    let shape = this.inputShape(index);
+    if (rgbArray.length != shape.reduce((a, b) => a * b))
+      throw new Error('Invalid input array size');
+
+    writeArrayToMemory(rgbArray,  this.inputBuffer(index));
+  }
+
+  tflite.Interpreter.prototype.setRgbaInput = function(index, rgbaArray) {
+    this.setRgbInput(index, rgbaArrayToRgbArray(rgbaArray));
+  }
+
+  tflite.Interpreter.prototype.getClassificationOutput = function(index, rgbaArray) {
+    let size = this.outputShape(index).reduce((a, b) => a * b);
+    let buf = this.outputBuffer(index);
+    let tensor = Module.HEAPU8.slice(buf, buf + size);
+    return tensor.indexOf(Math.max(...tensor))
+  }
+})();
diff --git a/tflite/BUILD b/tflite/BUILD
new file mode 100644
index 0000000..199ac67
--- /dev/null
+++ b/tflite/BUILD
@@ -0,0 +1,17 @@
+load("//emscripten_toolchain:wasm_rules.bzl", "wasm_cc_binary")
+
+cc_binary(
+    name = "interpreter",
+    srcs = ["interpreter.cc", "libusb.cc", "queue.h"],
+    deps = [
+      "@libedgetpu//tflite/public:edgetpu_c",
+      "@libedgetpu//tflite/public:oss_edgetpu_direct_usb",
+      "@org_tensorflow//tensorflow/lite:framework",
+      "@org_tensorflow//tensorflow/lite/kernels:builtin_ops",  # BuiltinOpResolver
+    ]
+)
+
+wasm_cc_binary(
+    name = "interpreter-wasm",
+    cc_target = ":interpreter",
+)
diff --git a/tflite/interpreter.cc b/tflite/interpreter.cc
new file mode 100644
index 0000000..9d6dbfd
--- /dev/null
+++ b/tflite/interpreter.cc
@@ -0,0 +1,284 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <thread>
+
+#include <emscripten.h>
+
+#include "tflite/public/edgetpu_c.h"
+
+#include "tensorflow/lite/model_builder.h"
+#include "tensorflow/lite/interpreter.h"
+#include "tensorflow/lite/mutable_op_resolver.h"
+#include "tensorflow/lite/interpreter_builder.h"
+#include "tensorflow/lite/kernels/register.h"  // BuiltinOpResolver
+
+#include "tflite/queue.h"
+
+namespace {
+
+constexpr int kExit = -1;
+
+class Interpreter {
+ public:
+  Interpreter(): thread_([this]() {
+    while (true) {
+      if (auto cmd = queue_.Pop(250)) {
+        if (cmd.value() == kExit) break;
+        Invoke();
+        MAIN_THREAD_ASYNC_EM_ASM({Module['invokeDone']($0);}, cmd.value());
+      }
+    }
+  }) {}
+
+  ~Interpreter() {
+    queue_.Push(kExit);
+    thread_.join();
+  }
+
+ public:
+  bool Init(const char* filename, int verbosity) {
+    auto model = tflite::FlatBufferModel::BuildFromFile(filename);
+    if (!model) {
+      std::cerr << "[ERROR] Cannot load model" << std::endl;
+      return false;
+    }
+    return Init(std::move(model), verbosity);
+  }
+
+  bool Init(const char* model_buffer, size_t model_buffer_size, int verbosity) {
+    auto model = tflite::FlatBufferModel::BuildFromBuffer(model_buffer, model_buffer_size);
+    if (!model) {
+      std::cerr << "[ERROR] Cannot load model" << std::endl;
+      return false;
+    }
+    return Init(std::move(model), verbosity);
+  }
+
+  bool Init(std::unique_ptr<tflite::FlatBufferModel> model, int verbosity) {
+    // Log level
+    edgetpu_verbosity(verbosity);
+
+    // Delegate
+    size_t num_devices;
+    std::unique_ptr<edgetpu_device, decltype(&edgetpu_free_devices)>
+      devices(edgetpu_list_devices(&num_devices), &edgetpu_free_devices);
+    if (num_devices < 1) {
+      std::cerr << "[ERROR] Edge TPU is not connected" << std::endl;
+      return false;
+    }
+
+    auto& device = devices.get()[0];
+    edgetpu_option option = {"Usb.AlwaysDfu", "False"};
+    auto* delegate = edgetpu_create_delegate(device.type, device.path, &option, 1);
+
+    // Model
+    model_ = std::move(model);
+
+    //tflite::MutableOpResolver resolver;
+    //resolver.AddCustom("TFLite_Detection_PostProcess",
+    //                   tflite::ops::custom::Register_DETECTION_POSTPROCESS());
+    tflite::ops::builtin::BuiltinOpResolver resolver;
+    if (tflite::InterpreterBuilder(*model_, resolver)(&interpreter_) != kTfLiteOk) {
+      std::cerr << "[ERROR] Cannot create interpreter" << std::endl;
+      return false;
+    }
+
+    if (interpreter_->ModifyGraphWithDelegate(
+        std::unique_ptr<TfLiteDelegate, decltype(&edgetpu_free_delegate)>(delegate, edgetpu_free_delegate)) != kTfLiteOk) {
+      std::cerr << "[ERROR] Cannot apply EdgeTPU delegate" << std::endl;
+      return false;
+    }
+
+    if (interpreter_->AllocateTensors() != kTfLiteOk) {
+      std::cerr << "[ERROR] Cannot allocated tensors" << std::endl;
+      return false;
+    }
+
+    return true;
+  }
+
+ public:
+  size_t NumInputs() const {
+    return interpreter_->inputs().size();
+  }
+
+  void* InputBuffer(size_t tensor_index) const {
+    return interpreter_->input_tensor(tensor_index)->data.data;
+  }
+
+  const size_t NumInputDims(size_t tensor_index) const {
+    return interpreter_->input_tensor(tensor_index)->dims->size;
+  }
+
+  const size_t InputDim(size_t tensor_index, size_t dim) const {
+    return interpreter_->input_tensor(tensor_index)->dims->data[dim];
+  }
+
+ public:
+  size_t NumOutputs() const {
+    return interpreter_->outputs().size();
+  }
+
+  const void* OutputBuffer(size_t tensor_index) const {
+    return interpreter_->output_tensor(tensor_index)->data.data;
+  }
+
+  const size_t NumOutputDims(size_t tensor_index) const {
+    return interpreter_->output_tensor(tensor_index)->dims->size;
+  }
+
+  const int OutputDim(size_t tensor_index, size_t dim) const {
+    return interpreter_->output_tensor(tensor_index)->dims->data[dim];
+  }
+
+ public:
+  void Invoke() {
+    if (interpreter_->Invoke() != kTfLiteOk) {
+      std::cout << "[ERROR] Cannot invoke interpreter" << std::endl;
+    }
+  }
+
+  void InvokeAsync(size_t id) {
+    queue_.Push(id);
+  }
+
+ private:
+  std::unique_ptr<tflite::FlatBufferModel> model_;
+  std::unique_ptr<tflite::Interpreter> interpreter_;
+  Queue<int> queue_;
+  std::thread thread_;
+};
+
+}  // namespace
+
+int main(int argc, char* argv[]) {
+  std::cout << "main()" << std::endl;
+  return 0;
+}
+
+extern "C" {
+
+EMSCRIPTEN_KEEPALIVE void inference_test() {
+  // auto* interpreter = new Interpreter();
+  // interpreter->Init("model.tflite", 10);
+
+  // std::ifstream file("cat.rgb", std::ios::binary);
+  // if (!file) {
+  //   std::cout << "[ERROR] Cannot open file" << std::endl;
+  //   return;
+  // }
+
+  // if (!file.read(reinterpret_cast<char*>(interpreter->Input()),
+  //                interpreter->InputSize())) {
+  //   std::cout << "[ERROR] Cannot read file" << std::endl;
+  //   return;
+  // }
+
+  // std::thread th([interpreter]() {
+  //   auto index = interpreter->Invoke();
+  //   std::cout << "====> Result: " << index << std::endl;
+  //   delete interpreter;
+  // });
+  // th.detach();
+}
+
+// EMSCRIPTEN_KEEPALIVE void update_firmware() {
+//   std::thread th([]() {
+//     platforms::darwinn::api::Driver::Options options;
+
+//     platforms::darwinn::api::Device device;
+//     device.chip = platforms::darwinn::api::Chip::kBeagle;
+//     device.type = platforms::darwinn::api::Device::Type::USB;
+//     device.path = "default";
+
+//     auto* factory = platforms::darwinn::api::DriverFactory::GetOrCreate();
+//     std::cout << factory << std::endl;
+//     auto driver = factory->CreateDriver(device);
+//     if (!driver.ok()) {
+//        std::cout << "[ERROR] Cannot create USB driver" << std::endl;
+//        MAIN_THREAD_ASYNC_EM_ASM({Module['firmwareDone']($0);}, 0);
+//        return;
+//     }
+
+//     auto* usb_driver = static_cast<platforms::darwinn::driver::UsbDriver*>(driver->get());
+//     auto status = usb_driver->FlashUsbDevice();
+//     if (!status.ok()) {
+//       std::cout << "[ERROR] Cannot flash USB device" << std::endl;
+//       MAIN_THREAD_ASYNC_EM_ASM({Module['firmwareDone']($0);}, 0);
+//       return;
+//     }
+
+//     std::cout << "USB device is ready!" << std::endl;
+//     MAIN_THREAD_ASYNC_EM_ASM({Module['firmwareDone']($0);}, 1);
+//     return;
+//   });
+//   th.detach();
+// }
+
+EMSCRIPTEN_KEEPALIVE void* interpreter_create(const char* model_buffer, size_t model_buffer_size, int verbosity) {
+  auto* interpreter = new Interpreter();
+  if (!interpreter->Init(model_buffer, model_buffer_size, verbosity)) {
+    delete interpreter;
+    return nullptr;
+  }
+  return interpreter;
+}
+
+EMSCRIPTEN_KEEPALIVE void interpreter_destroy(void* p) {
+  delete reinterpret_cast<Interpreter*>(p);
+}
+
+// Inputs
+EMSCRIPTEN_KEEPALIVE size_t interpreter_num_inputs(void* interpreter) {
+  return reinterpret_cast<Interpreter*>(interpreter)->NumInputs();
+}
+
+EMSCRIPTEN_KEEPALIVE void* interpreter_input_buffer(void* interpreter, size_t tensor_index) {
+  return reinterpret_cast<Interpreter*>(interpreter)->InputBuffer(tensor_index);
+}
+
+EMSCRIPTEN_KEEPALIVE size_t interpreter_num_input_dims(void *interpreter, size_t tensor_index) {
+  return reinterpret_cast<Interpreter*>(interpreter)->NumInputDims(tensor_index);
+}
+
+EMSCRIPTEN_KEEPALIVE size_t interpreter_input_dim(void *interpreter, size_t tensor_index, size_t dim) {
+  return reinterpret_cast<Interpreter*>(interpreter)->InputDim(tensor_index, dim);
+}
+
+// Outputs
+EMSCRIPTEN_KEEPALIVE size_t interpreter_num_outputs(void* interpreter) {
+  return reinterpret_cast<Interpreter*>(interpreter)->NumOutputs();
+}
+
+EMSCRIPTEN_KEEPALIVE const void* interpreter_output_buffer(void* interpreter, size_t tensor_index) {
+  return reinterpret_cast<Interpreter*>(interpreter)->OutputBuffer(tensor_index);
+}
+
+EMSCRIPTEN_KEEPALIVE size_t interpreter_num_output_dims(void *interpreter, size_t tensor_index) {
+  return reinterpret_cast<Interpreter*>(interpreter)->NumOutputDims(tensor_index);
+}
+
+EMSCRIPTEN_KEEPALIVE size_t interpreter_output_dim(void *interpreter, size_t tensor_index, size_t dim) {
+  return reinterpret_cast<Interpreter*>(interpreter)->OutputDim(tensor_index, dim);
+}
+
+EMSCRIPTEN_KEEPALIVE void interpreter_invoke_async(void *interpreter, size_t id) {
+  return reinterpret_cast<Interpreter*>(interpreter)->InvokeAsync(id);
+}
+
+}  // extern "C"
diff --git a/tflite/libusb.cc b/tflite/libusb.cc
new file mode 100644
index 0000000..6195c93
--- /dev/null
+++ b/tflite/libusb.cc
@@ -0,0 +1,495 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <iostream>
+
+#include <emscripten.h>
+#include <libusb-1.0/libusb.h>
+
+#include "tflite/queue.h"
+
+#define LIBUSB_MAJOR 1
+#define LIBUSB_MINOR 0
+#define LIBUSB_MICRO 24
+#define LIBUSB_NANO 0
+#define LIBUSB_RC ""
+
+struct libusb_device {
+  uint8_t bus_number;
+  uint8_t port_number;
+  struct libusb_context *ctx;
+
+  struct libusb_device_descriptor descriptor;
+};
+
+struct libusb_context {
+  Queue<libusb_transfer*> completed_transfers;
+  libusb_device dev;
+};
+
+struct libusb_device_handle {
+  struct libusb_device *dev;
+};
+
+static const struct libusb_version libusb_version_internal = {
+  LIBUSB_MAJOR,
+  LIBUSB_MINOR,
+  LIBUSB_MICRO,
+  LIBUSB_NANO,
+  LIBUSB_RC,
+  "http://libusb.info"
+};
+
+static int js_request_device(struct libusb_device *dev) {
+  return MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      console.log('js_request_device');
+      // Bus 001 Device 005: ID 1a6e:089a Global Unichip Corp.
+      // Bus 002 Device 007: ID 18d1:9302 Google Inc.
+      let options = {'filters': [{'vendorId': 0x18d1, 'productId': 0x9302}]};
+
+      let devices = await navigator.usb.getDevices();
+      if (!devices.length) {
+        try {
+          let device = await navigator.usb.requestDevice(options);
+          devices = [device];
+        } catch (error) {
+          devices = []
+        }
+      }
+
+      console.log('js_request_device, device list:', devices);
+
+      if (devices.length === 1) {
+        let d = devices[0];
+        _fill_device($0,
+                   /*bcdUSB=*/(d.usbVersionMajor << 8) | d.usbVersionMinor,
+                   /*bDeviceClass*/d.deviceClass,
+                   /*bDeviceSubClass=*/d.deviceSubClass,
+                   /*bDeviceProtocol=*/d.deviceProtocol,
+                   /*idVendor=*/d.vendorId,
+                   /*idProduct=*/d.productId,
+                   /*bcdDevice=*/(d.deviceVersionMajor << 8) | ((d.deviceVersionMinor << 4) | d.deviceVersionSubminor),
+                   /*bNumConfigurations=*/d.configurations.length);
+        this.libusb_device = devices[0];
+        return 1;
+      }
+      return 0;
+    });
+  }, dev);
+}
+
+static int js_control_transfer(uint8_t bmRequestType, uint8_t bRequest, uint16_t wValue,
+                         uint16_t wIndex, uint8_t *data, uint16_t wLength, unsigned int timeout) {
+  return MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      console.log('js_control_transfer');
+
+      let bmRequestType = $0;
+      let bRequest = $1;
+      let wValue = $2;
+      let wIndex = $3;
+      let data = $4;
+      let wLength = $5;
+      let timeout = $6;
+
+      let setup = {
+        'requestType': ['standard', 'class', 'vendor'][(bmRequestType & 0x60) >> 5],
+        'recipient': ['device', 'interface', 'endpoint', 'other'][(bmRequestType & 0x1f)],
+        'request': bRequest,
+        'value': wValue,
+        'index': wIndex,
+      };
+
+      let dir_in = (bmRequestType & 0x80) == 0x80;
+      if (dir_in) {
+        let result = await this.libusb_device.controlTransferIn(setup, wLength);
+        if (result.status != "ok") {
+          out("JS[js_control_transfer]: <ERROR> controlTransferIn response: " + result.status);
+          return 0;
+        }
+
+        let view = new Uint8Array(result.data.buffer);
+        writeArrayToMemory(view, data);
+        return result.data.buffer.byteLength;
+      } else {
+        let buffer = new Uint8Array(wLength);
+        for (let i = 0; i < wLength; ++i)
+          buffer[i] = getValue(data + i, "i8");
+
+        let result = await this.libusb_device.controlTransferOut(setup, buffer);
+        if(result.status != "ok") {
+          out("JS[js_control_transfer]: <ERROR> controlTransferOut response: " + result.status);
+          return 0;
+        }
+        return result.bytesWritten;
+      }
+    });
+  }, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout);
+}
+
+static void print_device_descriptor(const struct libusb_device_descriptor* d) {
+  std::cout << "Descriptor" << std::endl;
+  std::cout << "  bLength: "                 << static_cast<int>(d->bLength)            << std::endl;
+  std::cout << "  bDescriptorType: "         << static_cast<int>(d->bDescriptorType)    << std::endl;
+  std::cout << "  bcdUSB: 0x"    << std::hex << static_cast<int>(d->bcdUSB) << std::dec << std::endl;
+  std::cout << "  bDeviceClass: "            << static_cast<int>(d->bDeviceClass)       << std::endl;
+  std::cout << "  bDeviceSubClass: "         << static_cast<int>(d->bDeviceSubClass)    << std::endl;
+  std::cout << "  bDeviceProtocol: "         << static_cast<int>(d->bDeviceProtocol)    << std::endl;
+  std::cout << "  bMaxPacketSize0: "         << static_cast<int>(d->bMaxPacketSize0)    << std::endl;
+  std::cout << "  idVendor: 0x"  << std::hex << static_cast<int>(d->idVendor)  << std::dec << std::endl;
+  std::cout << "  idProduct: 0x" << std::hex << static_cast<int>(d->idProduct) << std::dec << std::endl;
+  std::cout << "  bcdDevice: 0x" << std::hex << static_cast<int>(d->bcdDevice) << std::dec << std::endl;
+  std::cout << "  iManufacturer: "           << static_cast<int>(d->iManufacturer)      << std::endl;
+  std::cout << "  iProduct: "                << static_cast<int>(d->iProduct)           << std::endl;
+  std::cout << "  iSerialNumber: "           << static_cast<int>(d->iSerialNumber)      << std::endl;
+  std::cout << "  bNumConfigurations: "      << static_cast<int>(d->bNumConfigurations) << std::endl;
+}
+
+extern "C" {
+
+int libusb_init(libusb_context **ctx) {
+  std::cout << "libusb_init" << std::endl;
+
+  if (!EM_ASM_INT(return navigator.usb !== undefined))
+    return LIBUSB_ERROR_NOT_SUPPORTED;
+
+  *ctx = new libusb_context;
+  return LIBUSB_SUCCESS;
+}
+
+void LIBUSB_CALL libusb_exit(libusb_context *ctx) {
+  std::cout << "libusb_exit" << std::endl;
+  delete ctx;
+}
+
+void LIBUSB_CALL libusb_set_debug(libusb_context *ctx, int level) {
+  std::cout << "<NOT IMPLEMENTED> libusb_set_debug" << std::endl;
+}
+
+const struct libusb_version* LIBUSB_CALL libusb_get_version() {
+  std::cout << "libusb_get_version" << std::endl;
+  return &libusb_version_internal;
+}
+
+struct libusb_transfer* LIBUSB_CALL libusb_alloc_transfer(int iso_packets) {
+  std::cout << "libusb_alloc_transfer" << std::endl;
+
+  size_t size = sizeof(struct libusb_transfer) +
+                sizeof(struct libusb_iso_packet_descriptor) * iso_packets;
+
+  return reinterpret_cast<libusb_transfer*>(calloc(1, size));;
+}
+
+int LIBUSB_CALL libusb_submit_transfer(struct libusb_transfer *transfer) {
+  std::cout << "libusb_submit_transfer" << std::endl;
+
+  bool dir_in = (transfer->endpoint & 0x80) == 0x80;
+  uint8_t endpoint = transfer->endpoint & 0x7f;
+
+  switch (transfer->type) {
+    case LIBUSB_TRANSFER_TYPE_BULK:
+    case LIBUSB_TRANSFER_TYPE_INTERRUPT:
+      if (dir_in) {
+        MAIN_THREAD_ASYNC_EM_ASM({
+          console.log('js_submit_bulk_in_transfer, endpoint: ', $0);
+          this.libusb_device.transferIn($0, $2).then(function(result) {
+            var data = new Uint8Array(result.data.buffer);
+            writeArrayToMemory(data, $1);
+            _set_transfer_completed($3, data.length);
+          }).catch(function(error) {
+            console.error('js_submit_bulk_in_transfer:', error);
+            _set_transfer_error($3);
+          });
+        }, endpoint, transfer->buffer, transfer->length, transfer);
+      } else {
+        MAIN_THREAD_ASYNC_EM_ASM({
+          console.log('js_submit_bulk_out_transfer, endpoint: ', $0);
+
+          var data = new Uint8Array($2);
+          for(let i = 0; i < $2; ++i)
+            data[i] = getValue($1 + i, "i8");
+
+          this.libusb_device.transferOut($0, data).then(function(result) {
+            _set_transfer_completed($3, result.bytesWritten);
+          }).catch(function(error) {
+            console.error('js_submit_bulk_out_transfer:', error);
+            _set_transfer_error($3);
+          });
+        }, endpoint, transfer->buffer, transfer->length, transfer);
+      }
+      break;
+    default:
+      printf("Transfer type not implemented: %u\n", transfer->type);
+      return LIBUSB_ERROR_IO;
+  }
+  return LIBUSB_SUCCESS;
+}
+
+int LIBUSB_CALL libusb_cancel_transfer(struct libusb_transfer *transfer) {
+  std::cout << "<NOT IMPLEMENTED> libusb_cancel_transfer" << std::endl;
+  return 0;
+}
+
+void LIBUSB_CALL libusb_free_transfer(struct libusb_transfer *transfer) {
+  std::cout << "libusb_free_transfer" << std::endl;
+  free(transfer);
+}
+
+uint8_t libusb_get_port_number(libusb_device * dev) {
+  std::cout << "libusb_get_port_number" << std::endl;
+  return dev->port_number;
+}
+
+int libusb_get_port_numbers(libusb_device *dev,uint8_t *port_numbers, int port_numbers_len) {
+  std::cout << "libusb_get_port_numbers" << std::endl;
+
+  if (port_numbers_len <= 0)
+    return LIBUSB_ERROR_INVALID_PARAM;
+
+  if (port_numbers_len < 1)
+    return LIBUSB_ERROR_OVERFLOW;
+
+  port_numbers[0] = dev->port_number;
+  return 1;
+}
+
+int LIBUSB_CALL libusb_handle_events(libusb_context *ctx) {
+  //std::cout << "libusb_handle_events" << std::endl;
+  while (true) {
+    if (auto item = ctx->completed_transfers.Pop(25)) {
+      auto* transfer = item.value();
+      std::cout << "==  Running callback ==" << std::endl;
+      transfer->callback(transfer);
+    } else {
+      break;
+    }
+  }
+  return LIBUSB_SUCCESS;
+}
+
+int LIBUSB_CALL libusb_reset_device(libusb_device_handle *dev) {
+  std::cout << "libusb_reset_device" << std::endl;
+  return MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      try {
+        console.log('js_reset_device');
+        await this.libusb_device.reset();
+        return 0;  // LIBUSB_SUCCESS
+      } catch (error) {
+        console.error('js_reset_device:', error);
+        //return -1;  // LIBUSB_ERROR_IO
+        return 0;  // TODO(dkovalev)
+      }
+    });
+  });
+}
+
+int LIBUSB_CALL libusb_control_transfer(libusb_device_handle *dev_handle,
+  uint8_t request_type, uint8_t bRequest, uint16_t wValue, uint16_t wIndex,
+  unsigned char *data, uint16_t wLength, unsigned int timeout) {
+  std::cout << "libusb_control_transfer" << std::endl;
+  return js_control_transfer(request_type, bRequest, wValue, wIndex, data, wLength, timeout);
+}
+
+int LIBUSB_CALL libusb_bulk_transfer(libusb_device_handle *dev_handle,
+  unsigned char endpoint, unsigned char *data, int length,
+  int *actual_length, unsigned int timeout) {
+  std::cout << "<NOT IMPLEMENTED> libusb_bulk_transfer" << std::endl;
+  return 0;
+}
+
+int LIBUSB_CALL libusb_interrupt_transfer(libusb_device_handle *dev_handle,
+  unsigned char endpoint, unsigned char *data, int length,
+  int *actual_length, unsigned int timeout) {
+  std::cout << "<NOT IMPLEMENTED> libusb_interrupt_transfer" << std::endl;
+  return 0;
+}
+
+int LIBUSB_CALL libusb_open(libusb_device *dev, libusb_device_handle **handle) {
+  std::cout << "libusb_open" << std::endl;
+  // dev->descriptor.idVendor, dev->descriptor.idProduct
+
+  MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      console.log('js_open_device');
+      await this.libusb_device.open();
+      try {
+        await this.libusb_device.reset(); // TODO(dkovalev)
+      } catch (error) {
+        console.error('this.libusb_device.reset()', error);
+      }
+      return 1;
+    });
+  });
+
+  if (handle) {
+    *handle = new libusb_device_handle;
+    (*handle)->dev = dev;
+  }
+
+  return LIBUSB_SUCCESS;
+}
+
+void LIBUSB_CALL libusb_close(libusb_device_handle *dev_handle) {
+  std::cout << "libusb_close" << std::endl;
+
+  MAIN_THREAD_EM_ASM_INT({
+    Asyncify.handleAsync(async () => {
+      console.log('js_close_device');
+      return await this.libusb_device.close();
+    });
+  });
+
+  delete dev_handle;
+}
+
+libusb_device * LIBUSB_CALL libusb_get_device(libusb_device_handle *dev_handle) {
+  std::cout << "libusb_get_device" << std::endl;
+  return dev_handle->dev;
+}
+
+ssize_t LIBUSB_CALL libusb_get_device_list(libusb_context *ctx, libusb_device ***list) {
+  std::cout << "libusb_get_device_list" << std::endl;
+
+  auto* dev = &ctx->dev;
+  dev->ctx = ctx;
+
+  if (js_request_device(dev)) {
+    print_device_descriptor(&dev->descriptor);
+    *list = new libusb_device*[2]{dev, nullptr};
+    return 1;
+  } else {
+    *list = new libusb_device*[1]{nullptr};
+    return 0;
+  }
+}
+
+void LIBUSB_CALL libusb_free_device_list(libusb_device* *list, int unref_devices) {
+  std::cout << "libusb_free_device_list: unref_devices=" << unref_devices << std::endl;
+
+  int i = 0;
+  while (true) {
+    libusb_device *device = list[i++];
+    if (device == nullptr) break;
+    delete device;
+  }
+
+  delete [] list;
+}
+
+int LIBUSB_CALL libusb_get_device_descriptor(libusb_device *dev, struct libusb_device_descriptor *desc) {
+  std::cout << "libusb_get_device_descriptor" << std::endl;
+  *desc = dev->descriptor;
+  return 0;
+}
+
+int LIBUSB_CALL libusb_get_device_speed(libusb_device *dev) {
+  std::cout << "libusb_get_device_speed" << std::endl;
+  return LIBUSB_SPEED_SUPER;  // TODO(dkovalev)
+}
+
+uint8_t LIBUSB_CALL libusb_get_bus_number(libusb_device *dev) {
+  std::cout << "libusb_get_bus_number" << std::endl;
+  return dev->bus_number;
+}
+
+int LIBUSB_CALL libusb_set_configuration(libusb_device_handle *dev, int configuration) {
+  std::cout << "<NOT IMPLEMENTED> libusb_set_configuration" << std::endl;
+  return 0;
+}
+
+int LIBUSB_CALL libusb_claim_interface(libusb_device_handle *dev, int interface_number) {
+  std::cout << "libusb_claim_interface: " << interface_number << std::endl;
+  return MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      try {
+        console.log('js_claim_interface: ', $0);
+        await this.libusb_device.claimInterface($0);
+        return 0;  // LIBUSB_SUCCESS
+      } catch (error) {
+        console.error('js_claim_interface:', error);
+        return -1;  // LIBUSB_ERROR_IO
+      }
+    });
+  }, interface_number);
+}
+
+int LIBUSB_CALL libusb_release_interface(libusb_device_handle *dev, int interface_number) {
+  std::cout << "libusb_release_interface: " << interface_number << std::endl;
+  return MAIN_THREAD_EM_ASM_INT({
+    return Asyncify.handleAsync(async () => {
+      try {
+        console.log('js_release_interface: ', $0, this.libusb_device);
+        await this.libusb_device.releaseInterface($0);
+        return 0;  // LIBUSB_SUCCESS
+      } catch (error) {
+        console.error('js_release_interface:', error);
+        return -1;  // LIBUSB_ERROR_IO
+      }
+    });
+  }, interface_number);
+}
+
+
+EMSCRIPTEN_KEEPALIVE void set_transfer_error(struct libusb_transfer* transfer) {
+  std::cout << "==> set_tansfer_error [" << transfer << "]"  << std::endl;
+  libusb_context* ctx = transfer->dev_handle->dev->ctx;
+
+  transfer->status = LIBUSB_TRANSFER_CANCELLED;
+  transfer->actual_length = 0;
+
+  ctx->completed_transfers.Push(transfer);
+}
+
+EMSCRIPTEN_KEEPALIVE void set_transfer_completed(struct libusb_transfer* transfer, int actual_length) {
+  std::cout << "==> set_tansfer_completed [" << transfer << "]: "  << actual_length << std::endl;
+  libusb_context* ctx = transfer->dev_handle->dev->ctx;
+
+  transfer->status = LIBUSB_TRANSFER_COMPLETED;
+  transfer->actual_length = actual_length;
+
+  ctx->completed_transfers.Push(transfer);
+}
+
+EMSCRIPTEN_KEEPALIVE void fill_device(struct libusb_device* dev,
+    uint16_t bcdUSB,
+    uint8_t bDeviceClass,
+    uint8_t bDeviceSubClass,
+    uint8_t bDeviceProtocol,
+    uint16_t idVendor,
+    uint16_t idProduct,
+    uint16_t  bcdDevice,
+    uint8_t bNumConfigurations) {
+  dev->bus_number = 0;
+  dev->port_number = 1;
+
+  struct libusb_device_descriptor* d = &dev->descriptor;
+  d->bLength = LIBUSB_DT_DEVICE_SIZE;
+  d->bDescriptorType = LIBUSB_DT_DEVICE;
+  d->bcdUSB = bcdUSB;
+  d->bDeviceClass = bDeviceClass;
+  d->bDeviceSubClass = bDeviceSubClass;
+  d->bDeviceProtocol = bDeviceProtocol;
+  d->bMaxPacketSize0 = 64;
+  d->idVendor = idVendor;
+  d->idProduct = idProduct;
+  d->bcdDevice = bcdDevice;
+  d->iManufacturer = 1;
+  d->iProduct = 2;
+  d->iSerialNumber = 3;
+  d->bNumConfigurations = bNumConfigurations;
+}
+
+}  // extern "C"
diff --git a/tflite/queue.h b/tflite/queue.h
new file mode 100644
index 0000000..18d9cce
--- /dev/null
+++ b/tflite/queue.h
@@ -0,0 +1,54 @@
+// Copyright 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef INTERPRETER_QUEUE_H_
+#define INTERPRETER_QUEUE_H_
+
+#include <chrono>
+#include <condition_variable>
+#include <mutex>
+#include <optional>
+#include <queue>
+
+template<typename T>
+class Queue {
+ public:
+  Queue() {}
+
+  void Push(T t){
+    {
+      std::lock_guard<std::mutex> lock(m_);
+      q_.push(t);
+    }
+    cv_.notify_one();
+  }
+
+  std::optional<T> Pop(int timeout_ms) {
+    std::unique_lock<std::mutex> lock(m_);
+    if (!cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms),
+                  [this]{ return !q_.empty(); }))
+      return std::nullopt;
+
+    auto t = q_.front();
+    q_.pop();
+    return t;
+  }
+
+ private:
+  std::mutex m_;
+  std::condition_variable cv_;
+  std::queue<T> q_;
+};
+
+#endif  // INTERPRETER_QUEUE_H_
diff --git a/webdfu b/webdfu
new file mode 160000
index 0000000..c32da16
--- /dev/null
+++ b/webdfu
@@ -0,0 +1 @@
+Subproject commit c32da16727ecd078222feb8a5d7c5cab134ce349