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()