pw_hdlc_lite: Added RPC server and client utility

CL also contains a HDLC RPC-server integration example and a python
RPC-client. Also, updated the definition for the SerialWriter.

Change-Id: I34963bd7e2df96bd58f82be6b04c7af8f1e0a5b0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/15860
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_hdlc_lite/BUILD b/pw_hdlc_lite/BUILD
index 049e9fc..26bfad1 100644
--- a/pw_hdlc_lite/BUILD
+++ b/pw_hdlc_lite/BUILD
@@ -26,6 +26,7 @@
     hdrs = [
       "public/pw_hdlc_lite/decoder.h",
       "public/pw_hdlc_lite/encoder.h",
+      "public/pw_hdlc_lite/rpc_server_packets.h",
       "public/pw_hdlc_lite/sys_io_stream.h",
       "public/pw_hdlc_lite/hdlc_channel.h",
     ],
@@ -35,6 +36,39 @@
       "hdlc_channel.cc"
     ],
     includes = ["public"],
+    deps = [
+        "//pw_log",
+        "//pw_span",
+        "//pw_status",
+        "//pw_stream",
+        "//pw_checksum",
+        "//pw_bytes",
+        "//pw_result",
+        "//pw_rpc",
+    ],
+)
+
+pw_cc_library(
+    name = "client_server_test",
+    srcs = [
+        "hdlc_server_example.cc",
+    ],
+    hdrs = [
+      "public/pw_hdlc_lite/decoder.h",
+      "public/pw_hdlc_lite/rpc_server_packets.h",
+      "public/pw_hdlc_lite/hdlc_channel.h",
+    ],
+    includes = ["public"],
+    deps = [
+        "//pw_log",
+        "//pw_span",
+        "//pw_status",
+        "//pw_stream",
+        "//pw_checksum",
+        "//pw_bytes",
+        "//pw_result",
+        "//pw_rpc",
+    ],
 )
 
 cc_test(
diff --git a/pw_hdlc_lite/BUILD.gn b/pw_hdlc_lite/BUILD.gn
index 266bed8..e11cd66 100644
--- a/pw_hdlc_lite/BUILD.gn
+++ b/pw_hdlc_lite/BUILD.gn
@@ -60,6 +60,20 @@
   ]
 }
 
+pw_executable("hdlc_server_example") {
+  public_configs = [ ":default_config" ]
+  sources = [
+    "hdlc_server_example.cc",
+    "public/pw_hdlc_lite/rpc_server_packets.h",
+  ]
+  public_deps = [ ":pw_hdlc_lite" ]
+  deps = [
+    ":pw_rpc",
+    "$dir_pw_rpc:nanopb_server",
+    "$dir_pw_rpc/nanopb:echo_service",
+  ]
+}
+
 pw_test_group("tests") {
   tests = [
     ":encoder_test",
diff --git a/pw_hdlc_lite/hdlc_server_example.cc b/pw_hdlc_lite/hdlc_server_example.cc
new file mode 100644
index 0000000..0d1e481
--- /dev/null
+++ b/pw_hdlc_lite/hdlc_server_example.cc
@@ -0,0 +1,50 @@
+// Copyright 2019 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_hdlc_lite/hdlc_channel.h"
+#include "pw_hdlc_lite/rpc_server_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"
+
+using std::byte;
+
+constexpr size_t kMaxTransmissionUnit = 100;
+
+void ConstructServerAndReadAndProcessData() {
+  pw::stream::SerialWriter channel_output_serial;
+  std::array<byte, kMaxTransmissionUnit> channel_output_buffer;
+  pw::rpc::HdlcChannelOutput hdlc_channel_output(
+      channel_output_serial, channel_output_buffer, "HdlcChannelOutput");
+
+  pw::rpc::Channel kChannels[] = {
+      pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+  pw::rpc::Server server(kChannels);
+
+  pw::rpc::EchoService echo_service;
+
+  server.RegisterService(echo_service);
+
+  pw::rpc::ReadAndProcessData<kMaxTransmissionUnit>(server);
+}
+
+int main() {
+  ConstructServerAndReadAndProcessData();
+  return 0;
+}
diff --git a/pw_hdlc_lite/public/pw_hdlc_lite/rpc_server_packets.h b/pw_hdlc_lite/public/pw_hdlc_lite/rpc_server_packets.h
new file mode 100644
index 0000000..45b639e
--- /dev/null
+++ b/pw_hdlc_lite/public/pw_hdlc_lite/rpc_server_packets.h
@@ -0,0 +1,54 @@
+// 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 <array>
+
+#include "pw_hdlc_lite/decoder.h"
+#include "pw_hdlc_lite/hdlc_channel.h"
+#include "pw_hdlc_lite/sys_io_stream.h"
+#include "pw_log/log.h"
+#include "pw_rpc/server.h"
+#include "pw_status/status.h"
+#include "pw_sys_io/sys_io.h"
+
+namespace pw::rpc {
+
+// Utility function for reading the bytes through serial, decode the Bytes using
+// the HDLC-Lite protocol and processing the decoded data.
+template <size_t max_transmission_unit>
+void ReadAndProcessData(Server& server) {
+  hdlc_lite::DecoderBuffer<max_transmission_unit> decoder;
+
+  pw::stream::SerialWriter channel_output_serial;
+  std::array<std::byte, max_transmission_unit> channel_output_buffer;
+  HdlcChannelOutput hdlc_channel_output(
+      channel_output_serial, channel_output_buffer, "HdlcChannelOutput");
+
+  std::byte data;
+
+  while (true) {
+    if (pw::sys_io::ReadByte(&data).ok()) {
+      return;
+    }
+
+    auto decoded_packet = decoder.AddByte(data);
+
+    if (decoded_packet.ok()) {
+      server.ProcessPacket(decoded_packet.value(), hdlc_channel_output);
+    }
+  }
+}
+
+}  // namespace pw::rpc
\ No newline at end of file
diff --git a/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h b/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h
index 32ddbb6..1326685 100644
--- a/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h
+++ b/pw_hdlc_lite/public/pw_hdlc_lite/sys_io_stream.h
@@ -15,6 +15,7 @@
 
 #include <array>
 #include <cstddef>
+#include <limits>
 #include <span>
 
 #include "pw_stream/stream.h"
@@ -26,6 +27,10 @@
  public:
   size_t bytes_written() const { return bytes_written_; }
 
+  size_t ConservativeWriteLimit() const override {
+    return std::numeric_limits<size_t>::max();
+  }
+
  private:
   // Implementation for writing data to this stream.
   Status DoWrite(std::span<const std::byte> data) override {
diff --git a/pw_hdlc_lite/py/pw_hdlc_lite/client_console_example.py b/pw_hdlc_lite/py/pw_hdlc_lite/client_console_example.py
new file mode 100644
index 0000000..73dc00d
--- /dev/null
+++ b/pw_hdlc_lite/py/pw_hdlc_lite/client_console_example.py
@@ -0,0 +1,117 @@
+# 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.
+"""Example console for creating a client.
+
+Console can be initiated by running:
+
+  python -m pw_hdlc_lite.client_console --device /dev/ttyUSB0
+
+An example echo RPC command:
+print(rpc_client.channel(1).rpcs.pw.rpc.EchoService.Echo(msg="hello!"))
+"""
+from pathlib import Path
+import argparse
+import threading
+import logging
+import time
+import code
+import serial
+
+from pw_hdlc_lite import decoder
+from pw_hdlc_lite import encoder
+from pw_protobuf_compiler import python_protos
+from pw_rpc import callback_client, client, descriptors
+
+_LOG = logging.getLogger(__name__)
+
+
+def parse_arguments():
+    """Parses and returns the command line arguments."""
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument('-d',
+                        '--device',
+                        required=True,
+                        help='used to specify device port')
+    parser.add_argument('-b',
+                        '--baudrate',
+                        type=int,
+                        required=True,
+                        help='used to specify baudrate')
+    return parser.parse_args()
+
+
+def configure_serial(device_port, baudrate):
+    """Configures and returns a serial."""
+    ser = serial.Serial(device_port)
+    ser.baudrate = baudrate
+    return ser
+
+
+def construct_rpc_client(ser):
+    """Constructs and returns an RPC client using serial ser."""
+    def delayed_write(data):
+        """Adds a delay between consective bytes written over serial"""
+        for byte in data:
+            time.sleep(0.001)
+            ser.write(bytes([byte]))
+
+    # Creating a channel object
+    hdlc_channel_output = lambda data: encoder.encode_and_write_payload(
+        data, delayed_write)
+    channel = descriptors.Channel(1, hdlc_channel_output)
+
+    # Creating a list of modules that provide the .proto service methods
+    modules = python_protos.compile_and_import([
+        Path(__file__, '..', '..', '..', '..', 'pw_rpc', 'pw_rpc_protos',
+             'echo.proto')
+    ])
+
+    # Using the modules and channel to create and return an RPC Client
+    return client.Client.from_modules(callback_client.Impl(), [channel],
+                                      modules)
+
+
+def read_and_process_data(rpc_client, ser):
+    """Reads in the data, decodes the bytes and then processes the rpc."""
+    decode = decoder.Decoder()
+
+    while True:
+        byte = ser.read()
+        try:
+            for packet in decode.add_bytes(byte):
+                if not rpc_client.process_packet(packet):
+                    _LOG.error('Packet not handled by rpc client: %s', packet)
+        except decoder.CrcMismatchError:
+            _LOG.exception('CRC verification failed')
+            return
+
+
+def main():
+    """Main function."""
+    args = parse_arguments()
+    ser = configure_serial(args.device, args.baudrate)
+
+    rpc_client = construct_rpc_client(ser)
+
+    # Starting the reading and processing on a thread
+    threading.Thread(target=read_and_process_data,
+                     daemon=True,
+                     args=(rpc_client, ser)).start()
+
+    # Opening an interactive console that allows sending RPCs.
+    code.interact(banner="Interactive console to run RPCs", local=locals())
+
+
+if __name__ == "__main__":
+    main()