pw_package: Arduino core installers and teensy presubmit

Summary of Changes
==================
1. Rename arduino build args to support cores in any location and have
   consistent naming.
2. Update docs to reflect above.
3. `pw package install teensy` working with nice status message on how
   to use ge pw_arduino_build_* args.
4. Added gn_teensy_build presubmit test step, not run by default.
   `pw presubmit --step gn_teensy_build`
5. Added missing system_rpc_server impl for //targets/arduino. This is
   just a copy of the stm32f429i one.

Build arg Change Examples
=========================

OLD:
    dir_pw_third_party_arduino = "//third_party/arduino"
    arduino_core_name = "teensy"
    arduino_package_name = "teensy/avr"
    arduino_board = "teensy41"
    arduino_menu_options = ["menu.usb.serial",
                            "menu.opt.o2std"]
NEW:
    pw_arduino_build_CORE_PATH =
      "/mnt/pigweed/pigweed/.environment/packages"
    pw_arduino_build_CORE_NAME = "teensy"
    pw_arduino_build_PACKAGE_NAME = "teensy/avr"
    pw_arduino_build_BOARD = "teensy41"
    pw_arduino_build_MENU_OPTIONS = ["menu.usb.serial",
                                     "menu.opt.o2std"]

All BUILD.gn checks for enabled arduino builds changed too:

OLD:
    if (dir_pw_third_party_arduino != "") {}
NEW:
    if (pw_arduino_build_CORE_PATH != "") {}

All gn target deps on arduino core sources changed.

OLD:
    "$dir_pw_third_party_arduino:arduino_core_sources",
NEW:
    "$dir_pw_third_party/arduino:arduino_core_sources",

Teensy package post install & status message
============================================

$ pw package status teensy

20210114 11:58:18 INF teensy is installed.
20210114 11:58:18 INF teensy currently installed in:
  /mnt/pigweed/pigweed/.environment/packages/teensy
20210114 11:58:18 INF Enable by running "gn args out" and adding
these lines:
  pw_arduino_build_CORE_PATH =
    "/mnt/pigweed/pigweed/.environment/packages"
  pw_arduino_build_CORE_NAME = "teensy"
  pw_arduino_build_PACKAGE_NAME = "teensy/avr"
  pw_arduino_build_BOARD = "BOARD_NAME"
20210114 11:58:18 INF Where BOARD_NAME is any supported board.
20210114 11:58:18 INF List available boards by running:
  arduino_builder
  --arduino-package-path
    /mnt/pigweed/pigweed/.environment/packages/teensy
  --arduino-package-name teensy/avr
  list-boards

Requires: pigweed:29490
Change-Id: Ifd0bd214777392a29af3ab24711edf2f2c1086f3
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/26240
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Reviewed-by: Rob Mohr <mohrr@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index c2c9796..73c9f40 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -104,7 +104,7 @@
   toolchain_prefix = "$dir_pigweed/targets/stm32f429i-disc1:stm32f429i_disc1_"
 }
 
-if (dir_pw_third_party_arduino != "") {
+if (pw_arduino_build_CORE_PATH != "") {
   _build_pigweed_default_at_all_optimization_levels("arduino") {
     toolchain_prefix = "$dir_pigweed/targets/arduino:arduino_"
   }
diff --git a/pw_arduino_build/BUILD.gn b/pw_arduino_build/BUILD.gn
index db72ea8..bbe2003 100644
--- a/pw_arduino_build/BUILD.gn
+++ b/pw_arduino_build/BUILD.gn
@@ -24,7 +24,7 @@
   pw_arduino_build_INIT_BACKEND = ""
 }
 
-if (dir_pw_third_party_arduino != "") {
+if (pw_arduino_build_CORE_PATH != "") {
   pw_facade("arduino_init") {
     backend = pw_arduino_build_INIT_BACKEND
     public = [ "public/pw_arduino_build/init.h" ]
@@ -39,7 +39,7 @@
     deps = [
       ":arduino_init",
       "$dir_pw_sys_io",
-      "$dir_pw_third_party_arduino:arduino_core_sources",
+      "$dir_pw_third_party/arduino:arduino_core_sources",
     ]
     sources = [ "arduino_main_wrapper.cc" ]
   }
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index dd1e6f7..0d1704c 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -16,59 +16,85 @@
 
 declare_args() {
   # Enable/disable Arduino builds via group("arduino").
-  # Set to the full path of ./third_party/arduino
-  dir_pw_third_party_arduino = ""
+  # Set to the full path of where cores are installed.
+  pw_arduino_build_CORE_PATH = ""
 
   # Expected args for an Arduino build:
-  arduino_core_name = "teensy"
+  pw_arduino_build_CORE_NAME = ""
 
   # TODO(tonymd): "teensy/avr" here should match the folders in this dir:
-  # "../third_party/arduino/cores/$arduino_core_name/hardware/*")
+  # "../third_party/arduino/cores/$pw_arduino_build_CORE_NAME/hardware/*")
   # For teensy: "teensy/avr", for adafruit-samd: "samd/1.6.2"
-  arduino_package_name = "teensy/avr"
-  arduino_board = "teensy40"
+  pw_arduino_build_PACKAGE_NAME = ""
+  pw_arduino_build_BOARD = ""
 
   # Menu options should be a list of strings.
-  arduino_menu_options = [
-    "menu.usb.serial",
-    "menu.keys.en-us",
-  ]
+  pw_arduino_build_MENU_OPTIONS = []
 }
 
-arduino_builder_script =
-    get_path_info("py/pw_arduino_build/__main__.py", "abspath")
+if (pw_arduino_build_CORE_PATH != "") {
+  # Check that enough pw_arduino_build_* args are set to find and use a core.
+  _required_args_message =
+      "The following build args must all be set: " +
+      "pw_arduino_build_CORE_PATH, pw_arduino_build_CORE_NAME, " +
+      "pw_arduino_build_PACKAGE_NAME."
+  assert(pw_arduino_build_CORE_NAME != "",
+         "Missing 'pw_arduino_build_CORE_NAME' build arg. " +
+             _required_args_message)
+  assert(pw_arduino_build_PACKAGE_NAME != "",
+         "Missing 'pw_arduino_build_PACKAGE_NAME' build arg. " +
+             _required_args_message)
 
-_arduino_core_path =
-    rebase_path("../third_party/arduino/cores/$arduino_core_name")
-_compiler_path_override =
-    rebase_path(getenv("_PW_ACTUAL_ENVIRONMENT_ROOT") + "/cipd/pigweed/bin")
+  _arduino_selected_core_path =
+      rebase_path("$pw_arduino_build_CORE_PATH/$pw_arduino_build_CORE_NAME")
 
-arduino_global_args = [
-  "--arduino-package-path",
-  _arduino_core_path,
-  "--arduino-package-name",
-  arduino_package_name,
-  "--compiler-path-override",
-  _compiler_path_override,
+  arduino_builder_script =
+      get_path_info("py/pw_arduino_build/__main__.py", "abspath")
 
-  # Save config files to "out/arduino_debug/gen/arduino_builder_config.json"
-  "--config-file",
-  rebase_path(root_gen_dir) + "/arduino_builder_config.json",
-  "--save-config",
-]
+  # Check pw_arduino_build_BOARD is set
+  assert(pw_arduino_build_BOARD != "",
+         "pw_arduino_build_BOARD build arg not set. " +
+             "To see supported boards run: " +
+             "arduino_builder --arduino-package-path " +
+             _arduino_selected_core_path + " --arduino-package-name " +
+             pw_arduino_build_PACKAGE_NAME + " list-boards")
 
-arduino_board_args = [
-  "--build-path",
-  rebase_path(root_build_dir),
-  "--board",
-  arduino_board,
-  "--menu-options",
-]
-arduino_board_args += arduino_menu_options
+  _compiler_path_override =
+      rebase_path(getenv("_PW_ACTUAL_ENVIRONMENT_ROOT") + "/cipd/pigweed/bin")
 
-arduino_show_command_args = arduino_global_args + [
-                              "show",
-                              "--delimit-with-newlines",
-                            ] + arduino_board_args
+  arduino_core_library_path = "$_arduino_selected_core_path/hardware/" +
+                              "$pw_arduino_build_PACKAGE_NAME/libraries"
 
-arduino_run_command_args = arduino_global_args + [ "run" ] + arduino_board_args
+  arduino_global_args = [
+    "--arduino-package-path",
+    _arduino_selected_core_path,
+    "--arduino-package-name",
+    pw_arduino_build_PACKAGE_NAME,
+    "--compiler-path-override",
+    _compiler_path_override,
+
+    # Save config files to "out/arduino_debug/gen/arduino_builder_config.json"
+    "--config-file",
+    rebase_path(root_gen_dir) + "/arduino_builder_config.json",
+    "--save-config",
+  ]
+
+  arduino_board_args = [
+    "--build-path",
+    rebase_path(root_build_dir),
+    "--board",
+    pw_arduino_build_BOARD,
+  ]
+  if (pw_arduino_build_MENU_OPTIONS != []) {
+    arduino_board_args += [ "--menu-options" ]
+    arduino_board_args += pw_arduino_build_MENU_OPTIONS
+  }
+
+  arduino_show_command_args = arduino_global_args + [
+                                "show",
+                                "--delimit-with-newlines",
+                              ] + arduino_board_args
+
+  arduino_run_command_args =
+      arduino_global_args + [ "run" ] + arduino_board_args
+}
diff --git a/pw_arduino_build/py/pw_arduino_build/__main__.py b/pw_arduino_build/py/pw_arduino_build/__main__.py
index f06506e..e6fa77d 100644
--- a/pw_arduino_build/py/pw_arduino_build/__main__.py
+++ b/pw_arduino_build/py/pw_arduino_build/__main__.py
@@ -23,6 +23,7 @@
 import subprocess
 import sys
 from collections import OrderedDict
+from pathlib import Path
 from typing import List
 
 from pw_arduino_build import core_installer, log
@@ -418,6 +419,14 @@
             raise argparse.ArgumentTypeError(
                 f'{arg.upper()} is not a valid log level')
 
+    def existing_directory(input_string: str):
+        """Argparse type that resolves to an absolute path."""
+        input_path = Path(os.path.expandvars(input_string)).absolute()
+        if not input_path.exists():
+            raise argparse.ArgumentTypeError(
+                "'{}' is not a valid directory.".format(str(input_path)))
+        return input_path.as_posix()
+
     parser = argparse.ArgumentParser()
     parser.add_argument("-q",
                         "--quiet",
@@ -434,10 +443,12 @@
 
     # Global command line options
     parser.add_argument("--arduino-package-path",
+                        type=existing_directory,
                         help="Path to the arduino IDE install location.")
     parser.add_argument("--arduino-package-name",
                         help="Name of the Arduino board package to use.")
     parser.add_argument("--compiler-path-override",
+                        type=existing_directory,
                         help="Path to arm-none-eabi-gcc bin folder. "
                         "Default: Arduino core specified gcc")
     parser.add_argument("-c", "--config-file", help="Path to a config file.")
diff --git a/pw_arduino_build/py/pw_arduino_build/core_installer.py b/pw_arduino_build/py/pw_arduino_build/core_installer.py
index 2b48195..3428ad7 100644
--- a/pw_arduino_build/py/pw_arduino_build/core_installer.py
+++ b/pw_arduino_build/py/pw_arduino_build/core_installer.py
@@ -153,17 +153,21 @@
 
 
 def install_core_command(args: argparse.Namespace):
-    install_prefix = os.path.realpath(
-        os.path.expanduser(os.path.expandvars(args.prefix)))
-    install_dir = os.path.join(install_prefix, args.core_name)
-    cache_dir = os.path.join(install_prefix, ".cache", args.core_name)
+    install_core(args.prefix, args.core_name)
 
-    if args.core_name in supported_cores():
+
+def install_core(prefix, core_name):
+    install_prefix = os.path.realpath(
+        os.path.expanduser(os.path.expandvars(prefix)))
+    install_dir = os.path.join(install_prefix, core_name)
+    cache_dir = os.path.join(install_prefix, ".cache", core_name)
+
+    if core_name in supported_cores():
         shutil.rmtree(install_dir, ignore_errors=True)
         os.makedirs(install_dir, exist_ok=True)
         os.makedirs(cache_dir, exist_ok=True)
 
-    if args.core_name == "teensy":
+    if core_name == "teensy":
         if platform.system() == "Linux":
             install_teensy_core_linux(install_prefix, install_dir, cache_dir)
         elif platform.system() == "Darwin":
@@ -171,16 +175,16 @@
         elif platform.system() == "Windows":
             install_teensy_core_windows(install_prefix, install_dir, cache_dir)
         apply_teensy_patches(install_dir)
-    elif args.core_name == "adafruit-samd":
+    elif core_name == "adafruit-samd":
         install_adafruit_samd_core(install_prefix, install_dir, cache_dir)
-    elif args.core_name == "stm32duino":
+    elif core_name == "stm32duino":
         install_stm32duino_core(install_prefix, install_dir, cache_dir)
-    elif args.core_name == "arduino-samd":
+    elif core_name == "arduino-samd":
         install_arduino_samd_core(install_prefix, install_dir, cache_dir)
     else:
         raise ArduinoCoreNotSupported(
             "Invalid core '{}'. Supported cores: {}".format(
-                args.core_name, ", ".join(supported_cores())))
+                core_name, ", ".join(supported_cores())))
 
 
 def supported_cores():
@@ -322,9 +326,6 @@
 
 
 def apply_teensy_patches(install_dir):
-    # Remember where we are to construct relative paths for running `git apply`
-    working_directory_path = Path(os.getcwd())
-
     # On Mac the "hardware" directory is a symlink:
     #   ls -l third_party/arduino/cores/teensy/
     #   hardware -> Teensyduino.app/Contents/Java/hardware
@@ -339,9 +340,9 @@
 
     # Apply each patch file.
     for diff_path in patch_file_paths:
-        file_operations.git_apply_patch(
-            patch_root_path.relative_to(working_directory_path).as_posix(),
-            diff_path.as_posix())
+        file_operations.git_apply_patch(patch_root_path.as_posix(),
+                                        diff_path.as_posix(),
+                                        unsafe_paths=True)
 
 
 def install_arduino_samd_core(install_prefix: str, install_dir: str,
diff --git a/pw_arduino_build/py/pw_arduino_build/file_operations.py b/pw_arduino_build/py/pw_arduino_build/file_operations.py
index 8158a99..e534373 100644
--- a/pw_arduino_build/py/pw_arduino_build/file_operations.py
+++ b/pw_arduino_build/py/pw_arduino_build/file_operations.py
@@ -223,12 +223,17 @@
     return json_file_options, file_path
 
 
-def git_apply_patch(root_directory, patch_file, ignore_whitespace=True):
+def git_apply_patch(root_directory,
+                    patch_file,
+                    ignore_whitespace=True,
+                    unsafe_paths=False):
     """Use `git apply` to apply a diff file."""
 
     _LOG.info("Applying Patch: %s", patch_file)
     git_apply_command = ["git", "apply"]
     if ignore_whitespace:
         git_apply_command.append("--ignore-whitespace")
+    if unsafe_paths:
+        git_apply_command.append("--unsafe-paths")
     git_apply_command += ["--directory", root_directory, patch_file]
     subprocess.run(git_apply_command)
diff --git a/pw_package/py/BUILD.gn b/pw_package/py/BUILD.gn
index 0e4296e..309ab2f 100644
--- a/pw_package/py/BUILD.gn
+++ b/pw_package/py/BUILD.gn
@@ -23,6 +23,7 @@
     "pw_package/git_repo.py",
     "pw_package/package_manager.py",
     "pw_package/packages/__init__.py",
+    "pw_package/packages/arduino_core.py",
     "pw_package/packages/nanopb.py",
     "pw_package/pigweed_packages.py",
   ]
diff --git a/pw_package/py/pw_package/package_manager.py b/pw_package/py/pw_package/package_manager.py
index f22845b..3f1cc80 100644
--- a/pw_package/py/pw_package/package_manager.py
+++ b/pw_package/py/pw_package/package_manager.py
@@ -66,8 +66,11 @@
 _PACKAGES: Dict[str, Package] = {}
 
 
-def register(package_class: type) -> None:
-    obj = package_class()
+def register(package_class: type, name: str = None) -> None:
+    if name:
+        obj = package_class(name)
+    else:
+        obj = package_class()
     _PACKAGES[obj.name] = obj
 
 
@@ -167,7 +170,7 @@
         return 0
 
     def run(self, command: str, pkg_root: pathlib.Path, **kwargs) -> int:
-        self._mgr = PackageManager(pkg_root)
+        self._mgr = PackageManager(pkg_root.resolve())
         return getattr(self, command)(**kwargs)
 
 
diff --git a/pw_package/py/pw_package/packages/arduino_core.py b/pw_package/py/pw_package/packages/arduino_core.py
new file mode 100644
index 0000000..65c0c91
--- /dev/null
+++ b/pw_package/py/pw_package/packages/arduino_core.py
@@ -0,0 +1,91 @@
+# Copyright 2020 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.
+"""Install and check status of teensy-core."""
+
+import logging
+import re
+from pathlib import Path
+from typing import Sequence
+
+from pw_arduino_build import core_installer
+
+import pw_package.package_manager
+
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+
+class ArduinoCore(pw_package.package_manager.Package):
+    """Install and check status of arduino cores."""
+    def __init__(self, core_name, *args, **kwargs):
+        super().__init__(*args, name=core_name, **kwargs)
+
+    def status(self, path: Path) -> bool:
+        return (path / 'hardware').is_dir()
+
+    def install(self, path: Path) -> None:
+        if self.status(path):
+            return
+        # Otherwise delete current version and reinstall
+        core_installer.install_core(path.parent.resolve().as_posix(),
+                                    path.name)
+
+    def info(self, path: Path) -> Sequence[str]:
+        packages_root = path.parent.resolve()
+        arduino_package_path = path
+        arduino_package_name = None
+
+        message = [
+            f'{self.name} currently installed in: {path}',
+        ]
+        # Make gn args sample copy/paste-able by omitting the starting timestamp
+        # and INF log on each line.
+        message_gn_args = [
+            'Enable by running "gn args out" and adding these lines:',
+            f'  pw_arduino_build_CORE_PATH = "{packages_root}"',
+            f'  pw_arduino_build_CORE_NAME = "{self.name}"'
+        ]
+
+        # Search for first valid 'package/version' directory
+        for hardware_dir in [
+                path for path in (path / 'hardware').iterdir()
+                if path.is_dir()
+        ]:
+            if path.name in ["arduino", "tools"]:
+                continue
+            for subdir in [
+                    path for path in hardware_dir.iterdir() if path.is_dir()
+            ]:
+                if subdir.name == 'avr' or re.match(r'[0-9.]+', subdir.name):
+                    arduino_package_name = f'{hardware_dir.name}/{subdir.name}'
+                    break
+
+        if arduino_package_name:
+            message_gn_args += [
+                f'  pw_arduino_build_PACKAGE_NAME = "{arduino_package_name}"',
+                '  pw_arduino_build_BOARD = "BOARD_NAME"'
+            ]
+            message += ["\n".join(message_gn_args)]
+            message += [
+                'Where BOARD_NAME is any supported board.',
+                # Have arduino_builder command appear on it's own line.
+                'List available boards by running:\n'
+                '  arduino_builder '
+                f'--arduino-package-path {arduino_package_path} '
+                f'--arduino-package-name {arduino_package_name} list-boards'
+            ]
+        return message
+
+
+for arduino_core_name in core_installer.supported_cores():
+    pw_package.package_manager.register(ArduinoCore, name=arduino_core_name)
diff --git a/pw_package/py/pw_package/pigweed_packages.py b/pw_package/py/pw_package/pigweed_packages.py
index 3cca0ad..6c39266 100644
--- a/pw_package/py/pw_package/pigweed_packages.py
+++ b/pw_package/py/pw_package/pigweed_packages.py
@@ -17,6 +17,7 @@
 
 from pw_package import package_manager
 from pw_package.packages import nanopb
+from pw_package.packages import arduino_core  # pylint: disable=unused-import
 
 
 def initialize():
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index e6ec62a..7b593b2 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -122,6 +122,19 @@
 
 
 @filter_paths(endswith=_BUILD_EXTENSIONS)
+def gn_teensy_build(ctx: PresubmitContext):
+    build.install_package(ctx.package_root, 'teensy')
+    build.gn_gen(ctx.root,
+                 ctx.output_dir,
+                 pw_arduino_build_CORE_PATH='"{}"'.format(str(
+                     ctx.package_root)),
+                 pw_arduino_build_CORE_NAME='teensy',
+                 pw_arduino_build_PACKAGE_NAME='teensy/avr',
+                 pw_arduino_build_BOARD='teensy40')
+    build.ninja(ctx.output_dir, *_at_all_optimization_levels('arduino'))
+
+
+@filter_paths(endswith=_BUILD_EXTENSIONS)
 def gn_qemu_build(ctx: PresubmitContext):
     build.gn_gen(ctx.root, ctx.output_dir)
     build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu'))
@@ -531,6 +544,9 @@
     source_is_in_build_files,
     python_checks,
     build_env_setup,
+    # Skip gn_teensy_build if running on Windows. The Teensycore installer is
+    # an exe that requires an admin role.
+    gn_teensy_build if sys.platform in ['linux', 'darwin'] else (),
 )
 
 PROGRAMS = Programs(broken=BROKEN, quick=QUICK, full=FULL)
diff --git a/pw_sys_io_arduino/BUILD.gn b/pw_sys_io_arduino/BUILD.gn
index ee9b770..eb4b51c 100644
--- a/pw_sys_io_arduino/BUILD.gn
+++ b/pw_sys_io_arduino/BUILD.gn
@@ -22,7 +22,7 @@
   include_dirs = [ "public" ]
 }
 
-if (dir_pw_third_party_arduino != "") {
+if (pw_arduino_build_CORE_PATH != "") {
   pw_source_set("pw_sys_io_arduino") {
     remove_configs = [ "$dir_pw_build:strict_warnings" ]
     public_configs = [ ":default_config" ]
@@ -31,7 +31,7 @@
     deps = [
       "$dir_pw_sys_io:default_putget_bytes",
       "$dir_pw_sys_io:facade",
-      "$dir_pw_third_party_arduino:arduino_core_sources",
+      "$dir_pw_third_party/arduino:arduino_core_sources",
     ]
     sources = [ "sys_io_arduino.cc" ]
   }
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index d0df948..d4f3708 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -208,7 +208,7 @@
   ]
   deps = [ ":pw_tokenizer" ]
 
-  if (dir_pw_third_party_arduino != "") {
+  if (pw_arduino_build_CORE_PATH != "") {
     remove_configs = [ "$dir_pw_build:strict_warnings" ]
   }
 }
diff --git a/targets/arduino/BUILD b/targets/arduino/BUILD
index 458ddff..7514235 100644
--- a/targets/arduino/BUILD
+++ b/targets/arduino/BUILD
@@ -30,4 +30,13 @@
         "//pw_preprocessor",
         "//pw_sys_io_arduino",
     ],
+)
+
+pw_cc_library(
+    name = "system_rpc_server",
+    srcs = ["system_rpc_server.cc"],
+    deps = [
+        "//pw_rpc/system_server:facade",
+        "//pw_hdlc:pw_rpc",
+    ],
 )
\ No newline at end of file
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
index 1949252..7217dff 100644
--- a/targets/arduino/BUILD.gn
+++ b/targets/arduino/BUILD.gn
@@ -22,7 +22,7 @@
   sources = [ "target_docs.rst" ]
 }
 
-if (dir_pw_third_party_arduino != "") {
+if (pw_arduino_build_CORE_PATH != "") {
   import("target_toolchains.gni")
 
   generate_toolchains("target_toolchains") {
@@ -91,13 +91,24 @@
       sources = [ "init.cc" ]
       public_deps = [
         "$dir_pw_sys_io_arduino",
-        "$dir_pw_third_party_arduino:arduino_core_sources",
+        "$dir_pw_third_party/arduino:arduino_core_sources",
       ]
       deps = [
         "$dir_pw_arduino_build:arduino_init.facade",
         "$dir_pw_preprocessor",
       ]
     }
+
+    pw_source_set("system_rpc_server") {
+      deps = [
+        "$dir_pw_hdlc:pw_rpc",
+        "$dir_pw_hdlc:rpc_channel_output",
+        "$dir_pw_rpc/system_server:facade",
+        "$dir_pw_stream:sys_io_stream",
+        dir_pw_log,
+      ]
+      sources = [ "system_rpc_server.cc" ]
+    }
   }
 } else {
   config("arduino_build") {
diff --git a/targets/arduino/system_rpc_server.cc b/targets/arduino/system_rpc_server.cc
new file mode 100644
index 0000000..00b9bf9
--- /dev/null
+++ b/targets/arduino/system_rpc_server.cc
@@ -0,0 +1,70 @@
+// Copyright 2020 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 <cstddef>
+
+#include "pw_hdlc/rpc_channel.h"
+#include "pw_hdlc/rpc_packets.h"
+#include "pw_log/log.h"
+#include "pw_rpc_system_server/rpc_server.h"
+#include "pw_stream/sys_io_stream.h"
+
+namespace pw::rpc::system_server {
+namespace {
+
+constexpr size_t kMaxTransmissionUnit = 256;
+
+// Used to write HDLC data to pw::sys_io.
+stream::SysIoWriter writer;
+stream::SysIoReader reader;
+
+// Set up the output channel for the pw_rpc server to use.
+hdlc::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
+    writer, pw::hdlc::kDefaultRpcAddress, "HDLC channel");
+Channel channels[] = {pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+rpc::Server server(channels);
+
+}  // namespace
+
+void Init() {
+  // Send log messages to HDLC address 1. This prevents logs from interfering
+  // with pw_rpc communications.
+  pw::log_basic::SetOutput([](std::string_view log) {
+    pw::hdlc::WriteUIFrame(1, std::as_bytes(std::span(log)), writer);
+  });
+}
+
+rpc::Server& Server() { return server; }
+
+Status Start() {
+  // Declare a buffer for decoding incoming HDLC frames.
+  std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+  hdlc::Decoder decoder(input_buffer);
+
+  while (true) {
+    std::byte byte;
+    Status ret_val = pw::sys_io::ReadByte(&byte);
+    if (!ret_val.ok()) {
+      return ret_val;
+    }
+    if (auto result = decoder.Process(byte); result.ok()) {
+      hdlc::Frame& frame = result.value();
+      if (frame.address() == hdlc::kDefaultRpcAddress) {
+        server.ProcessPacket(frame.data(), hdlc_channel_output);
+      }
+    }
+  }
+}
+
+}  // namespace pw::rpc::system_server
diff --git a/targets/arduino/target_docs.rst b/targets/arduino/target_docs.rst
index 5db55e8..a6f6b0b 100644
--- a/targets/arduino/target_docs.rst
+++ b/targets/arduino/target_docs.rst
@@ -43,8 +43,7 @@
 =====
 
 You must first install an Arduino core or let Pigweed know where you have cores
-installed using the ``dir_pw_third_party_arduino`` and ``arduino_package_path``
-build arguments.
+installed using the ``pw_arduino_build_CORE_PATH`` build arg.
 
 Installing Arduino Cores
 ------------------------
@@ -66,10 +65,12 @@
 
 .. code:: sh
 
-  gn gen out --args='dir_pw_third_party_arduino="//third_party/arduino"
-                     arduino_core_name="teensy"
-                     arduino_board="teensy40"
-                     arduino_menu_options=["menu.usb.serial", "menu.keys.en-us"]'
+  gn gen out --args='
+    pw_arduino_build_CORE_PATH="//third_party/arduino/cores"
+    pw_arduino_build_CORE_NAME="teensy"
+    pw_arduino_build_PACKAGE_NAME="teensy/avr"
+    pw_arduino_build_BOARD="teensy40"
+    pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]'
 
 On a Windows machine it's easier to run:
 
@@ -81,10 +82,11 @@
 
 .. code:: text
 
-  dir_pw_third_party_arduino="//third_party/arduino"
-  arduino_core_name="teensy"
-  arduino_board="teensy40"
-  arduino_menu_options=["menu.usb.serial", "menu.keys.en-us"]
+  pw_arduino_build_CORE_PATH = "//third_party/arduino/cores"
+  pw_arduino_build_CORE_NAME = "teensy"
+  pw_arduino_build_PACKAGE_NAME="teensy/avr"
+  pw_arduino_build_BOARD = "teensy40"
+  pw_arduino_build_MENU_OPTIONS = ["menu.usb.serial", "menu.keys.en-us"]
 
 Save the file and close the text editor.
 
@@ -112,7 +114,7 @@
   teensy31    Teensy 3.2 / 3.1
 
 You may wish to set different arduino build options in
-``arduino_menu_options``. Run this to see what's available for your core:
+``pw_arduino_build_MENU_OPTIONS``. Run this to see what's available for your core:
 
 .. code:: sh
 
@@ -161,10 +163,11 @@
 
   #!/bin/bash
   gn gen out --export-compile-commands \
-      --args='dir_pw_third_party_arduino="//third_party/arduino"
-              arduino_core_name="teensy"
-              arduino_board="teensy40"
-              arduino_menu_options=["menu.usb.serial", "menu.keys.en-us"]' && \
+      --args='pw_arduino_build_CORE_PATH="//third_party/arduino/cores"
+              pw_arduino_build_CORE_NAME="teensy"
+              pw_arduino_build_PACKAGE_NAME="teensy/avr"
+              pw_arduino_build_BOARD="teensy40"
+              pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]' && \
     ninja -C out arduino
 
   for f in $(find out/arduino_debug/obj/ -iname "*.elf"); do
@@ -214,9 +217,7 @@
 
   _library_args = [
     "--library-path",
-    rebase_path(
-        "$dir_pw_third_party_arduino/cores/teensy/hardware/teensy/avr/libraries"
-    ),
+    rebase_path(arduino_core_library_path),
     "--library-names",
     "Time",
     "Wire",
@@ -250,10 +251,8 @@
                                    [ "--library-include-dirs" ],
                                "list lines")
 
-    # Required if using Arduino.h and any Arduino API functions
-    if (dir_pw_third_party_arduino != "") {
-      remove_configs = [ "$dir_pw_build:strict_warnings" ]
-      deps += [ "$dir_pw_third_party_arduino:arduino_core_sources" ]
-    }
+    # Required for using Arduino.h and any Arduino API functions
+    remove_configs = [ "$dir_pw_build:strict_warnings" ]
+    deps += [ "$dir_pw_third_party/arduino:arduino_core_sources" ]
   }
 
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
index e7cad07..78d3e8e 100644
--- a/targets/arduino/target_toolchains.gni
+++ b/targets/arduino/target_toolchains.gni
@@ -45,6 +45,8 @@
   pw_assert_BACKEND = dir_pw_assert_basic
   pw_log_BACKEND = dir_pw_log_basic
   pw_sys_io_BACKEND = dir_pw_sys_io_arduino
+  pw_rpc_system_server_BACKEND =
+      "$dir_pigweed/targets/arduino:system_rpc_server"
   pw_arduino_build_INIT_BACKEND = "$dir_pigweed/targets/arduino:pre_init"
 
   current_cpu = "arm"
diff --git a/third_party/arduino/BUILD.gn b/third_party/arduino/BUILD.gn
index da82742..36a8247 100644
--- a/third_party/arduino/BUILD.gn
+++ b/third_party/arduino/BUILD.gn
@@ -17,7 +17,7 @@
 import("$dir_pw_arduino_build/arduino.gni")
 import("$dir_pw_build/target_types.gni")
 
-if (dir_pw_third_party_arduino != "") {
+if (pw_arduino_build_CORE_PATH != "") {
   pw_source_set("arduino_core_sources") {
     remove_configs = [ "$dir_pw_build:strict_warnings" ]