1 | # -*- coding: utf-8 -*- |
---|
2 | """ |
---|
3 | ------------------------------------------------------------------------------ |
---|
4 | Mango 802.11 Reference Design Experiments Framework - Log Entry Types |
---|
5 | ------------------------------------------------------------------------------ |
---|
6 | License: Copyright 2019 Mango Communications, Inc. All rights reserved. |
---|
7 | Use and distribution subject to terms in LICENSE.txt |
---|
8 | ------------------------------------------------------------------------------ |
---|
9 | |
---|
10 | This module defines each type of log entry that may exist in the event log of |
---|
11 | an 802.11 Reference Design Node. |
---|
12 | |
---|
13 | The log entry definitions in this file must match the corresponding |
---|
14 | definitions in the wlan_mac_entries.h header file in the C code |
---|
15 | running on the node. |
---|
16 | |
---|
17 | This module maintains a dictionary which contains a reference to each |
---|
18 | known log entry type. This dictionary is stored in the variable |
---|
19 | ``wlan_exp_log_entry_types``. The :class:`WlanExpLogEntryType` constructor |
---|
20 | automatically adds each log entry type definition to this dictionary. Users |
---|
21 | may access the dictionary to view currently defined log entry types. But |
---|
22 | user code should not modify the dictionary contents directly. |
---|
23 | |
---|
24 | |
---|
25 | Custom Log Entry Types |
---|
26 | ---------------------- |
---|
27 | The :mod:`log_entries` module includes definitions for the log entry types |
---|
28 | implemented in the current 802.11 Reference Design C code. |
---|
29 | |
---|
30 | Log entry types defined here must match the corresponding entry definitions in |
---|
31 | the node C code. Custom entries can be defined and added to the global |
---|
32 | dictionary by user scripts. |
---|
33 | |
---|
34 | Log entry type definitions are instances of the :class:`WlanExpLogEntryType` |
---|
35 | class. The :class:`WlanExpLogEntryType` constructor requires two arguments: |
---|
36 | ``name`` and ``entry_type_id``. Both the name and entry type ID **must** be |
---|
37 | unique relative to the existing entry types defined in :mod:`log_entries`. |
---|
38 | |
---|
39 | To define a custom log entry type:: |
---|
40 | |
---|
41 | import wlan_exp.log.entry_types as entry_types |
---|
42 | |
---|
43 | #name and entry_type_id must not collide with existing log entry type definitions |
---|
44 | my_entry_type = entry_types.WlanExpLogEntryType(name='MY_ENTRY', entry_type_id=999) |
---|
45 | my_entry_type.append_field_defs([ |
---|
46 | ('timestamp', 'Q', 'uint64'), |
---|
47 | ('field_A', 'H', 'uint16'), |
---|
48 | ('field_B', 'H', 'uint16')]) |
---|
49 | |
---|
50 | """ |
---|
51 | import sys, os |
---|
52 | from struct import pack, unpack, calcsize, error |
---|
53 | import wlan_exp.util as util |
---|
54 | |
---|
55 | # Fix to support Python 2.x and 3.x |
---|
56 | if sys.version[0]=="3": long=None |
---|
57 | |
---|
58 | |
---|
59 | # Event Log Constants |
---|
60 | # Must match corresponding C definitions in wlan_mac_event_log.h |
---|
61 | WLAN_EXP_LOG_DELIM = 0xACED |
---|
62 | |
---|
63 | |
---|
64 | # Log Entry Type Constants |
---|
65 | # Must match corresponding C definitions in wlan_mac_entries.h |
---|
66 | ENTRY_TYPE_NULL = 0 |
---|
67 | ENTRY_TYPE_NODE_INFO = 1 |
---|
68 | ENTRY_TYPE_EXP_INFO = 2 |
---|
69 | |
---|
70 | ENTRY_TYPE_NODE_TEMPERATURE = 4 |
---|
71 | |
---|
72 | ENTRY_TYPE_TIME_INFO = 6 |
---|
73 | |
---|
74 | ENTRY_TYPE_RX_OFDM = 10 |
---|
75 | ENTRY_TYPE_RX_OFDM_LTG = 11 |
---|
76 | |
---|
77 | ENTRY_TYPE_RX_DSSS = 15 |
---|
78 | |
---|
79 | ENTRY_TYPE_TX_HIGH = 20 |
---|
80 | ENTRY_TYPE_TX_HIGH_LTG = 21 |
---|
81 | |
---|
82 | ENTRY_TYPE_TX_LOW = 25 |
---|
83 | ENTRY_TYPE_TX_LOW_LTG = 26 |
---|
84 | |
---|
85 | # ----------------------------------------------------------------------------- |
---|
86 | # Log Entry Type Container |
---|
87 | # ----------------------------------------------------------------------------- |
---|
88 | |
---|
89 | log_entry_types = dict() |
---|
90 | |
---|
91 | # ----------------------------------------------------------------------------- |
---|
92 | # Log Entry Type Base Class |
---|
93 | # ----------------------------------------------------------------------------- |
---|
94 | |
---|
95 | # Function whose docstring is updated every time a WlanExpLogEntryType is created |
---|
96 | # Sphinx autodoc will read this function's docstring via the .__doc__ attribute |
---|
97 | # The .__doc__ attribute of a Type (like WlanExpLogEntryType) cannot be modified at |
---|
98 | # runtime, but the .__doc__ of a function can. |
---|
99 | def _log_entry_types_doc_container(): |
---|
100 | """Default docstring""" |
---|
101 | pass |
---|
102 | |
---|
103 | |
---|
104 | class WlanExpLogEntryType(object): |
---|
105 | """Class to define a log entry type.""" |
---|
106 | # _fields is a list of 3-tuples: |
---|
107 | # (field_name, field_fmt_struct, field_fmt_np) |
---|
108 | _fields = None |
---|
109 | |
---|
110 | entry_type_id = None # Unique integer ID for entry type |
---|
111 | name = None # Unique string name for entry type |
---|
112 | description = '' # Description of log entry type, used for generating documentation |
---|
113 | |
---|
114 | fields_np_dt = None # numpy dtype object describing format |
---|
115 | fields_fmt_struct = None # List of field formats, in struct module format |
---|
116 | _field_offsets = None |
---|
117 | |
---|
118 | gen_numpy_callbacks = None |
---|
119 | |
---|
120 | consts = None # Container for user-defined, entry-specific constants |
---|
121 | |
---|
122 | def __init__(self, name=None, entry_type_id=None): |
---|
123 | |
---|
124 | self.name = name |
---|
125 | self.entry_type_id = entry_type_id |
---|
126 | |
---|
127 | # Check if this new WlanExpLogEntryType instannce should be added to the global log_entry_types dictionary |
---|
128 | # The log_entry_types dictionary is used to map entry type IDs (integers) and names (strings) by the log |
---|
129 | # processing utilities. The code below prints warnings if any IDs or names are duplicated. Only entry types |
---|
130 | # with not-None type IDs and names are added to the dictionary. Types with null IDs or names can still be used |
---|
131 | # as containers for common field sets shared by multiple entry types (i.e. tx_common for TX_HIGH and TX_HIGH_LTG) |
---|
132 | if (name is not None) and (entry_type_id is not None): |
---|
133 | if type(entry_type_id) is not int: |
---|
134 | raise Exception("ERROR: Invalid entry_type_id {0} - WlanExpLogEntryType entry_type_id must be int".format(entry_type_id)) |
---|
135 | |
---|
136 | if(name in log_entry_types.keys()): |
---|
137 | print("WARNING: replacing exisitng WlanExpLogEntryType with name {0}".format(name)) |
---|
138 | |
---|
139 | if(entry_type_id in log_entry_types.keys()): |
---|
140 | print("WARNING: replacing exisitng WlanExpLogEntryType with ID {0}".format(entry_type_id)) |
---|
141 | |
---|
142 | log_entry_types[str(name)] = self |
---|
143 | log_entry_types[entry_type_id] = self |
---|
144 | |
---|
145 | # Initialize fields to empty lists |
---|
146 | self._fields = [] |
---|
147 | |
---|
148 | # Initialize unpack variables |
---|
149 | self.fields_fmt_struct = '' |
---|
150 | |
---|
151 | # Initialize callbacks |
---|
152 | self.gen_numpy_callbacks = [] |
---|
153 | |
---|
154 | # Initialize dictionary to contain constants specific to each entry type |
---|
155 | self.consts = util.consts_dict() |
---|
156 | |
---|
157 | # Initialize variable that contains field names and byte offsets |
---|
158 | self._field_offsets = {} |
---|
159 | |
---|
160 | |
---|
161 | # ------------------------------------------------------------------------- |
---|
162 | # Accessor methods for the WlanExpLogEntryType |
---|
163 | # ------------------------------------------------------------------------- |
---|
164 | def get_field_names(self): |
---|
165 | """Get the field names of the log entry. |
---|
166 | |
---|
167 | Returns: |
---|
168 | names (list of str): List of string field names for the entry |
---|
169 | """ |
---|
170 | return [f[0] for f in self._fields] |
---|
171 | |
---|
172 | def get_field_struct_formats(self): |
---|
173 | """Get the Python struct format of the log entry fields. |
---|
174 | |
---|
175 | Returns: |
---|
176 | formats (list of str): List of Python struct formats for the fields in the log entry |
---|
177 | """ |
---|
178 | return [f[1] for f in self._fields] |
---|
179 | |
---|
180 | def get_field_defs(self): return self._fields |
---|
181 | |
---|
182 | def get_field_offsets(self): return self._field_offsets |
---|
183 | |
---|
184 | def get_entry_type_id(self): |
---|
185 | """Get the ID for the entry type. |
---|
186 | |
---|
187 | Returns: |
---|
188 | ID (int): Integer ID for the log entry |
---|
189 | """ |
---|
190 | return self.entry_type_id |
---|
191 | |
---|
192 | def append_field_defs(self, field_info): |
---|
193 | """Adds fields to the definition of the log entry type. |
---|
194 | |
---|
195 | Args: |
---|
196 | field_info (list of tuple): Each must be of the form |
---|
197 | ``(field_name, field_type_struct, field_type_numpy)`` where: |
---|
198 | |
---|
199 | * ``field_name``: Name of field as string |
---|
200 | * ``field_type_struct``: Field type as string, using formats specified by ``struct`` module |
---|
201 | * ``field_type_numpy``: Field type as string, using formats specified by numpy ``dtype`` |
---|
202 | * ``field_desc``: String describing the field's meaning, used to generate wiki docs |
---|
203 | |
---|
204 | """ |
---|
205 | if type(field_info) is list: |
---|
206 | self._fields.extend(field_info) |
---|
207 | else: |
---|
208 | self._fields.append(field_info) |
---|
209 | self._update_field_defs() |
---|
210 | |
---|
211 | |
---|
212 | def modify_field_def(self, name, struct_type, numpy_type, doc_str=None): |
---|
213 | """Modifies fields of the definition of the log entry type. |
---|
214 | |
---|
215 | Args: |
---|
216 | name (str): Name of field to modify |
---|
217 | struct_type (str): New Python struct type for the field |
---|
218 | numpy_type (str): New numpy type for the field |
---|
219 | doc_str (str, optional): New documentation string |
---|
220 | """ |
---|
221 | index = None |
---|
222 | |
---|
223 | for idx, f in enumerate(self._fields): |
---|
224 | if (f[0] == name): |
---|
225 | index = idx |
---|
226 | |
---|
227 | if index is None: |
---|
228 | print("WARNING: Field {0} not found in {1}.".format(name, self.name)) |
---|
229 | return |
---|
230 | |
---|
231 | if doc_str is None: |
---|
232 | field = (name, struct_type, numpy_type, self._fields[index][3]) |
---|
233 | else: |
---|
234 | field = (name, struct_type, numpy_type, doc_str) |
---|
235 | |
---|
236 | self._fields[index] = field |
---|
237 | |
---|
238 | |
---|
239 | def add_gen_numpy_array_callback(self, callback): |
---|
240 | """Add callback that is run after the numpy array is generated from |
---|
241 | the entry type. |
---|
242 | |
---|
243 | The callbacks will be executed in the order the are added to the log |
---|
244 | entry |
---|
245 | |
---|
246 | Args: |
---|
247 | callback (function): Function to run after the numpy array is generated |
---|
248 | """ |
---|
249 | if callable(callback): |
---|
250 | self.gen_numpy_callbacks.append(callback) |
---|
251 | else: |
---|
252 | print("ERROR: Callback must be callable function.") |
---|
253 | |
---|
254 | |
---|
255 | # ------------------------------------------------------------------------- |
---|
256 | # Utility methods for the WlanExpLogEntryType |
---|
257 | # ------------------------------------------------------------------------- |
---|
258 | def generate_numpy_array(self, log_data, byte_offsets): |
---|
259 | """Generate a NumPy array from the log_bytes of the given WlanExpLogEntryType instance |
---|
260 | at the given byte_offsets. |
---|
261 | """ |
---|
262 | import numpy as np |
---|
263 | |
---|
264 | index_iter = [log_data[o : o + self.fields_np_dt.itemsize] for o in byte_offsets] |
---|
265 | # fromiter began throwing an error in numpy ~1.14.5: |
---|
266 | # ValueError: invalid literal for long() with base 10: `xxx` |
---|
267 | # where xxx are the first 3 bytes of the first entry of index_iter |
---|
268 | # I can't find the underlying change that triggered this, so |
---|
269 | # the explicit iteration below is the slower-but-functional workaround |
---|
270 | #np_arr = np.fromiter(index_iter[2:], self.fields_np_dt, len(byte_offsets)) |
---|
271 | |
---|
272 | # Workaround: explicitly create the numpy array then populate it one |
---|
273 | # entry at a time using fromstring(), which is happy with the index_iter |
---|
274 | # list that somehow breaks fromiter() |
---|
275 | np_arr = np.empty(dtype=self.fields_np_dt, shape=(len(byte_offsets),)) |
---|
276 | for ii in range(len(index_iter)): |
---|
277 | np_arr[ii] = np.fromstring(index_iter[ii], self.fields_np_dt) |
---|
278 | |
---|
279 | if self.gen_numpy_callbacks: |
---|
280 | for callback in self.gen_numpy_callbacks: |
---|
281 | np_arr = callback(np_arr) |
---|
282 | |
---|
283 | return np_arr |
---|
284 | |
---|
285 | |
---|
286 | def generate_entry_doc(self, fmt='wiki'): |
---|
287 | """Generate wiki documentation.""" |
---|
288 | import textwrap |
---|
289 | import wlan_exp.log.util as log_util |
---|
290 | |
---|
291 | field_descs = list() |
---|
292 | for f in self._fields: |
---|
293 | # Field tuple is (name, struct_type, np_type, (optional)desc) |
---|
294 | # Construct new tuple of (name, np_type, desc ('' if not defined)) |
---|
295 | try: |
---|
296 | field_descs.append( (f[0], f[2], f[3])) |
---|
297 | except IndexError: |
---|
298 | # Field missing description; use empty string |
---|
299 | field_descs.append( (f[0], f[2], '')) |
---|
300 | |
---|
301 | def doc_fields_table(field_list, fmt='wiki'): |
---|
302 | """field_list must be iterable of 3-tuples: |
---|
303 | 0: Field name (string) |
---|
304 | 1: Field data type (string, preferably the numpy datatype string) |
---|
305 | 2: Field description (string) |
---|
306 | """ |
---|
307 | |
---|
308 | doc_str = '' |
---|
309 | |
---|
310 | if fmt == 'wiki': |
---|
311 | doc_str += '{{{#!th align=center\nField Name\n}}}\n' |
---|
312 | doc_str += '{{{#!th align=center\nData Type\n}}}\n' |
---|
313 | doc_str += '{{{#!th align=center\nDescription\n}}}\n' |
---|
314 | doc_str += '|----------------\n' |
---|
315 | |
---|
316 | for fd in field_list: |
---|
317 | import re |
---|
318 | # Wiki-ify some string formats: |
---|
319 | # Line breaks in descriptions must be explicit "[[BR]]" |
---|
320 | # Braces with numeric contents need escape (![) to disable Trac interpretting as changeset number |
---|
321 | fd_desc = fd[2] |
---|
322 | fd_desc = re.sub(r'(\[[\d:,]+\])', '!\\1', fd_desc) # do this first, so other wiki tags inserted below aren't escaped |
---|
323 | #fd_desc = fd_desc.replace('\n', '[[BR]]') |
---|
324 | |
---|
325 | try: |
---|
326 | consts = self.consts[fd[0]] |
---|
327 | fd_desc += '\n\nConstants defined for this field:\n' |
---|
328 | fd_desc += '||= Name =||= Value =||\n' |
---|
329 | for c in consts.keys(): |
---|
330 | #fd_desc += ' * {{{{{{\'{0}\': 0x{1:X}}}}}}}\n'.format(c, consts[c]) |
---|
331 | fd_desc += '|| {{{{{{{0}}}}}}} || {{{{{{0x{1:X}}}}}}} ||\n'.format(c, consts[c]) |
---|
332 | except: |
---|
333 | # Field didn't have constants defined |
---|
334 | pass |
---|
335 | |
---|
336 | doc_str += '{{{{{{#!td align=left\n{0}\n}}}}}}\n'.format(fd[0]) |
---|
337 | doc_str += '{{{{{{#!td align=center\n{0}\n}}}}}}\n'.format(fd[1]) |
---|
338 | doc_str += '{{{{{{#!td align=left\n{0}\n}}}}}}\n'.format(fd_desc) |
---|
339 | doc_str += '|----------------\n' |
---|
340 | |
---|
341 | elif fmt == 'txt': |
---|
342 | doc_str += 'Field Name\t\t\t| Data Type\t| Description\n' |
---|
343 | doc_str += '---------------------------------------------------------------------------------------------------------------------\n' |
---|
344 | |
---|
345 | for fd in field_descs: |
---|
346 | doc_str += '{0:30}\t| {1:10}\t| {2}\n'.format(fd[0], fd[1], fd[2]) |
---|
347 | |
---|
348 | doc_str += '---------------------------------------------------------------------------------------------------------------------\n' |
---|
349 | |
---|
350 | elif fmt == 'rst': |
---|
351 | import re |
---|
352 | |
---|
353 | rst_table_row = lambda c0, c1, c2: ' * - {0}\n - {1}\n - {2}\n'.format(c0, c1, c2) |
---|
354 | |
---|
355 | doc_str += '\n.. list-table::\n :widths: 25 10 65\n :header-rows: 1\n\n' |
---|
356 | doc_str += rst_table_row('Field', 'Data Type', 'Description') |
---|
357 | |
---|
358 | for fd in field_list: |
---|
359 | fd_desc = fd[2] |
---|
360 | |
---|
361 | # Apply some transformations for rst formatting |
---|
362 | # Prefix any explicit line breaks with spaces to align the rst block quote |
---|
363 | fd_desc = re.sub('\n +', '\n\n ', fd_desc) # do this first, so other wiki tags inserted below aren't escaped |
---|
364 | |
---|
365 | try: |
---|
366 | consts = self.consts[fd[0]] |
---|
367 | fd_desc += '\n\n Constants defined for this field:\n\n' |
---|
368 | for c in consts.keys(): |
---|
369 | fd_desc += ' * ``{0}``: ``0x{1:X}``\n'.format(c, consts[c]) |
---|
370 | except: |
---|
371 | # Field didn't have constants defined |
---|
372 | pass |
---|
373 | |
---|
374 | doc_str += rst_table_row(fd[0], fd[1], fd_desc) |
---|
375 | |
---|
376 | return doc_str |
---|
377 | |
---|
378 | if fmt == 'wiki': |
---|
379 | # Construct the Trac-wiki-style documentation string for this entry type |
---|
380 | doc_str = '\n----\n\n' |
---|
381 | doc_str += '=== Entry Type {0} ===\n'.format(self.name) |
---|
382 | doc_str += self.description + '\n\n' |
---|
383 | doc_str += 'Entry type ID: {0}\n\n'.format(self.entry_type_id) |
---|
384 | |
---|
385 | doc_str += doc_fields_table(field_descs, fmt='wiki') |
---|
386 | |
---|
387 | elif fmt == 'txt': |
---|
388 | # Construct plain text version of documentation string for this entry type |
---|
389 | doc_str = '---------------------------------------------------------------------------------------------------------------------\n' |
---|
390 | doc_str += 'Entry Type {0}\n'.format(self.name) |
---|
391 | doc_str += 'Entry type ID: {0}\n\n'.format(self.entry_type_id) |
---|
392 | doc_str += textwrap.fill(self.description) + '\n\n' |
---|
393 | |
---|
394 | doc_str += doc_fields_table(field_descs, fmt='txt') |
---|
395 | |
---|
396 | elif fmt == 'rst': |
---|
397 | # Construct the Rst documentation string for this entry type |
---|
398 | doc_str = '\n\n' |
---|
399 | doc_str += '``{0}``\n'.format(self.name) |
---|
400 | doc_str += '{0}\n\n'.format('*'*(11+len(self.name))) |
---|
401 | |
---|
402 | doc_str += self.description + '\n\n' |
---|
403 | doc_str += 'Entry type ID: {0}\n\n'.format(self.entry_type_id) |
---|
404 | |
---|
405 | doc_str += doc_fields_table(field_descs, fmt='rst') |
---|
406 | |
---|
407 | # Check each post-numpy array generation callback to any documentation to include |
---|
408 | # The callbacks can define "virtual" fields - convenience fields appended to the numpy |
---|
409 | # array that are calculated from the "real" fields |
---|
410 | for cb in self.gen_numpy_callbacks: |
---|
411 | field_descs = [] |
---|
412 | try: |
---|
413 | (cb_doc_str, cb_doc_fields) = cb(np_arr_orig=None, docs_only=True) |
---|
414 | |
---|
415 | for f in cb_doc_fields: |
---|
416 | field_descs.append((f[0], f[1], f[2])) |
---|
417 | |
---|
418 | doc_str += '\n\n' |
---|
419 | doc_str += cb_doc_str + '\n\n' |
---|
420 | doc_str += doc_fields_table(field_descs, fmt=fmt) |
---|
421 | |
---|
422 | except (TypeError, IndexError): |
---|
423 | # Callback didn't implement suitable 'docs_only' output; fail quietly and return |
---|
424 | # print('Error generating callback field docs for {0}\n'.format(cb)) |
---|
425 | pass |
---|
426 | |
---|
427 | return doc_str |
---|
428 | |
---|
429 | |
---|
430 | def _entry_as_string(self, buf): |
---|
431 | """Generate a string representation of the entry from a buffer. |
---|
432 | |
---|
433 | This method should only be used for debugging log data parsing and log |
---|
434 | index generation, not for general creation of text log files. |
---|
435 | |
---|
436 | This method does not work correctly on RX_OFDM entries due to the way |
---|
437 | the channel estimates are defined. The channel_est, mac_payload_len, |
---|
438 | and mac_payload will all be zeros. |
---|
439 | """ |
---|
440 | entry_size = calcsize(self.fields_fmt_struct) |
---|
441 | entry = self.deserialize(buf[0:entry_size])[0] |
---|
442 | |
---|
443 | str_out = self.name + ': ' |
---|
444 | |
---|
445 | for k in entry.keys(): |
---|
446 | s = entry[k] |
---|
447 | if((type(s) is int) or (type(s) is long)): |
---|
448 | str_out += "\n {0:30s} = {1:20d} (0x{1:16x})".format(k, s) |
---|
449 | elif(type(s) is str): |
---|
450 | s = map(ord, list(entry[k])) |
---|
451 | str_out += "\n {0:30s} = [".format(k) |
---|
452 | for x in s: |
---|
453 | str_out += "{0:d}, ".format(x) |
---|
454 | str_out += "\b\b]" |
---|
455 | |
---|
456 | str_out += "\n" |
---|
457 | |
---|
458 | return str_out |
---|
459 | |
---|
460 | |
---|
461 | def _entry_as_byte_array_string(self, buf): |
---|
462 | """Generate a string representation of the entry from a buffer as an array of bytes. This |
---|
463 | method should only be used for debugging log data parsing and log index generation, not |
---|
464 | for general creation of text log files. |
---|
465 | """ |
---|
466 | entry_size = calcsize(self.fields_fmt_struct) |
---|
467 | entry = self.deserialize(buf[0:entry_size])[0] |
---|
468 | |
---|
469 | str_out = self.name + ': ' |
---|
470 | |
---|
471 | line_num = 0 |
---|
472 | |
---|
473 | for byte in range(entry_size): |
---|
474 | if (byte % 16) == 0: |
---|
475 | str_out += "\n{0:>8d}: ".format(line_num) |
---|
476 | line_num += 16 |
---|
477 | str_out += "0x{0:02x} ".format(ord(buf[byte])) |
---|
478 | |
---|
479 | str_out += "\n" |
---|
480 | |
---|
481 | print(entry) |
---|
482 | |
---|
483 | return str_out |
---|
484 | |
---|
485 | |
---|
486 | def deserialize(self, buf): |
---|
487 | """Unpacks one or more raw log entries of the same type into a list of dictionaries |
---|
488 | |
---|
489 | Args: |
---|
490 | buf (bytearray): Array of raw log data containing 1 or more log entries of the same type. |
---|
491 | |
---|
492 | Returns: |
---|
493 | entries (List of dict): |
---|
494 | Each dictionary in the list has one value per field in the log entry definition using |
---|
495 | the field names as keys. |
---|
496 | """ |
---|
497 | from collections import OrderedDict |
---|
498 | |
---|
499 | ret_val = [] |
---|
500 | buf_size = len(buf) |
---|
501 | entry_size = calcsize(self.fields_fmt_struct) |
---|
502 | index = 0 |
---|
503 | |
---|
504 | while (index < buf_size): |
---|
505 | try: |
---|
506 | dataTuple = unpack(self.fields_fmt_struct, buf[index:index+entry_size]) |
---|
507 | all_names = self.get_field_names() |
---|
508 | all_fmts = self.get_field_struct_formats() |
---|
509 | |
---|
510 | # Filter out names for fields ignored during unpacking |
---|
511 | names = [n for (n,f) in zip(all_names, all_fmts) if 'x' not in f] |
---|
512 | |
---|
513 | # Use OrderedDict to preserve user-specified field order |
---|
514 | ret_val.append(OrderedDict(zip(names, dataTuple))) |
---|
515 | |
---|
516 | except error as err: |
---|
517 | print("Error unpacking {0} buffer with len {1}: {2}".format(self.name, len(buf), err)) |
---|
518 | |
---|
519 | index += entry_size |
---|
520 | |
---|
521 | return ret_val |
---|
522 | |
---|
523 | |
---|
524 | def serialize(self, entries): |
---|
525 | """Packs one or more list of dictionaries into a buffer of log entries of the same type |
---|
526 | |
---|
527 | Args: |
---|
528 | entry_list (dictionary): Array of dictionaries for 1 or more log entries of the same type |
---|
529 | |
---|
530 | Returns: |
---|
531 | data (bytearray): Bytearray of packed binary data |
---|
532 | """ |
---|
533 | length = 1 |
---|
534 | ret_val = "" |
---|
535 | entry_size = calcsize(self.fields_fmt_struct) |
---|
536 | |
---|
537 | # Convert entries to a list if it is not already one |
---|
538 | if type(entries) is not list: |
---|
539 | entries = [entries] |
---|
540 | |
---|
541 | # Pack each of the entries into a single data buffer |
---|
542 | for entry in entries: |
---|
543 | fields = [] |
---|
544 | tmp_values = [] |
---|
545 | used_field = [] |
---|
546 | |
---|
547 | for field in self._fields: |
---|
548 | if 'x' not in field[1]: |
---|
549 | fields.append(field[0]) |
---|
550 | try: |
---|
551 | tmp_values.append(entry[field[0]]) |
---|
552 | used_field.append(True) |
---|
553 | except KeyError: |
---|
554 | tmp_values.append(0) |
---|
555 | used_field.append(False) |
---|
556 | |
---|
557 | if (False): |
---|
558 | print("Serialize Entry:") |
---|
559 | print(fields) |
---|
560 | print(used_field) |
---|
561 | print(tmp_values) |
---|
562 | |
---|
563 | ret_val += pack(self.fields_fmt_struct, *tmp_values) |
---|
564 | |
---|
565 | if (entry_size * length) != len(ret_val): |
---|
566 | msg = "WARNING: Sizes do not match.\n" |
---|
567 | msg += " Expected {0} bytes".format(entry_size * length) |
---|
568 | msg += " Buffer is {0} bytes".format(len(ret_val)) |
---|
569 | print(msg) |
---|
570 | |
---|
571 | return ret_val |
---|
572 | |
---|
573 | |
---|
574 | # ------------------------------------------------------------------------- |
---|
575 | # Internal methods for the WlanExpLogEntryType |
---|
576 | # ------------------------------------------------------------------------- |
---|
577 | def _update_field_defs(self): |
---|
578 | """Internal method to update fields.""" |
---|
579 | import numpy as np |
---|
580 | |
---|
581 | # Update the fields format used by struct unpack/calcsize |
---|
582 | self.fields_fmt_struct = ' '.join(self.get_field_struct_formats()) |
---|
583 | |
---|
584 | # Update the numpy dtype definition |
---|
585 | # fields_np_dt is a numpy dtype, built using a dictionary of names/formats/offsets: |
---|
586 | # {'names':[field_names], 'formats':[field_formats], 'offsets':[field_offsets]} |
---|
587 | # We specify each field's byte offset explicitly. Byte offsets for fields in _fields |
---|
588 | # are inferred, assuming tight C-struct-type packing (same assumption as struct.unpack) |
---|
589 | |
---|
590 | get_np_size = (lambda f: np.dtype(f).itemsize) |
---|
591 | |
---|
592 | # Compute the offset of each real field, inferred by the sizes of all previous fields |
---|
593 | # This loop must look at all real fields, even ignored/padding fields |
---|
594 | sizes = list(map(get_np_size, [f[2] for f in self._fields])) |
---|
595 | offsets = [sum(sizes[0:i]) for i in range(len(sizes))] |
---|
596 | |
---|
597 | np_fields = self._fields |
---|
598 | |
---|
599 | # numpy processing ignores the same fields ignored by struct.unpack |
---|
600 | # !!!BAD!!! Doing this filtering breaks HDF5 export |
---|
601 | # offsets = [o for (o,f) in zip(offsets_all, self._fields) if 'x' not in f[1]] |
---|
602 | # np_fields = [f for f in self._fields if 'x' not in f[1]] |
---|
603 | |
---|
604 | names = [f[0] for f in np_fields] |
---|
605 | formats = [f[2] for f in np_fields] |
---|
606 | |
---|
607 | self.fields_np_dt = np.dtype({'names':names, 'formats':formats, 'offsets':offsets}) |
---|
608 | |
---|
609 | # Update the field offsets |
---|
610 | self._field_offsets = dict(zip(names, offsets)) |
---|
611 | |
---|
612 | # Check our definitions of struct vs numpy are in sync |
---|
613 | struct_size = calcsize(self.fields_fmt_struct) |
---|
614 | np_size = self.fields_np_dt.itemsize |
---|
615 | |
---|
616 | if (struct_size != np_size): |
---|
617 | msg = "WARNING: Definitions of struct {0} do not match.\n".format(self.name) |
---|
618 | msg += " Struct size = {0} Numpy size = {1}".format(struct_size, np_size) |
---|
619 | print(msg) |
---|
620 | |
---|
621 | def __eq__(self, other): |
---|
622 | """WlanExpLogEntryType are equal if their names are equal.""" |
---|
623 | if type(other) is str: |
---|
624 | return self.name == other |
---|
625 | else: |
---|
626 | return (isinstance(other, self.__class__) and (self.name == other.name)) |
---|
627 | |
---|
628 | def __lt__(self, other): |
---|
629 | """WlanExpLogEntryType are less than if their names are less than.""" |
---|
630 | if type(other) is str: |
---|
631 | return self.name < other |
---|
632 | else: |
---|
633 | return (isinstance(other, self.__class__) and (self.name < other.name)) |
---|
634 | |
---|
635 | def __gt__(self, other): |
---|
636 | """WlanExpLogEntryType are greater than if their names are greater than.""" |
---|
637 | if type(other) is str: |
---|
638 | return self.name > other |
---|
639 | else: |
---|
640 | return (isinstance(other, self.__class__) and (self.name > other.name)) |
---|
641 | |
---|
642 | def __ne__(self, other): |
---|
643 | return not self.__eq__(other) |
---|
644 | |
---|
645 | def __hash__(self): |
---|
646 | return hash(self.name) |
---|
647 | |
---|
648 | def __repr__(self): |
---|
649 | return self.name |
---|
650 | |
---|
651 | # End class |
---|
652 | |
---|
653 | |
---|
654 | # ----------------------------------------------------------------------------- |
---|
655 | # Log Entry Type Functions |
---|
656 | # ----------------------------------------------------------------------------- |
---|
657 | |
---|
658 | def np_array_add_txrx_ltg_fields(np_arr_orig, docs_only=False): |
---|
659 | """Add 'virtual' fields to TX/RX LTG packets.""" |
---|
660 | return np_array_add_fields(np_arr_orig, mac_addr=True, ltg=True, docs_only=docs_only) |
---|
661 | |
---|
662 | # End def |
---|
663 | |
---|
664 | |
---|
665 | def np_array_add_txrx_fields(np_arr_orig, docs_only=False): |
---|
666 | """Add 'virtual' fields to TX/RX packets.""" |
---|
667 | return np_array_add_fields(np_arr_orig, mac_addr=True, ltg=False, docs_only=docs_only) |
---|
668 | |
---|
669 | # End def |
---|
670 | |
---|
671 | |
---|
672 | def np_array_add_fields(np_arr_orig, mac_addr=False, ltg=False, docs_only=False): |
---|
673 | """Add 'virtual' fields to the numpy array. |
---|
674 | |
---|
675 | This is an example of a gen_numpy_array_callback |
---|
676 | |
---|
677 | Args: |
---|
678 | np_arr_orig (Numpy Array): Numpy array to extend |
---|
679 | mac_addr (bool, optional): Add MAC address information to the numpy array? |
---|
680 | ltg (bool, optional): Add LTG information ot the numpy array? |
---|
681 | docs_only (bool): Only generate documentation strings for virtual fields? |
---|
682 | If this is True, then only documentation strings are returned. |
---|
683 | |
---|
684 | Returns: |
---|
685 | np_array (Numpy Array): Updated numpy array |
---|
686 | |
---|
687 | Extend the default np_arr with convenience fields for: |
---|
688 | * MAC header addresses |
---|
689 | * LTG packet information |
---|
690 | |
---|
691 | IMPORTANT: np_arr uses the original bytearray as its underlying data |
---|
692 | We must operate on a copy to avoid clobbering log entries adjacent to the |
---|
693 | Tx or Rx entries being extended. |
---|
694 | """ |
---|
695 | import numpy as np |
---|
696 | |
---|
697 | names = () |
---|
698 | formats = () |
---|
699 | descs = () |
---|
700 | |
---|
701 | # Add the MAC address fields |
---|
702 | if mac_addr: |
---|
703 | names += ('addr1', 'addr2', 'addr3', 'mac_seq') |
---|
704 | formats += ('uint64', 'uint64', 'uint64', 'uint16') |
---|
705 | descs += ('MAC Header Address 1', 'MAC Header Address 2', 'MAC Header Address 3', 'MAC Header Sequence Number') |
---|
706 | |
---|
707 | # Add the LTG fields |
---|
708 | if ltg: |
---|
709 | names += ('ltg_uniq_seq', 'ltg_flow_id') |
---|
710 | formats += ('uint64', 'uint64') |
---|
711 | descs += ('Unique sequence number for LTG packet', 'LTG Flow ID, calculated as:\n 16LSB: LTG instance ID\n 48MSB: Destination MAC address') |
---|
712 | |
---|
713 | # If there are no fields to add, just return the original array |
---|
714 | if not names: |
---|
715 | return np_arr_orig |
---|
716 | |
---|
717 | if docs_only: |
---|
718 | ret_str = 'The following fields are populated when the log entry is part of a numpy array generated via the {{{generate_numpy_array}}} method. ' |
---|
719 | ret_str += 'These fields are calculated from the underlying bytes in the raw log entries and are stored in more convenient formats tha the raw ' |
---|
720 | ret_str += 'log fields. For example, these MAC address fields are 48-bit values stored in 64-bit integers. These integer addresses are much easier ' |
---|
721 | ret_str += 'to use when filtering Tx/Rx log entries using numpy and pandas.' |
---|
722 | |
---|
723 | ret_list = zip(names, formats, descs) |
---|
724 | |
---|
725 | return (ret_str, ret_list) |
---|
726 | |
---|
727 | # Return if array is None to not raise an Attribute Exception |
---|
728 | if np_arr_orig is None: |
---|
729 | return np_arr_orig |
---|
730 | |
---|
731 | # Create a new numpy dtype with additional fields |
---|
732 | dt_new = extend_np_dt(np_arr_orig.dtype, {'names': names, 'formats': formats}) |
---|
733 | |
---|
734 | # Initialize the output array (same shape, new dtype) |
---|
735 | np_arr_out = np.zeros(np_arr_orig.shape, dtype=dt_new) |
---|
736 | |
---|
737 | # Copy data from the base numpy array into the output array |
---|
738 | for f in np_arr_orig.dtype.names: |
---|
739 | # TODO: maybe don't copy fields that are ignored in the struct format? |
---|
740 | # problem is non-TxRx entries would still have these fields in their numpy versions |
---|
741 | np_arr_out[f] = np_arr_orig[f] |
---|
742 | |
---|
743 | # Extract the MAC payload (the payload is at a minimum a 24-entry uint8 array) |
---|
744 | mac_hdrs = np_arr_orig['mac_payload'] |
---|
745 | |
---|
746 | # Populate the MAC Address fields |
---|
747 | if mac_addr: |
---|
748 | # Helper array of powers of 2 |
---|
749 | # this array arranges bytes such that they match other u64 representations of MAC addresses |
---|
750 | # elsewhere in the framework |
---|
751 | addr_conv_arr = np.uint64(2)**np.array(range(40, -1, -8), dtype='uint64') |
---|
752 | |
---|
753 | # Compute values for address-as-int fields using numpy's dot-product routine |
---|
754 | # MAC header offsets here select the 3 6-byte address fields and 1 3-byte sequence number |
---|
755 | # Each field is conditioned on the MAC payload length exceeding the bytes needed to compute the field |
---|
756 | # This check handles the case of logging code being changed to record short/no payloads or |
---|
757 | # for short packet Tx/Rx, such as ACKs, which do not contain all 3 address and sequence number fields |
---|
758 | |
---|
759 | arr_filt = np_arr_out['mac_payload_len'] >= 10 |
---|
760 | np_arr_out['addr1'][arr_filt] = np.dot(addr_conv_arr, np.transpose(mac_hdrs[arr_filt, 4:10])) |
---|
761 | |
---|
762 | arr_filt = np_arr_out['mac_payload_len'] >= 16 |
---|
763 | np_arr_out['addr2'][arr_filt] = np.dot(addr_conv_arr, np.transpose(mac_hdrs[arr_filt, 10:16])) |
---|
764 | |
---|
765 | arr_filt = np_arr_out['mac_payload_len'] >= 22 |
---|
766 | np_arr_out['addr3'][arr_filt] = np.dot(addr_conv_arr, np.transpose(mac_hdrs[arr_filt, 16:22])) |
---|
767 | |
---|
768 | arr_filt = np_arr_out['mac_payload_len'] >= 24 |
---|
769 | np_arr_out['mac_seq'][arr_filt] = np.dot(mac_hdrs[arr_filt, 22:24], [1, 256]) // 16 |
---|
770 | |
---|
771 | # Populate the LTG fields |
---|
772 | if ltg: |
---|
773 | # Helper array of powers of 2 |
---|
774 | # this array arranges bytes such that they match other u64 representations of MAC addresses |
---|
775 | # elsewhere in the framework |
---|
776 | uniq_seq_conv_arr = np.uint64(2)**np.array(range(0, 64, 8), dtype='uint64') |
---|
777 | flow_id_conv_arr = np.uint64(2)**np.array(range(0, 32, 8), dtype='uint64') |
---|
778 | |
---|
779 | # Compute the LTG unique sequence number from the bytes in the LTG mac payload |
---|
780 | np_arr_out['ltg_uniq_seq'] = np.dot(uniq_seq_conv_arr, np.transpose(mac_hdrs[:, 32:40])) |
---|
781 | |
---|
782 | # Compute the LTG flow ID from the bytes in the LTG mac payload and the transmitting address (ie 'addr2') if present. |
---|
783 | # flow_id[63:16] = Transmitting address |
---|
784 | # flow_id[15: 0] = LTG ID from LTG mac payload |
---|
785 | try: |
---|
786 | np_arr_out['ltg_flow_id'] = (np_arr_out['addr2'] << 16) + (np.dot(flow_id_conv_arr, np.transpose(mac_hdrs[:, 40:44])) & 0xFFFF) |
---|
787 | except KeyError: |
---|
788 | np_arr_out['ltg_flow_id'] = np.dot(flow_id_conv_arr, np.transpose(mac_hdrs[:, 40:44])) |
---|
789 | |
---|
790 | return np_arr_out |
---|
791 | |
---|
792 | # End def |
---|
793 | |
---|
794 | |
---|
795 | def extend_np_dt(dt_orig, new_fields=None): |
---|
796 | """Extends a numpy dtype object with additional fields. |
---|
797 | |
---|
798 | Args: |
---|
799 | dt_orig (Numpy DataType): Original Numpy data type |
---|
800 | new_fields (dict): Dictionary with keys 'names' and 'formats', same as when specifying |
---|
801 | new dtype objects. The return dtype will *not* contain byte offset values for existing or |
---|
802 | new fields, even if exisiting fields had specified offsets. Thus the original dtype should |
---|
803 | be used to interpret raw data buffers before the extended dtype is used to add new fields. |
---|
804 | """ |
---|
805 | import numpy as np |
---|
806 | from collections import OrderedDict |
---|
807 | |
---|
808 | if(type(dt_orig) is not np.dtype): |
---|
809 | raise Exception("ERROR: extend_np_dt requires valid numpy dtype as input") |
---|
810 | else: |
---|
811 | # Use ordered dictionary to preserve original field order (not required, just convenient) |
---|
812 | dt_ext = OrderedDict() |
---|
813 | |
---|
814 | # Extract the names/formats/offsets dictionary for the base dtype |
---|
815 | # dt.fields returns dictionary with field names as keys and |
---|
816 | # values of (dtype, offset). The dtype objects in the first tuple field |
---|
817 | # can be used as values in the 'formats' entry when creating a new dtype |
---|
818 | # This approach will preserve the types and dimensions of scalar and non-scalar fields |
---|
819 | dt_ext['names'] = list(dt_orig.names) |
---|
820 | dt_ext['formats'] = [dt_orig.fields[f][0] for f in dt_orig.names] |
---|
821 | |
---|
822 | if(type(new_fields) is dict): |
---|
823 | # Add new fields to the extended dtype |
---|
824 | dt_ext['names'].extend(new_fields['names']) |
---|
825 | dt_ext['formats'].extend(new_fields['formats']) |
---|
826 | elif type(new_fields) is not None: |
---|
827 | raise Exception("ERROR: new_fields argument must be dictionary with keys 'names' and 'formats'") |
---|
828 | |
---|
829 | # Construct and return the new numpy dtype object |
---|
830 | dt_new = np.dtype(dt_ext) |
---|
831 | |
---|
832 | return dt_new |
---|
833 | |
---|
834 | # End def |
---|
835 | |
---|
836 | |
---|
837 | # ----------------------------------------------------------------------------- |
---|
838 | # Log Entry Type Definitions |
---|
839 | # ----------------------------------------------------------------------------- |
---|
840 | |
---|
841 | # The code below runs when this module is imported to define each log entry type implemented |
---|
842 | # in the reference design. However the append_field_defs method depends on numpy, which is |
---|
843 | # not avaiable on the warpproject.org server, so this code will break autodoc as-is |
---|
844 | # The server sets an environment variable BUILDING_DOCS_ON_SERVER when it runs post-svn-commit |
---|
845 | # Skip all code below if this variable is set |
---|
846 | if not os.environ.get('BUILDING_DOCS_ON_SERVER', False): |
---|
847 | |
---|
848 | # ----------------------------------------------------------------------------- |
---|
849 | # Common Log Entry Contants |
---|
850 | # ----------------------------------------------------------------------------- |
---|
851 | |
---|
852 | # Packet Type/Sub-type values |
---|
853 | # Matches definition in wlan_mac_802_11_defs.h (802.11-2012 Table 8-1) |
---|
854 | common_pkt_types = util.consts_dict({ |
---|
855 | # Management sub-types |
---|
856 | 'ASSOC_REQ' : 0x00, |
---|
857 | 'ASSOC_RESP' : 0x10, |
---|
858 | 'REASSOC_REQ' : 0x20, |
---|
859 | 'REASSOC_RESP' : 0x30, |
---|
860 | 'PROBE_REQ' : 0x40, |
---|
861 | 'PROBE_RESP' : 0x50, |
---|
862 | 'BEACON' : 0x80, |
---|
863 | 'DISASSOC' : 0xA0, |
---|
864 | 'AUTH' : 0xB0, |
---|
865 | 'DEAUTH' : 0xC0, |
---|
866 | 'ACTION' : 0xD0, |
---|
867 | |
---|
868 | # Control sub-types |
---|
869 | 'BLOCK_ACK_REQ' : 0x84, |
---|
870 | 'BLOCK_ACK' : 0x94, |
---|
871 | 'RTS' : 0xB4, |
---|
872 | 'CTS' : 0xC4, |
---|
873 | 'ACK' : 0xD4, |
---|
874 | |
---|
875 | # Data sub-types |
---|
876 | 'DATA' : 0x08, |
---|
877 | 'NULLDATA' : 0x48, |
---|
878 | 'QOSDATA' : 0x88, |
---|
879 | }) |
---|
880 | |
---|
881 | |
---|
882 | # ----------------------------------------------------------------------------- |
---|
883 | # NULL Log Entry Instance |
---|
884 | # ----------------------------------------------------------------------------- |
---|
885 | |
---|
886 | # The NULL entry is used to "remove" an existing entry within the log. |
---|
887 | # By replacing the current entry type with the NULL entry type and zeroing |
---|
888 | # out all data following the header, this can effectively remove an entry |
---|
889 | # without changing the memory footprint of the log. NULL entries will |
---|
890 | # be filtered and never show up in the raw_log_index. |
---|
891 | |
---|
892 | entry_null = WlanExpLogEntryType(name='NULL', entry_type_id=ENTRY_TYPE_NULL) |
---|
893 | |
---|
894 | |
---|
895 | # ----------------------------------------------------------------------------- |
---|
896 | # Virtual Log Entry Instances |
---|
897 | # ----------------------------------------------------------------------------- |
---|
898 | |
---|
899 | ########################################################################### |
---|
900 | # Rx Common |
---|
901 | # |
---|
902 | entry_rx_common = WlanExpLogEntryType(name='RX_COMMON_FIELDS', entry_type_id=None) |
---|
903 | |
---|
904 | entry_rx_common.description = 'These log entries will only be created for packets that are passed to the high-level MAC code in CPU High. If ' |
---|
905 | entry_rx_common.description += 'the low-level MAC filter drops the packet, it will not be logged. For full "monitor mode" ensure the low-level ' |
---|
906 | entry_rx_common.description += 'MAC filter is configured to pass all receptions up to CPU High.' |
---|
907 | |
---|
908 | entry_rx_common.append_field_defs([ |
---|
909 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds at PHY RX_START'), |
---|
910 | ('timestamp_frac', 'B', 'uint8', 'Fractional part of timestamp (units of 6.25ns)'), |
---|
911 | ('phy_samp_rate', 'B', 'uint8', 'PHY sampling rate in MSps'), |
---|
912 | ('length', 'H', 'uint16', 'Length of payload in bytes'), |
---|
913 | ('cfo_est', 'i', 'int32', 'Time-domain CFO estimate from Rx PHY; Fix32_31 value, CFO as fraction of sampling frequency'), |
---|
914 | ('mcs', 'B', 'uint8', 'MCS index, in [0:7]'), |
---|
915 | ('phy_mode', 'B', 'uint8', 'PHY mode index, in [0:2]'), |
---|
916 | ('ant_mode', 'B', 'uint8', 'Antenna mode: [1,2,3,4] for SISO Rx on RF [A,B,C,D]'), |
---|
917 | ('power', 'b', 'int8', 'Rx power in dBm'), |
---|
918 | ('padding0', 'x', 'uint8', ''), |
---|
919 | ('pkt_type', 'B', 'uint8', 'Packet type, first frame control byte of 802.11 header'), |
---|
920 | ('channel', 'B', 'uint8', 'Channel (center frequency) index'), |
---|
921 | ('padding1', 'x', 'uint8', ''), |
---|
922 | ('rx_gain_index', 'B', 'uint8', 'Radio Rx gain index; larger values mean larger Rx gains, mapping to absolute dB is radio-dependent'), |
---|
923 | ('padding2', 'B', 'uint8', ''), |
---|
924 | ('flags', 'H', 'uint16', '1-bit flags')]) |
---|
925 | |
---|
926 | entry_rx_common.consts = util.consts_dict({ |
---|
927 | 'ant_mode' : util.consts_dict({ |
---|
928 | 'RF_A': 0x01, |
---|
929 | 'RF_B': 0x02, |
---|
930 | 'RF_C': 0x03, |
---|
931 | 'RF_D': 0x04, |
---|
932 | }), |
---|
933 | 'pkt_type' : common_pkt_types, |
---|
934 | 'phy_mode' : util.phy_modes, |
---|
935 | 'flags' : util.consts_dict({ |
---|
936 | 'FCS_GOOD' : 0x0001, |
---|
937 | 'DUPLICATE' : 0x0002, |
---|
938 | 'UNEXPECTED_RESPONSE' : 0x0004, |
---|
939 | 'LTG_PYLD' : 0x0040, |
---|
940 | 'LTG' : 0x0080 |
---|
941 | }) |
---|
942 | }) |
---|
943 | |
---|
944 | |
---|
945 | ########################################################################### |
---|
946 | # Tx CPU High Common |
---|
947 | # |
---|
948 | entry_tx_common = WlanExpLogEntryType(name='TX_HIGH_COMMON_FIELDS', entry_type_id=None) |
---|
949 | |
---|
950 | entry_tx_common.description = 'Tx events in CPU High, logged for each data and management frame created and enqueued in CPU High. See TX_LOW for log entries of ' |
---|
951 | entry_tx_common.description += 'actual Tx events, including re-transmissions. The time values in this log entry can be used to determine time in queue ' |
---|
952 | entry_tx_common.description += '(time_to_accept), time taken by CPU Low for all Tx attempts (time_to_done) and total time from creation to completion ' |
---|
953 | entry_tx_common.description += '(time_to_accept+time_to_done).' |
---|
954 | |
---|
955 | entry_tx_common.append_field_defs([ |
---|
956 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when packet was created, immediately before it was enqueued'), |
---|
957 | ('time_to_accept', 'I', 'uint32', 'Time duration in microseconds between packet creation and acceptance by frame_transmit() in CPU Low'), |
---|
958 | ('time_to_done', 'I', 'uint32', 'Time duration in microseconds between packet acceptance by CPU Low and completion of all transmissions by CPU Low'), |
---|
959 | ('uniq_seq', 'Q', 'uint64', 'Unique sequence number for Tx packet; 12 LSB of this used for 802.11 MAC header sequence number'), |
---|
960 | ('padding0', 'I', 'uint32', ''), |
---|
961 | ('num_tx', 'H', 'uint16', 'Number of Tx attempts that were made for this packet'), |
---|
962 | ('length', 'H', 'uint16', 'Length in bytes of MPDU; includes MAC header, payload and FCS'), |
---|
963 | ('padding1', 'x', 'uint8', ''), |
---|
964 | ('pkt_type', 'B', 'uint8', 'Packet type, first frame control byte of 802.11 header'), |
---|
965 | ('queue_id', 'H', 'uint16', 'Tx queue ID from which the packet was retrieved'), |
---|
966 | ('queue_occupancy', 'H', 'uint16', 'Occupancy of the Tx queue immediately after this packet was enqueued'), |
---|
967 | ('flags', 'H', 'uint16', '1-bit flags')]) |
---|
968 | |
---|
969 | entry_tx_common.consts = util.consts_dict({ |
---|
970 | 'pkt_type' : common_pkt_types, |
---|
971 | 'flags' : util.consts_dict({ |
---|
972 | 'SUCCESSFUL' : 0x0001, |
---|
973 | 'LTG_PYLD' : 0x0040, |
---|
974 | 'LTG' : 0x0080 |
---|
975 | }) |
---|
976 | }) |
---|
977 | |
---|
978 | |
---|
979 | ########################################################################### |
---|
980 | # Tx CPU Low Common |
---|
981 | # |
---|
982 | entry_tx_low_common = WlanExpLogEntryType(name='TX_LOW_COMMON_FIELDS', entry_type_id=None) |
---|
983 | |
---|
984 | entry_tx_low_common.description = 'Record of actual PHY transmission. At least one TX_LOW will be logged for every TX_HIGH entry. Multiple TX_LOW entries ' |
---|
985 | entry_tx_low_common.description += 'may be created for the same TX_HIGH entry if the low-level MAC re-transmitted the frame. Use the uniq_seq fields to match ' |
---|
986 | entry_tx_low_common.description += 'TX_HIGH and TX_LOW entries to find records common to the same MPDU.' |
---|
987 | |
---|
988 | entry_tx_low_common.append_field_defs([ |
---|
989 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when packet transmission actually started (PHY TX_START time)'), |
---|
990 | ('uniq_seq', 'Q', 'uint64', 'Unique sequence number of original MPDU'), |
---|
991 | ('mcs', 'B', 'uint8', 'MCS index in [0:7]'), |
---|
992 | ('phy_mode', 'B', 'uint8', 'PHY mode index, in [1:2]'), |
---|
993 | ('ant_mode', 'B', 'uint8', 'PHY antenna mode in [0x10, 0x20, 0x30, 0x40]'), |
---|
994 | ('tx_power', 'b', 'int8', 'Tx power in dBm'), |
---|
995 | ('reserved0', 'B', 'uint8', ''), |
---|
996 | ('channel', 'B', 'uint8', 'Channel (center frequency) index'), |
---|
997 | ('length', 'H', 'uint16', 'Length in bytes of MPDU; includes MAC header, payload and FCS'), |
---|
998 | ('num_slots', 'h', 'int16', 'Number of backoff slots allotted prior to this transmission; may not have been used for initial Tx (attempt_number==1); A value of -1 in this field means no backoff occured'), |
---|
999 | ('cw', 'H', 'uint16', 'Contention window value at time of this Tx'), |
---|
1000 | ('pkt_type', 'B', 'uint8', 'Packet type, (first frame control byte of 802.11 header)'), |
---|
1001 | ('flags', 'B', 'uint8', '1-bit Flags'), |
---|
1002 | ('timestamp_frac', 'B', 'uint8', 'Fractional part of Tx timestamp (units of 6.25ns)'), |
---|
1003 | ('phy_samp_rate', 'B', 'uint8', 'PHY Sampling Rate Mode'), |
---|
1004 | ('attempt_number', 'H', 'uint16', 'Transmission index for this attempt, starting at 1 (1 = first Tx)'), |
---|
1005 | ('reserved1', 'H', 'uint16', '')]) |
---|
1006 | |
---|
1007 | entry_tx_low_common.consts = util.consts_dict({ |
---|
1008 | 'ant_mode' : util.consts_dict({ |
---|
1009 | 'RF_A': 0x10, |
---|
1010 | 'RF_B': 0x20, |
---|
1011 | 'RF_C': 0x30, |
---|
1012 | 'RF_D': 0x40, |
---|
1013 | }), |
---|
1014 | 'phy_mode' : util.phy_modes, |
---|
1015 | 'pkt_type' : common_pkt_types, |
---|
1016 | 'flags' : util.consts_dict({ |
---|
1017 | 'RECEIVED_RESPONSE' : 0x01, |
---|
1018 | 'LTG' : 0x40, |
---|
1019 | 'LTG_PYLD' : 0x80 |
---|
1020 | }) |
---|
1021 | }) |
---|
1022 | |
---|
1023 | |
---|
1024 | # ----------------------------------------------------------------------------- |
---|
1025 | # Log Entry Type Instances |
---|
1026 | # ----------------------------------------------------------------------------- |
---|
1027 | |
---|
1028 | ########################################################################### |
---|
1029 | # Node Info |
---|
1030 | # |
---|
1031 | entry_node_info = WlanExpLogEntryType(name='NODE_INFO', entry_type_id=ENTRY_TYPE_NODE_INFO) |
---|
1032 | |
---|
1033 | entry_node_info.description = 'Details about the node hardware and its configuration. Node info values are static after boot.' |
---|
1034 | |
---|
1035 | entry_node_info.append_field_defs([ |
---|
1036 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when log entry created'), |
---|
1037 | ('wlan_mac_addr', 'Q', 'uint64', 'MAC address of station'), |
---|
1038 | ('high_sw_id', 'B', 'uint8', 'ID of CPU_HIGH project'), |
---|
1039 | ('low_sw_id', 'B', 'uint8', 'ID of CPU_LOW project'), |
---|
1040 | ('padding', 'H', 'uint16', 'padding for alignment'), |
---|
1041 | ('high_sw_config', 'I', 'uint32', 'Configuration of CPU_HIGH'), |
---|
1042 | ('low_sw_config', 'I', 'uint32', 'Configuration of CPU_LOW'), |
---|
1043 | ('node_id', 'I', 'uint32', 'Node ID, as set during wlan_exp init'), |
---|
1044 | ('platform_id', 'I', 'uint32', 'Platform ID'), |
---|
1045 | ('serial_num', 'I', 'uint32', 'Node serial number'), |
---|
1046 | ('wlan_exp_version', 'I', 'uint32', 'Version of wlan_exp'), |
---|
1047 | ('max_tx_power_dbm', 'h', 'int16', 'Maximum transmit power'), |
---|
1048 | ('min_tx_power_dbm', 'h', 'int16', 'Minimum transmit power'), |
---|
1049 | ('cpu_high_compilation_date', '12s', '12S', 'CPU High Compilation Date string'), |
---|
1050 | ('cpu_high_compilation_time', '12s', '12S', 'CPU High Compilation Time string'), |
---|
1051 | ('cpu_low_compilation_date', '12s', '12S', 'CPU Low Compilation Date string'), |
---|
1052 | ('cpu_low_compilation_time', '12s', '12S', 'CPU Low Compilation Time string')]) |
---|
1053 | |
---|
1054 | entry_node_info.consts = util.consts_dict({ |
---|
1055 | 'high_sw_id': util.consts_dict({ |
---|
1056 | 'AP': 0x01, |
---|
1057 | 'STA': 0x02, |
---|
1058 | 'IBSS': 0x03, |
---|
1059 | }), |
---|
1060 | 'low_sw_id': util.consts_dict({ |
---|
1061 | 'DCF': 0x01, |
---|
1062 | 'NOMAC': 0x02, |
---|
1063 | }) |
---|
1064 | }) |
---|
1065 | |
---|
1066 | ########################################################################### |
---|
1067 | # Experiment Info header - actual exp_info contains a "message" field that |
---|
1068 | # follows this header. Since the message is variable length it is not described |
---|
1069 | # in the fields list below. Full exp_info entries (header + message) must be extracted |
---|
1070 | # directly by a user script. |
---|
1071 | # |
---|
1072 | entry_exp_info_hdr = WlanExpLogEntryType(name='EXP_INFO', entry_type_id=ENTRY_TYPE_EXP_INFO) |
---|
1073 | |
---|
1074 | entry_exp_info_hdr.description = 'Header for generic experiment info entries created by the user application. ' |
---|
1075 | entry_exp_info_hdr.description += 'The payload of the EXP_INFO entry is not described by the Python entry type. User ' |
---|
1076 | entry_exp_info_hdr.description += 'code must access the payload in the binary log data directly.' |
---|
1077 | |
---|
1078 | entry_exp_info_hdr.append_field_defs([ |
---|
1079 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when log entry created'), |
---|
1080 | ('info_type', 'I', 'uint32', 'Exp info type - arbitrary value supplied by application'), |
---|
1081 | ('msg_len', 'I', 'uint32', 'Message length in bytes'),]) |
---|
1082 | |
---|
1083 | |
---|
1084 | ########################################################################### |
---|
1085 | # Time Info |
---|
1086 | # |
---|
1087 | entry_time_info = WlanExpLogEntryType(name='TIME_INFO', entry_type_id=ENTRY_TYPE_TIME_INFO) |
---|
1088 | |
---|
1089 | entry_time_info.description = 'Record of a time base event at the node. This log entry is used to enable parsing of log data ' |
---|
1090 | entry_time_info.description += 'recorded before and after changes to the node\'s microsecond MAC timer. This entry also allows a wlan_exp controler to ' |
---|
1091 | entry_time_info.description += 'write the current host time to the node log without affecting the node\'s MAC timer value. This enables adjustment ' |
---|
1092 | entry_time_info.description += 'of log entry timestamps to real timestamps in post-proessing.' |
---|
1093 | |
---|
1094 | entry_time_info.append_field_defs([ |
---|
1095 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when log entry created (before any time change is applied)'), |
---|
1096 | ('time_id', 'I', 'uint32', 'Random ID value included in wlan_exp TIME_INFO command; used to find common entries across nodes'), |
---|
1097 | ('reason', 'I', 'uint32', 'Reason code for TIME_INFO log entry creation'), |
---|
1098 | ('mac_timestamp', 'Q', 'uint64', 'New value of MAC Time in microseconds '), |
---|
1099 | ('system_timestamp', 'Q', 'uint64', 'Value of System Time in microseconds'), |
---|
1100 | ('host_timestamp', 'Q', 'uint64', 'Host time in microseconds-since-epoch; 0xFFFFFFFFFFFFFFFF if unknown')]) |
---|
1101 | |
---|
1102 | entry_time_info.consts = util.consts_dict({ |
---|
1103 | 'reason' : util.consts_dict({ |
---|
1104 | 'SYSTEM' : 0x00000000, |
---|
1105 | 'WLAN_EXP_SET_TIME' : 0x00000001, |
---|
1106 | 'WLAN_EXP_ADD_LOG' : 0x00000002 |
---|
1107 | }) |
---|
1108 | }) |
---|
1109 | |
---|
1110 | |
---|
1111 | ########################################################################### |
---|
1112 | # Temperature |
---|
1113 | # |
---|
1114 | entry_node_temperature = WlanExpLogEntryType(name='NODE_TEMPERATURE', entry_type_id=ENTRY_TYPE_NODE_TEMPERATURE) |
---|
1115 | |
---|
1116 | entry_node_temperature.description = 'Record of the FPGA system monitor die temperature. This entry is only created when directed by a wlan_exp ' |
---|
1117 | entry_node_temperature.description += 'command. Temperature values are stored as 32-bit unsigned integers. To convert to degrees Celcius, apply ' |
---|
1118 | entry_node_temperature.description += '(((float)temp_u32)/(65536.0*0.00198421639)) - 273.15' |
---|
1119 | |
---|
1120 | entry_node_temperature.append_field_defs([ |
---|
1121 | ('timestamp', 'Q', 'uint64', 'Value of MAC Time in microseconds when log entry created'), |
---|
1122 | ('temp_current', 'I', 'uint32', 'Current FPGA die temperature (deg C)'), |
---|
1123 | ('temp_min', 'I', 'uint32', 'Minimum FPGA die temperature (deg C) since FPGA configuration or sysmon reset'), |
---|
1124 | ('temp_max', 'I', 'uint32', 'Maximum FPGA die temperature (deg C) since FPGA configuration or sysmon reset')]) |
---|
1125 | |
---|
1126 | |
---|
1127 | ########################################################################### |
---|
1128 | # Receive OFDM |
---|
1129 | # |
---|
1130 | entry_rx_ofdm = WlanExpLogEntryType(name='RX_OFDM', entry_type_id=ENTRY_TYPE_RX_OFDM) |
---|
1131 | |
---|
1132 | entry_rx_ofdm.description = 'Rx events from OFDM PHY. ' + entry_rx_common.description |
---|
1133 | |
---|
1134 | entry_rx_ofdm.append_field_defs(entry_rx_common.get_field_defs()) |
---|
1135 | entry_rx_ofdm.append_field_defs([ |
---|
1136 | ('chan_est', '256B', '(64,2)i2', 'OFDM Rx channel estimates, packed as [(uint16)I (uint16)Q] values, one per subcarrier'), |
---|
1137 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1138 | ('mac_payload', '24s', '24uint8', 'First 24 bytes of MAC payload, typically the 802.11 MAC header')]) |
---|
1139 | |
---|
1140 | entry_rx_ofdm.add_gen_numpy_array_callback(np_array_add_txrx_fields) |
---|
1141 | |
---|
1142 | entry_rx_ofdm.consts = entry_rx_common.consts.copy() |
---|
1143 | |
---|
1144 | |
---|
1145 | ########################################################################### |
---|
1146 | # Receive OFDM LTG packet |
---|
1147 | # |
---|
1148 | entry_rx_ofdm_ltg = WlanExpLogEntryType(name='RX_OFDM_LTG', entry_type_id=ENTRY_TYPE_RX_OFDM_LTG) |
---|
1149 | |
---|
1150 | entry_rx_ofdm_ltg.description = 'LTG ' + entry_rx_ofdm.description |
---|
1151 | |
---|
1152 | entry_rx_ofdm_ltg.append_field_defs(entry_rx_common.get_field_defs()) |
---|
1153 | entry_rx_ofdm_ltg.append_field_defs([ |
---|
1154 | ('chan_est', '256B', '(64,2)i2', 'OFDM Rx channel estimates, packed as [(uint16)I (uint16)Q] values, one per subcarrier'), |
---|
1155 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1156 | ('mac_payload', '44s', '44uint8', 'First 44 bytes of MAC payload: the 802.11 MAC header, LLC header, Packet ID, LTG ID')]) |
---|
1157 | |
---|
1158 | entry_rx_ofdm_ltg.add_gen_numpy_array_callback(np_array_add_txrx_ltg_fields) |
---|
1159 | |
---|
1160 | entry_rx_ofdm_ltg.consts = entry_rx_common.consts.copy() |
---|
1161 | |
---|
1162 | |
---|
1163 | ########################################################################### |
---|
1164 | # Receive DSSS |
---|
1165 | # |
---|
1166 | entry_rx_dsss = WlanExpLogEntryType(name='RX_DSSS', entry_type_id=ENTRY_TYPE_RX_DSSS) |
---|
1167 | |
---|
1168 | entry_rx_dsss.description = 'Rx events from DSSS PHY. ' + entry_rx_common.description |
---|
1169 | |
---|
1170 | entry_rx_dsss.append_field_defs(entry_rx_common.get_field_defs()) |
---|
1171 | entry_rx_dsss.append_field_defs([ |
---|
1172 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1173 | ('mac_payload', '24s', '24uint8', 'First 24 bytes of MAC payload, typically the 802.11 MAC header')]) |
---|
1174 | |
---|
1175 | entry_rx_dsss.add_gen_numpy_array_callback(np_array_add_txrx_fields) |
---|
1176 | |
---|
1177 | entry_rx_dsss.consts = entry_rx_common.consts.copy() |
---|
1178 | |
---|
1179 | |
---|
1180 | ########################################################################### |
---|
1181 | # Transmit from CPU High |
---|
1182 | # |
---|
1183 | entry_tx_high = WlanExpLogEntryType(name='TX_HIGH', entry_type_id=ENTRY_TYPE_TX_HIGH) |
---|
1184 | |
---|
1185 | entry_tx_high.description = entry_tx_common.description |
---|
1186 | |
---|
1187 | entry_tx_high.append_field_defs(entry_tx_common.get_field_defs()) |
---|
1188 | entry_tx_high.append_field_defs([ |
---|
1189 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1190 | ('mac_payload', '24s', '24uint8', 'First 24 bytes of MAC payload, typically the 802.11 MAC header')]) |
---|
1191 | |
---|
1192 | entry_tx_high.add_gen_numpy_array_callback(np_array_add_txrx_fields) |
---|
1193 | |
---|
1194 | entry_tx_high.consts = entry_tx_common.consts.copy() |
---|
1195 | |
---|
1196 | |
---|
1197 | ########################################################################### |
---|
1198 | # Transmit from CPU High LTG packet |
---|
1199 | # |
---|
1200 | entry_tx_high_ltg = WlanExpLogEntryType(name='TX_HIGH_LTG', entry_type_id=ENTRY_TYPE_TX_HIGH_LTG) |
---|
1201 | |
---|
1202 | entry_tx_high_ltg.description = entry_tx_common.description |
---|
1203 | |
---|
1204 | entry_tx_high_ltg.append_field_defs(entry_tx_common.get_field_defs()) |
---|
1205 | entry_tx_high_ltg.append_field_defs([ |
---|
1206 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1207 | ('mac_payload', '44s', '44uint8', 'First 44 bytes of MAC payload: the 802.11 MAC header, LLC header, Packet ID, LTG ID')]) |
---|
1208 | |
---|
1209 | entry_tx_high_ltg.add_gen_numpy_array_callback(np_array_add_txrx_ltg_fields) |
---|
1210 | |
---|
1211 | entry_tx_high_ltg.consts = entry_tx_common.consts.copy() |
---|
1212 | |
---|
1213 | |
---|
1214 | ########################################################################### |
---|
1215 | # Transmit from CPU Low |
---|
1216 | # |
---|
1217 | entry_tx_low = WlanExpLogEntryType(name='TX_LOW', entry_type_id=ENTRY_TYPE_TX_LOW) |
---|
1218 | |
---|
1219 | entry_tx_low.description = entry_tx_low_common.description |
---|
1220 | |
---|
1221 | entry_tx_low.append_field_defs(entry_tx_low_common.get_field_defs()) |
---|
1222 | entry_tx_low.append_field_defs([ |
---|
1223 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1224 | ('mac_payload', '24s', '24uint8', 'First 24 bytes of MAC payload, typically the 802.11 MAC header')]) |
---|
1225 | |
---|
1226 | entry_tx_low.add_gen_numpy_array_callback(np_array_add_txrx_fields) |
---|
1227 | |
---|
1228 | entry_tx_low.consts = entry_tx_low_common.consts.copy() |
---|
1229 | |
---|
1230 | |
---|
1231 | ########################################################################### |
---|
1232 | # Transmit from CPU Low LTG packet |
---|
1233 | # |
---|
1234 | entry_tx_low_ltg = WlanExpLogEntryType(name='TX_LOW_LTG', entry_type_id=ENTRY_TYPE_TX_LOW_LTG) |
---|
1235 | |
---|
1236 | entry_tx_low_ltg.description = entry_tx_low_common.description |
---|
1237 | |
---|
1238 | entry_tx_low_ltg.append_field_defs(entry_tx_low_common.get_field_defs()) |
---|
1239 | entry_tx_low_ltg.append_field_defs([ |
---|
1240 | ('mac_payload_len', 'I', 'uint32', 'Length in bytes of MAC payload recorded in log for this packet'), |
---|
1241 | ('mac_payload', '44s', '44uint8', 'First 44 bytes of MAC payload: the 802.11 MAC header, LLC header, Packet ID, LTG ID')]) |
---|
1242 | |
---|
1243 | entry_tx_low_ltg.add_gen_numpy_array_callback(np_array_add_txrx_ltg_fields) |
---|
1244 | |
---|
1245 | entry_tx_low_ltg.consts = entry_tx_low_common.consts.copy() |
---|
1246 | |
---|