pw_system: Python package
- Initial pw_system Python package with the console copied from
rpc_console.py
Change-Id: I5d8d2e6bef5cd87130726bed032c266dbd618b46
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/72980
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index e3b8743..6cf0dfc 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -52,6 +52,7 @@
"$dir_pw_status/py",
"$dir_pw_stm32cube_build/py",
"$dir_pw_symbolizer/py",
+ "$dir_pw_system/py",
"$dir_pw_thread/py",
"$dir_pw_tls_client/py",
"$dir_pw_tokenizer/py",
diff --git a/pw_system/py/BUILD.gn b/pw_system/py/BUILD.gn
new file mode 100644
index 0000000..3a75676
--- /dev/null
+++ b/pw_system/py/BUILD.gn
@@ -0,0 +1,40 @@
+# Copyright 2021 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/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_system/__init__.py",
+ "pw_system/console.py",
+ ]
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_console/py",
+ "$dir_pw_hdlc/py",
+ "$dir_pw_protobuf_compiler/py",
+ "$dir_pw_rpc/py",
+ "$dir_pw_tokenizer/py",
+ ]
+ inputs = []
+
+ pylintrc = "$dir_pigweed/.pylintrc"
+}
diff --git a/pw_system/py/pw_system/__init__.py b/pw_system/py/pw_system/__init__.py
new file mode 100644
index 0000000..486d1ed
--- /dev/null
+++ b/pw_system/py/pw_system/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2021 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.
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py
new file mode 100644
index 0000000..e4c3dad
--- /dev/null
+++ b/pw_system/py/pw_system/console.py
@@ -0,0 +1,299 @@
+# Copyright 2021 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.
+"""Console for interacting with devices using HDLC.
+
+To start the console, provide a serial port as the --device argument and paths
+or globs for .proto files that define the RPC services to support:
+
+ python -m pw_hdlc.rpc_console --device /dev/ttyUSB0 sample.proto
+
+This starts an IPython console for communicating with the connected device. A
+few variables are predefined in the interactive console. These include:
+
+ rpcs - used to invoke RPCs
+ device - the serial device used for communication
+ client - the pw_rpc.Client
+ protos - protocol buffer messages indexed by proto package
+
+An example echo RPC command:
+
+ rpcs.pw.rpc.EchoService.Echo(msg="hello!")
+"""
+
+import argparse
+import glob
+from inspect import cleandoc
+import logging
+from pathlib import Path
+import sys
+from types import ModuleType
+from typing import (
+ Any,
+ BinaryIO,
+ Collection,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Union,
+)
+import socket
+
+import serial # type: ignore
+
+import pw_cli.log
+import pw_console.python_logging
+from pw_console import PwConsoleEmbed
+from pw_console.pyserial_wrapper import SerialWithLogging
+
+from pw_log.proto import log_pb2
+from pw_rpc.console_tools.console import ClientInfo, flattened_rpc_completions
+from pw_rpc import callback_client
+from pw_tokenizer.database import LoadTokenDatabases
+from pw_tokenizer.detokenize import Detokenizer, detokenize_base64
+from pw_tokenizer import tokens
+
+from pw_hdlc.rpc import HdlcRpcClient, default_channels
+
+_LOG = logging.getLogger(__name__)
+_DEVICE_LOG = logging.getLogger('rpc_device')
+
+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__)
+ 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,
+ default=115200,
+ help='the baud rate to use')
+ parser.add_argument(
+ '--serial-debug',
+ action='store_true',
+ help=('Enable debug log tracing of all data passed through'
+ 'pyserial read and write.'))
+ parser.add_argument(
+ '-o',
+ '--output',
+ type=argparse.FileType('wb'),
+ default=sys.stdout.buffer,
+ help=('The file to which to write device output (HDLC channel 1); '
+ 'provide - or omit for stdout.'))
+ parser.add_argument('--logfile', help='Console debug log file.')
+ 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("--token-databases",
+ metavar='elf_or_token_database',
+ nargs="+",
+ action=LoadTokenDatabases,
+ help="Path to tokenizer database csv file(s).")
+ parser.add_argument('--config-file',
+ type=Path,
+ help='Path to a pw_console yaml config file.')
+ parser.add_argument('--proto-globs',
+ nargs='+',
+ help='glob pattern for .proto files')
+ return parser.parse_args()
+
+
+def _expand_globs(globs: Iterable[str]) -> Iterator[Path]:
+ for pattern in globs:
+ for file in glob.glob(pattern, recursive=True):
+ yield Path(file)
+
+
+def _start_ipython_terminal(client: HdlcRpcClient,
+ serial_debug: bool = False,
+ config_file_path: Optional[Path] = None) -> None:
+ """Starts an interactive IPython terminal with preset variables."""
+ local_variables = dict(
+ client=client,
+ device=client.client.channel(1),
+ rpcs=client.client.channel(1).rpcs,
+ protos=client.protos.packages,
+ # Include the active pane logger for creating logs in the repl.
+ DEVICE_LOG=_DEVICE_LOG,
+ LOG=logging.getLogger(),
+ )
+
+ welcome_message = cleandoc("""
+ Welcome to the Pigweed Console!
+
+ Help: Press F1 or click the [Help] menu
+ To move focus: Press Shift-Tab or click on a window
+
+ Example Python commands:
+
+ device.rpcs.pw.rpc.EchoService.Echo(msg='hello!')
+ LOG.warning('Message appears in Host Logs window.')
+ DEVICE_LOG.warning('Message appears in Device Logs window.')
+ """)
+
+ client_info = ClientInfo('device',
+ client.client.channel(1).rpcs, client.client)
+ completions = flattened_rpc_completions([client_info])
+
+ log_windows = {
+ 'Device Logs': [_DEVICE_LOG],
+ 'Host Logs': [logging.getLogger()],
+ }
+ if serial_debug:
+ log_windows['Serial Debug'] = [
+ logging.getLogger('pw_console.serial_debug_logger')
+ ]
+
+ interactive_console = PwConsoleEmbed(
+ global_vars=local_variables,
+ local_vars=None,
+ loggers=log_windows,
+ repl_startup_message=welcome_message,
+ help_text=__doc__,
+ config_file_path=config_file_path,
+ )
+ interactive_console.hide_windows('Host Logs')
+ interactive_console.add_sentence_completer(completions)
+
+ # Setup Python logger propagation
+ interactive_console.setup_python_logging()
+
+ # Don't send device logs to the root logger.
+ _DEVICE_LOG.propagate = False
+
+ interactive_console.embed()
+
+
+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(':')
+ socket_port = int(socket_port_str)
+ 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],
+ token_databases: Collection[tokens.Database],
+ socket_addr: str,
+ logfile: str,
+ output: Any,
+ serial_debug: bool = False,
+ config_file: Optional[Path] = None) -> 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
+
+ if not logfile:
+ # Create a temp logfile to prevent logs from appearing over stdout. This
+ # would corrupt the prompt toolkit UI.
+ logfile = pw_console.python_logging.create_temp_log_file()
+ pw_cli.log.install(logging.INFO, True, False, logfile)
+
+ detokenizer = None
+ if token_databases:
+ detokenizer = Detokenizer(tokens.Database.merged(*token_databases),
+ show_errors=False)
+
+ if not proto_globs:
+ proto_globs = ['**/*.proto']
+
+ protos: List[Union[ModuleType, Path]] = list(_expand_globs(proto_globs))
+
+ # Append compiled log.proto library to avoid include errors when manually
+ # provided, and shadowing errors due to ordering when the default global
+ # search path is used.
+ protos.append(log_pb2)
+
+ if not protos:
+ _LOG.critical('No .proto files were found with %s',
+ ', '.join(proto_globs))
+ _LOG.critical('At least one .proto file is required')
+ return 1
+
+ _LOG.debug('Found %d .proto files found with %s', len(protos),
+ ', '.join(proto_globs))
+
+ serial_impl = serial.Serial
+ if serial_debug:
+ serial_impl = SerialWithLogging
+
+ if socket_addr is None:
+ serial_device = serial_impl(device, baudrate, timeout=1)
+ read = lambda: serial_device.read(8192)
+ write = serial_device.write
+ else:
+ try:
+ socket_device = SocketClientImpl(socket_addr)
+ read = socket_device.read
+ write = socket_device.write
+ except ValueError:
+ _LOG.exception('Failed to initialize socket at %s', socket_addr)
+ return 1
+
+ callback_client_impl = callback_client.Impl(
+ default_unary_timeout_s=5.0,
+ default_stream_timeout_s=None,
+ )
+ _start_ipython_terminal(
+ HdlcRpcClient(read,
+ protos,
+ default_channels(write),
+ lambda data: detokenize_and_write_to_output(
+ data, output, detokenizer),
+ client_impl=callback_client_impl), serial_debug,
+ config_file)
+ return 0
+
+
+def detokenize_and_write_to_output(data: bytes,
+ unused_output: BinaryIO = sys.stdout.buffer,
+ detokenizer=None):
+ log_line = data
+ if detokenizer:
+ log_line = detokenize_base64(detokenizer, data)
+
+ for line in log_line.decode(errors="surrogateescape").splitlines():
+ _DEVICE_LOG.info(line)
+
+
+def main() -> int:
+ return console(**vars(_parse_args()))
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_system/py/pyproject.toml b/pw_system/py/pyproject.toml
new file mode 100644
index 0000000..798b747
--- /dev/null
+++ b/pw_system/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2021 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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_system/py/setup.cfg b/pw_system/py/setup.cfg
new file mode 100644
index 0000000..52111d7
--- /dev/null
+++ b/pw_system/py/setup.cfg
@@ -0,0 +1,34 @@
+# Copyright 2021 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.
+[metadata]
+name = pw_system
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Pigweed System
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+ pw_cli
+ pw_console
+ pw_hdlc
+ pw_protobuf_compiler
+ pw_rpc
+ pw_tokenizer
+
+[options.package_data]
+pw_system =
+ py.typed
diff --git a/pw_system/py/setup.py b/pw_system/py/setup.py
new file mode 100644
index 0000000..7308b8c
--- /dev/null
+++ b/pw_system/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2021 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.
+"""pw_console"""
+
+import setuptools # type: ignore
+
+setuptools.setup() # Package definition in setup.cfg