read_trace.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. # SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. import argparse
  4. import datetime
  5. import json
  6. import os
  7. import signal
  8. import sys
  9. from enum import Enum
  10. from functools import partial
  11. from typing import Any, List
  12. try:
  13. import espytrace.apptrace
  14. except ImportError: # cheat and use IDF's copy of espytrace if available
  15. idf_path = os.getenv('IDF_PATH')
  16. if not idf_path or not os.path.exists(idf_path):
  17. print('IDF not found. Please export idf')
  18. raise SystemExit(1)
  19. sys.path.insert(0, os.path.join(idf_path, 'tools', 'esp_app_trace'))
  20. import espytrace.apptrace
  21. try:
  22. import dash
  23. from dash import dcc, html
  24. from dash.dependencies import Input, Output
  25. from plotly.subplots import make_subplots
  26. except ImportError:
  27. print('Dash not found. Try to run \'pip install dash\'')
  28. raise SystemExit(1)
  29. plots = [] # type: List[Any]
  30. output_lines = [] # type: List[Any]
  31. COMMENT_LINE = '//'
  32. class States(Enum):
  33. STX_WAIT = 1
  34. LENGTH_WAIT = 2
  35. DATA_WAIT = 3
  36. ETX_WAIT = 4
  37. app = dash.Dash(__name__)
  38. app.layout = html.Div(
  39. html.Div([
  40. html.H2('Telemetry Data'),
  41. html.Div(id='live-update-data'),
  42. dcc.Graph(id='live-update-graph', style={'height': 800}), # Height of the plotting area setted to 800px
  43. dcc.Interval(
  44. id='interval-component',
  45. interval=5 * 100, # Graph will be updated every 500 ms
  46. n_intervals=0
  47. )
  48. ])
  49. )
  50. # Multiple components can update everytime interval gets fired.
  51. @app.callback(Output('live-update-graph', 'figure'),
  52. Input('interval-component', 'n_intervals'))
  53. def update_graph_live(_n: Any) -> Any: # pylint: disable=undefined-argument
  54. excluded_keys_for_plot = {'id', 'x_axis_data_size','y_axis_data_size', 'data_type', 'precision', 'x_axis_timestamp'}
  55. fig = make_subplots(rows=len(plots), cols=1, vertical_spacing=0.2, subplot_titles=[each_plot['title'] for each_plot in plots])
  56. for i, each_plot in enumerate(plots, start=1):
  57. for each_subplot in each_plot['plots']:
  58. plot_dict = {k: each_subplot[k] for k in each_subplot.keys() - excluded_keys_for_plot}
  59. fig.append_trace(plot_dict, i, 1)
  60. fig['layout']['xaxis{}'.format(i)]['title'] = each_plot['xaxis_title']
  61. fig['layout']['yaxis{}'.format(i)]['title'] = each_plot['yaxis_title']
  62. return fig
  63. def get_value_from_key(input_id: int, key: str) -> Any:
  64. for each_plot in plots:
  65. for each_subplot in each_plot['plots']:
  66. if each_subplot['id'] == input_id:
  67. return each_subplot[key]
  68. return None
  69. def parse_data_and_print(packet: bytes, offset_time: Any) -> None:
  70. ID_LENGTH = 4
  71. x_axis_data_length = 4
  72. y_axis_data_length = 4
  73. input_id = int.from_bytes(packet[:ID_LENGTH], 'little')
  74. data_size = get_value_from_key(input_id, 'x_axis_data_size')
  75. x_axis_data_length = data_size
  76. x_axis_raw_data = int.from_bytes(packet[ID_LENGTH:ID_LENGTH + x_axis_data_length], 'little')
  77. is_timestamp = get_value_from_key(input_id, 'x_axis_timestamp')
  78. if is_timestamp:
  79. x_axis_data = offset_time + datetime.timedelta(seconds=x_axis_raw_data / 1000)
  80. else:
  81. x_axis_data = float(x_axis_raw_data)
  82. data_size = get_value_from_key(input_id, 'y_axis_data_size')
  83. y_axis_data_length = data_size
  84. y_axis_data = int.from_bytes(packet[x_axis_data_length + ID_LENGTH:x_axis_data_length + ID_LENGTH + y_axis_data_length], 'little', signed=True)
  85. if get_value_from_key(input_id, 'data_type') in ['float', 'double']:
  86. precision = get_value_from_key(input_id, 'precision')
  87. y_axis_data = y_axis_data / (10 ** precision)
  88. ct = datetime.datetime.now()
  89. str_ctr = ct.strftime('%x-%X.%f')
  90. output_str = str_ctr + '-> ' + str(packet)
  91. output_lines.append(output_str)
  92. arr = [x_axis_data, y_axis_data]
  93. update_specific_graph_list(int(input_id), arr)
  94. class CustomRequestHandler(espytrace.apptrace.TCPRequestHandler):
  95. """
  96. Handler for incoming TCP connections
  97. """
  98. def handle(self) -> None:
  99. STX = b'esp32'
  100. ETX = b'\x03'
  101. STX_LEN = 5
  102. ETX_LEN = 1
  103. DATA_LENGTH_LEN = 1
  104. PACKET_TIMEOUT_SECOND = 3
  105. state = States.STX_WAIT
  106. val_to_parse = b''
  107. length = 0
  108. offset_time = datetime.datetime.now()
  109. start_time = datetime.datetime.now()
  110. data = b''
  111. while not self.server.need_stop:
  112. data += self.request.recv(1024)
  113. if state == States.STX_WAIT:
  114. if len(data) >= STX_LEN and data.find(STX) != -1:
  115. data = data[data.find(STX) + STX_LEN:]
  116. state = States.LENGTH_WAIT
  117. start_time = datetime.datetime.now()
  118. else:
  119. data = data[-STX_LEN:]
  120. if state == States.LENGTH_WAIT:
  121. if len(data) > 0:
  122. state = States.DATA_WAIT
  123. length = int.from_bytes(data[:DATA_LENGTH_LEN], 'little')
  124. data = data[DATA_LENGTH_LEN:]
  125. if state == States.DATA_WAIT:
  126. if len(data) >= length:
  127. state = States.ETX_WAIT
  128. val_to_parse = data[:length]
  129. data = data[length:]
  130. if state == States.ETX_WAIT:
  131. if len(data) >= ETX_LEN and data[:ETX_LEN] == ETX:
  132. state = States.STX_WAIT
  133. parse_data_and_print(val_to_parse, offset_time)
  134. data = data[ETX_LEN:]
  135. if state != States.STX_WAIT and (datetime.datetime.now() - start_time).seconds > PACKET_TIMEOUT_SECOND:
  136. print('Packet timed out. Dropping!')
  137. state = States.STX_WAIT
  138. def read_json(file_path: str) -> Any:
  139. with open(file_path, 'r') as f:
  140. data = json.load(f)
  141. return data
  142. def save_data(file_path: str) -> None:
  143. with open(file_path, 'w') as f:
  144. f.writelines(output_lines)
  145. def signal_handler(output_file_path: str, reader: Any, sig: Any, frame: Any) -> None:
  146. del sig, frame
  147. reader.cleanup()
  148. if output_file_path is not None:
  149. save_data(output_file_path)
  150. sys.exit(0)
  151. def update_specific_graph_list(input_id: int, val: List[Any]) -> None:
  152. for each_plot in plots:
  153. for each_subplot in each_plot['plots']:
  154. if each_subplot['id'] == input_id:
  155. each_subplot['x'].append((val[0]))
  156. each_subplot['y'].append(float(val[1]))
  157. return
  158. def check_entry_and_get_struct(entry: dict, data_label: str) -> dict:
  159. mandatory_key = 'id'
  160. other_keys = {'x': [], 'y': [], 'type': 'scatter'}
  161. ret_dict = entry
  162. if ret_dict.get(mandatory_key) is None:
  163. raise KeyError('ID key is missing')
  164. ret_dict['name'] = data_label
  165. for each_key, each_value in other_keys.items():
  166. if ret_dict.get(each_key) is None:
  167. ret_dict[each_key] = each_value
  168. return ret_dict
  169. def validate_json(json_file: Any) -> None:
  170. mandatory_keys = {'id':int, 'x_axis_data_size': int, 'y_axis_data_size': int, 'data_type': str, 'x_axis_timestamp': bool}
  171. for each_plot in json_file:
  172. if each_plot.startswith(COMMENT_LINE):
  173. continue
  174. try:
  175. each_subplot = json_file[each_plot]['data_streams']
  176. except KeyError:
  177. print('data_streams key not found. Aborting')
  178. raise SystemExit(1)
  179. for each_subplot in json_file[each_plot]['data_streams']:
  180. for key, value in mandatory_keys.items():
  181. try:
  182. val = json_file[each_plot]['data_streams'][each_subplot][key]
  183. if not isinstance(val, value):
  184. print('[{}][data_streams][{}][{}] expected {} found {}'.format(each_plot, each_subplot, key, mandatory_keys[key], type(val)))
  185. raise SystemExit(1)
  186. except KeyError:
  187. print('[{}][data_streams][{}][{}] key not found. Aborting'.format(each_plot, each_subplot, key))
  188. raise SystemExit(1)
  189. def configure_plots(json_file: Any) -> None:
  190. for each_plot in json_file:
  191. if each_plot.startswith(COMMENT_LINE):
  192. continue
  193. data_struct = {'title': each_plot, 'plots': [], 'xaxis_title': json_file[each_plot]['xaxis_title'], 'yaxis_title': json_file[each_plot]['yaxis_title']}
  194. for each_subplot in json_file[each_plot]['data_streams']:
  195. subplot_items = json_file[each_plot]['data_streams'][each_subplot]
  196. plot_data_struct = check_entry_and_get_struct(subplot_items, each_subplot)
  197. data_struct['plots'].append(plot_data_struct)
  198. plots.append(data_struct)
  199. def main() -> None:
  200. parser = argparse.ArgumentParser(description='Apptrace Visualizing Tool')
  201. parser.add_argument('--plot-config', help='Path to json file', required=False, type=str,
  202. default=os.path.realpath(os.path.join(os.path.dirname(__file__), 'data.json')))
  203. parser.add_argument('--source', help='Data source path', required=True, type=str)
  204. parser.add_argument('--output-file', help='Path to program output file in txt format', type=str)
  205. args = parser.parse_args()
  206. output_file_path = args.output_file
  207. json_file_name = args.plot_config
  208. data_source = args.source
  209. json_file = read_json(json_file_name)
  210. validate_json(json_file)
  211. configure_plots(json_file)
  212. reader = espytrace.apptrace.reader_create(data_source, 1, CustomRequestHandler)
  213. signal.signal(signal.SIGINT, partial(signal_handler, output_file_path, reader))
  214. app.run_server(debug=True, use_reloader=False, port=8055)
  215. if __name__ == '__main__':
  216. main()