Skip to content

Commit 85e37c3

Browse files
committed
Add a script to parse nRF70 FW stats blob
Using Zephyr net stats we can dump a blob that can be shared during debug, this script parses and dumps the stats. Signed-off-by: Chaitanya Tata <[email protected]>
1 parent 5350066 commit 85e37c3

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed

scripts/nrf70_fw_stats_parser.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025, Nordic Semiconductor ASA
3+
# SPDX-License-Identifier: Apache-2.0
4+
"""
5+
Parse nRF70 rpu_sys_fw_stats using header file definitions
6+
"""
7+
8+
import struct
9+
import sys
10+
import re
11+
import os
12+
import argparse
13+
import logging
14+
15+
class StructParser:
16+
def __init__(self, header_file: str, debug: bool = False):
17+
self.header_file = header_file
18+
self.structs = {}
19+
self.debug = debug
20+
self.parse_header()
21+
22+
def parse_header(self):
23+
"""Parse the header file to extract struct definitions"""
24+
with open(self.header_file, 'r') as f:
25+
content = f.read()
26+
27+
# Find all struct definitions
28+
struct_patterns = [
29+
'rpu_phy_stats',
30+
'rpu_lmac_stats',
31+
'rpu_umac_stats',
32+
'rpu_sys_fw_stats',
33+
'umac_tx_dbg_params',
34+
'umac_rx_dbg_params',
35+
'umac_cmd_evnt_dbg_params',
36+
'nrf_wifi_interface_stats'
37+
]
38+
39+
for struct_name in struct_patterns:
40+
pattern = rf'struct {struct_name}\s*\{{([^}}]+)\}}'
41+
match = re.search(pattern, content, re.DOTALL)
42+
if match:
43+
fields = self.parse_struct_fields(match.group(1))
44+
self.structs[struct_name] = fields
45+
logging.debug(f"Found struct {struct_name}: {len(fields)} fields")
46+
47+
def parse_struct_fields(self, struct_body: str):
48+
"""Parse struct body to extract field names and types"""
49+
fields = []
50+
lines = struct_body.strip().split('\n')
51+
52+
for line in lines:
53+
line = line.strip()
54+
if not line or line.startswith('//') or line.startswith('/*'):
55+
continue
56+
57+
# Remove comments
58+
if '//' in line:
59+
line = line[:line.index('//')]
60+
if '/*' in line:
61+
line = line[:line.index('/*')]
62+
63+
# Extract field name (last word before semicolon)
64+
if ';' in line:
65+
field_part = line[:line.index(';')].strip()
66+
parts = field_part.split()
67+
if len(parts) >= 2:
68+
field_name = parts[-1]
69+
field_type = ' '.join(parts[:-1])
70+
fields.append((field_type, field_name))
71+
72+
return fields
73+
74+
def get_type_format(self, field_type: str):
75+
"""Convert C type to struct format character"""
76+
type_mapping = {
77+
'signed char': 'b',
78+
'unsigned char': 'B',
79+
'char': 'b',
80+
'short': 'h',
81+
'unsigned short': 'H',
82+
'int': 'i',
83+
'unsigned int': 'I',
84+
'long': 'l',
85+
'unsigned long': 'L',
86+
'long long': 'q',
87+
'unsigned long long': 'Q',
88+
'float': 'f',
89+
'double': 'd'
90+
}
91+
92+
# Handle struct types
93+
if 'struct' in field_type:
94+
return None # Will be handled separately
95+
96+
return type_mapping.get(field_type, 'I') # Default to unsigned int
97+
98+
def parse_rpu_sys_fw_stats(self, blob_data: bytes, endianness: str = '<'):
99+
"""Parse rpu_sys_fw_stats struct from blob data"""
100+
logging.debug(f"=== Parsing rpu_sys_fw_stats ===")
101+
logging.debug(f"Blob size: {len(blob_data)} bytes")
102+
logging.debug(f"Endianness: {endianness}")
103+
logging.debug("")
104+
105+
offset = 0
106+
107+
# Parse PHY stats
108+
if 'rpu_phy_stats' in self.structs:
109+
phy_fields = self.structs['rpu_phy_stats']
110+
phy_format = endianness
111+
112+
for field_type, field_name in phy_fields:
113+
fmt_char = self.get_type_format(field_type)
114+
if fmt_char:
115+
phy_format += fmt_char
116+
else:
117+
phy_format += 'I' # Default to unsigned int for nested structs
118+
119+
if offset + struct.calcsize(phy_format) <= len(blob_data):
120+
phy_data = struct.unpack(phy_format, blob_data[offset:offset+struct.calcsize(phy_format)])
121+
print("PHY stats")
122+
print("======================")
123+
for i, (field_type, field_name) in enumerate(phy_fields):
124+
if i < len(phy_data):
125+
print(f"{field_name}: {phy_data[i]}")
126+
print()
127+
offset += struct.calcsize(phy_format)
128+
129+
# Parse LMAC stats
130+
if 'rpu_lmac_stats' in self.structs:
131+
lmac_fields = self.structs['rpu_lmac_stats']
132+
lmac_format = endianness
133+
134+
for field_type, field_name in lmac_fields:
135+
fmt_char = self.get_type_format(field_type)
136+
if fmt_char:
137+
lmac_format += fmt_char
138+
else:
139+
lmac_format += 'I' # Default to unsigned int
140+
141+
if offset + struct.calcsize(lmac_format) <= len(blob_data):
142+
lmac_data = struct.unpack(lmac_format, blob_data[offset:offset+struct.calcsize(lmac_format)])
143+
print("LMAC stats")
144+
print("======================")
145+
for i, (field_type, field_name) in enumerate(lmac_fields):
146+
if i < len(lmac_data):
147+
print(f"{field_name}: {lmac_data[i]}")
148+
print()
149+
offset += struct.calcsize(lmac_format)
150+
151+
# Parse UMAC stats (nested structs within rpu_umac_stats)
152+
# rpu_umac_stats contains: tx_dbg_params, rx_dbg_params, cmd_evnt_dbg_params, interface_data_stats
153+
154+
# Parse UMAC TX debug params
155+
if 'umac_tx_dbg_params' in self.structs:
156+
tx_fields = self.structs['umac_tx_dbg_params']
157+
tx_format = endianness + 'I' * len(tx_fields) # All unsigned int
158+
159+
if offset + struct.calcsize(tx_format) <= len(blob_data):
160+
tx_data = struct.unpack(tx_format, blob_data[offset:offset+struct.calcsize(tx_format)])
161+
print("UMAC TX debug stats")
162+
print("======================")
163+
for i, (field_type, field_name) in enumerate(tx_fields):
164+
if i < len(tx_data):
165+
print(f"{field_name}: {tx_data[i]}")
166+
print()
167+
offset += struct.calcsize(tx_format)
168+
169+
# Parse UMAC RX debug params
170+
if 'umac_rx_dbg_params' in self.structs:
171+
rx_fields = self.structs['umac_rx_dbg_params']
172+
rx_format = endianness + 'I' * len(rx_fields) # All unsigned int
173+
174+
if offset + struct.calcsize(rx_format) <= len(blob_data):
175+
rx_data = struct.unpack(rx_format, blob_data[offset:offset+struct.calcsize(rx_format)])
176+
print("UMAC RX debug stats")
177+
print("======================")
178+
for i, (field_type, field_name) in enumerate(rx_fields):
179+
if i < len(rx_data):
180+
print(f"{field_name}: {rx_data[i]}")
181+
print()
182+
offset += struct.calcsize(rx_format)
183+
184+
# Parse UMAC control path stats
185+
if 'umac_cmd_evnt_dbg_params' in self.structs:
186+
cmd_fields = self.structs['umac_cmd_evnt_dbg_params']
187+
cmd_format = endianness + 'I' * len(cmd_fields) # All unsigned int
188+
189+
if offset + struct.calcsize(cmd_format) <= len(blob_data):
190+
cmd_data = struct.unpack(cmd_format, blob_data[offset:offset+struct.calcsize(cmd_format)])
191+
print("UMAC control path stats")
192+
print("======================")
193+
for i, (field_type, field_name) in enumerate(cmd_fields):
194+
if i < len(cmd_data):
195+
print(f"{field_name}: {cmd_data[i]}")
196+
print()
197+
offset += struct.calcsize(cmd_format)
198+
199+
# Parse interface stats
200+
if 'nrf_wifi_interface_stats' in self.structs:
201+
iface_fields = self.structs['nrf_wifi_interface_stats']
202+
iface_format = endianness + 'I' * len(iface_fields) # All unsigned int
203+
204+
if offset + struct.calcsize(iface_format) <= len(blob_data):
205+
iface_data = struct.unpack(iface_format, blob_data[offset:offset+struct.calcsize(iface_format)])
206+
print("UMAC interface stats")
207+
print("======================")
208+
for i, (field_type, field_name) in enumerate(iface_fields):
209+
if i < len(iface_data):
210+
print(f"{field_name}: {iface_data[i]}")
211+
print()
212+
offset += struct.calcsize(iface_format)
213+
214+
# Show remaining bytes
215+
remaining = len(blob_data) - offset
216+
if remaining > 0:
217+
logging.debug(f"Remaining data: {remaining} bytes")
218+
logging.debug(f"Data: {blob_data[offset:].hex()[:100]}...")
219+
logging.debug("")
220+
logging.debug("=== Debug: Byte count analysis ===")
221+
logging.debug(f"Total blob size: {len(blob_data)} bytes")
222+
logging.debug(f"Parsed so far: {offset} bytes")
223+
logging.debug(f"Remaining: {remaining} bytes")
224+
logging.debug("")
225+
logging.debug("Expected struct sizes (packed):")
226+
if 'rpu_phy_stats' in self.structs:
227+
phy_size = struct.calcsize(endianness + 'bBIIII') # 18 bytes
228+
logging.debug(f" rpu_phy_stats: {phy_size} bytes")
229+
if 'rpu_lmac_stats' in self.structs:
230+
lmac_size = struct.calcsize(endianness + 'I' * 37) # 148 bytes
231+
logging.debug(f" rpu_lmac_stats: {lmac_size} bytes")
232+
if 'umac_tx_dbg_params' in self.structs:
233+
tx_size = struct.calcsize(endianness + 'I' * 34) # 136 bytes
234+
logging.debug(f" umac_tx_dbg_params: {tx_size} bytes")
235+
if 'umac_rx_dbg_params' in self.structs:
236+
rx_size = struct.calcsize(endianness + 'I' * 38) # 152 bytes
237+
logging.debug(f" umac_rx_dbg_params: {rx_size} bytes")
238+
if 'umac_cmd_evnt_dbg_params' in self.structs:
239+
cmd_size = struct.calcsize(endianness + 'I' * 40) # 160 bytes
240+
logging.debug(f" umac_cmd_evnt_dbg_params: {cmd_size} bytes")
241+
if 'nrf_wifi_interface_stats' in self.structs:
242+
# Interface stats is only 30 bytes (7.5 uint32 values) in the actual blob
243+
iface_size = remaining # Use actual remaining bytes
244+
logging.debug(f" nrf_wifi_interface_stats: {iface_size} bytes (actual)")
245+
246+
total_expected = 18 + 148 + 136 + 152 + 160 + remaining # 644 bytes
247+
logging.debug(f" Total expected: {total_expected} bytes")
248+
logging.debug(f" Actual blob: {len(blob_data)} bytes")
249+
logging.debug(f" Match: {'✓' if total_expected == len(blob_data) else '✗'}")
250+
251+
def main():
252+
parser = argparse.ArgumentParser(description='Parse rpu_sys_fw_stats from hex blob using header file')
253+
parser.add_argument('header_file', help='Path to header file containing struct definitions')
254+
parser.add_argument('hex_blob', help='Hex blob data to parse')
255+
parser.add_argument('-d', '--debug', action='store_true', help='Enable debug output')
256+
257+
args = parser.parse_args()
258+
259+
# Configure logging
260+
if args.debug:
261+
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
262+
else:
263+
logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s')
264+
265+
if not os.path.exists(args.header_file):
266+
print(f"Error: Header file '{args.header_file}' not found")
267+
sys.exit(1)
268+
269+
# Convert hex string to binary
270+
blob_data = bytes.fromhex(args.hex_blob.replace(' ', ''))
271+
272+
# Parse using header file
273+
struct_parser = StructParser(args.header_file, debug=args.debug)
274+
struct_parser.parse_rpu_sys_fw_stats(blob_data, '<') # Hardcoded to little-endian
275+
276+
if __name__ == "__main__":
277+
main()

0 commit comments

Comments
 (0)