1#!/usr/bin/env python3 2# Copyright 2021 The Pigweed Authors 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not 5# use this file except in compliance with the License. You may obtain a copy of 6# the License at 7# 8# https://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations under 14# the License. 15""" 16Generates json trace files viewable using chrome://tracing using RPCs from a 17connected HdlcRpcClient. 18 19Example usage: 20python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py -s localhost:33000 21 -o trace.json 22 -t 23 out/pw_strict_host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc 24 pw_trace_tokenized/pw_trace_protos/trace_rpc.proto 25""" # pylint: disable=line-too-long 26# pylint: enable=line-too-long 27 28import argparse 29import logging 30import glob 31from pathlib import Path 32import sys 33from typing import Collection, Iterable, Iterator 34import serial # type: ignore 35from pw_tokenizer import database 36from pw_trace import trace 37from pw_hdlc.rpc import HdlcRpcClient, default_channels 38from pw_hdlc.rpc_console import SocketClientImpl 39from pw_trace_tokenized import trace_tokenized 40 41_LOG = logging.getLogger('pw_trace_tokenizer') 42 43PW_RPC_MAX_PACKET_SIZE = 256 44SOCKET_SERVER = 'localhost' 45SOCKET_PORT = 33000 46MKFIFO_MODE = 0o666 47 48 49def _expand_globs(globs: Iterable[str]) -> Iterator[Path]: 50 for pattern in globs: 51 for file in glob.glob(pattern, recursive=True): 52 yield Path(file) 53 54 55def get_hdlc_rpc_client(device: str, baudrate: int, 56 proto_globs: Collection[str], socket_addr: str, 57 **kwargs): 58 """Get the HdlcRpcClient based on arguments.""" 59 del kwargs # ignore 60 if not proto_globs: 61 proto_globs = ['**/*.proto'] 62 63 protos = list(_expand_globs(proto_globs)) 64 65 if not protos: 66 _LOG.critical('No .proto files were found with %s', 67 ', '.join(proto_globs)) 68 _LOG.critical('At least one .proto file is required') 69 return 1 70 71 _LOG.debug('Found %d .proto files found with %s', len(protos), 72 ', '.join(proto_globs)) 73 74 # TODO(rgoliver): When pw has a generalized transport for RPC this should 75 # use it so it isn't specific to HDLC 76 if socket_addr is None: 77 serial_device = serial.Serial(device, baudrate, timeout=1) 78 read = lambda: serial_device.read(8192) 79 write = serial_device.write 80 else: 81 try: 82 socket_device = SocketClientImpl(socket_addr) 83 read = socket_device.read 84 write = socket_device.write 85 except ValueError: 86 _LOG.exception('Failed to initialize socket at %s', socket_addr) 87 return 1 88 89 return HdlcRpcClient(read, protos, default_channels(write)) 90 91 92def get_trace_data_from_device(client): 93 """Get the trace data using RPC from a Client""" 94 data = b'' 95 service = client.client.channel(1).rpcs.pw.trace.TraceService 96 result = service.GetTraceData().responses 97 for streamed_data in result: 98 data = data + bytes([len(streamed_data.data)]) 99 data = data + streamed_data.data 100 _LOG.debug(''.join(format(x, '02x') for x in streamed_data.data)) 101 return data 102 103 104def _parse_args(): 105 """Parse and return command line arguments.""" 106 107 parser = argparse.ArgumentParser( 108 description=__doc__, 109 formatter_class=argparse.RawDescriptionHelpFormatter) 110 group = parser.add_mutually_exclusive_group(required=True) 111 group.add_argument('-d', '--device', help='the serial port to use') 112 parser.add_argument('-b', 113 '--baudrate', 114 type=int, 115 default=115200, 116 help='the baud rate to use') 117 group.add_argument('-s', 118 '--socket-addr', 119 type=str, 120 help='use socket to connect to server, type default for\ 121 localhost:33000, or manually input the server address:port') 122 parser.add_argument('-o', 123 '--trace_output', 124 dest='trace_output_file', 125 help=('The json file to which to write the output.')) 126 parser.add_argument( 127 '-t', 128 '--trace_token_database', 129 help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.') 130 parser.add_argument('proto_globs', 131 nargs='+', 132 help='glob pattern for .proto files') 133 parser.add_argument( 134 '-f', 135 '--ticks_per_second', 136 type=int, 137 dest='ticks_per_second', 138 default=1000, 139 help=('The clock rate of the trace events (Default 1000).')) 140 return parser.parse_args() 141 142 143def _main(args): 144 token_database = \ 145 database.load_token_database(args.trace_token_database, domain="trace") 146 _LOG.info(database.database_summary(token_database)) 147 client = get_hdlc_rpc_client(**vars(args)) 148 data = get_trace_data_from_device(client) 149 events = trace_tokenized.get_trace_events([token_database], data, 150 args.ticks_per_second) 151 json_lines = trace.generate_trace_json(events) 152 trace_tokenized.save_trace_file(json_lines, args.trace_output_file) 153 154 155if __name__ == '__main__': 156 if sys.version_info[0] < 3: 157 sys.exit('ERROR: The detokenizer command line tools require Python 3.') 158 _main(_parse_args()) 159