Setup sample_project structure

- build_overrides/pigweed.gni declares new modules.
  e.g. $dir_pw_board_led
- Copy sample_project/workshop/{string,rpc} examples to applications/

Change-Id: Ia136853a382ab5f24d2dc269ee9bf22e2d855a7b
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/experimental/+/35000
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Joe Ethier <jethier@google.com>
Reviewed-by: Rob Mohr <mohrr@google.com>
diff --git a/.clang-format b/.clang-format
new file mode 120000
index 0000000..1c7cede
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1 @@
+third_party/pigweed/.clang-format
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a9a504a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Force batch scripts to use CRLF.
+*.bat text eol=crlf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eb82667
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# Build
+out/
+.presubmit/
+compile_commands.json
+
+# Environment
+.environment
+
+# Editors
+.idea/
+.project
+.cproject
+.vscode
+.clangd/
+*.swp
+*.swo
+
+# Python
+python*-env/
+.python*-env/
+venv/
+*.pyc
+*.egg/
+*.eggs/
+*.egg-info/
+.cache/
+.mypy_cache/
+__pycache__/
+
+# Mac
+.DS_Store
+
+# GDB
+.gdb_history
+
+# Git
+*.orig
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+# Other
+logfile.txt
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..317cd91
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "third_party/pigweed"]
+	path = third_party/pigweed
+	url = https://pigweed.googlesource.com/pigweed/pigweed
+[submodule "third_party/nanopb"]
+	path = third_party/nanopb
+	url = https://pigweed.googlesource.com/third_party/github/nanopb/nanopb
diff --git a/.gn b/.gn
new file mode 100644
index 0000000..9e68813
--- /dev/null
+++ b/.gn
@@ -0,0 +1,15 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+buildconfig = "//BUILDCONFIG.gn"
diff --git a/BUILD.gn b/BUILD.gn
new file mode 100644
index 0000000..a246e17
--- /dev/null
+++ b/BUILD.gn
@@ -0,0 +1,121 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/python.gni")
+import("$dir_pw_tokenizer/database.gni")
+import("$dir_pw_unit_test/test.gni")
+
+# Lists all the targets build by default with e.g. `ninja -C out`.
+group("default") {
+  deps = [
+    ":applications",
+    ":arduino",
+    ":host",
+    ":python.lint",
+    ":python.tests",
+  ]
+}
+
+# Group all targets that run on host only, e.g. tests, utils.
+group("host") {
+  deps = [ ":host_tests(//targets/host:host_debug_tests)" ]
+}
+
+# Arduino specific targets.
+group("arduino") {
+  if (pw_arduino_build_CORE_PATH != "") {
+    deps = [ ":arduino_tests(//targets/arduino:arduino_debug_tests)" ]
+
+    if (pw_arduino_build_CORE_NAME == "teensy") {
+      deps += []
+    }
+  }
+}
+
+# stm32f429i_disc1 specific targets.
+group("stm32f429i_disc1") {
+  _default_toolchain = "//targets/stm32f429i-disc1:stm32f429i_disc1_debug"
+  _testing_toolchain = "${_default_toolchain}_tests"
+  deps = [
+    # ":app(${_default_toolchain})",
+    ":tests(${_testing_toolchain})",
+  ]
+}
+
+# Python packages are built using the host toolchain.
+pw_python_group("python") {
+  # This depends on the 'tools' target in //tools/BUILD.gn
+  python_deps = [
+    "tools",
+    "//third_party/pigweed/pw_env_setup:python",
+    "//third_party/pigweed/pw_env_setup:target_support_packages",
+  ]
+}
+
+group("applications") {
+  deps = []
+
+  # Teensy 3.0/4.0 applications steps.
+  if (pw_arduino_build_CORE_PATH != "") {
+    deps += [
+      ":applications_tests(//targets/arduino:arduino_debug_tests)",
+      "//applications/rpc:all(//targets/arduino:arduino_debug)",
+      "//applications/strings:all(//targets/arduino:arduino_debug)",
+    ]
+  }
+
+  # STMicroelectronics STM32F429I-DISC1 applications steps.
+  deps += [
+    ":applications_tests(//targets/stm32f429i-disc1:stm32f429i_disc1_debug_tests)",
+    "//applications/rpc:all(//targets/stm32f429i-disc1:stm32f429i_disc1_debug)",
+    "//applications/strings:all(//targets/stm32f429i-disc1:stm32f429i_disc1_debug)",
+  ]
+
+  # Host applications steps.
+  deps += [
+    ":applications_tests(//targets/host:host_debug_tests)",
+    "//applications/strings:all(//targets/host:host_debug)",
+  ]
+}
+
+# Group the different modules tests together.
+pw_test_group("tests") {
+  group_deps = [ "//applications/strings:tests" ]
+
+  if (pw_arduino_build_CORE_NAME == "teensy") {
+    group_deps += []
+  }
+}
+
+# Test groups for each platform that will build and optionally run the tests.
+foreach(test_group,
+        [
+          "arduino",
+          "host",
+          "applications",
+        ]) {
+  group("${test_group}_tests") {
+    deps = []
+    if (pw_unit_test_AUTOMATIC_RUNNER == "") {
+      # Without a test runner defined, build the tests but don't run them.
+      deps += [ ":tests" ]
+    } else {
+      # With a test runner, build and run tests.
+      deps += [ ":tests_run" ]
+    }
+  }
+}
diff --git a/BUILDCONFIG.gn b/BUILDCONFIG.gn
new file mode 100644
index 0000000..4452fbe
--- /dev/null
+++ b/BUILDCONFIG.gn
@@ -0,0 +1,23 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+# This is imported into a scope so as not to pollute the global variable space.
+_pigweed_directory = {
+  import("//build_overrides/pigweed.gni")
+}
+
+# The default toolchain is not used in Pigweed builds, so it is set to a dummy
+# toolchain. The top-level BUILD.gn should stamp a group with all of the build
+# targets and their toolchains.
+set_default_toolchain("${_pigweed_directory.dir_pw_toolchain}/dummy")
diff --git a/PW_PLUGINS b/PW_PLUGINS
new file mode 100644
index 0000000..2d865e5
--- /dev/null
+++ b/PW_PLUGINS
@@ -0,0 +1,17 @@
+# The PW_PLUGINS file lists commands that should be included with the pw command
+# when it is invoked in this directory or its subdirectories. Commands in this
+# file override those registered in parent directories.
+#
+# Entries in this file have three columns:
+#
+#   <name> <Python module> <function>
+#
+# The Python package containing that module must be installed in the Pigweed
+# virtual environment. The function must have no required arguments and should
+# return an int to use as the exit code.
+
+# Pigweed's presubmit check script
+presubmit pigweed_experimental_tools.presubmit_checks main
+heap-viewer pw_allocator.heap_viewer main
+rpc pw_hdlc.rpc_console main
+package pw_package.pigweed_packages main
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..327ef96
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# Pigweed Experimental
+
+[TOC]
+
+This repository contains experimental pigweed modules.
+
+## Repository setup
+
+Clone this repo with `--recursive` to get all required submodules.
+
+```sh
+git clone --recursive https://pigweed.googlesource.com/pigweed/experimental
+```
+
+This will pull the [Pigweed source
+repository](https://pigweed.googlesource.com/pigweed/pigweed) into
+`third_party/pigweed`. If you already cloned but forgot to `--recursive` run
+`git submodule update --init` to pull all submodules.
+
diff --git a/activate.bat b/activate.bat
new file mode 100644
index 0000000..303ce4f
--- /dev/null
+++ b/activate.bat
@@ -0,0 +1,31 @@
+:<<"::WINDOWS_ONLY"
+@echo off
+:: Copyright 2021 The Pigweed Authors
+::
+:: 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.
+::WINDOWS_ONLY
+:; echo "ERROR: Attempting to run Windows .bat from a Unix/POSIX shell!"
+:; echo "Instead, run the following command."
+:; echo ""
+:; echo "    source ./activate.sh"
+:; echo ""
+:<<"::WINDOWS_ONLY"
+
+:: First, activate the development environment.
+set PW_SKIP_BOOTSTRAP=1
+call "%~dp0\bootstrap.bat"
+set PW_SKIP_BOOTSTRAP=
+
+:: Add user-defined environment configuration here.
+
+::WINDOWS_ONLY
diff --git a/activate.sh b/activate.sh
new file mode 120000
index 0000000..eba5389
--- /dev/null
+++ b/activate.sh
@@ -0,0 +1 @@
+bootstrap.sh
\ No newline at end of file
diff --git a/applications/rpc/BUILD.gn b/applications/rpc/BUILD.gn
new file mode 100644
index 0000000..174cefc
--- /dev/null
+++ b/applications/rpc/BUILD.gn
@@ -0,0 +1,75 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("$dir_pw_tokenizer/database.gni")
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+group("all") {
+  deps = [ ":rpc" ]
+
+  # Build tokenizer_database for elf binaries only
+  if (host_os == "linux" || current_toolchain != "//targets/host:host_debug") {
+    deps += [ ":tokenizer_database" ]
+  }
+}
+
+pw_executable("rpc") {
+  sources = [ "main.cc" ]
+  deps = [
+    "$dir_pw_board_led",
+    "$dir_pw_spin_delay",
+    dir_pw_log,
+
+    # RPC related dependencies.
+    "$dir_pw_hdlc:pw_rpc",
+    "$dir_pw_hdlc:rpc_channel_output",
+    "$dir_pw_rpc:server",
+    "$dir_pw_stream:sys_io_stream",
+    dir_pw_hdlc,
+
+    # This is the service we are implementing!
+    ":remoticon_service_nanopb",
+
+    # This is an example service that just responds with that it was sent.
+    "$dir_pw_rpc/nanopb:echo_service",
+  ]
+}
+
+pw_tokenizer_database("tokenizer_database") {
+  database = "tokenizer_database.csv"
+  targets = [ ":rpc" ]
+}
+
+################################################################################
+# Service
+
+pw_proto_library("remoticon_proto") {
+  sources = [ "remoticon_proto/remoticon.proto" ]
+}
+
+pw_source_set("remoticon_service_nanopb") {
+  public_configs = [ ":default_config" ]
+  public_deps = [ ":remoticon_proto.nanopb_rpc" ]
+  public = [ "public/remoticon/remoticon_service_nanopb.h" ]
+  deps = [ ":remoticon_proto.nanopb_rpc" ]
+  sources = [ "remoticon_service_nanopb.cc" ]
+}
diff --git a/applications/rpc/README.md b/applications/rpc/README.md
new file mode 100644
index 0000000..752941a
--- /dev/null
+++ b/applications/rpc/README.md
@@ -0,0 +1,89 @@
+# RPC
+
+|||---|||
+
+*** aside
+#### [00: <br/> Setup](/workshop/README.md)
+
+`Intro + setup.`
+***
+
+*** aside
+#### [01: <br/> Blinky](/workshop/01-blinky/README.md)
+
+`Getting to blinky.`
+***
+
+*** aside
+#### [02: <br/> Testing](/workshop/02-string-functions/README.md)
+
+`Writing tests.`
+***
+
+*** promo
+#### [03: <br/> RPC](/workshop/03-rpc/README.md)
+
+`Calling RPCs.`
+***
+
+*** aside
+#### [04: <br/> KVS](/workshop/04-kvs/README.md)
+
+`Key Value Store.`
+***
+
+*** aside
+#### [05: <br/> FactoryTest](/workshop/05-factory-test/README.md)
+
+`Testing in the factory.`
+***
+
+|||---|||
+
+[TOC]
+
+## Build and Flash
+
+Instructions are the same as flashing [blinky](/workshop/01-blinky/README.md)
+but passing in a different `.elf`.
+
+1. Run the compile with `pw watch out` or `ninja -C out`.
+
+1. Flash `rpc.elf`.
+
+   **Teensy**
+
+   ```sh
+   arduino_unit_test_runner --config out/arduino_debug/gen/arduino_builder_config.json --upload-tool teensyloader --verbose --flash-only out/arduino_debug/obj/workshop/03-rpc/bin/rpc.elf
+   ```
+
+   **stm32f429i_disc1**
+
+   ```sh
+   openocd -s ${PW_PIGWEED_CIPD_INSTALL_DIR}/share/openocd/scripts -f ${PW_ROOT}/targets/stm32f429i-disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg -c "program out/arduino_debug/obj/workshop/03-rpc/bin/rpc.elf reset exit"
+   ```
+
+## View HDLC Encoded Log Output
+
+1. **Optional:** Create / update the log token database. This will be
+   automatically updated when compiling.
+
+   ```sh
+   python -m pw_tokenizer.database create --force --database workshop/03-rpc/tokenizer_database.csv out/arduino_debug/obj/workshop/03-rpc/bin/rpc.elf
+   ```
+
+1. Start the rpc_console that saves log output to a file.
+
+   ```sh
+   python -m pw_hdlc_lite.rpc_console -o logfile.txt -d /dev/ttyACM0 third_party/pigweed/pw_rpc/pw_rpc_protos/echo.proto workshop/03-rpc/remoticon_proto/remoticon.proto
+   ```
+
+   This will launch an interactive `ipython` console where you can send
+   RPCs. Try `rpcs.pw.rpc.EchoService.Echo(msg="hello!")`. IPython provides nice
+   tab completion for the RPC interfaces as well.
+
+1. Tail the log output.
+
+   ```sh
+   python -m pw_tokenizer.detokenize base64 workshop/03-rpc/tokenizer_database.csv -i logfile.txt --follow
+   ```
diff --git a/applications/rpc/main.cc b/applications/rpc/main.cc
new file mode 100644
index 0000000..c9ea5f0
--- /dev/null
+++ b/applications/rpc/main.cc
@@ -0,0 +1,192 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include <array>
+#include <span>
+#include <string_view>
+
+#include "pw_board_led/led.h"
+#include "pw_hdlc/encoder.h"
+#include "pw_hdlc/rpc_channel.h"
+#include "pw_hdlc/rpc_packets.h"
+#include "pw_log/log.h"
+#include "pw_rpc/echo_service_nanopb.h"
+#include "pw_rpc/server.h"
+#include "pw_spin_delay/delay.h"
+#include "pw_stream/sys_io_stream.h"
+#include "remoticon/remoticon_service_nanopb.h"
+
+// ------------------- superloop data -------------------
+// This is some "application" state that eventually exported by RPCs.
+
+unsigned superloop_iterations;
+
+// ------------------- pw_rpc subsystem setup -------------------
+// There are multiple ways to plumb pw_rpc in your product. In the future,
+// Pigweed may offer an optional pre-canned setup; but for now, you must
+// manually snap together the modular pieces.
+//
+// The RPC system in this code is layered as:
+//
+//   UART --> pw_sys_io ------> hdlc -------> pw_rpc
+//   (phy)                   (transport)
+//
+// HDLC converts the raw UART/serial byte stream into a packet stream. Then RPC
+// operates at the packet level.
+//
+// This is just one way to configure pw_rpc, which is designed to be flexible
+// and work over wh atever physical or logical transport you have available.
+
+constexpr size_t kMaxTransmissionUnit = 256;  // bytes
+
+// Used to write HDLC data to pw::sys_io. This is an implementation of the
+// pw::stream::Stream interface.
+pw::stream::SysIoWriter sys_io_writer;
+
+// Set up the output channel for the pw_rpc server to use. This one happens to
+// implement the packet in / packet out with HDLC. pw_rpc can use any
+// ChannelOptput implementation, including custom ones for your product.
+pw::hdlc::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
+    sys_io_writer, pw::hdlc::kDefaultRpcAddress, "HDLC channel");
+
+// A pw::rpc::Server can have multiple channels (e.g. a UART and a BLE
+// connection). In this case, there is only one (HDLC over UART).
+pw::rpc::Channel channels[] = {
+    pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+
+// Declare the pw_rpc server with the HDLC channel.
+pw::rpc::Server server(channels);
+
+// Declare a buffer for decoding incoming HDLC frames.
+std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+
+// Decoder object consumes bytes and return if a HDLC packet was completed.
+pw::hdlc::Decoder hdlc_decoder(input_buffer);
+
+// ------------------- pw_rpc service registration  -------------------
+pw::rpc::EchoService echo_service;
+remoticon::SuperloopService superloop_service(superloop_iterations);
+
+// TODO FOR WORKSHOP: Declare your service object here!
+
+void RegisterServices() {
+  server.RegisterService(echo_service);
+  server.RegisterService(superloop_service);
+  // TODO FOR WORKSHOP: Register your service here!
+}
+
+// ------------------- pw_rpc  -------------------
+
+constexpr unsigned kHdlcChannelForRpc = pw::hdlc::kDefaultRpcAddress;
+constexpr unsigned kHdlcChannelForLogs = 1;
+
+void ParseByteFromUartAndHandleRpcs() {
+  // Read a byte from the UART if one is available; if not, bail.
+  std::byte data;
+  if (!pw::sys_io::TryReadByte(&data).ok()) {
+    return;
+  }
+
+  // Byte received. Send the byte to the HDLC decoder; see if a packet finished.
+  auto result = hdlc_decoder.Process(data);
+
+  // Packet didn't parse correctly, so ignore it. In production, this should
+  // perhaps log or increment a metric (see pw_metric) to track bad packets.
+  if (!result.ok()) {
+    // POST-WORKSHOP EXERCISE: Add a tracking metric for bad packets, and
+    // expose the metric via the pw_metric RPC service. This will require
+    // making some metrics objects, incrementing them; then creating and
+    // registering a metric RPC service.
+    //
+    // See https://pigweed.dev/pw_metric/
+    //     https://pigweed.dev/pw_metric/#exporting-metrics
+    return;
+  }
+
+  PW_LOG_INFO("Got complete HDLC packet");
+
+  // A frame was completed.
+  pw::hdlc::Frame& hdlc_frame = result.value();
+  if (hdlc_frame.address() != kHdlcChannelForRpc) {
+    // We ignore frames that are for unknown addresses, but you could put
+    // some code here if you wanted to stream custom data from PC --> device.
+    PW_LOG_WARN("Got packet with no destination; address: %d",
+                hdlc_frame.address());
+    return;
+  }
+
+  // Packet was validated and correct (CRC, etc); so send it to the RPC server.
+  // The RPC server may send response packets before returning from this call.
+  server.ProcessPacket(hdlc_frame.data(), hdlc_channel_output);
+}
+
+// TODO FOR WORKSHOP: Add an RPC to change the blink time.
+int state = 0;
+int counter = 0;
+// TODO FOR WORKSHOP: Change this value. 5M is good for Teensy 4.0; what value
+// is good for the Discovery?
+int counter_max = 5'000'000;
+
+void Blink() {
+  // Toggle the state if needed.
+  counter += 1;
+  // PW_LOG_INFO("Counter: %d", counter);
+  if (counter < counter_max) {
+    // Haven't hit a toggle event yet; bail.
+    return;
+  }
+  state = 1 - state;
+  counter = 0;
+
+  if (state == 0) {
+    PW_LOG_INFO("Blink High!");
+    pw::board_led::TurnOn();
+  } else {
+    PW_LOG_INFO("Blink Low!");
+    pw::board_led::TurnOff();
+  }
+}
+
+// TODO FOR WORKSHOP: Why doesn't this work, when of Blink() above does?
+void BlinkNoWorky() {
+  PW_LOG_INFO("Blink High!");
+  pw::board_led::TurnOn();
+  pw::spin_delay::WaitMillis(1000);
+
+  PW_LOG_INFO("Blink Low!");
+  pw::board_led::TurnOff();
+  pw::spin_delay::WaitMillis(1000);
+}
+
+int main() {
+  pw::board_led::Init();
+
+  PW_LOG_INFO("Registering pw_rpc services");
+  RegisterServices();
+
+  // Superloop!
+  while (true) {
+    // Toggle the LED if needed.
+    Blink();
+    // BlinkNoWorky();  // Pop quiz: This doesn't work. Why?
+
+    // Examine incoming serial byte; if a packet finished, send it to RPC.
+    ParseByteFromUartAndHandleRpcs();
+
+    // Increment the number of iterations.
+    superloop_iterations++;
+  }
+
+  return 0;
+}
diff --git a/applications/rpc/public/remoticon/remoticon_service_nanopb.h b/applications/rpc/public/remoticon/remoticon_service_nanopb.h
new file mode 100644
index 0000000..fa811b2
--- /dev/null
+++ b/applications/rpc/public/remoticon/remoticon_service_nanopb.h
@@ -0,0 +1,45 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+#pragma once
+
+#include <cstring>
+#include <span>
+
+#include "pw_status/status.h"
+#include "remoticon_proto/remoticon.rpc.pb.h"
+
+namespace remoticon {
+
+class SuperloopService final : public generated::Superloop<SuperloopService> {
+ public:
+  SuperloopService(unsigned& loop_iterations)
+      : loop_iterations_(loop_iterations) {}
+
+  // RPC method - this is exposed through the RPC server once the service is
+  // registered.
+  pw::Status GetStats(ServerContext&,
+                      const remoticon_StatsRequest& request,
+                      remoticon_StatsResponse& response);
+
+  // TODO FOR WORKSHOP: Add blink control.
+
+ private:
+  // Points to the super loop iteration count; this is updated externally.
+  //
+  // Note: In production storing an unsynchronized pointer is not a great
+  // pattern (no synchronization, no accessor/encapsulation).
+  unsigned& loop_iterations_;
+};
+
+}  // namespace remoticon
diff --git a/applications/rpc/remoticon_proto/remoticon.proto b/applications/rpc/remoticon_proto/remoticon.proto
new file mode 100644
index 0000000..0abf925
--- /dev/null
+++ b/applications/rpc/remoticon_proto/remoticon.proto
@@ -0,0 +1,37 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+syntax = "proto3";
+
+package remoticon;
+
+// -------- A simple example RPC service ----
+message StatsRequest {}
+
+message StatsResponse {
+  uint32 loop_iterations = 1;
+}
+
+// For interacting with the system superloop.
+service Superloop {
+  rpc GetStats(StatsRequest) returns (StatsResponse) {}
+}
+
+// ------------------------------------------
+// TODO FOR WORKSHOP:
+//
+//   Turn the LED on
+//   Turn the LED off
+//   Change blink time
+//
+// service LedService { ... }
diff --git a/applications/rpc/remoticon_service_nanopb.cc b/applications/rpc/remoticon_service_nanopb.cc
new file mode 100644
index 0000000..554844e
--- /dev/null
+++ b/applications/rpc/remoticon_service_nanopb.cc
@@ -0,0 +1,42 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "remoticon/remoticon_service_nanopb.h"
+
+#include <cstring>
+#include <span>
+
+#include "pw_status/status.h"
+
+namespace remoticon {
+
+pw::Status SuperloopService::GetStats(
+    ServerContext&,
+    const remoticon_StatsRequest& /* request */,
+    remoticon_StatsResponse& response) {
+  // Send back the number of superloop iterations by setting the field in the
+  // response proto. In this case, the request proto is unused.
+  response.loop_iterations = loop_iterations_;
+
+  // pw_rpc's surrounding code handles serializing the nanopb StatsResponse
+  // struct and sending it out.
+  //
+  // Note: pw_rpc also supports raw methods where you are responsible for
+  // serializing and deserializing the request & response.
+  return pw::OkStatus();
+}
+
+// TODO FOR WORKSHOP: Implement methods for your service here!
+
+}  // namespace remoticon
diff --git a/applications/rpc/tokenizer_database.csv b/applications/rpc/tokenizer_database.csv
new file mode 100644
index 0000000..3540a40
--- /dev/null
+++ b/applications/rpc/tokenizer_database.csv
@@ -0,0 +1,28 @@
+002007e1,          ," "
+0f831860,          ,"Failed to decode request payload from channel %u"
+0fbd23ba,          ,"Failed to send response packet for channel %u"
+130e2c4b,          ,"Received incomplete packet on interface %s"
+1ea870fb,          ,"Blink Low!"
+2bc3cb0f,          ,"          The current allocated heap memory is %u bytes."
+39b2c339,          ,"Failed to decode packet on interface %s"
+3f774ea7,          ,"Unable to handle packet of type %u"
+5a592684,          ,"          malloc() is called %u times. (realloc()/calloc() counted as one time)"
+6271ad9a,          ,"Got complete packet!"
+74627b3f,          ,"Received CANCEL packet for method that is not pending"
+92a63809,          ,"Received %lu-byte frame; frame must be at least 6 bytes"
+974389bc,          ,"Got packet with no destination; address: %d"
+9dcacd5c,          ,"          The cumulative allocated heap memory is %u bytes."
+aecb244f,          ,"Frame size [%lu] exceeds the maximum buffer size [%lu]"
+af49fb70,          ,"Blink High!"
+b0ebaaab,          ,"Registering pw_rpc services"
+b12fd0ac,          ,"Starting pw_rpc server"
+b3e37df6,          ,"Frame check sequence verification failed"
+be290f47,          ,"          free() is called %u times. (realloc() counted as one time)"
+c0c64a7a,          ,"Got complete HDLC packet"
+c51612b3,          ,"Failed to encode response packet to channel buffer"
+c5792a96,          ,"    The current heap information: "
+d2d5d9aa,          ,"Failed to encode response packet for channel %u"
+da03df49,          ,"          The cumulative freed heap memory is %u bytes."
+e457fb7d,          ,"          The total heap size is %u bytes."
+f716dbeb,          ,"[ ]"
+fc0376f5,          ,"[*]"
diff --git a/applications/strings/BUILD.gn b/applications/strings/BUILD.gn
new file mode 100644
index 0000000..7974be5
--- /dev/null
+++ b/applications/strings/BUILD.gn
@@ -0,0 +1,59 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_tokenizer/database.gni")
+import("$dir_pw_unit_test/test.gni")
+
+group("all") {
+  deps = [ ":string_demo" ]
+
+  # Build tokenizer_database for elf binaries only
+  if (host_os == "linux" || current_toolchain != "//targets/host:host_debug") {
+    deps += [ ":tokenizer_database" ]
+  }
+}
+
+pw_executable("string_demo") {
+  sources = [ "string_demo.cc" ]
+  deps = [
+    "$dir_pw_board_led",
+    "$dir_pw_hex_dump",
+    "$dir_pw_log",
+    "$dir_pw_spin_delay",
+    "$dir_pw_string",
+  ]
+}
+
+pw_source_set("system_status") {
+  public = [ "system_status.h" ]
+  public_deps = [ "$dir_pw_string" ]
+  sources = [ "system_status.cc" ]
+}
+
+pw_tokenizer_database("tokenizer_database") {
+  database = "tokenizer_database.csv"
+  targets = [ ":string_demo" ]
+}
+
+pw_test("system_status_test") {
+  deps = [ ":system_status" ]
+  sources = [ "system_status_test.cc" ]
+}
+
+pw_test_group("tests") {
+  tests = [ ":system_status_test" ]
+}
diff --git a/applications/strings/README.md b/applications/strings/README.md
new file mode 100644
index 0000000..5978bc4
--- /dev/null
+++ b/applications/strings/README.md
@@ -0,0 +1,151 @@
+# Unit Tests & Strings
+
+|||---|||
+
+*** aside
+#### [00: <br/> Setup](/workshop/README.md)
+
+`Intro + setup.`
+***
+
+*** aside
+#### [01: <br/> Blinky](/workshop/01-blinky/README.md)
+
+`Getting to blinky.`
+***
+
+*** promo
+#### [02: <br/> Testing](/workshop/02-string-functions/README.md)
+
+`Writing tests.`
+***
+
+*** aside
+#### [03: <br/> RPC](/workshop/03-rpc/README.md)
+
+`Calling RPCs.`
+***
+
+*** aside
+#### [04: <br/> KVS](/workshop/04-kvs/README.md)
+
+`Key Value Store.`
+***
+
+*** aside
+#### [05: <br/> FactoryTest](/workshop/05-factory-test/README.md)
+
+`Testing in the factory.`
+***
+
+|||---|||
+
+[TOC]
+
+## Build and Flash
+
+Instructions are the same as flashing [blinky](/workshop/01-blinky/README.md)
+but passing in a different `.elf`.
+
+1. Run the compile with `pw watch out` or `ninja -C out`.
+
+1. Flash the test `.elf`
+
+   ```sh
+   arduino_unit_test_runner --config out/arduino_debug/gen/arduino_builder_config.json --upload-tool teensyloader --verbose --flash-only out/arduino_debug/obj/workshop/02-string-functions/bin/string_demo.elf
+   ```
+
+1. Tail the output with `miniterm`, (use `Ctrl-]` to quit).
+
+   ```sh
+   python -m serial.tools.miniterm --raw - 115200
+   ```
+
+   You should see something like:
+
+   ```text
+   $ python -m serial.tools.miniterm --raw - 115200
+   --- Available ports:
+   ---  1: /dev/ttyACM0         'USB Serial'
+   --- Enter port index or full name: 1
+   --- Miniterm on /dev/ttyACM0  115200,8,N,1 ---
+   --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
+   INF  Teensy Time: RTC has set the system time.
+   INF  Offs.  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  Text
+   INF  0000: 53 75 70 65 72 20 53 69 6d 70 6c 65 20 54 69 6d  Super Simple Tim
+   INF  0010: 65 20 4c 6f 67 67 69 6e 67 21 00                 e Logging!.
+   INF  2020-11-04 14:35:35
+   INF  2020-11-04 14:35:36
+   INF  2020-11-04 14:35:37
+   INF  2020-11-04 14:35:38
+   INF  2020-11-04 14:35:39
+
+   --- exit ---
+   ```
+
+   All tests are set to use plain text logging. This is specified by the
+   `pw_log_BACKEND` variable in the `target_toolchain.gni` files. For example
+   the `arduino_debug_tests` toolchain in
+   [`//targets/arduino/target_toolchains.gni`](/targets/arduino/target_toolchains.gni)
+   defines: `pw_log_BACKEND = "$dir_pw_log_basic"`
+
+## Exercise
+
+**Goal:** Write a test for the
+[`GetStatusString()`](/workshop/02-string-functions/system_status.cc#9)
+function.
+
+It can be added to
+[/workshop/02-string-functions/system_status_test.cc](/workshop/02-string-functions/system_status_test.cc).
+
+*** promo
+- Refer to the [gTest string comparison
+  documentation](https://github.com/google/googletest/blob/master/googletest/docs/primer.md#string-comparison)
+  for how to check the contents of strings.
+- Note that [gMock
+  matchers](https://github.com/google/googletest/blob/master/googletest/docs/advanced.md#more-string-assertions)
+  are not supported in Pigweed yet.
+***
+
+## Runing Tests
+
+### Run a Single Test Manually
+
+After compiling use `arduino_unit_test_runner` to flash and check test output.
+
+   **Teensy**
+
+   ```sh
+   arduino_unit_test_runner --config out/arduino_debug/gen/arduino_builder_config.json --upload-tool teensyloader --verbose out/arduino_debug/obj/workshop/02-string-functions/test/system_status_test.elf
+   ```
+
+   **stm32f429i_disc1**
+
+   ```sh
+   stm32f429i_disc1_unit_test_runner --verbose out/stm32f429i_disc1_debug/obj/workshop/02-string-functions/test/system_status_test.elf
+   ```
+
+
+### Using the Test Server.
+
+1. Make sure the `pw_arduino_use_test_server=true` build arg is set.
+
+1. Start the unit test server for your board.
+
+   **Teensy**
+
+   ```sh
+   arduino_test_server --verbose --config-file ./out/arduino_debug/gen/arduino_builder_config.json
+   ```
+
+   **stm32f429i_disc1**
+
+   ```sh
+   stm32f429i_disc1_test_server --verbose
+   ```
+
+1. In a separate terminal start pw watch.
+
+   ```sh
+   pw watch out
+   ```
diff --git a/applications/strings/string_demo.cc b/applications/strings/string_demo.cc
new file mode 100644
index 0000000..27be3f9
--- /dev/null
+++ b/applications/strings/string_demo.cc
@@ -0,0 +1,61 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include <array>
+#include <span>
+
+#include "pw_board_led/led.h"
+#include "pw_hex_dump/hex_dump.h"
+#include "pw_log/log.h"
+#include "pw_spin_delay/delay.h"
+#include "pw_string/format.h"
+#include "pw_string/string_builder.h"
+
+int main() {
+  pw::board_led::Init();
+
+  const char my_data[] = "Super Simple Status Logging";
+  std::array<char, 80> hex_dump_buffer;
+  pw::dump::FormattedHexDumper hex_dumper(hex_dump_buffer);
+  hex_dumper.BeginDump(std::as_bytes(std::span(my_data)));
+  while (hex_dumper.DumpLine().ok()) {
+    PW_LOG_INFO("%s", hex_dump_buffer.data());
+  }
+
+  unsigned seconds = 0;
+  unsigned update_count = 0;
+
+  char buffer[64];
+  while (true) {
+    pw::string::Format(buffer,
+                       "[%d-%02d-%02d %02d:%02d:%02d] Message number: %u",
+                       2020,
+                       11,
+                       8,
+                       14,
+                       15,
+                       seconds,
+                       update_count);
+
+    PW_LOG_INFO("%s", buffer);
+
+    pw::board_led::TurnOn();
+    pw::spin_delay::WaitMillis(1000);
+
+    seconds = (seconds + 1) % 60;
+    update_count = (update_count + 1) % 65535;
+  }
+
+  return 0;
+}
diff --git a/applications/strings/system_status.cc b/applications/strings/system_status.cc
new file mode 100644
index 0000000..0f2795f
--- /dev/null
+++ b/applications/strings/system_status.cc
@@ -0,0 +1,16 @@
+#include "system_status.h"
+
+#include <cinttypes>
+
+#include "pw_string/string_builder.h"
+
+namespace system_status {
+
+const char* GetStatusString(unsigned led_state) {
+  pw::StringBuffer<64> sb;
+  sb << "[SystemStatus] ";
+  sb.Format("LED: %02u", led_state);
+  return sb.data();
+}
+
+}  // namespace system_status
diff --git a/applications/strings/system_status.h b/applications/strings/system_status.h
new file mode 100644
index 0000000..ab1ff11
--- /dev/null
+++ b/applications/strings/system_status.h
@@ -0,0 +1,20 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+#pragma once
+
+namespace system_status {
+
+const char* GetStatusString(unsigned led_state);
+
+}  // namespace system_status
diff --git a/applications/strings/system_status_test.cc b/applications/strings/system_status_test.cc
new file mode 100644
index 0000000..9e2382d
--- /dev/null
+++ b/applications/strings/system_status_test.cc
@@ -0,0 +1,27 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "system_status.h"
+
+#include "gtest/gtest.h"
+#include "pw_string/string_builder.h"
+
+namespace system_status {
+
+TEST(GetStatusString, LEDState) {
+  // TODO: Add test here!
+  EXPECT_TRUE(true);
+}
+
+}  // namespace system_status
diff --git a/applications/strings/tokenizer_database.csv b/applications/strings/tokenizer_database.csv
new file mode 100644
index 0000000..64799f0
--- /dev/null
+++ b/applications/strings/tokenizer_database.csv
@@ -0,0 +1,13 @@
+002007e1,          ," "
+1ea870fb,          ,"Blink Low!"
+2bc3cb0f,          ,"          The current allocated heap memory is %u bytes."
+38c60010,          ,"%s"
+5a592684,          ,"          malloc() is called %u times. (realloc()/calloc() counted as one time)"
+9dcacd5c,          ,"          The cumulative allocated heap memory is %u bytes."
+af49fb70,          ,"Blink High!"
+be290f47,          ,"          free() is called %u times. (realloc() counted as one time)"
+c5792a96,          ,"    The current heap information: "
+da03df49,          ,"          The cumulative freed heap memory is %u bytes."
+e457fb7d,          ,"          The total heap size is %u bytes."
+f716dbeb,          ,"[ ]"
+fc0376f5,          ,"[*]"
diff --git a/banner.txt b/banner.txt
new file mode 100644
index 0000000..7a6b653
--- /dev/null
+++ b/banner.txt
@@ -0,0 +1,2 @@
+   ╱╱╱  █▀█ █░█░█   █▀▀ ▀▄▀ █▀█ █▀▀ █▀█ █ █▀▄▀█ █▀▀ █▄░█ ▀█▀ ▄▀█ █░░
+  ╱╱╱   █▀▀ ▀▄▀▄▀   ██▄ █░█ █▀▀ ██▄ █▀▄ █ █░▀░█ ██▄ █░▀█ ░█░ █▀█ █▄▄
diff --git a/bootstrap.bat b/bootstrap.bat
new file mode 100644
index 0000000..89d9e1b
--- /dev/null
+++ b/bootstrap.bat
@@ -0,0 +1,50 @@
+:<<"::WINDOWS_ONLY"
+@echo off
+:: Copyright 2021 The Pigweed Authors
+::
+:: 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.
+::WINDOWS_ONLY
+:; echo "ERROR: Attempting to run Windows .bat from a Unix/POSIX shell!"
+:; echo "Instead, run the following command."
+:; echo ""
+:; echo "    source ./activate.sh"
+:; echo ""
+:<<"::WINDOWS_ONLY"
+
+:: The bootstrap.bat must be run initially to install all required programs.
+:: After that, use activate.bat to enter the environment in a shell.
+
+:: First, activate the Pigweed development environment.
+set "_pw_bootstrap_script=%~dp0.\third_party\pigweed\bootstrap.bat"
+set "PW_PROJECT_ROOT=%~dp0."
+set "PIGWEED_EXPERIMENTAL_ROOT=%PW_PROJECT_ROOT%"
+
+:: Set your project's banner and color.
+set "PW_BRANDING_BANNER=%PW_PROJECT_ROOT%\banner.txt"
+set "PW_BRANDING_BANNER_COLOR=cyan"
+
+if not exist "%_pw_bootstrap_script%" (
+  echo Error: "%_pw_bootstrap_script%" not found.
+  echo Did you forget to initialize the git submodules?
+  echo To setup the git submodules run:
+  echo   git submodule init
+  echo   git submodule update
+  goto finish
+)
+
+call "%_pw_bootstrap_script%"
+
+:: Add user-defined initial setup here.
+
+:finish
+::WINDOWS_ONLY
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100755
index 0000000..6cd0205
--- /dev/null
+++ b/bootstrap.sh
@@ -0,0 +1,141 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+# This script must be sourced, not executed.
+#
+# Create a new environment:
+#
+#   source bootstrap.sh
+#
+# Activate an existing environment:
+#
+#   source activate.sh
+
+_bootstrap_abspath () {
+  python -c "import os.path; print(os.path.abspath('$@'))"
+}
+
+# Users are not expected to set PW_CHECKOUT_ROOT, it's only used because it
+# seems to be impossible to reliably determine the path to a sourced file in
+# dash when sourced from a dash script instead of a dash interactive prompt.
+# To reinforce that users should not be using PW_CHECKOUT_ROOT, it is cleared
+# here after it is used, and other pw tools will complain if they see that
+# variable set.
+# TODO(mohrr) find out a way to do this without PW_CHECKOUT_ROOT.
+if test -n "$PW_CHECKOUT_ROOT"; then
+  _BOOTSTRAP_PATH="$(_bootstrap_abspath "$PW_CHECKOUT_ROOT/bootstrap.sh")"
+  unset PW_CHECKOUT_ROOT
+# Shell: bash.
+elif test -n "$BASH"; then
+  _BOOTSTRAP_PATH="$(_bootstrap_abspath "$BASH_SOURCE")"
+# Shell: zsh.
+elif test -n "$ZSH_NAME"; then
+  _BOOTSTRAP_PATH="$(_bootstrap_abspath "${(%):-%N}")"
+# Shell: dash.
+elif test ${0##*/} = dash; then
+  _BOOTSTRAP_PATH="$(_bootstrap_abspath \
+    "$(lsof -p $$ -Fn0 | tail -1 | sed 's#^[^/]*##;')")"
+# If everything else fails, try $0. It could work.
+else
+  _BOOTSTRAP_PATH="$(_bootstrap_abspath "$0")"
+fi
+
+# Check if this file is being executed or sourced.
+_pw_sourced=0
+if [ -n "$SWARMING_BOT_ID" ]; then
+  # If set we're running on swarming and don't need this check.
+  _pw_sourced=1
+elif [ -n "$ZSH_EVAL_CONTEXT" ]; then
+  case $ZSH_EVAL_CONTEXT in *:file) _pw_sourced=1;; esac
+elif [ -n "$KSH_VERSION" ]; then
+  [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != \
+    "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] \
+    && _pw_sourced=1
+elif [ -n "$BASH_VERSION" ]; then
+  (return 0 2>/dev/null) && _pw_sourced=1
+else  # All other shells: examine $0 for known shell binary filenames
+  # Detects `sh` and `dash`; add additional shell filenames as needed.
+  case ${0##*/} in sh|dash) _pw_sourced=1;; esac
+fi
+
+# Downstream projects need to set something other than PW_ROOT here, like
+# YOUR_PROJECT_ROOT. Please also set PW_PROJECT_ROOT and PW_ROOT before
+# invoking pw_bootstrap or pw_activate.
+######### BEGIN PROJECT-SPECIFIC CODE #########
+PIGWEED_EXPERIMENTAL_ROOT="$(_bootstrap_abspath "$(dirname "$_BOOTSTRAP_PATH")")"
+export PIGWEED_EXPERIMENTAL_ROOT
+PW_PROJECT_ROOT="$PIGWEED_EXPERIMENTAL_ROOT"
+PW_ROOT="$PIGWEED_EXPERIMENTAL_ROOT/third_party/pigweed"
+
+# Set your project's banner and color.
+export PW_BRANDING_BANNER="$PIGWEED_EXPERIMENTAL_ROOT/banner.txt"
+export PW_BRANDING_BANNER_COLOR=magenta
+
+PIGWEED_EXPERIMENTAL_banner() {
+  cat "$PW_BRANDING_BANNER"
+  echo
+}
+
+PW_BANNER_FUNC="PIGWEED_EXPERIMENTAL_banner"
+########## END PROJECT-SPECIFIC CODE ##########
+export PW_BANNER_FUNC
+export PW_PROJECT_ROOT
+export PW_ROOT
+
+_util_sh="$PW_ROOT/pw_env_setup/util.sh"
+
+# Double-check that the Pigweed submodule has been checked out.
+if [ ! -e "$_util_sh" ]; then
+  echo "Error: $_util_sh not found."
+  echo "Did you forget to initialize the git submodules?"
+  echo "To setup the git submodules run:"
+  echo "  git submodule update --init"
+  return
+fi
+
+. "$_util_sh"
+
+pw_deactivate
+pw_eval_sourced "$_pw_sourced"
+pw_check_root "$PW_ROOT"
+_PW_ACTUAL_ENVIRONMENT_ROOT="$(pw_get_env_root)"
+export _PW_ACTUAL_ENVIRONMENT_ROOT
+SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
+
+# Run full bootstrap when invoked as bootstrap, or env file is missing/empty.
+if [ "$(basename "$_BOOTSTRAP_PATH")" = "bootstrap.sh" ] || \
+  [ ! -f "$SETUP_SH" ] || \
+  [ ! -s "$SETUP_SH" ]; then
+# This is where pw_bootstrap is called. Most small projects will include
+# --use-pigweed-defaults.
+######### BEGIN PROJECT-SPECIFIC CODE #########
+  pw_bootstrap --shell-file "$SETUP_SH" --install-dir "$_PW_ACTUAL_ENVIRONMENT_ROOT" --use-pigweed-defaults --virtualenv-gn-target "$PW_PROJECT_ROOT#:python.install" --virtualenv-gn-out-dir "$PW_PROJECT_ROOT/out"
+########## END PROJECT-SPECIFIC CODE ##########
+  pw_finalize bootstrap "$SETUP_SH"
+else
+  pw_activate
+  pw_finalize activate "$SETUP_SH"
+fi
+
+# This is where any additional checks about the environment should go.
+######### BEGIN PROJECT-SPECIFIC CODE #########
+########## END PROJECT-SPECIFIC CODE ##########
+
+unset _pw_sourced
+unset _BOOTSTRAP_PATH
+unset SETUP_SH
+unset _bootstrap_abspath
+unset _util_sh
+
+pw_cleanup
diff --git a/build_overrides/pigweed.gni b/build_overrides/pigweed.gni
new file mode 100644
index 0000000..d627ec5
--- /dev/null
+++ b/build_overrides/pigweed.gni
@@ -0,0 +1,39 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+declare_args() {
+  # Location of the Pigweed repository.
+  dir_pigweed = "//third_party/pigweed/"
+}
+
+# Upstream Pigweed modules.
+import("$dir_pigweed/modules.gni")
+
+# Experimental modules.
+declare_args() {
+  dir_pw_board_led = get_path_info("//modules/pw_board_led", "abspath")
+  dir_pw_board_led_host =
+      get_path_info("//modules/pw_board_led_host", "abspath")
+  dir_pw_board_led_stm32f429i_disc1 =
+      get_path_info("//modules/pw_board_led_stm32f429i_disc1", "abspath")
+  dir_pw_board_led_arduino =
+      get_path_info("//modules/pw_board_led_arduino", "abspath")
+  dir_pw_spin_delay = get_path_info("//modules/pw_spin_delay", "abspath")
+  dir_pw_spin_delay_arduino =
+      get_path_info("//modules/pw_spin_delay_arduino", "abspath")
+  dir_pw_spin_delay_host =
+      get_path_info("//modules/pw_spin_delay_host", "abspath")
+  dir_pw_spin_delay_stm32f429i_disc1 =
+      get_path_info("//modules/pw_spin_delay_stm32f429i_disc1", "abspath")
+}
diff --git a/modules/pw_board_led/BUILD.gn b/modules/pw_board_led/BUILD.gn
new file mode 100644
index 0000000..bf09f23
--- /dev/null
+++ b/modules/pw_board_led/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/facade.gni")
+
+declare_args() {
+  # The backend (driver) to use for the board LED.
+  pw_board_led_BACKEND = ""
+}
+
+config("public_includes") {
+  include_dirs = [ "public" ]
+}
+
+pw_facade("pw_board_led") {
+  backend = pw_board_led_BACKEND
+  public_configs = [ ":public_includes" ]
+  public = [ "public/pw_board_led/led.h" ]
+}
diff --git a/modules/pw_board_led/public/pw_board_led/led.h b/modules/pw_board_led/public/pw_board_led/led.h
new file mode 100644
index 0000000..972a480
--- /dev/null
+++ b/modules/pw_board_led/public/pw_board_led/led.h
@@ -0,0 +1,25 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+#pragma once
+
+namespace pw::board_led {
+
+void Init();
+void TurnOn();
+void TurnOff();
+void Toggle();
+
+// TODO(amontanez): Maybe we should add a GetState()? 🤔
+
+}  // namespace pw::board_led
diff --git a/modules/pw_board_led_arduino/BUILD.gn b/modules/pw_board_led_arduino/BUILD.gn
new file mode 100644
index 0000000..c88215b
--- /dev/null
+++ b/modules/pw_board_led_arduino/BUILD.gn
@@ -0,0 +1,27 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_board_led_arduino") {
+  deps = [
+    "$dir_pw_board_led:pw_board_led.facade",
+    "$dir_pw_third_party/arduino:arduino_core_sources",
+  ]
+  sources = [ "led.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/modules/pw_board_led_arduino/led.cc b/modules/pw_board_led_arduino/led.cc
new file mode 100644
index 0000000..3f9a85f
--- /dev/null
+++ b/modules/pw_board_led_arduino/led.cc
@@ -0,0 +1,53 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_board_led/led.h"
+
+#include <Arduino.h>
+
+#include <cinttypes>
+
+namespace pw::board_led {
+namespace {
+
+constexpr int kLedPin = 13;
+bool led_on = false;
+
+}  // namespace
+
+void Init() {
+  pinMode(kLedPin, OUTPUT);
+  TurnOff();
+}
+
+void TurnOff() {
+  digitalWrite(kLedPin, LOW);
+  led_on = false;
+}
+
+void TurnOn() {
+  digitalWrite(kLedPin, HIGH);
+  led_on = true;
+}
+
+void Toggle() {
+  // Check if the LED is on. If so, turn it off.
+  if (led_on) {
+    TurnOff();
+  } else {
+    TurnOn();
+  }
+}
+
+}  // namespace pw::board_led
diff --git a/modules/pw_board_led_host/BUILD.gn b/modules/pw_board_led_host/BUILD.gn
new file mode 100644
index 0000000..0266972
--- /dev/null
+++ b/modules/pw_board_led_host/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_board_led_host") {
+  deps = [
+    "$dir_pw_board_led:pw_board_led.facade",
+    dir_pw_log,
+  ]
+  sources = [ "led.cc" ]
+}
diff --git a/modules/pw_board_led_host/led.cc b/modules/pw_board_led_host/led.cc
new file mode 100644
index 0000000..1aa8cf3
--- /dev/null
+++ b/modules/pw_board_led_host/led.cc
@@ -0,0 +1,49 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_board_led/led.h"
+
+#include <cinttypes>
+
+#include "pw_log/log.h"
+
+namespace pw::board_led {
+namespace {
+
+bool led_on = false;
+
+}  // namespace
+
+void Init() { TurnOff(); }
+
+void TurnOff() {
+  PW_LOG_INFO("[ ]");
+  led_on = false;
+}
+
+void TurnOn() {
+  PW_LOG_INFO("[*]");
+  led_on = true;
+}
+
+void Toggle() {
+  // Check if the LED is on. If so, turn it off.
+  if (led_on) {
+    TurnOff();
+  } else {
+    TurnOn();
+  }
+}
+
+}  // namespace pw::board_led
diff --git a/modules/pw_board_led_stm32f429i_disc1/BUILD.gn b/modules/pw_board_led_stm32f429i_disc1/BUILD.gn
new file mode 100644
index 0000000..65f6342
--- /dev/null
+++ b/modules/pw_board_led_stm32f429i_disc1/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_board_led_stm32f429i_disc1") {
+  deps = [
+    "$dir_pw_board_led:pw_board_led.facade",
+    dir_pw_preprocessor,
+  ]
+  sources = [ "led.cc" ]
+}
diff --git a/modules/pw_board_led_stm32f429i_disc1/led.cc b/modules/pw_board_led_stm32f429i_disc1/led.cc
new file mode 100644
index 0000000..8f5247a
--- /dev/null
+++ b/modules/pw_board_led_stm32f429i_disc1/led.cc
@@ -0,0 +1,108 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_board_led/led.h"
+
+#include <cinttypes>
+
+#include "pw_preprocessor/compiler.h"
+
+namespace pw::board_led {
+namespace {
+
+// Base address for everything peripheral-related on the STM32F4xx.
+constexpr uint32_t kPeripheralBaseAddr = 0x40000000u;
+// Base address for everything AHB1-related on the STM32F4xx.
+constexpr uint32_t kAhb1PeripheralBase = kPeripheralBaseAddr + 0x00020000U;
+// Base address for everything APB2-related on the STM32F4xx.
+constexpr uint32_t kApb2PeripheralBase = kPeripheralBaseAddr + 0x00010000U;
+
+// Reset/clock configuration block (RCC).
+// `reserved` fields are unimplemented features, and are present to ensure
+// proper alignment of registers that are in use.
+PW_PACKED(struct) RccBlock {
+  uint32_t reserved1[12];
+  uint32_t ahb1_config;
+  uint32_t reserved2[4];
+  uint32_t apb2_config;
+};
+
+// GPIO register block definition.
+PW_PACKED(struct) GpioBlock {
+  uint32_t modes;
+  uint32_t out_type;
+  uint32_t out_speed;
+  uint32_t pull_up_down;
+  uint32_t input_data;
+  uint32_t output_data;
+  uint32_t gpio_bit_set;
+  uint32_t port_config_lock;
+  uint32_t alt_low;
+  uint32_t alt_high;
+};
+
+// Constants related to GPIO mode register masks.
+constexpr uint32_t kGpioPortModeMask = 0x3u;
+constexpr uint32_t kGpio13PortModePos = 26;
+constexpr uint32_t kGpioPortModeOutput = 1;
+
+// Constants related to GPIO output mode register masks.
+constexpr uint32_t kGpioOutputModeMask = 0x1u;
+constexpr uint32_t kGpio13OutputModePos = 13;
+constexpr uint32_t kGpioOutputModePushPull = 0;
+
+constexpr uint32_t kGpio13BitSetHigh = 0x1u << 13;
+constexpr uint32_t kGpio13BitSetLow = kGpio13BitSetHigh << 16;
+
+// Mask for ahb1_config (AHB1ENR) to enable the "G" GPIO pins.
+constexpr uint32_t kGpioGEnable = 0x1u << 6;
+
+// Declare a reference to the memory mapped RCC block.
+volatile RccBlock& platform_rcc =
+    *reinterpret_cast<volatile RccBlock*>(kAhb1PeripheralBase + 0x3800U);
+
+// Declare a reference to the 'G' GPIO memory mapped block.
+volatile GpioBlock& gpio_g =
+    *reinterpret_cast<volatile GpioBlock*>(kAhb1PeripheralBase + 0x1800U);
+
+}  // namespace
+
+void Init() {
+  // Enable 'G' GIPO clocks.
+  platform_rcc.ahb1_config |= kGpioGEnable;
+
+  // Enable Pin 13 in output mode.
+  gpio_g.modes = (gpio_g.modes & ~(kGpioPortModeMask << kGpio13PortModePos)) |
+                 (kGpioPortModeOutput << kGpio13PortModePos);
+
+  // Enable Pin 13 in output mode "push pull"
+  gpio_g.out_type =
+      (gpio_g.out_type & ~(kGpioOutputModeMask << kGpio13OutputModePos)) |
+      (kGpioOutputModePushPull << kGpio13OutputModePos);
+}
+
+void TurnOff() { gpio_g.gpio_bit_set = kGpio13BitSetLow; }
+
+void TurnOn() { gpio_g.gpio_bit_set = kGpio13BitSetHigh; }
+
+void Toggle() {
+  // Check if the LED is on. If so, turn it off.
+  if (gpio_g.output_data & kGpio13BitSetHigh) {
+    TurnOff();
+  } else {
+    TurnOn();
+  }
+}
+
+}  // namespace pw::board_led
diff --git a/modules/pw_spin_delay/BUILD.gn b/modules/pw_spin_delay/BUILD.gn
new file mode 100644
index 0000000..41b9275
--- /dev/null
+++ b/modules/pw_spin_delay/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/facade.gni")
+
+declare_args() {
+  # The backend (driver) to use for the board LED.
+  pw_spin_delay_BACKEND = ""
+}
+
+config("public_includes") {
+  include_dirs = [ "public" ]
+}
+
+pw_facade("pw_spin_delay") {
+  backend = pw_spin_delay_BACKEND
+  public_configs = [ ":public_includes" ]
+  public = [ "public/pw_spin_delay/delay.h" ]
+}
diff --git a/modules/pw_spin_delay/public/pw_spin_delay/delay.h b/modules/pw_spin_delay/public/pw_spin_delay/delay.h
new file mode 100644
index 0000000..3e83651
--- /dev/null
+++ b/modules/pw_spin_delay/public/pw_spin_delay/delay.h
@@ -0,0 +1,25 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+#pragma once
+
+#include <cinttypes>
+#include <cstddef>
+
+namespace pw::spin_delay {
+
+void WaitMillis(size_t delay_ms);
+uint32_t Millis();
+uint32_t Micros();
+
+}  // namespace pw::spin_delay
diff --git a/modules/pw_spin_delay_arduino/BUILD.gn b/modules/pw_spin_delay_arduino/BUILD.gn
new file mode 100644
index 0000000..d7410cc
--- /dev/null
+++ b/modules/pw_spin_delay_arduino/BUILD.gn
@@ -0,0 +1,27 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_spin_delay_arduino") {
+  deps = [
+    "$dir_pw_spin_delay:pw_spin_delay.facade",
+    "$dir_pw_third_party/arduino:arduino_core_sources",
+  ]
+  sources = [ "delay.cc" ]
+  remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
diff --git a/modules/pw_spin_delay_arduino/delay.cc b/modules/pw_spin_delay_arduino/delay.cc
new file mode 100644
index 0000000..ccb0d77
--- /dev/null
+++ b/modules/pw_spin_delay_arduino/delay.cc
@@ -0,0 +1,27 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_spin_delay/delay.h"
+
+#include <Arduino.h>
+
+#include <cstddef>
+
+namespace pw::spin_delay {
+
+void WaitMillis(size_t delay_ms) { delay(delay_ms); }
+uint32_t Millis() { return millis(); }
+uint32_t Micros() { return micros(); }
+
+}  // namespace pw::spin_delay
diff --git a/modules/pw_spin_delay_host/BUILD.gn b/modules/pw_spin_delay_host/BUILD.gn
new file mode 100644
index 0000000..fea87c4
--- /dev/null
+++ b/modules/pw_spin_delay_host/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_spin_delay_host") {
+  deps = [ "$dir_pw_spin_delay:pw_spin_delay.facade" ]
+  sources = [ "delay.cc" ]
+}
diff --git a/modules/pw_spin_delay_host/delay.cc b/modules/pw_spin_delay_host/delay.cc
new file mode 100644
index 0000000..0e0c1c4
--- /dev/null
+++ b/modules/pw_spin_delay_host/delay.cc
@@ -0,0 +1,61 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_spin_delay/delay.h"
+
+#include <chrono>
+#include <cstddef>
+#include <cstdint>
+
+namespace {
+std::chrono::system_clock::time_point kProgramStart =
+    std::chrono::system_clock::now();
+}
+
+namespace pw::spin_delay {
+
+void WaitMillis(size_t delay_ms) {
+  std::chrono::system_clock::time_point start =
+      std::chrono::system_clock::now();
+  std::chrono::system_clock::time_point now;
+  size_t difference;
+  do {
+    now = std::chrono::system_clock::now();
+    difference =
+        std::chrono::duration_cast<std::chrono::milliseconds>(now - start)
+            .count();
+  } while (difference < delay_ms);
+}
+
+uint32_t Millis() {
+  std::chrono::system_clock::time_point now;
+  uint32_t difference;
+  now = std::chrono::system_clock::now();
+  difference =
+      std::chrono::duration_cast<std::chrono::milliseconds>(now - kProgramStart)
+          .count();
+  return difference;
+}
+
+uint32_t Micros() {
+  std::chrono::system_clock::time_point now;
+  uint32_t difference;
+  now = std::chrono::system_clock::now();
+  difference =
+      std::chrono::duration_cast<std::chrono::microseconds>(now - kProgramStart)
+          .count();
+  return difference;
+}
+
+}  // namespace pw::spin_delay
diff --git a/modules/pw_spin_delay_stm32f429i_disc1/BUILD.gn b/modules/pw_spin_delay_stm32f429i_disc1/BUILD.gn
new file mode 100644
index 0000000..6708b33
--- /dev/null
+++ b/modules/pw_spin_delay_stm32f429i_disc1/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("pw_spin_delay_stm32f429i_disc1") {
+  deps = [
+    "$dir_pw_spin_delay:pw_spin_delay.facade",
+    dir_pw_preprocessor,
+  ]
+  sources = [ "delay.cc" ]
+}
diff --git a/modules/pw_spin_delay_stm32f429i_disc1/delay.cc b/modules/pw_spin_delay_stm32f429i_disc1/delay.cc
new file mode 100644
index 0000000..780575b
--- /dev/null
+++ b/modules/pw_spin_delay_stm32f429i_disc1/delay.cc
@@ -0,0 +1,57 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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.
+
+#include "pw_spin_delay/delay.h"
+
+#include <cstddef>
+#include <cstdint>
+
+namespace pw::spin_delay {
+
+// !!!WARNING!!!: This delay is not truly accurate! It's mostly just a rough
+// estimate! Also, it only works in a baremetal context with no interrupts
+// getting in the way or threads getting CPU time.
+//
+// TODO(amontanez): Replace this implementation with a loop checking a
+// pw_chrono clock.
+void WaitMillis(size_t delay_ms) {
+  // Default core clock. This is technically not a constant, but since Pigweed
+  // doesn't change the system clock a constant will suffice.
+  constexpr uint32_t kSystemCoreClock = 16000000;
+  constexpr uint32_t kCyclesPerMs = kSystemCoreClock / 1000;
+
+  // This is not totally accurate, but is close enough.
+  for (size_t i = 0; i < delay_ms; i++) {
+    // Do a 4 instruction loop enough times to be running for a millisecond.
+    // This is set up with assembly rather than a regular loop to make the
+    // instruction count predictable (no compiler variation).
+    uint32_t cycles = kCyclesPerMs;
+    asm volatile(
+        " mov r0, %[cycles] \n"
+        " mov r1, #0        \n"
+        "loop:              \n"
+        " cmp r1, r0        \n"
+        " itt lt            \n"
+        " addlt r1, r1, #4  \n"
+        " blt loop          \n"
+        // clang-format off
+        : /*output=*/
+        : /*input=*/[cycles]"r"(cycles)
+        : /*clobbers=*/"r0", "r1"
+        // clang-format on
+    );
+  }
+}
+
+}  // namespace pw::spin_delay
diff --git a/navbar.md b/navbar.md
new file mode 100644
index 0000000..0324365
--- /dev/null
+++ b/navbar.md
@@ -0,0 +1,3 @@
+# Pigweed Experimental
+
+* [Home](/README.md)
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
new file mode 100644
index 0000000..200c021
--- /dev/null
+++ b/targets/arduino/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("toolchains") {
+  toolchains = toolchains_list
+}
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
new file mode 100644
index 0000000..d556802
--- /dev/null
+++ b/targets/arduino/target_toolchains.gni
@@ -0,0 +1,63 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("//targets/common_backends.gni")
+import("$dir_pigweed/targets/arduino/target_toolchains.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
+
+target_toolchain_arduino = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+
+  arduino_debug = {
+    name = "arduino_debug"
+    _toolchain_base = pw_target_toolchain_arduino.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Configure backend for pw_board_led
+      pw_board_led_BACKEND = "$dir_pw_board_led_arduino"
+
+      # Configure backend for pw_spin_delay
+      pw_spin_delay_BACKEND = "$dir_pw_spin_delay_arduino"
+    }
+  }
+
+  # Toolchain for tests only.
+  arduino_debug_tests = {
+    name = "arduino_debug_tests"
+    _toolchain_base = pw_target_toolchain_arduino.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*")
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Force tests to use basic log backend to avoid generating and loading its
+      # own tokenized database.
+      pw_log_BACKEND = "$dir_pw_log_basic"
+    }
+  }
+}
+
+toolchains_list = [
+  target_toolchain_arduino.arduino_debug,
+  target_toolchain_arduino.arduino_debug_tests,
+]
diff --git a/targets/common_backends.gni b/targets/common_backends.gni
new file mode 100644
index 0000000..eebec8b
--- /dev/null
+++ b/targets/common_backends.gni
@@ -0,0 +1,32 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+# Inherit from a Pigweed upstream toolchain and override backends as needed.
+toolchain_overrides = {
+  # Configure backend for assert facade.
+  pw_assert_BACKEND = "$dir_pw_assert_basic"
+
+  # Configure the pw_log facade for Base64 tokenized logging.
+  pw_log_BACKEND = "$dir_pw_log_tokenized:log_backend"
+  pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
+      "$dir_pw_log_tokenized:base64_over_hdlc"
+
+  # Alternately, configure pw_log for plain text logging
+  # pw_log_BACKEND = "$dir_pw_log_basic"
+
+  # Path to the nanopb installation. Defaults to included git module.
+  dir_pw_third_party_nanopb = "//third_party/nanopb"
+}
diff --git a/targets/host/BUILD.gn b/targets/host/BUILD.gn
new file mode 100644
index 0000000..200c021
--- /dev/null
+++ b/targets/host/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("toolchains") {
+  toolchains = toolchains_list
+}
diff --git a/targets/host/target_toolchains.gni b/targets/host/target_toolchains.gni
new file mode 100644
index 0000000..72e8cf6
--- /dev/null
+++ b/targets/host/target_toolchains.gni
@@ -0,0 +1,80 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("//targets/common_backends.gni")
+import("$dir_pigweed/targets/host/target_toolchains.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
+
+target_toolchain_host = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+
+  _excluded_defaults = [
+    "pw_trace_BACKEND",
+    "pw_trace_tokenizer_time",
+  ]
+
+  clang_debug = {
+    name = "host_debug"
+    if (host_os == "win") {
+      _toolchain_base = pw_target_toolchain_host.gcc_debug
+    } else {
+      _toolchain_base = pw_target_toolchain_host.clang_debug
+    }
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Force hosts to use basic log backend to avoid generating and loading its
+      # own tokenized database.
+      pw_log_BACKEND = "$dir_pw_log_basic"
+
+      # Configure backend for pw_sys_io facade.
+      pw_sys_io_BACKEND = "$dir_pw_sys_io_stdio"
+
+      pw_board_led_BACKEND = "$dir_pw_board_led_host"
+      pw_spin_delay_BACKEND = "$dir_pw_spin_delay_host"
+    }
+  }
+
+  # Toolchain for tests only.
+  clang_debug_tests = {
+    name = "host_debug_tests"
+    if (host_os == "win") {
+      _toolchain_base = pw_target_toolchain_host.gcc_debug
+    } else {
+      _toolchain_base = pw_target_toolchain_host.clang_debug
+    }
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Force tests to use basic log backend to avoid generating and loading its
+      # own tokenized database.
+      pw_log_BACKEND = "$dir_pw_log_basic"
+    }
+  }
+}
+
+toolchains_list = [
+  target_toolchain_host.clang_debug,
+  target_toolchain_host.clang_debug_tests,
+]
diff --git a/targets/stm32f429i-disc1/BUILD.gn b/targets/stm32f429i-disc1/BUILD.gn
new file mode 100644
index 0000000..200c021
--- /dev/null
+++ b/targets/stm32f429i-disc1/BUILD.gn
@@ -0,0 +1,22 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("toolchains") {
+  toolchains = toolchains_list
+}
diff --git a/targets/stm32f429i-disc1/target_toolchains.gni b/targets/stm32f429i-disc1/target_toolchains.gni
new file mode 100644
index 0000000..1746ce1
--- /dev/null
+++ b/targets/stm32f429i-disc1/target_toolchains.gni
@@ -0,0 +1,64 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("//targets/common_backends.gni")
+import("$dir_pigweed/targets/stm32f429i-disc1/target_toolchains.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
+
+target_toolchain_stm32f429i_disc1 = {
+  _excluded_members = [
+    "defaults",
+    "name",
+  ]
+  _excluded_defaults = [
+    "pw_cpu_exception_ENTRY_BACKEND",
+    "pw_cpu_exception_HANDLER_BACKEND",
+    "pw_cpu_exception_SUPPORT_BACKEND",
+  ]
+
+  debug = {
+    name = "stm32f429i_disc1_debug"
+    _toolchain_base = pw_target_toolchain_stm32f429i_disc1.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+      pw_board_led_BACKEND = "$dir_pw_board_led_stm32f429i_disc1"
+      pw_spin_delay_BACKEND = "$dir_pw_spin_delay_stm32f429i_disc1"
+    }
+  }
+
+  # Toolchain for tests only.
+  debug_tests = {
+    name = "stm32f429i_disc1_debug_tests"
+    _toolchain_base = pw_target_toolchain_stm32f429i_disc1.debug
+    forward_variables_from(_toolchain_base, "*", _excluded_members)
+    defaults = {
+      forward_variables_from(_toolchain_base.defaults, "*", _excluded_defaults)
+      forward_variables_from(toolchain_overrides, "*")
+
+      # Force tests to use basic log backend to avoid generating and loading its
+      # own tokenized database.
+      pw_log_BACKEND = "$dir_pw_log_basic"
+    }
+  }
+}
+
+toolchains_list = [
+  target_toolchain_stm32f429i_disc1.debug,
+  target_toolchain_stm32f429i_disc1.debug_tests,
+]
diff --git a/third_party/nanopb b/third_party/nanopb
new file mode 160000
index 0000000..049485f
--- /dev/null
+++ b/third_party/nanopb
@@ -0,0 +1 @@
+Subproject commit 049485ff557178f646d573eca3bd647f543b760b
diff --git a/third_party/pigweed b/third_party/pigweed
new file mode 160000
index 0000000..b3ea980
--- /dev/null
+++ b/third_party/pigweed
@@ -0,0 +1 @@
+Subproject commit b3ea980f10c763ea8d018c017886691afa992993
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
new file mode 100644
index 0000000..b93a53a
--- /dev/null
+++ b/tools/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("tools") {
+  setup = [ "setup.py" ]
+  sources = [
+    "pigweed_experimental_tools/__init__.py",
+    "pigweed_experimental_tools/presubmit_checks.py",
+  ]
+  python_deps = [
+    "$dir_pw_cli/py",
+    "$dir_pw_presubmit/py",
+  ]
+  pylintrc = "$dir_pigweed/.pylintrc"
+}
diff --git a/tools/pigweed_experimental_tools/__init__.py b/tools/pigweed_experimental_tools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/pigweed_experimental_tools/__init__.py
diff --git a/tools/pigweed_experimental_tools/presubmit_checks.py b/tools/pigweed_experimental_tools/presubmit_checks.py
new file mode 100755
index 0000000..c5635f1
--- /dev/null
+++ b/tools/pigweed_experimental_tools/presubmit_checks.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+"""Example presubmit check script."""
+
+import argparse
+import logging
+import os
+from pathlib import Path
+import re
+import sys
+
+try:
+    import pw_cli.log
+except ImportError:
+    print('ERROR: Activate the environment before running presubmits!',
+          file=sys.stderr)
+    sys.exit(2)
+
+import pw_presubmit
+from pw_presubmit import build, cli, environment, format_code, git_repo
+from pw_presubmit import python_checks, filter_paths, PresubmitContext
+from pw_presubmit.install_hook import install_hook
+
+_LOG = logging.getLogger(__name__)
+
+# Set up variables for key project paths.
+try:
+    PROJECT_ROOT = Path(os.environ['PIGWEED_EXPERIMENTAL_ROOT'])
+except KeyError:
+    print(
+        'ERROR: The presubmit checks must be run in the sample project\'s root'
+        ' directory',
+        file=sys.stderr)
+    sys.exit(2)
+
+PIGWEED_ROOT = PROJECT_ROOT / 'third_party' / 'pigweed'
+REPOS = (
+    PROJECT_ROOT,
+    PIGWEED_ROOT,
+    PROJECT_ROOT / 'third_party' / 'nanopb',
+)
+
+
+#
+# Initialization
+#
+def init_cipd(ctx: PresubmitContext):
+    """Initialize CIPD for project dependencies."""
+    environment.init_cipd(PIGWEED_ROOT, ctx.output_dir)
+
+
+def init_virtualenv(ctx: PresubmitContext):
+    """Initialize a virtual environment to run presubmits."""
+    environment.init_virtualenv(
+        PIGWEED_ROOT,
+        ctx.output_dir,
+        gn_targets=(
+            f'{ctx.root}#:python.install',
+            f'{ctx.root}/third_party/pigweed#:python.install',
+            f'{ctx.root}/third_party/pigweed#:target_support_packages.install',
+        ))
+
+
+# Rerun the build if files with these extensions change.
+_BUILD_EXTENSIONS = frozenset(
+    ['.rst', '.gn', '.gni', *format_code.C_FORMAT.extensions])
+
+
+#
+# Presubmit checks
+#
+def default_build(ctx: PresubmitContext):
+    """Creates a default build."""
+    build.gn_gen(PROJECT_ROOT, ctx.output_dir)
+    build.ninja(ctx.output_dir)
+
+
+def check_for_git_changes(_: PresubmitContext):
+    """Checks that repositories have all changes commited."""
+    checked_repos = (PIGWEED_ROOT, *REPOS)
+    changes = [r for r in checked_repos if git_repo.has_uncommitted_changes(r)]
+    for repo in changes:
+        _LOG.error('There are uncommitted changes in the %s repo!', repo.name)
+    if changes:
+        _LOG.warning(
+            'Commit or stash pending changes before running the presubmit.')
+        raise pw_presubmit.PresubmitFailure
+
+
+# Avoid running some checks on certain paths.
+PATH_EXCLUSIONS = (
+    re.compile(r'^external/'),
+    re.compile(r'^third_party/'),
+    re.compile(r'^vendor/'),
+)
+
+
+# Use the upstream pragma_once check, but apply a different set of path
+# filters with @filter_paths.
+@filter_paths(endswith='.h', exclude=PATH_EXCLUSIONS)
+def pragma_once(ctx: PresubmitContext):
+    """Presubmit check that ensures all header files contain '#pragma once'."""
+    pw_presubmit.pragma_once(ctx)
+
+
+#
+# Presubmit check programs
+#
+QUICK = (
+    # List some presubmit checks to run
+    default_build,
+    # Use the upstream formatting checks, with custom path filters applied.
+    format_code.presubmit_checks(exclude=PATH_EXCLUSIONS),
+)
+FULL = (
+    # Initialize an environment for running presubmit checks.
+    init_cipd,
+    init_virtualenv,
+    pragma_once,
+    QUICK,  # Add all checks from the 'quick' program
+    # Use the upstream Python checks, with custom path filters applied.
+    python_checks.all_checks(exclude=PATH_EXCLUSIONS),
+)
+PROGRAMS = pw_presubmit.Programs(quick=QUICK, full=FULL)
+
+
+def run(install: bool, **presubmit_args) -> int:
+    """Process the --install argument then invoke pw_presubmit."""
+
+    # Install the presubmit Git pre-push hook, if requested.
+    if install:
+        install_hook(__file__, 'pre-push', ['--base', 'HEAD~'],
+                     git_repo.root())
+        return 0
+
+    return cli.run(root=PROJECT_ROOT, **presubmit_args)
+
+
+def main() -> int:
+    """Run the presubmit checks for this repository."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    cli.add_arguments(parser, PROGRAMS, 'quick')
+
+    # Define an option for installing a Git pre-push hook for this script.
+    parser.add_argument(
+        '--install',
+        action='store_true',
+        help='Install the presubmit as a Git pre-push hook and exit.')
+
+    return run(**vars(parser.parse_args()))
+
+
+if __name__ == '__main__':
+    pw_cli.log.install(logging.INFO)
+    sys.exit(main())
diff --git a/tools/pigweed_experimental_tools/py.typed b/tools/pigweed_experimental_tools/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/pigweed_experimental_tools/py.typed
diff --git a/tools/setup.py b/tools/setup.py
new file mode 100644
index 0000000..7f4c6ff
--- /dev/null
+++ b/tools/setup.py
@@ -0,0 +1,34 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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.
+"""Utilities for Sample Project development."""
+
+import setuptools  # type: ignore
+
+setuptools.setup(
+    name='pigweed_experimental_tools',
+    version='0.0.1',
+    author='Pigweed Authors',
+    author_email='pigweed-developers@googlegroups.com',
+    description=__doc__,
+    packages=setuptools.find_packages(),
+    package_data={'pigweed_experimental_tools': ['py.typed']},
+    zip_safe=False,
+    entry_points={
+        'console_scripts': [
+            'find-files = pigweed_experimental_tools.find_files:main',
+        ]
+    },
+    install_requires=[
+        'pw_cli',
+    ])