1#!/usr/bin/env python3 2# Copyright 2023 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 trace service. 18 19Example usage: 20python pw_console/py/pw_console/trace_client.py 21 -o trace.json 22 -t out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example 23""" 24 25import argparse 26import logging 27import sys 28from pathlib import Path 29from types import ModuleType 30 31 32from pw_transfer import transfer_pb2 33from pw_log.proto import log_pb2 34from pw_trace_protos import trace_service_pb2 35from pw_trace import trace 36from pw_trace_tokenized import trace_tokenized 37import pw_transfer 38from pw_file import file_pb2 39from pw_hdlc import rpc 40from pw_system import device_tracing 41from pw_tokenizer import detokenize 42from pw_console import socket_client 43 44 45_LOG = logging.getLogger('pw_console_trace_client') 46_LOG.level = logging.DEBUG 47_LOG.addHandler(logging.StreamHandler(sys.stdout)) 48 49 50def start_tracing_on_device(client): 51 """Start tracing on the device""" 52 service = client.rpcs.pw.trace.proto.TraceService 53 service.Start() 54 55 56def stop_tracing_on_device(client): 57 """Stop tracing on the device""" 58 service = client.rpcs.pw.trace.proto.TraceService 59 return service.Stop() 60 61 62def list_files_on_device(client): 63 """List files on the device""" 64 service = client.rpcs.pw.file.FileSystem 65 return service.List() 66 67 68def delete_file_on_device(client, path): 69 """Delete a file on the device""" 70 service = client.rpcs.pw.file.FileSystem 71 req = file_pb2.DeleteRequest(path=path) 72 return service.Delete(req) 73 74 75def _parse_args(): 76 """Parse and return command line arguments.""" 77 78 parser = argparse.ArgumentParser( 79 description=__doc__, 80 formatter_class=argparse.RawDescriptionHelpFormatter, 81 ) 82 group = parser.add_mutually_exclusive_group(required=False) 83 group.add_argument('-d', '--device', help='the serial port to use') 84 parser.add_argument( 85 '-b', 86 '--baudrate', 87 type=int, 88 default=115200, 89 help='The baud rate to use', 90 ) 91 group.add_argument( 92 '-s', 93 '--socket-addr', 94 type=str, 95 default='default', 96 help=( 97 'Use socket to connect to server, type default for ' 98 'localhost:33000, or manually input the server address:port' 99 ), 100 ) 101 parser.add_argument( 102 '-o', 103 '--trace_output', 104 dest='trace_output_file', 105 help='The json file to which to write the output.', 106 ) 107 parser.add_argument( 108 '-t', 109 '--trace_token_database', 110 help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.', 111 ) 112 parser.add_argument( 113 '-f', 114 '--ticks_per_second', 115 type=int, 116 dest='ticks_per_second', 117 help='The clock rate of the trace events.', 118 ) 119 parser.add_argument( 120 '--time_offset', 121 type=int, 122 dest='time_offset', 123 default=0, 124 help='Time offset (us) of the trace events (Default 0).', 125 ) 126 parser.add_argument( 127 '--channel-id', 128 type=int, 129 dest='channel_id', 130 default=rpc.DEFAULT_CHANNEL_ID, 131 help='Channel ID used in RPC communications.', 132 ) 133 return parser.parse_args() 134 135 136def _main(args) -> int: 137 detokenizer = detokenize.AutoUpdatingDetokenizer( 138 args.trace_token_database + '#trace' 139 ) 140 detokenizer.show_errors = True 141 142 socket_impl = socket_client.SocketClient 143 try: 144 socket_device = socket_impl(args.socket_addr) 145 reader = rpc.SelectableReader(socket_device) 146 write = socket_device.write 147 except ValueError: 148 _LOG.exception('Failed to initialize socket at %s', args.socket_addr) 149 return 1 150 151 protos: list[ModuleType | Path] = [ 152 log_pb2, 153 file_pb2, 154 transfer_pb2, 155 trace_service_pb2, 156 ] 157 158 with reader: 159 device_client = device_tracing.DeviceWithTracing( 160 args.channel_id, 161 reader, 162 write, 163 protos, 164 detokenizer=detokenizer, 165 timestamp_decoder=None, 166 rpc_timeout_s=5, 167 use_rpc_logging=True, 168 use_hdlc_encoding=True, 169 ticks_per_second=args.ticks_per_second, 170 ) 171 172 with device_client: 173 _LOG.info('Starting tracing') 174 start_tracing_on_device(device_client) 175 176 _LOG.info('Stopping tracing') 177 file_id = stop_tracing_on_device(device_client) 178 _LOG.info('Trace file id = %d', file_id.response.file_id) 179 180 _LOG.info('Listing Files') 181 stream_response = list_files_on_device(device_client) 182 183 if not stream_response.status.ok(): 184 _LOG.error('Failed to list files %s', stream_response.status) 185 return 1 186 187 for list_response in stream_response.responses: 188 for file in list_response.paths: 189 _LOG.info('Transfering File: %s', file.path) 190 try: 191 data = device_client.transfer_manager.read(file.file_id) 192 events = trace_tokenized.get_trace_events( 193 [detokenizer.database], 194 data, 195 device_client.ticks_per_second, 196 args.time_offset, 197 ) 198 json_lines = trace.generate_trace_json(events) 199 trace_tokenized.save_trace_file( 200 json_lines, args.trace_output_file 201 ) 202 except pw_transfer.Error as err: 203 print('Failed to read:', err.status) 204 205 _LOG.info('Deleting File: %s', file.path) 206 delete_file_on_device(device_client, file.path) 207 208 _LOG.info('All trace transfers completed successfully') 209 210 return 0 211 212 213if __name__ == '__main__': 214 _main(_parse_args()) 215