blob: 8817a26cd91ae66d006691e8c24bc4cff0c01acb [file] [log] [blame]
Austin Schuhd1b28992014-10-26 20:55:06 -07001#!/usr/bin/python3
Dave Smith6b2cb012016-03-06 13:10:31 -08002
3import collections
Philipp Schrader7861dce2015-02-23 00:27:59 +00004import matplotlib
5from matplotlib import pylab
Philipp Schrader0db60452015-03-15 07:32:38 +00006from matplotlib.font_manager import FontProperties
Dave Smith6b2cb012016-03-06 13:10:31 -08007import re
8import sys
Philipp Schrader7861dce2015-02-23 00:27:59 +00009
10class Dataset(object):
11 def __init__(self):
12 self.time = []
13 self.data = []
14
15 def Add(self, time, data):
16 self.time.append(time)
17 self.data.append(data)
18
19
20class Plotter(object):
21 def __init__(self):
Philipp Schradereef41a52016-02-24 04:17:56 +000022 self.signal = collections.OrderedDict()
Philipp Schrader7861dce2015-02-23 00:27:59 +000023
24 def Add(self, binary, struct_instance_name, *data_search_path):
25 """
26 Specifies a specific piece of data to plot
27
28 Args:
29 binary: str, The name of the executable that generated the log.
30 struct_instance_name: str, The name of the struct instance whose data
31 contents should be plotted.
32 data_search_path: [str], The path into the struct of the exact piece of
33 data to plot.
34
35 Returns:
36 None
37 """
38 self.signal[(binary, struct_instance_name, data_search_path)] = Dataset()
39
40 def HandleLine(self, line):
41 """
42 Parses a line from a log file and adds the data to the plot data.
43
44 Args:
45 line: str, The line from the log file to parse
46
47 Returns:
48 None
49 """
50 pline = ParseLine(line)
Dave Smithbf4c9902016-02-18 14:30:55 -080051 pline_data = None
52
Philipp Schrader7861dce2015-02-23 00:27:59 +000053 for key in self.signal:
54 value = self.signal[key]
55 binary = key[0]
56 struct_instance_name = key[1]
57 data_search_path = key[2]
Philipp Schrader26681bc2015-04-02 04:25:30 +000058 boolean_multiplier = None
59
60 # If the plot definition line ends with a "-b X" where X is a number then
61 # that number gets drawn when the value is True. Zero gets drawn when the
62 # value is False.
63 if len(data_search_path) >= 2 and data_search_path[-2] == '-b':
64 boolean_multiplier = float(data_search_path[-1])
65 data_search_path = data_search_path[:-2]
Philipp Schrader7861dce2015-02-23 00:27:59 +000066
67 # Make sure that we're looking at the right binary structure instance.
68 if binary == pline.name:
69 if pline.msg.startswith(struct_instance_name + ': '):
Dave Smithbf4c9902016-02-18 14:30:55 -080070 # Parse the structure once.
71 if pline_data is None:
72 _, _, pline_data = pline.ParseStruct()
73 # Traverse the structure as specified in `data_search_path`.
74 # This lets the user access very deeply nested structures.
75 data = pline_data
Philipp Schrader7861dce2015-02-23 00:27:59 +000076 for path in data_search_path:
77 data = data[path]
78
Philipp Schrader26681bc2015-04-02 04:25:30 +000079 if boolean_multiplier is not None:
80 if data == 'T':
81 value.Add(pline.time, boolean_multiplier)
82 else:
83 value.Add(pline.time, 0)
84 else:
85 value.Add(pline.time, data)
Philipp Schrader7861dce2015-02-23 00:27:59 +000086
Philipp Schrader0db60452015-03-15 07:32:38 +000087 def Plot(self, no_binary_in_legend):
Philipp Schrader7861dce2015-02-23 00:27:59 +000088 """
89 Plots all the data after it's parsed.
90
91 This should only be called after `HandleFile` has been called so that there
92 is actual data to plot.
93 """
94 for key in self.signal:
95 value = self.signal[key]
Philipp Schrader0db60452015-03-15 07:32:38 +000096
97 # Create a legend label using the binary name (optional), the structure
98 # name and the data search path.
99 label = key[1] + '.' + '.'.join(key[2])
100 if not no_binary_in_legend:
101 label = key[0] + ' ' + label
102
103 pylab.plot(value.time, value.data, label=label)
104
105 # Set legend font size to small and move it to the top center.
106 fontP = FontProperties()
107 fontP.set_size('small')
108 pylab.legend(bbox_to_anchor=(0.5, 1.05), prop=fontP)
109
Philipp Schrader7861dce2015-02-23 00:27:59 +0000110 pylab.show()
111
Philipp Schrader0db60452015-03-15 07:32:38 +0000112 def PlotFile(self, f, no_binary_in_legend=False):
Philipp Schrader7861dce2015-02-23 00:27:59 +0000113 """
114 Parses and plots all the data.
115
116 Args:
117 f: str, The filename of the log whose data to parse and plot.
118
119 Returns:
120 None
121 """
122 self.HandleFile(f)
Philipp Schrader0db60452015-03-15 07:32:38 +0000123 self.Plot(no_binary_in_legend)
Philipp Schrader7861dce2015-02-23 00:27:59 +0000124
125 def HandleFile(self, f):
126 """
127 Parses the specified log file.
128
129 Args:
130 f: str, The filename of the log whose data to parse.
131
132 Returns:
133 None
134 """
135 with open(f, 'r') as fd:
136 for line in fd:
137 self.HandleLine(line)
138
Austin Schuhd1b28992014-10-26 20:55:06 -0700139
Dave Smith6b2cb012016-03-06 13:10:31 -0800140"""
141A regular expression to match the envelope part of the log entry.
142Parsing of the JSON msg is handled elsewhere.
143"""
144LOG_RE = re.compile("""
145 (.*?) # 1 name
146 \((\d+)\) # 2 pid
147 \((\d+)\) # 3 message_index
148 :\s
149 (\w+?) # 4 level
150 \s+at\s+
151 (\d+\.\d+)s # 5 time
152 :\s
153 ([A-Za-z0-9_./-]+) # 6 filename
154 :\s
155 (\d+) # 7 linenumber
156 :\s
157 (.*) # 8 msg
158 """, re.VERBOSE)
159
Austin Schuhd1b28992014-10-26 20:55:06 -0700160class LogEntry:
161 """This class provides a way to parse log entries."""
162
163 def __init__(self, line):
Dave Smith6b2cb012016-03-06 13:10:31 -0800164 """Populates a LogEntry from a line."""
165 m = LOG_RE.match(line)
166 if m is None:
167 print("LOG_RE failed on", line)
168 sys.exit(1)
169 self.name = m.group(1)
170 self.pid_index = int(m.group(2))
171 self.msg_index = int(m.group(3))
172 self.level = m.group(4)
173 self.time = float(m.group(5))
174 self.filename = m.group(6)
175 self.linenumber = m.group(7)
176 self.msg = m.group(8)
Austin Schuhd1b28992014-10-26 20:55:06 -0700177
178 def __str__(self):
179 """Formats the data cleanly."""
180 return '%s(%d)(%d): %s at %fs: %s: %d: %s' % (self.name, self.pid, self.msg_index, self.level, self.time, self.filename, self.linenumber, self.msg)
181
182 def __JsonizeTokenArray(self, sub_array, tokens, token_index):
183 """Parses an array from the provided tokens.
184
185 Args:
186 sub_array: list, The list to stick the elements in.
187 tokens: list of strings, The list with all the tokens in it.
188 token_index: int, Where to start in the token list.
189
190 Returns:
191 int, The last token used.
192 """
193 # Make sure the data starts with a '['
194 if tokens[token_index] != '[':
195 print(tokens)
196 print('Expected [ at beginning, found', tokens[token_index + 1])
197 return None
198
199 # Eat the '['
200 token_index += 1
201
202 # Loop through the tokens.
203 while token_index < len(tokens):
204 if tokens[token_index + 1] == ',':
205 # Next item is a comma, so we should just add the element.
206 sub_array.append(tokens[token_index])
207 token_index += 2
208 elif tokens[token_index + 1] == ']':
209 # Next item is a ']', so we should just add the element and finish.
210 sub_array.append(tokens[token_index])
211 token_index += 1
212 return token_index
213 else:
214 # Otherwise, it must be a sub-message.
215 sub_json = dict()
216 token_index = self.JsonizeTokens(sub_json, tokens, token_index + 1)
217 sub_array.append(sub_json)
218 if tokens[token_index] == ',':
219 # Handle there either being another data element.
220 token_index += 1
221 elif tokens[token_index] == ']':
222 # Handle the end of the array.
223 return token_index
224 else:
225 print('Unexpected ', tokens[token_index])
226 return None
227
228 print('Unexpected end')
229 return None
230
231 def JsonizeTokens(self, json, tokens, token_index):
232 """Creates a json-like dictionary from the provided tokens.
233
234 Args:
235 json: dict, The dict to stick the elements in.
236 tokens: list of strings, The list with all the tokens in it.
237 token_index: int, Where to start in the token list.
238
239 Returns:
240 int, The last token used.
241 """
242 # Check that the message starts with a {
243 if tokens[token_index] != '{':
244 print(tokens)
245 print('Expected { at beginning, found', tokens[token_index])
246 return None
247
248 # Eat the {
249 token_index += 1
250
251 # States and state variable for parsing elements.
252 STATE_INIT = 'init'
253 STATE_HAS_NAME = 'name'
254 STATE_HAS_COLON = 'colon'
255 STATE_EXPECTING_SUBMSG = 'submsg'
256 STATE_EXPECTING_COMMA = 'comma'
257 parser_state = STATE_INIT
258
259 while token_index < len(tokens):
260 if tokens[token_index] == '}':
261 # Finish if there is a }
262 return token_index + 1
263 elif tokens[token_index] == '{':
264 if parser_state != STATE_EXPECTING_SUBMSG:
265 print(tokens)
266 print(parser_state)
267 print('Bad input, was not expecting {')
268 return None
269 # Found a submessage, parse it.
270 sub_json = dict()
271 token_index = self.JsonizeTokens(sub_json, tokens, token_index)
272 json[token_name] = sub_json
273 parser_state = STATE_EXPECTING_COMMA
274 else:
275 if parser_state == STATE_INIT:
276 # This token is the name.
277 token_name = tokens[token_index]
278 parser_state = STATE_HAS_NAME
279 elif parser_state == STATE_HAS_NAME:
280 if tokens[token_index] != ':':
281 print(tokens)
282 print(parser_state)
283 print('Bad input, found', tokens[token_index], 'expected :')
284 return None
285 # After a name, comes a :
286 parser_state = STATE_HAS_COLON
287 elif parser_state == STATE_HAS_COLON:
288 # After the colon, figure out what is next.
289 if tokens[token_index] == '[':
290 # Found a sub-array!
291 sub_array = []
292 token_index = self.__JsonizeTokenArray(sub_array, tokens, token_index)
293 json[token_name] = sub_array
294 parser_state = STATE_EXPECTING_COMMA
295 elif tokens[token_index + 1] == '{':
296 # Found a sub-message, trigger parsing it.
297 parser_state = STATE_EXPECTING_SUBMSG
298 else:
299 # This is just an element, move on.
300 json[token_name] = tokens[token_index]
301 parser_state = STATE_EXPECTING_COMMA
302 elif parser_state == STATE_EXPECTING_COMMA:
303 # Complain if there isn't a comma here.
304 if tokens[token_index] != ',':
305 print(tokens)
306 print(parser_state)
307 print('Bad input, found', tokens[token_index], 'expected ,')
308 return None
309 parser_state = STATE_INIT
310 else:
311 print('Bad parser state')
312 return None
313 token_index += 1
314
315 print('Unexpected end')
316 return None
317
318 def ParseStruct(self):
319 """Parses the message as a structure.
320
321 Returns:
322 struct_name, struct_type, json dict.
323 """
324 struct_name_index = self.msg.find(':')
325 struct_name = self.msg[0:struct_name_index]
326
327 struct_body = self.msg[struct_name_index+2:]
328 tokens = []
329 this_token = ''
330 # For the various deliminators, append what we have found so far to the
331 # list and the token.
332 for char in struct_body:
333 if char == '{':
334 if this_token:
335 tokens.append(this_token)
336 this_token = ''
337 tokens.append('{')
338 elif char == '}':
339 if this_token:
340 tokens.append(this_token)
341 this_token = ''
342 tokens.append('}')
343 elif char == '[':
344 if this_token:
345 tokens.append(this_token)
346 this_token = ''
347 tokens.append('[')
348 elif char == ']':
349 if this_token:
350 tokens.append(this_token)
351 this_token = ''
352 tokens.append(']')
353 elif char == ':':
354 if this_token:
355 tokens.append(this_token)
356 this_token = ''
357 tokens.append(':')
358 elif char == ',':
359 if this_token:
360 tokens.append(this_token)
361 this_token = ''
362 tokens.append(',')
363 elif char == ' ':
364 if this_token:
365 tokens.append(this_token)
366 this_token = ''
367 else:
368 this_token += char
369 if this_token:
370 tokens.append(this_token)
371
372 struct_type = tokens[0]
373 json = dict()
374 # Now that we have tokens, parse them.
375 self.JsonizeTokens(json, tokens, 1)
376
377 return (struct_name, struct_type, json)
378
379
380def ParseLine(line):
381 return LogEntry(line)
382
383if __name__ == '__main__':
384 print('motor_writer(2240)(07421): DEBUG at 0000000819.99620s: ../../frc971/output/motor_writer.cc: 105: sending: .aos.controls.OutputCheck{pwm_value:221, pulse_length:2.233333}')
385 line = ParseLine('motor_writer(2240)(07421): DEBUG at 0000000819.99620s: ../../frc971/output/motor_writer.cc: 105: sending: .aos.controls.OutputCheck{pwm_value:221, pulse_length:2.233333}')
386 if '.aos.controls.OutputCheck' in line.msg:
387 print(line)
388 print(line.ParseStruct())
389
390 line = ParseLine('claw(2263)(19404): DEBUG at 0000000820.00000s: ../../aos/common/controls/control_loop-tmpl.h: 104: position: .frc971.control_loops.ClawGroup.Position{top:.frc971.control_loops.HalfClawPosition{position:1.672153, front:.frc971.HallEffectStruct{current:f, posedge_count:0, negedge_count:52}, calibration:.frc971.HallEffectStruct{current:f, posedge_count:6, negedge_count:13}, back:.frc971.HallEffectStruct{current:f, posedge_count:0, negedge_count:62}, posedge_value:0.642681, negedge_value:0.922207}, bottom:.frc971.control_loops.HalfClawPosition{position:1.353539, front:.frc971.HallEffectStruct{current:f, posedge_count:2, negedge_count:150}, calibration:.frc971.HallEffectStruct{current:f, posedge_count:8, negedge_count:18}, back:.frc971.HallEffectStruct{current:f, posedge_count:0, negedge_count:6}, posedge_value:0.434514, negedge_value:0.759491}}')
391 print(line.ParseStruct())
392
393 line = ParseLine('joystick_proxy(2255)(39560): DEBUG at 0000000820.00730s: ../../aos/prime/input/joystick_input.cc: 61: sending: .aos.RobotState{joysticks:[.aos.Joystick{buttons:0, axis:[0.000000, 1.000000, 1.000000, 0.000000]}, .aos.Joystick{buttons:0, axis:[-0.401575, 1.000000, -1.007874, 0.000000]}, .aos.Joystick{buttons:0, axis:[0.007874, 0.000000, 1.000000, -1.007874]}, .aos.Joystick{buttons:0, axis:[0.000000, 0.000000, 0.000000, 0.000000]}], test_mode:f, fms_attached:f, enabled:T, autonomous:f, team_id:971, fake:f}')
394 print(line.ParseStruct())