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 time
7import argparse
8import struct
9from csv_common import *
10
11# default max packet length (phdr + access address + pdu + crc)
12SNAPLEN = 512
13
14def write(outfile, *inputs, snaplen = SNAPLEN, basetime=None):
15	#For information on the pcap format see https://wiki.wireshark.org/Development/LibpcapFileFormat
16
17	buf = bytearray(30)
18
19	if basetime == None:
20		basetime = int(time.time() * 1000000)
21
22	# write pcap header
23	struct.pack_into('<IHHiIII', buf, 0,
24			0xa1b2c3d4, # magic_number
25			2,          # version_major
26			4,          # version_minor
27			0,          # thiszone
28			0,          # sigfigs
29			snaplen,    # snaplen
30			215)        # network, 215 = DLT_IEEE802_15_4_NONASK_PHY ; https://www.tcpdump.org/linktypes.html
31	outfile.write(buf[:24])
32
33	inputs = [ CSVFile(f) for f in inputs ]
34	rows = [ next(cf, None) for cf in inputs ]
35
36	while True:
37		min_ts = None
38		min_idx = None
39		for idx, row in enumerate(rows):
40			if not row:
41				continue
42			ts = row['start_time']
43			if min_ts == None or ts < min_ts:
44				min_ts = ts
45				min_idx = idx
46
47		if min_idx == None:
48			break
49
50		row = rows[min_idx]
51		rows[min_idx] = next(inputs[min_idx], None)
52
53		orig_len = int(row['packet_size'], 10)
54		if orig_len == 0:
55			continue
56
57		modulation = int(row['modulation'], 10)
58		if modulation != 256:
59			continue
60
61		freq = float(row['center_freq'])
62		if freq >= 1.0 and freq < 81.0:
63			rf_channel = int((freq - 1.0) / 2)
64		elif freq >= 2401.0 and freq < 2481.0:
65			rf_channel = int((freq - 2401.0) / 2)
66		else:
67			raise ValueError
68
69		try:
70			pdu_crc = bytes.fromhex(row['packet'])
71		except: #In case the packet is broken mid byte
72			pdu_crc = bytes.fromhex("00")
73
74		if len(pdu_crc) != orig_len:  # Let's handle this somehow gracefully
75			print("Truncated input file (partial packet), writing partial packet in output")
76			orig_len = len(pdu_crc)
77		orig_len += 5; # 4 bytes preamble + 1 bytes address/SFD
78		incl_len = min(orig_len, snaplen)
79
80		access_address = int(row['phy_address'], 16)
81
82		ts = basetime + min_ts
83		ts_sec = ts // 1000000
84		ts_usec = ts % 1000000
85
86		struct.pack_into('<IIII', buf, 0,
87				# pcap record header, 16 bytes
88				ts_sec,
89				ts_usec,
90				incl_len,
91				orig_len)
92		outfile.write(buf[:16])
93
94		struct.pack_into('<IB', buf, 0,
95				#Header
96				0, # preamble (0)
97				access_address) #SFD
98		outfile.write(buf[:5])
99
100		#The packet starting from the lenght byte
101		outfile.write(pdu_crc[:(incl_len - 5)])
102
103def parse_args():
104	parser = argparse.ArgumentParser(description='Convert BabbleSim Phy csv files to pcap')
105	parser.add_argument(
106			'-er', '--epoch_real',
107			action='store_true',
108			dest='epoch_real',
109			required=False,
110			help='If set, the pcap timestamps will be offset based on the host time when the simulation was run. Otherwise they are directly the simulation timestamps',
111			)
112	parser.add_argument(
113			'-es', '--epoch_simu',
114			action='store_false',
115			dest='epoch_real',
116			required=False,
117			help='If set, pcap timestamp are directly the simulation timestamps (default)',
118			)
119	parser.add_argument('-o', '--output',
120			dest='output',
121			metavar='OUTFILE',
122			help='Write to this pcap file (required)',
123			required=True,
124			type=argparse.FileType(mode='wb'))
125	parser.add_argument(
126			dest='inputs',
127			metavar='INFILE',
128			help='Input csv file(s) (at least one is required)',
129			nargs='+',
130			type=open_input)
131	return parser.parse_args()
132
133args = parse_args()
134
135if (args.epoch_real == True):
136	basetime = int(min([ p[0] for p in args.inputs ]) * 1000000)
137else:
138	basetime = 0
139
140write(args.output, *[ p[1] for p in args.inputs ], basetime=basetime)
141
142# vim: set ts=4 sw=4 noet:
143