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