pw_hdlc_lite: HdlcRpcClient class
Provide a class that configures a pw_rpc.Client for use over HDLC.
Change-Id: I191b9ef85926b11421c870f9d914b6a111dd8795
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/18461
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py b/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
index 1246ff6..4388dff 100644
--- a/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
+++ b/pw_hdlc_lite/py/pw_hdlc_lite/rpc.py
@@ -15,14 +15,18 @@
import logging
import os
+from pathlib import Path
+import sys
+import threading
import time
-from typing import Any, Callable, NoReturn, BinaryIO
-
-import serial
+from types import ModuleType
+from typing import Any, BinaryIO, Callable, Iterable, List, NoReturn, Union
from pw_hdlc_lite.decoder import FrameDecoder
from pw_hdlc_lite.encoder import encode_information_frame
-from pw_rpc.client import Client
+import pw_rpc
+from pw_rpc import callback_client
+from pw_protobuf_compiler import python_protos
_LOG = logging.getLogger(__name__)
@@ -38,7 +42,7 @@
if delay_s:
def slow_write(data: bytes) -> None:
- """Slows down writes to support unbuffered serial."""
+ """Slows down writes in case unbuffered serial is in use."""
for byte in data:
time.sleep(delay_s)
writer(bytes([byte]))
@@ -48,8 +52,8 @@
return lambda data: writer(encode_information_frame(address, data))
-def read_and_process_data(rpc_client: Client,
- device: serial.Serial,
+def read_and_process_data(rpc_client: pw_rpc.Client,
+ device: BinaryIO,
output: BinaryIO,
output_sep: bytes = os.linesep.encode(),
rpc_address: int = DEFAULT_ADDRESS) -> NoReturn:
@@ -73,3 +77,60 @@
else:
_LOG.error('Unhandled frame for address %d: %s', frame.address,
frame.data.decoder(errors='replace'))
+
+
+_PathOrModule = Union[str, Path, ModuleType]
+
+
+class HdlcRpcClient:
+ """An RPC client configured to run over HDLC."""
+ def __init__(self,
+ device: BinaryIO,
+ proto_paths_or_modules: Iterable[_PathOrModule],
+ output: BinaryIO = sys.stdout.buffer,
+ channels: Iterable[pw_rpc.Channel] = None,
+ client_impl: pw_rpc.client.ClientImpl = None):
+ """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
+ """
+ self.device = device
+
+ proto_modules = []
+ proto_paths: List[Union[Path, str]] = []
+ for proto in proto_paths_or_modules:
+ if isinstance(proto, (Path, str)):
+ proto_paths.append(proto)
+ else:
+ proto_modules.append(proto)
+
+ proto_modules += python_protos.compile_and_import(proto_paths)
+
+ if channels is None:
+ channels = [pw_rpc.Channel(1, channel_output(device.write))]
+
+ if client_impl is None:
+ client_impl = callback_client.Impl()
+
+ self.client = pw_rpc.Client.from_modules(client_impl, channels,
+ proto_modules)
+
+ # Start background thread that reads and processes RPC packets.
+ threading.Thread(target=read_and_process_data,
+ daemon=True,
+ args=(self.client, device, output)).start()
+
+ def rpcs(self, channel_id: int = None) -> pw_rpc.client.Services:
+ """Returns object for accessing services on the specified channel.
+
+ This skips some intermediate layers to make it simpler to invoke RPCs
+ from an HdlcRpcClient. If only one channel is in use, the channel ID is
+ not necessary.
+ """
+ if channel_id is None:
+ return next(iter(self.client.channels())).rpcs
+
+ return self.client.channel(channel_id).rpcs
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 11bbf18..a4bfb96 100644
--- a/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py
+++ b/pw_hdlc_lite/py/pw_hdlc_lite/rpc_console.py
@@ -35,16 +35,12 @@
import logging
from pathlib import Path
import sys
-import threading
from typing import Collection, Iterable, Iterator, BinaryIO
import IPython
import serial
-from pw_hdlc_lite import rpc
-from pw_protobuf_compiler import python_protos
-from pw_rpc import callback_client, descriptors
-from pw_rpc.client import Client
+from pw_hdlc_lite.rpc import HdlcRpcClient
_LOG = logging.getLogger(__name__)
@@ -82,46 +78,22 @@
yield Path(file)
-def _start_ipython_terminal(device: serial.Serial, client: Client) -> None:
+def _start_ipython_terminal(client: HdlcRpcClient) -> None:
"""Starts an interactive IPython terminal with preset variables."""
local_variables = dict(
client=client,
- device=device,
- channel_client=client.channel(1),
- rpcs=client.channel(1).rpcs,
+ channel_client=client.client.channel(1),
+ rpcs=client.client.channel(1).rpcs,
)
- module = argparse.Namespace() # serves as an empty module
- IPython.terminal.embed.InteractiveShellEmbed(banner1=__doc__).mainloop(
- local_variables, module)
+ print(__doc__) # Print the banner
+ IPython.terminal.embed.InteractiveShellEmbed().mainloop(
+ local_ns=local_variables, module=argparse.Namespace())
-def console(device: serial.Serial, protos: Iterable[Path],
- output: BinaryIO) -> None:
- """Starts an interactive RPC console for HDLC.
-
- Args:
- device: the serial device from which to read HDLC frames
- protos: .proto files with RPC services
- """
-
- # Compile the proto files that define the RPC services to expose.
- modules = python_protos.compile_and_import(protos)
-
- # Set up the pw_rpc server with a single channel with ID 1.
- channel = descriptors.Channel(1, rpc.channel_output(device.write))
- client = Client.from_modules(callback_client.Impl(), [channel], modules)
-
- # Start background thread that reads serial data and processes RPC packets.
- threading.Thread(target=rpc.read_and_process_data,
- daemon=True,
- args=(client, device, output)).start()
-
- _start_ipython_terminal(device, client)
-
-
-def _prepare_console(device: str, baudrate: int, proto_globs: Collection[str],
- output: BinaryIO) -> int:
+def console(device: str, baudrate: int, proto_globs: Collection[str],
+ output: BinaryIO) -> int:
+ """Starts an interactive RPC console for HDLC."""
# argparse.FileType doesn't correctly handle '-' for binary files.
if output is sys.stdout:
output = sys.stdout.buffer
@@ -140,12 +112,13 @@
_LOG.debug('Found %d .proto files found with %s', len(protos),
', '.join(proto_globs))
- console(serial.Serial(device, baudrate), protos, output)
+ _start_ipython_terminal(
+ HdlcRpcClient(serial.Serial(device, baudrate), protos, output))
return 0
def main() -> int:
- return _prepare_console(**vars(_parse_args()))
+ return console(**vars(_parse_args()))
if __name__ == '__main__':
diff --git a/pw_rpc/py/pw_rpc/__init__.py b/pw_rpc/py/pw_rpc/__init__.py
index d7a70c7..1f1e72e 100644
--- a/pw_rpc/py/pw_rpc/__init__.py
+++ b/pw_rpc/py/pw_rpc/__init__.py
@@ -14,3 +14,4 @@
"""Package for calling Pigweed RPCs from Python."""
from pw_rpc.client import Client
+from pw_rpc.descriptors import Channel
diff --git a/pw_rpc/py/pw_rpc/client.py b/pw_rpc/py/pw_rpc/client.py
index 755b521..5bd5e4a 100644
--- a/pw_rpc/py/pw_rpc/client.py
+++ b/pw_rpc/py/pw_rpc/client.py
@@ -126,7 +126,7 @@
"""
-class _MethodClients(descriptors.ServiceAccessor):
+class ServiceClient(descriptors.ServiceAccessor):
"""Navigates the methods in a service provided by a ChannelClient."""
def __init__(self, rpcs: PendingRpcs, client_impl: ClientImpl,
channel: Channel, service: Service):
@@ -149,13 +149,13 @@
return str(self._service)
-class _ServiceClients(descriptors.ServiceAccessor[_MethodClients]):
+class Services(descriptors.ServiceAccessor[ServiceClient]):
"""Navigates the services provided by a ChannelClient."""
def __init__(self, rpcs: PendingRpcs, client_impl, channel: Channel,
services: Collection[Service]):
super().__init__(
{
- s: _MethodClients(rpcs, client_impl, channel, s)
+ s: ServiceClient(rpcs, client_impl, channel, s)
for s in services
},
as_attrs='packages')
@@ -221,7 +221,7 @@
"""
client: 'Client'
channel: Channel
- rpcs: _ServiceClients
+ rpcs: Services
def method(self, method_name: str):
"""Returns a method client matching the given name.
@@ -272,8 +272,7 @@
self._channels_by_id = {
channel.id: ChannelClient(
self, channel,
- _ServiceClients(self._rpcs, self._impl, channel,
- self.services))
+ Services(self._rpcs, self._impl, channel, self.services))
for channel in channels
}
@@ -281,6 +280,10 @@
"""Returns a ChannelClient, which is used to call RPCs on a channel."""
return self._channels_by_id[channel_id]
+ def channels(self) -> Iterable[ChannelClient]:
+ """Accesses the ChannelClients in this client."""
+ return self._channels_by_id.values()
+
def method(self, method_name: str) -> Method:
"""Returns a Method matching the given name.
diff --git a/pw_status/py/pw_status/__init__.py b/pw_status/py/pw_status/__init__.py
index 060ac51..6d22bda 100644
--- a/pw_status/py/pw_status/__init__.py
+++ b/pw_status/py/pw_status/__init__.py
@@ -34,3 +34,6 @@
INTERNAL = 13
UNAVAILABLE = 14
DATA_LOSS = 15
+
+ def ok(self) -> bool:
+ return self is self.OK