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