1# Copyright 2020 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Console for interacting with pw_rpc over HDLC. 15 16To start the console, provide a serial port as the --device argument and paths 17or globs for .proto files that define the RPC services to support: 18 19 python -m pw_hdlc.rpc_console --device /dev/ttyUSB0 sample.proto 20 21This starts an IPython console for communicating with the connected device. A 22few variables are predefined in the interactive console. These include: 23 24 rpcs - used to invoke RPCs 25 device - the serial device used for communication 26 client - the pw_rpc.Client 27 protos - protocol buffer messages indexed by proto package 28 29An example echo RPC command: 30 31 rpcs.pw.rpc.EchoService.Echo(msg="hello!") 32""" 33 34import argparse 35import glob 36import logging 37from pathlib import Path 38import sys 39from typing import Any, Collection, Iterable, Iterator 40import socket 41 42import IPython # type: ignore 43import serial # type: ignore 44 45from pw_hdlc.rpc import HdlcRpcClient, default_channels, write_to_file 46 47_LOG = logging.getLogger(__name__) 48 49PW_RPC_MAX_PACKET_SIZE = 256 50SOCKET_SERVER = 'localhost' 51SOCKET_PORT = 33000 52MKFIFO_MODE = 0o666 53 54 55def _parse_args(): 56 """Parses and returns the command line arguments.""" 57 parser = argparse.ArgumentParser(description=__doc__) 58 group = parser.add_mutually_exclusive_group(required=True) 59 group.add_argument('-d', '--device', help='the serial port to use') 60 parser.add_argument('-b', 61 '--baudrate', 62 type=int, 63 default=115200, 64 help='the baud rate to use') 65 parser.add_argument( 66 '-o', 67 '--output', 68 type=argparse.FileType('wb'), 69 default=sys.stdout.buffer, 70 help=('The file to which to write device output (HDLC channel 1); ' 71 'provide - or omit for stdout.')) 72 group.add_argument('-s', 73 '--socket-addr', 74 type=str, 75 help='use socket to connect to server, type default for\ 76 localhost:33000, or manually input the server address:port') 77 parser.add_argument('proto_globs', 78 nargs='+', 79 help='glob pattern for .proto files') 80 return parser.parse_args() 81 82 83def _expand_globs(globs: Iterable[str]) -> Iterator[Path]: 84 for pattern in globs: 85 for file in glob.glob(pattern, recursive=True): 86 yield Path(file) 87 88 89def _start_ipython_terminal(client: HdlcRpcClient) -> None: 90 """Starts an interactive IPython terminal with preset variables.""" 91 local_variables = dict( 92 client=client, 93 channel_client=client.client.channel(1), 94 rpcs=client.client.channel(1).rpcs, 95 protos=client.protos.packages, 96 ) 97 98 print(__doc__) # Print the banner 99 IPython.terminal.embed.InteractiveShellEmbed().mainloop( 100 local_ns=local_variables, module=argparse.Namespace()) 101 102 103class SocketClientImpl: 104 def __init__(self, config: str): 105 self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 106 socket_server = '' 107 socket_port = 0 108 109 if config == 'default': 110 socket_server = SOCKET_SERVER 111 socket_port = SOCKET_PORT 112 else: 113 socket_server, socket_port_str = config.split(':') 114 socket_port = int(socket_port_str) 115 self.socket.connect((socket_server, socket_port)) 116 117 def write(self, data: bytes): 118 self.socket.sendall(data) 119 120 def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE): 121 return self.socket.recv(num_bytes) 122 123 124def console(device: str, baudrate: int, proto_globs: Collection[str], 125 socket_addr: str, output: Any) -> int: 126 """Starts an interactive RPC console for HDLC.""" 127 # argparse.FileType doesn't correctly handle '-' for binary files. 128 if output is sys.stdout: 129 output = sys.stdout.buffer 130 131 if not proto_globs: 132 proto_globs = ['**/*.proto'] 133 134 protos = list(_expand_globs(proto_globs)) 135 136 if not protos: 137 _LOG.critical('No .proto files were found with %s', 138 ', '.join(proto_globs)) 139 _LOG.critical('At least one .proto file is required') 140 return 1 141 142 _LOG.debug('Found %d .proto files found with %s', len(protos), 143 ', '.join(proto_globs)) 144 145 if socket_addr is None: 146 serial_device = serial.Serial(device, baudrate, timeout=1) 147 read = lambda: serial_device.read(8192) 148 write = serial_device.write 149 else: 150 try: 151 socket_device = SocketClientImpl(socket_addr) 152 read = socket_device.read 153 write = socket_device.write 154 except ValueError: 155 _LOG.exception('Failed to initialize socket at %s', socket_addr) 156 return 1 157 158 _start_ipython_terminal( 159 HdlcRpcClient(read, protos, default_channels(write), 160 lambda data: write_to_file(data, output))) 161 return 0 162 163 164def main() -> int: 165 return console(**vars(_parse_args())) 166 167 168if __name__ == '__main__': 169 sys.exit(main()) 170