1#!/usr/bin/env python3 2# Copyright 2019 Oticon A/S 3# Copyright 2023 Nordic Semiconductor ASA 4# SPDX-License-Identifier: Apache-2.0 5 6import os 7import time 8import argparse 9import csv 10import struct 11 12# default max packet length (phdr + access address + pdu + crc) 13SNAPLEN = 512 14 15KEY_ALTERNATIVES = [ 16 ('start_time', 'Tx_Start_Time'), 17 ('center_freq', 'CenterFreq'), 18 ('phy_address', 'PhyAddress'), 19 ('packet_size', 'PacketSize'), 20 ('packet', 'Packet'), 21] 22 23class CSVFile: 24 def __init__(self, f): 25 if not hasattr(f, 'read'): 26 f = open(f, newline='') 27 self.file = f 28 self.reader = csv.reader(self.file, delimiter=',', lineterminator='\n') 29 headers = self.reader.__next__() 30 for (key, alt) in KEY_ALTERNATIVES: 31 if not key in headers and alt in headers: 32 headers[headers.index(alt)] = key 33 self.headers = headers 34 35 def __del__(self): 36 self.file.close() 37 38 def __enter__(self): 39 return self 40 41 def __exit__(self, exc_type, exc_val, exc_tb): 42 self.file.close() 43 return False 44 45 def __iter__(self): 46 return self 47 48 def __next__(self): 49 row = {} 50 lst = self.reader.__next__() 51 for idx, header in enumerate(self.headers): 52 row[header] = lst[idx] 53 row['start_time'] = int(row['start_time'], 10) 54 return row 55 56def write(outfile, *inputs, snaplen = SNAPLEN, basetime=None): 57 #For information on the pcap format see https://wiki.wireshark.org/Development/LibpcapFileFormat 58 59 buf = bytearray(30) 60 61 if basetime == None: 62 basetime = int(time.time() * 1000000) 63 64 # write pcap header 65 struct.pack_into('<IHHiIII', buf, 0, 66 0xa1b2c3d4, # magic_number 67 2, # version_major 68 4, # version_minor 69 0, # thiszone 70 0, # sigfigs 71 snaplen, # snaplen 72 215) # network, 215 = DLT_IEEE802_15_4_NONASK_PHY ; https://www.tcpdump.org/linktypes.html 73 outfile.write(buf[:24]) 74 75 inputs = [ CSVFile(f) for f in inputs ] 76 rows = [ next(cf, None) for cf in inputs ] 77 78 while True: 79 min_ts = None 80 min_idx = None 81 for idx, row in enumerate(rows): 82 if not row: 83 continue 84 ts = row['start_time'] 85 if min_ts == None or ts < min_ts: 86 min_ts = ts 87 min_idx = idx 88 89 if min_idx == None: 90 break 91 92 row = rows[min_idx] 93 rows[min_idx] = next(inputs[min_idx], None) 94 95 orig_len = int(row['packet_size'], 10) 96 if orig_len == 0: 97 continue 98 99 modulation = int(row['modulation'], 10) 100 if modulation != 256: 101 continue 102 103 freq = float(row['center_freq']) 104 if freq >= 1.0 and freq < 81.0: 105 rf_channel = int((freq - 1.0) / 2) 106 elif freq >= 2401.0 and freq < 2481.0: 107 rf_channel = int((freq - 2401.0) / 2) 108 else: 109 raise ValueError 110 111 pdu_crc = bytes.fromhex(row['packet']) 112 if len(pdu_crc) != orig_len: 113 raise ValueError 114 orig_len += 5; # 4 bytes preamble + 1 bytes address/SFD 115 incl_len = min(orig_len, snaplen) 116 117 access_address = int(row['phy_address'], 16) 118 119 ts = basetime + min_ts 120 ts_sec = ts // 1000000 121 ts_usec = ts % 1000000 122 123 struct.pack_into('<IIII', buf, 0, 124 # pcap record header, 16 bytes 125 ts_sec, 126 ts_usec, 127 incl_len, 128 orig_len) 129 outfile.write(buf[:16]) 130 131 struct.pack_into('<IB', buf, 0, 132 #Header 133 0, # preamble (0) 134 access_address) #SFD 135 outfile.write(buf[:5]) 136 137 #The packet starting from the lenght byte 138 outfile.write(pdu_crc[:(incl_len - 5)]) 139 140def open_input(filename): 141 try: 142 mtime = os.path.getmtime(filename) 143 return (mtime, open(filename, mode='r', newline='')) 144 except OSError as e: 145 raise argparse.ArgumentTypeError( 146 "can't open '{}': {}".format(filename, e)) 147 148def parse_args(): 149 parser = argparse.ArgumentParser(description='Convert BabbleSim Phy csv files to pcap') 150 parser.add_argument('-o', '--output', 151 dest='output', 152 metavar='OUTFILE', 153 help='write to this pcap file (required)', 154 required=True, 155 type=argparse.FileType(mode='wb')) 156 parser.add_argument( 157 dest='inputs', 158 metavar='INFILE', 159 help='input csv file(s) (at least one is required)', 160 nargs='+', 161 type=open_input) 162 return parser.parse_args() 163 164args = parse_args() 165 166write(args.output, *[ p[1] for p in args.inputs ], basetime=0) 167 168# vim: set ts=4 sw=4 noet: 169