blob: e477bdda6e740ea5ba858959f65246e63bda0d66 [file] [log] [blame]
Austin Schuhd1b28992014-10-26 20:55:06 -07001#!/usr/bin/python3
Philipp Schrader7861dce2015-02-23 00:27:59 +00002import matplotlib
3from matplotlib import pylab
Philipp Schrader0db60452015-03-15 07:32:38 +00004from matplotlib.font_manager import FontProperties
Philipp Schrader7861dce2015-02-23 00:27:59 +00005
6class Dataset(object):
7 def __init__(self):
8 self.time = []
9 self.data = []
10
11 def Add(self, time, data):
12 self.time.append(time)
13 self.data.append(data)
14
15
16class Plotter(object):
17 def __init__(self):
18 self.signal = dict()
19
20 def Add(self, binary, struct_instance_name, *data_search_path):
21 """
22 Specifies a specific piece of data to plot
23
24 Args:
25 binary: str, The name of the executable that generated the log.
26 struct_instance_name: str, The name of the struct instance whose data
27 contents should be plotted.
28 data_search_path: [str], The path into the struct of the exact piece of
29 data to plot.
30
31 Returns:
32 None
33 """
34 self.signal[(binary, struct_instance_name, data_search_path)] = Dataset()
35
36 def HandleLine(self, line):
37 """
38 Parses a line from a log file and adds the data to the plot data.
39
40 Args:
41 line: str, The line from the log file to parse
42
43 Returns:
44 None
45 """
46 pline = ParseLine(line)
47 for key in self.signal:
48 value = self.signal[key]
49 binary = key[0]
50 struct_instance_name = key[1]
51 data_search_path = key[2]
Philipp Schrader26681bc2015-04-02 04:25:30 +000052 boolean_multiplier = None
53
54 # If the plot definition line ends with a "-b X" where X is a number then
55 # that number gets drawn when the value is True. Zero gets drawn when the
56 # value is False.
57 if len(data_search_path) >= 2 and data_search_path[-2] == '-b':
58 boolean_multiplier = float(data_search_path[-1])
59 data_search_path = data_search_path[:-2]
Philipp Schrader7861dce2015-02-23 00:27:59 +000060
61 # Make sure that we're looking at the right binary structure instance.
62 if binary == pline.name:
63 if pline.msg.startswith(struct_instance_name + ': '):
64 # Parse the structure and traverse it as specified in
65 # `data_search_path`. This lets the user access very deeply nested
66 # structures.
67 _, _, data = pline.ParseStruct()
68 for path in data_search_path:
69 data = data[path]
70
Philipp Schrader26681bc2015-04-02 04:25:30 +000071 if boolean_multiplier is not None:
72 if data == 'T':
73 value.Add(pline.time, boolean_multiplier)
74 else:
75 value.Add(pline.time, 0)
76 else:
77 value.Add(pline.time, data)
Philipp Schrader7861dce2015-02-23 00:27:59 +000078
Philipp Schrader0db60452015-03-15 07:32:38 +000079 def Plot(self, no_binary_in_legend):
Philipp Schrader7861dce2015-02-23 00:27:59 +000080 """
81 Plots all the data after it's parsed.
82
83 This should only be called after `HandleFile` has been called so that there
84 is actual data to plot.
85 """
86 for key in self.signal:
87 value = self.signal[key]
Philipp Schrader0db60452015-03-15 07:32:38 +000088
89 # Create a legend label using the binary name (optional), the structure
90 # name and the data search path.
91 label = key[1] + '.' + '.'.join(key[2])
92 if not no_binary_in_legend:
93 label = key[0] + ' ' + label
94
95 pylab.plot(value.time, value.data, label=label)
96
97 # Set legend font size to small and move it to the top center.
98 fontP = FontProperties()
99 fontP.set_size('small')
100 pylab.legend(bbox_to_anchor=(0.5, 1.05), prop=fontP)
101
Philipp Schrader7861dce2015-02-23 00:27:59 +0000102 pylab.show()
103
Philipp Schrader0db60452015-03-15 07:32:38 +0000104 def PlotFile(self, f, no_binary_in_legend=False):
Philipp Schrader7861dce2015-02-23 00:27:59 +0000105 """
106 Parses and plots all the data.
107
108 Args:
109 f: str, The filename of the log whose data to parse and plot.
110
111 Returns:
112 None
113 """
114 self.HandleFile(f)
Philipp Schrader0db60452015-03-15 07:32:38 +0000115 self.Plot(no_binary_in_legend)
Philipp Schrader7861dce2015-02-23 00:27:59 +0000116
117 def HandleFile(self, f):
118 """
119 Parses the specified log file.
120
121 Args:
122 f: str, The filename of the log whose data to parse.
123
124 Returns:
125 None
126 """
127 with open(f, 'r') as fd:
128 for line in fd:
129 self.HandleLine(line)
130
Austin Schuhd1b28992014-10-26 20:55:06 -0700131
132class LogEntry:
133 """This class provides a way to parse log entries."""
134
135 def __init__(self, line):
136 """Creates a LogEntry from a line."""
137 name_index = line.find('(')
138 self.name = line[0:name_index]
139
140 pid_index = line.find(')', name_index + 1)
141 self.pid = int(line[name_index + 1:pid_index])
142
143 msg_index_index = line.find(')', pid_index + 1)
144 self.msg_index = int(line[pid_index + 2:msg_index_index])
145
146 level_index = line.find(' ', msg_index_index + 3)
147 self.level = line[msg_index_index + 3:level_index]
148
149 time_index_start = line.find(' at ', level_index) + 4
150 time_index_end = line.find('s:', level_index)
151 self.time = float(line[time_index_start:time_index_end])
152
153 filename_end = line.find(':', time_index_end + 3)
154 self.filename = line[time_index_end + 3:filename_end]
155
156 linenumber_end = line.find(':', filename_end + 2)
157 self.linenumber = int(line[filename_end + 2:linenumber_end])
158
159 self.msg = line[linenumber_end+2:]
160
161 def __str__(self):
162 """Formats the data cleanly."""
163 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)
164
165 def __JsonizeTokenArray(self, sub_array, tokens, token_index):
166 """Parses an array from the provided tokens.
167
168 Args:
169 sub_array: list, The list to stick the elements in.
170 tokens: list of strings, The list with all the tokens in it.
171 token_index: int, Where to start in the token list.
172
173 Returns:
174 int, The last token used.
175 """
176 # Make sure the data starts with a '['
177 if tokens[token_index] != '[':
178 print(tokens)
179 print('Expected [ at beginning, found', tokens[token_index + 1])
180 return None
181
182 # Eat the '['
183 token_index += 1
184
185 # Loop through the tokens.
186 while token_index < len(tokens):
187 if tokens[token_index + 1] == ',':
188 # Next item is a comma, so we should just add the element.
189 sub_array.append(tokens[token_index])
190 token_index += 2
191 elif tokens[token_index + 1] == ']':
192 # Next item is a ']', so we should just add the element and finish.
193 sub_array.append(tokens[token_index])
194 token_index += 1
195 return token_index
196 else:
197 # Otherwise, it must be a sub-message.
198 sub_json = dict()
199 token_index = self.JsonizeTokens(sub_json, tokens, token_index + 1)
200 sub_array.append(sub_json)
201 if tokens[token_index] == ',':
202 # Handle there either being another data element.
203 token_index += 1
204 elif tokens[token_index] == ']':
205 # Handle the end of the array.
206 return token_index
207 else:
208 print('Unexpected ', tokens[token_index])
209 return None
210
211 print('Unexpected end')
212 return None
213
214 def JsonizeTokens(self, json, tokens, token_index):
215 """Creates a json-like dictionary from the provided tokens.
216
217 Args:
218 json: dict, The dict to stick the elements in.
219 tokens: list of strings, The list with all the tokens in it.
220 token_index: int, Where to start in the token list.
221
222 Returns:
223 int, The last token used.
224 """
225 # Check that the message starts with a {
226 if tokens[token_index] != '{':
227 print(tokens)
228 print('Expected { at beginning, found', tokens[token_index])
229 return None
230
231 # Eat the {
232 token_index += 1
233
234 # States and state variable for parsing elements.
235 STATE_INIT = 'init'
236 STATE_HAS_NAME = 'name'
237 STATE_HAS_COLON = 'colon'
238 STATE_EXPECTING_SUBMSG = 'submsg'
239 STATE_EXPECTING_COMMA = 'comma'
240 parser_state = STATE_INIT
241
242 while token_index < len(tokens):
243 if tokens[token_index] == '}':
244 # Finish if there is a }
245 return token_index + 1
246 elif tokens[token_index] == '{':
247 if parser_state != STATE_EXPECTING_SUBMSG:
248 print(tokens)
249 print(parser_state)
250 print('Bad input, was not expecting {')
251 return None
252 # Found a submessage, parse it.
253 sub_json = dict()
254 token_index = self.JsonizeTokens(sub_json, tokens, token_index)
255 json[token_name] = sub_json
256 parser_state = STATE_EXPECTING_COMMA
257 else:
258 if parser_state == STATE_INIT:
259 # This token is the name.
260 token_name = tokens[token_index]
261 parser_state = STATE_HAS_NAME
262 elif parser_state == STATE_HAS_NAME:
263 if tokens[token_index] != ':':
264 print(tokens)
265 print(parser_state)
266 print('Bad input, found', tokens[token_index], 'expected :')
267 return None
268 # After a name, comes a :
269 parser_state = STATE_HAS_COLON
270 elif parser_state == STATE_HAS_COLON:
271 # After the colon, figure out what is next.
272 if tokens[token_index] == '[':
273 # Found a sub-array!
274 sub_array = []
275 token_index = self.__JsonizeTokenArray(sub_array, tokens, token_index)
276 json[token_name] = sub_array
277 parser_state = STATE_EXPECTING_COMMA
278 elif tokens[token_index + 1] == '{':
279 # Found a sub-message, trigger parsing it.
280 parser_state = STATE_EXPECTING_SUBMSG
281 else:
282 # This is just an element, move on.
283 json[token_name] = tokens[token_index]
284 parser_state = STATE_EXPECTING_COMMA
285 elif parser_state == STATE_EXPECTING_COMMA:
286 # Complain if there isn't a comma here.
287 if tokens[token_index] != ',':
288 print(tokens)
289 print(parser_state)
290 print('Bad input, found', tokens[token_index], 'expected ,')
291 return None
292 parser_state = STATE_INIT
293 else:
294 print('Bad parser state')
295 return None
296 token_index += 1
297
298 print('Unexpected end')
299 return None
300
301 def ParseStruct(self):
302 """Parses the message as a structure.
303
304 Returns:
305 struct_name, struct_type, json dict.
306 """
307 struct_name_index = self.msg.find(':')
308 struct_name = self.msg[0:struct_name_index]
309
310 struct_body = self.msg[struct_name_index+2:]
311 tokens = []
312 this_token = ''
313 # For the various deliminators, append what we have found so far to the
314 # list and the token.
315 for char in struct_body:
316 if char == '{':
317 if this_token:
318 tokens.append(this_token)
319 this_token = ''
320 tokens.append('{')
321 elif char == '}':
322 if this_token:
323 tokens.append(this_token)
324 this_token = ''
325 tokens.append('}')
326 elif char == '[':
327 if this_token:
328 tokens.append(this_token)
329 this_token = ''
330 tokens.append('[')
331 elif char == ']':
332 if this_token:
333 tokens.append(this_token)
334 this_token = ''
335 tokens.append(']')
336 elif char == ':':
337 if this_token:
338 tokens.append(this_token)
339 this_token = ''
340 tokens.append(':')
341 elif char == ',':
342 if this_token:
343 tokens.append(this_token)
344 this_token = ''
345 tokens.append(',')
346 elif char == ' ':
347 if this_token:
348 tokens.append(this_token)
349 this_token = ''
350 else:
351 this_token += char
352 if this_token:
353 tokens.append(this_token)
354
355 struct_type = tokens[0]
356 json = dict()
357 # Now that we have tokens, parse them.
358 self.JsonizeTokens(json, tokens, 1)
359
360 return (struct_name, struct_type, json)
361
362
363def ParseLine(line):
364 return LogEntry(line)
365
366if __name__ == '__main__':
367 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}')
368 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}')
369 if '.aos.controls.OutputCheck' in line.msg:
370 print(line)
371 print(line.ParseStruct())
372
373 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}}')
374 print(line.ParseStruct())
375
376 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}')
377 print(line.ParseStruct())