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