pw_rpc_system_server: Local pw_rpc support

Using socket or sys_io to connect to server and client on host.

Test: Manually tested

Requires: pigweed:25460
Change-Id: I174121b90cea69621a885202b0d4d2df78a9aba8
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/20040
Commit-Queue: Jiaming (Charlie) Wang <jiamingw@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/pw_hdlc_lite/BUILD b/pw_hdlc_lite/BUILD
index 125064a..ef4e127 100644
--- a/pw_hdlc_lite/BUILD
+++ b/pw_hdlc_lite/BUILD
@@ -32,7 +32,6 @@
     hdrs = [
         "public/pw_hdlc_lite/decoder.h",
         "public/pw_hdlc_lite/encoder.h",
-        "public/pw_hdlc_lite/sys_io_stream.h",
     ],
     includes = ["public"],
     deps = [
diff --git a/pw_hdlc_lite/BUILD.gn b/pw_hdlc_lite/BUILD.gn
index 75eb537..877526c 100644
--- a/pw_hdlc_lite/BUILD.gn
+++ b/pw_hdlc_lite/BUILD.gn
@@ -50,15 +50,13 @@
 
 pw_source_set("encoder") {
   public_configs = [ ":default_config" ]
-  public = [
-    "public/pw_hdlc_lite/encoder.h",
-    "public/pw_hdlc_lite/sys_io_stream.h",
-  ]
+  public = [ "public/pw_hdlc_lite/encoder.h" ]
   sources = [
     "encoder.cc",
     "pw_hdlc_lite_private/protocol.h",
   ]
   public_deps = [
+    "$dir_pw_stream:sys_io_stream",
     dir_pw_bytes,
     dir_pw_status,
     dir_pw_stream,
diff --git a/pw_hdlc_lite/public/pw_hdlc_lite/rpc_packets.h b/pw_hdlc_lite/public/pw_hdlc_lite/rpc_packets.h
index de2a191..8d66c32 100644
--- a/pw_hdlc_lite/public/pw_hdlc_lite/rpc_packets.h
+++ b/pw_hdlc_lite/public/pw_hdlc_lite/rpc_packets.h
@@ -31,4 +31,4 @@
                              std::span<std::byte> decode_buffer,
                              unsigned rpc_address = kDefaultRpcAddress);
 
-}  // namespace pw::hdlc_lite
+}  // namespace pw::hdlc_lite
\ No newline at end of file
diff --git a/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py b/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
index 642f2f3..50ed66d 100644
--- a/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
+++ b/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
@@ -88,7 +88,7 @@
 class HdlcRpcClient:
     """An RPC client configured to run over HDLC."""
     def __init__(self,
-                 device: BinaryIO,
+                 device: Any,
                  proto_paths_or_modules: Iterable[python_protos.PathOrModule],
                  output: Callable[[bytes], Any] = write_to_file,
                  channels: Iterable[pw_rpc.Channel] = None,
@@ -96,9 +96,10 @@
         """Creates an RPC client configured to communicate using HDLC.
 
         Args:
-          device: serial.Serial (or any BinaryIO class) for reading/writing data
-          proto_paths_or_modules: paths to .proto files or proto modules
-          output: where to write "stdout" output from the device
+          device: serial.Serial (or any class that implements read and
+          write) for reading/writing data proto_paths_or_modules: paths
+          to .proto files or proto modules output: where to write
+          "stdout" output from the device
         """
         self.device = device
         self.protos = python_protos.Library.from_paths(proto_paths_or_modules)
diff --git a/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py b/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py
index 0dbef7b..2325c61 100644
--- a/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py
+++ b/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py
@@ -36,7 +36,8 @@
 import logging
 from pathlib import Path
 import sys
-from typing import BinaryIO, Collection, Iterable, Iterator
+from typing import Any, Collection, Iterable, Iterator
+import socket
 
 import IPython  # type: ignore
 import serial  # type: ignore
@@ -45,14 +46,17 @@
 
 _LOG = logging.getLogger(__name__)
 
+PW_RPC_MAX_PACKET_SIZE = 256
+SOCKET_SERVER = 'localhost'
+SOCKET_PORT = 33000
+MKFIFO_MODE = 0o666
+
 
 def _parse_args():
     """Parses and returns the command line arguments."""
     parser = argparse.ArgumentParser(description=__doc__)
-    parser.add_argument('-d',
-                        '--device',
-                        required=True,
-                        help='the serial port to use')
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument('-d', '--device', help='the serial port to use')
     parser.add_argument('-b',
                         '--baudrate',
                         type=int,
@@ -65,6 +69,11 @@
         default=sys.stdout.buffer,
         help=('The file to which to write device output (HDLC channel 1); '
               'provide - or omit for stdout.'))
+    group.add_argument('-s',
+                       '--socket-addr',
+                       type=str,
+                       help='use socket to connect to server, type default for\
+            localhost:33000, or manually input the server address:port')
     parser.add_argument('proto_globs',
                         nargs='+',
                         help='glob pattern for .proto files')
@@ -91,8 +100,32 @@
         local_ns=local_variables, module=argparse.Namespace())
 
 
+class SocketClientImpl():
+    def __init__(self, config: str):
+
+        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        socket_server = ''
+        socket_port = 0
+        if config == 'default':
+            socket_server = SOCKET_SERVER
+            socket_port = SOCKET_PORT
+        else:
+            socket_server, socket_port_str = config.split(':')
+            try:
+                socket_port = int(socket_port_str)
+            except ValueError as err:
+                raise Exception('Invalid port number provided') from err
+        self.socket.connect((socket_server, socket_port))
+
+    def write(self, data: bytes):
+        self.socket.sendall(data)
+
+    def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE):
+        return self.socket.recv(num_bytes)
+
+
 def console(device: str, baudrate: int, proto_globs: Collection[str],
-            output: BinaryIO) -> int:
+            socket_addr: str, output: Any) -> int:
     """Starts an interactive RPC console for HDLC."""
     # argparse.FileType doesn't correctly handle '-' for binary files.
     if output is sys.stdout:
@@ -112,8 +145,17 @@
     _LOG.debug('Found %d .proto files found with %s', len(protos),
                ', '.join(proto_globs))
 
+    if socket_addr is None:
+        client_device = serial.Serial(device, baudrate)
+    else:
+        try:
+            client_device = SocketClientImpl(socket_addr)
+        except ValueError as err:
+            print("ValueError: {0}".format(err), file=sys.stderr)
+            sys.exit(1)
+
     _start_ipython_terminal(
-        HdlcRpcClient(serial.Serial(device, baudrate), protos,
+        HdlcRpcClient(client_device, protos,
                       lambda data: write_to_file(data, output)))
     return 0
 
diff --git a/pw_hdlc_lite/rpc_example/BUILD.gn b/pw_hdlc_lite/rpc_example/BUILD.gn
index e0e31fa..42f6c0c 100644
--- a/pw_hdlc_lite/rpc_example/BUILD.gn
+++ b/pw_hdlc_lite/rpc_example/BUILD.gn
@@ -31,6 +31,7 @@
     deps = [
       "$dir_pw_rpc:server",
       "$dir_pw_rpc/nanopb:echo_service",
+      "$dir_pw_rpc/system_server",
       "..:pw_rpc",
       dir_pw_hdlc_lite,
       dir_pw_log,
diff --git a/pw_hdlc_lite/rpc_example/docs.rst b/pw_hdlc_lite/rpc_example/docs.rst
index 492ec0e..11aa87b 100644
--- a/pw_hdlc_lite/rpc_example/docs.rst
+++ b/pw_hdlc_lite/rpc_example/docs.rst
@@ -98,3 +98,37 @@
   The payload was msg: "Hello"
 
   The device says: Goodbye!
+
+-------------------------
+Local RPC example project
+-------------------------
+
+This example is similar to the above example, except it use socket to
+connect server and client running on the host.
+
+1. Build Pigweed
+================
+Activate the Pigweed environment and build the code.
+
+.. code-block:: sh
+
+  source activate.sh
+  gn gen out
+  pw watch
+
+2. Start client side and server side
+====================================
+
+Run pw_rpc client (i.e. use echo.proto)
+
+.. code-block:: sh
+
+  python -m pw_hdlc_lite.rpc_console path/to/echo.proto -s localhost:33000
+
+Run pw_rpc server
+
+.. code-block:: sh
+
+  out/host_clang_debug/obj/pw_hdlc_lite/rpc_example/bin/rpc_example
+
+Then you can invoke RPCs from the interactive console on the client side.
\ No newline at end of file
diff --git a/pw_hdlc_lite/rpc_example/hdlc_rpc_server.cc b/pw_hdlc_lite/rpc_example/hdlc_rpc_server.cc
index 5d3f376..0da47ff 100644
--- a/pw_hdlc_lite/rpc_example/hdlc_rpc_server.cc
+++ b/pw_hdlc_lite/rpc_example/hdlc_rpc_server.cc
@@ -17,59 +17,32 @@
 #include <string_view>
 
 #include "pw_hdlc_lite/encoder.h"
-#include "pw_hdlc_lite/rpc_channel.h"
 #include "pw_hdlc_lite/rpc_packets.h"
-#include "pw_hdlc_lite/sys_io_stream.h"
 #include "pw_log/log.h"
 #include "pw_rpc/echo_service_nanopb.h"
 #include "pw_rpc/server.h"
-#include "rpc_task_loop.h"
+#include "pw_rpc_system_server/rpc_server.h"
 
 namespace hdlc_example {
 namespace {
 
 using std::byte;
 
-constexpr size_t kMaxTransmissionUnit = 256;
-
-// Used to write HDLC data to pw::sys_io.
-pw::stream::SysIoWriter writer;
-
-// Set up the output channel for the pw_rpc server to use to use.
-pw::hdlc_lite::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
-    writer, pw::hdlc_lite::kDefaultRpcAddress, "HDLC channel");
-
-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);
-
 pw::rpc::EchoService echo_service;
 
-void RegisterServices() { server.RegisterService(echo_service); }
-
-static void PumpServices(void*){};
+void RegisterServices() {
+  pw::rpc_system_server::Server().RegisterService(echo_service);
+}
 
 }  // namespace
 
 void Start() {
-  // 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_lite::WriteInformationFrame(
-        1, std::as_bytes(std::span(log)), writer);
-  });
-
+  pw::rpc_system_server::Init();
   // Set up the server and start processing data.
   RegisterServices();
 
-  // Declare a buffer for decoding incoming HDLC frames.
-  std::array<std::byte, kMaxTransmissionUnit> input_buffer;
-
   PW_LOG_INFO("Starting pw_rpc server");
-  RpcTaskLoop::RunForever(
-      server, hdlc_channel_output, input_buffer, PumpServices);
+  pw::rpc_system_server::Start();
 }
 
 }  // namespace hdlc_example
diff --git a/pw_log_tokenized/base64_over_hdlc.cc b/pw_log_tokenized/base64_over_hdlc.cc
index b8c7bdf..ee2f5d6 100644
--- a/pw_log_tokenized/base64_over_hdlc.cc
+++ b/pw_log_tokenized/base64_over_hdlc.cc
@@ -20,7 +20,7 @@
 #include <span>
 
 #include "pw_hdlc_lite/encoder.h"
-#include "pw_hdlc_lite/sys_io_stream.h"
+#include "pw_stream/sys_io_stream.h"
 #include "pw_tokenizer/base64.h"
 #include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
 
diff --git a/pw_rpc/system_server/BUILD b/pw_rpc/system_server/BUILD
new file mode 100644
index 0000000..a2fb600
--- /dev/null
+++ b/pw_rpc/system_server/BUILD
@@ -0,0 +1,45 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+    name = "facade",
+    hdrs = ["public/pw_rpc_system_server/rpc_server.h"],
+    includes = ["public"],
+    deps = [
+        "//pw_span",
+        "//pw_status",
+    ],
+)
+
+pw_cc_library(
+    name = "system_server",
+    hdrs = ["public/pw_rpc_system_server/rpc_server.h"],
+    deps = [
+        ":facade",
+        "//pw_span",
+        "//pw_status",
+        # For now, hard-code to depend on stdio until bazel build is updated
+        # to support multiple target configurations.
+        "//pw_rpc/system_server:sys_io",
+    ],
+)
diff --git a/pw_rpc/system_server/BUILD.gn b/pw_rpc/system_server/BUILD.gn
new file mode 100644
index 0000000..b5b3c8a
--- /dev/null
+++ b/pw_rpc/system_server/BUILD.gn
@@ -0,0 +1,44 @@
+# 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.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/facade.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+declare_args() {
+  # Backend for the pw_rpc_system_server module.
+  pw_rpc_system_server_BACKEND = ""
+}
+
+config("default_config") {
+  include_dirs = [ "public" ]
+}
+
+pw_facade("system_server") {
+  backend = pw_rpc_system_server_BACKEND
+  public_configs = [ ":default_config" ]
+  public_deps = [
+    "$dir_pw_rpc:server",
+    "$dir_pw_stream",
+  ]
+  public = [ "public/pw_rpc_system_server/rpc_server.h" ]
+}
+
+pw_source_set("socket") {
+  public_deps = [ "pw_rpc_system_server_socket" ]
+}
+
+pw_source_set("sys_io") {
+  public_deps = [ "pw_rpc_system_server_sys_io" ]
+}
diff --git a/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h b/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h
new file mode 100644
index 0000000..5a3071b
--- /dev/null
+++ b/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h
@@ -0,0 +1,25 @@
+// 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.
+#pragma once
+
+#include "pw_rpc/server.h"
+#include "pw_stream/stream.h"
+namespace pw::rpc_system_server {
+// Initialization.
+void Init();
+// Get the reference of RPC Server instance.
+pw::rpc::Server& Server();
+// Start the server and processing packets. May not return.
+Status Start();
+}  // namespace pw::rpc_system_server
diff --git a/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD b/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD
new file mode 100644
index 0000000..0d23a86
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD
@@ -0,0 +1,31 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # Apache License 2.0
+
+pw_cc_library(
+    name = "pw_rpc_system_server_socket",
+    srcs = ["rpc_server.cc"],
+    deps = [
+        "//pw_rpc/system_server:facade",
+        "//pw_hdlc_lite:pw_rpc",
+    ],
+)
diff --git a/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD.gn b/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD.gn
new file mode 100644
index 0000000..3e54063
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_socket/BUILD.gn
@@ -0,0 +1,26 @@
+# 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.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+pw_source_set("pw_rpc_system_server_socket") {
+  deps = [
+    "$dir_pw_hdlc_lite:pw_rpc",
+    "$dir_pw_rpc/system_server:facade",
+    "$dir_pw_stream:socket_stream",
+    dir_pw_log,
+  ]
+  sources = [ "rpc_server.cc" ]
+}
diff --git a/pw_rpc/system_server/pw_rpc_system_server_socket/rpc_server.cc b/pw_rpc/system_server/pw_rpc_system_server_socket/rpc_server.cc
new file mode 100644
index 0000000..1f0970c
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_socket/rpc_server.cc
@@ -0,0 +1,65 @@
+// 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 "pw_rpc_system_server/rpc_server.h"
+
+#include "pw_hdlc_lite/rpc_channel.h"
+#include "pw_hdlc_lite/rpc_packets.h"
+#include "pw_log/log.h"
+#include "pw_stream/socket_stream.h"
+
+namespace pw::rpc_system_server {
+constexpr size_t kMaxTransmissionUnit = 256;
+constexpr uint32_t kMaxHdlcFrameSize = 256;
+constexpr uint32_t kSocketPort = 33000;
+inline constexpr uint8_t kDefaultRpcAddress = 'R';
+
+pw::stream::SocketStream socket_stream;
+pw::hdlc_lite::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
+    socket_stream, pw::hdlc_lite::kDefaultRpcAddress, "HDLC channel");
+pw::rpc::Channel channels[] = {
+    pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+pw::rpc::Server server(channels);
+
+void Init() {
+  pw::log_basic::SetOutput([](std::string_view log) {
+    pw::hdlc_lite::WriteInformationFrame(
+        1, std::as_bytes(std::span(log)), socket_stream);
+  });
+
+  socket_stream.Init(kSocketPort);
+}
+
+pw::rpc::Server& Server() { return server; }
+
+Status Start() {
+  // Declare a buffer for decoding incoming HDLC frames.
+  std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+  hdlc_lite::Decoder decoder(input_buffer);
+
+  while (true) {
+    std::array<std::byte, kMaxHdlcFrameSize> data;
+    auto ret_val = socket_stream.Read(data);
+    if (ret_val.ok()) {
+      for (auto byte : ret_val.value()) {
+        if (auto result = decoder.Process(byte); result.ok()) {
+          hdlc_lite::Frame& frame = result.value();
+          if (frame.address() == kDefaultRpcAddress) {
+            server.ProcessPacket(frame.data(), hdlc_channel_output);
+          }
+        }
+      }
+    }
+  }
+}
+}  // namespace pw::rpc_system_server
diff --git a/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD b/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD
new file mode 100644
index 0000000..60834c2
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD
@@ -0,0 +1,31 @@
+# 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.
+
+load(
+    "//pw_build:pigweed.bzl",
+    "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])  # Apache License 2.0
+
+pw_cc_library(
+    name = "pw_rpc_system_server_sys_io",
+    srcs = ["rpc_server.cc"],
+    deps = [
+        "//pw_rpc/system_server:facade",
+        "//pw_hdlc_lite:pw_rpc",
+    ],
+)
diff --git a/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD.gn b/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD.gn
new file mode 100644
index 0000000..4ea0203
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_sys_io/BUILD.gn
@@ -0,0 +1,26 @@
+# 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.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+pw_source_set("pw_rpc_system_server_sys_io") {
+  deps = [
+    "$dir_pw_hdlc_lite:pw_rpc",
+    "$dir_pw_rpc/system_server:facade",
+    "$dir_pw_stream:sys_io_stream",
+    dir_pw_log,
+  ]
+  sources = [ "rpc_server.cc" ]
+}
diff --git a/pw_rpc/system_server/pw_rpc_system_server_sys_io/rpc_server.cc b/pw_rpc/system_server/pw_rpc_system_server_sys_io/rpc_server.cc
new file mode 100644
index 0000000..528b604
--- /dev/null
+++ b/pw_rpc/system_server/pw_rpc_system_server_sys_io/rpc_server.cc
@@ -0,0 +1,68 @@
+// 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 "pw_rpc_system_server/rpc_server.h"
+
+#include "pw_hdlc_lite/rpc_channel.h"
+#include "pw_hdlc_lite/rpc_packets.h"
+#include "pw_log/log.h"
+#include "pw_stream/sys_io_stream.h"
+
+namespace pw::rpc_system_server {
+constexpr size_t kMaxTransmissionUnit = 256;
+constexpr uint32_t kMaxHdlcFrameSize = 256;
+inline constexpr uint8_t kDefaultRpcAddress = 'R';
+
+// Used to write HDLC data to pw::sys_io.
+pw::stream::SysIoWriter writer;
+pw::stream::SysIoReader reader;
+
+// Set up the output channel for the pw_rpc server to use.
+pw::hdlc_lite::RpcChannelOutputBuffer<kMaxTransmissionUnit> hdlc_channel_output(
+    writer, pw::hdlc_lite::kDefaultRpcAddress, "HDLC channel");
+pw::rpc::Channel channels[] = {
+    pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+pw::rpc::Server server(channels);
+
+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_lite::WriteInformationFrame(
+        1, std::as_bytes(std::span(log)), writer);
+  });
+}
+
+pw::rpc::Server& Server() { return server; }
+
+Status Start() {
+  // Declare a buffer for decoding incoming HDLC frames.
+  std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+  hdlc_lite::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_lite::Frame& frame = result.value();
+      if (frame.address() == kDefaultRpcAddress) {
+        server.ProcessPacket(frame.data(), hdlc_channel_output);
+      }
+    }
+  }
+}
+
+}  // namespace pw::rpc_system_server
diff --git a/pw_stream/BUILD b/pw_stream/BUILD
index a28ebae..242d460 100644
--- a/pw_stream/BUILD
+++ b/pw_stream/BUILD
@@ -44,6 +44,25 @@
     ],
 )
 
+pw_cc_library(
+    name = "pw_stream_socket",
+    srcs = ["socket_stream.cc"],
+    hdrs = ["public/pw_stream/socket_stream.h"],
+    deps = [
+        "//pw_sys_io",
+        "//pw_stream",
+    ],
+)
+
+pw_cc_library(
+    name = "pw_stream_sys_io",
+    hdrs = ["public/pw_stream/sys_io_stream.h"],
+    deps = [
+        "//pw_sys_io",
+        "//pw_stream",
+    ],
+)
+
 pw_cc_test(
     name = "memory_stream_test",
     srcs = [
diff --git a/pw_stream/BUILD.gn b/pw_stream/BUILD.gn
index cfd3583..a8efd15 100644
--- a/pw_stream/BUILD.gn
+++ b/pw_stream/BUILD.gn
@@ -39,6 +39,25 @@
   ]
 }
 
+pw_source_set("socket_stream") {
+  public_configs = [ ":default_config" ]
+  deps = [
+    "$dir_pw_stream",
+    "$dir_pw_sys_io",
+  ]
+  sources = [ "socket_stream.cc" ]
+  public = [ "public/pw_stream/socket_stream.h" ]
+}
+
+pw_source_set("sys_io_stream") {
+  public_configs = [ ":default_config" ]
+  deps = [
+    "$dir_pw_stream",
+    "$dir_pw_sys_io",
+  ]
+  public = [ "public/pw_stream/sys_io_stream.h" ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
 }
diff --git a/pw_stream/public/pw_stream/socket_stream.h b/pw_stream/public/pw_stream/socket_stream.h
new file mode 100644
index 0000000..ac8e9e0
--- /dev/null
+++ b/pw_stream/public/pw_stream/socket_stream.h
@@ -0,0 +1,60 @@
+// 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.
+#pragma once
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <array>
+#include <cstddef>
+#include <limits>
+#include <span>
+
+#include "pw_stream/stream.h"
+
+namespace pw::stream {
+
+static constexpr int kExitCode = -1;
+static constexpr int kInvalidFd = -1;
+
+class SocketStream : public Writer, public Reader {
+ public:
+  explicit SocketStream() {}
+
+  // Listen to the port and return after a client is connected
+  Status Init(uint16_t port);
+
+  size_t ConservativeWriteLimit() const override {
+    return std::numeric_limits<size_t>::max();
+  }
+
+  size_t ConservativeReadLimit() const override {
+    return std::numeric_limits<size_t>::max();
+  }
+
+ private:
+  Status DoWrite(std::span<const std::byte> data) override;
+
+  StatusWithSize DoRead(ByteSpan dest) override;
+
+  uint16_t listen_port_ = 0;
+  int socket_fd_ = kInvalidFd;
+  int conn_fd_ = kInvalidFd;
+  struct sockaddr_in sockaddr_client_;
+};
+
+}  // namespace pw::stream
diff --git a/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h b/pw_stream/public/pw_stream/sys_io_stream.h
similarity index 80%
rename from pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h
rename to pw_stream/public/pw_stream/sys_io_stream.h
index 219d8eb..e4c79ce 100644
--- a/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h
+++ b/pw_stream/public/pw_stream/sys_io_stream.h
@@ -35,4 +35,16 @@
   }
 };
 
+class SysIoReader : public Reader {
+ public:
+  size_t ConservativeReadLimit() const override {
+    return std::numeric_limits<size_t>::max();
+  }
+
+ private:
+  StatusWithSize DoRead(ByteSpan dest) override {
+    return pw::sys_io::ReadBytes(dest);
+  }
+};
+
 }  // namespace pw::stream
diff --git a/pw_stream/socket_stream.cc b/pw_stream/socket_stream.cc
new file mode 100644
index 0000000..1146797
--- /dev/null
+++ b/pw_stream/socket_stream.cc
@@ -0,0 +1,71 @@
+// 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 "pw_stream/socket_stream.h"
+namespace pw::stream {
+
+static constexpr uint32_t kMaxConcurrentUser = 1;
+
+// Listen to the port and return after a client is connected
+Status SocketStream::Init(uint16_t port) {
+  listen_port_ = port;
+  socket_fd_ = socket(AF_INET, SOCK_STREAM, 0);
+  if (socket_fd_ == kInvalidFd) {
+    return Status::Internal();
+  }
+
+  struct sockaddr_in addr;
+  addr.sin_family = AF_INET;
+  addr.sin_port = htons(listen_port_);
+  addr.sin_addr.s_addr = INADDR_ANY;
+
+  int result =
+      bind(socket_fd_, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
+  if (result < 0) {
+    return Status::Internal();
+  }
+
+  result = listen(socket_fd_, kMaxConcurrentUser);
+  if (result < 0) {
+    return Status::Internal();
+  }
+
+  socklen_t len = sizeof(sockaddr_client_);
+
+  conn_fd_ =
+      accept(socket_fd_, reinterpret_cast<sockaddr*>(&sockaddr_client_), &len);
+  if (conn_fd_ < 0) {
+    return Status::Internal();
+  }
+  return Status::Ok();
+}
+
+Status SocketStream::DoWrite(std::span<const std::byte> data) {
+  ssize_t bytes_sent = send(conn_fd_, data.data(), data.size_bytes(), 0);
+
+  if (bytes_sent < 0 || static_cast<uint64_t>(bytes_sent) != data.size()) {
+    return Status::Internal();
+  }
+  return Status::Ok();
+}
+
+StatusWithSize SocketStream::DoRead(ByteSpan dest) {
+  ssize_t bytes_rcvd = recv(conn_fd_, dest.data(), dest.size_bytes(), 0);
+  if (bytes_rcvd < 0) {
+    return StatusWithSize::Internal();
+  }
+  return StatusWithSize::Ok(bytes_rcvd);
+}
+
+};  // namespace pw::stream
\ No newline at end of file
diff --git a/targets/host/target_toolchains.gni b/targets/host/target_toolchains.gni
index cd89d8f..9cf16d3 100644
--- a/targets/host/target_toolchains.gni
+++ b/targets/host/target_toolchains.gni
@@ -35,6 +35,9 @@
   # Configure backend for pw_sys_io facade.
   pw_sys_io_BACKEND = "$dir_pw_sys_io_stdio"
 
+  # Configure backend for pw_rpc_system_server.
+  pw_rpc_system_server_BACKEND = "$dir_pw_rpc/system_server:socket"
+
   # Configure backend for trace facade.
   pw_trace_BACKEND = "$dir_pw_trace_tokenized"
 
diff --git a/targets/stm32f429i-disc1/target_toolchains.gni b/targets/stm32f429i-disc1/target_toolchains.gni
index b6a95a7..6344f81 100644
--- a/targets/stm32f429i-disc1/target_toolchains.gni
+++ b/targets/stm32f429i-disc1/target_toolchains.gni
@@ -48,6 +48,7 @@
   pw_cpu_exception_SUPPORT_BACKEND = "$dir_pw_cpu_exception_armv7m:support"
   pw_log_BACKEND = dir_pw_log_basic
   pw_sys_io_BACKEND = dir_pw_sys_io_baremetal_stm32f429
+  pw_rpc_system_server_BACKEND = "$dir_pw_rpc/system_server:sys_io"
   pw_malloc_BACKEND = dir_pw_malloc_freelist
 
   pw_boot_armv7m_LINK_CONFIG_DEFINES = [