weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2018 The Android Open Source Project |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 5 | # use this file except in compliance with the License. You may obtain a copy of |
| 6 | # the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | # License for the specific language governing permissions and limitations under |
| 14 | # the License. |
| 15 | # |
| 16 | # |
| 17 | # |
| 18 | # |
| 19 | # This script extracts Hearing Aid audio data from btsnoop. |
| 20 | # Generates a valid audio file which can be played using player like smplayer. |
| 21 | # |
| 22 | # Audio File Name Format: |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 23 | # [PEER_ADDRESS]-[START_TIMESTAMP]-[AUDIO_TYPE]-[SAMPLE_RATE].[CODEC] |
| 24 | # |
| 25 | # Debug Infomation File Name Format: |
| 26 | # debug_ver_[DEBUG_VERSION]-[PEER_ADDRESS]-[START_TIMESTAMP]-[AUDIO_TYPE]-[SAMPLE_RATE].txt |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 27 | # |
| 28 | # Player: |
| 29 | # smplayer |
| 30 | # |
| 31 | # NOTE: |
| 32 | # Please make sure you HCI Snoop data file includes the following frames: |
| 33 | # HearingAid "LE Enhanced Connection Complete", GATT write for Audio Control |
| 34 | # Point with "Start cmd", and the data frames. |
| 35 | |
| 36 | import argparse |
| 37 | import os |
| 38 | import struct |
| 39 | import sys |
| 40 | import time |
| 41 | |
| 42 | IS_SENT = "IS_SENT" |
| 43 | PEER_ADDRESS = "PEER_ADDRESS" |
| 44 | CONNECTION_HANDLE = "CONNECTION_HANDLE" |
| 45 | AUDIO_CONTROL_ATTR_HANDLE = "AUDIO_CONTROL_ATTR_HANDLE" |
| 46 | START = "START" |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 47 | TIMESTAMP_STR_FORMAT = "TIMESTAMP_STR_FORMAT" |
| 48 | TIMESTAMP_TIME_FORMAT = "TIMESTAMP_TIME_FORMAT" |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 49 | CODEC = "CODEC" |
| 50 | SAMPLE_RATE = "SAMPLE_RATE" |
| 51 | AUDIO_TYPE = "AUDIO_TYPE" |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 52 | DEBUG_VERSION = "DEBUG_VERSION" |
| 53 | DEBUG_DATA = "DEBUG_DATA" |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 54 | AUDIO_DATA_B = "AUDIO_DATA_B" |
| 55 | |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 56 | # Debug packet header struct |
| 57 | header_list_str = ["Event Processed", |
| 58 | "Number Packet Nacked By Slave", |
| 59 | "Number Packet Nacked By Master"] |
| 60 | # Debug frame information structs |
| 61 | data_list_str = ["Event Number", |
| 62 | "Overrun", |
| 63 | "Underrun", |
| 64 | "Skips", |
| 65 | "Rendered Audio Frame", |
| 66 | "First PDU Option", |
| 67 | "Second PDU Option", |
| 68 | "Third PDU Option"] |
| 69 | |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 70 | AUDIO_CONTROL_POINT_UUID = "f0d4de7e4a88476c9d9f1937b0996cc0" |
| 71 | SEC_CONVERT = 1000000 |
| 72 | folder = None |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 73 | full_debug = False |
| 74 | simple_debug = False |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 75 | |
| 76 | force_audio_control_attr_handle = None |
| 77 | default_audio_control_attr_handle = 0x0079 |
| 78 | |
| 79 | audio_data = {} |
| 80 | |
| 81 | #======================================================================= |
| 82 | # Parse ACL Data Function |
| 83 | #======================================================================= |
| 84 | |
| 85 | #----------------------------------------------------------------------- |
| 86 | # Parse Hearing Aid Packet |
| 87 | #----------------------------------------------------------------------- |
| 88 | |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 89 | def parse_acl_ha_debug_buffer(data, result): |
| 90 | """This function extracts HA debug buffer""" |
| 91 | if len(data) < 5: |
| 92 | return |
| 93 | |
| 94 | version, data = unpack_data(data, 1) |
| 95 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], DEBUG_VERSION, str(version)) |
| 96 | |
| 97 | debug_str = result[TIMESTAMP_TIME_FORMAT]; |
| 98 | for p in range(3): |
| 99 | byte_data, data = unpack_data(data, 1) |
| 100 | debug_str = debug_str + ", " + header_list_str[p] + "=" + str(byte_data).rjust(3) |
| 101 | |
| 102 | if full_debug: |
| 103 | debug_str = debug_str + "\n" + "|".join(data_list_str) + "\n" |
| 104 | while True: |
| 105 | if len(data) < 7: |
| 106 | break |
| 107 | base = 0 |
| 108 | data_list_content = [] |
| 109 | for counter in range(6): |
| 110 | p = base + counter |
| 111 | byte_data, data = unpack_data(data, 1) |
| 112 | if p == 1: |
| 113 | data_list_content.append(str(byte_data & 0x03).rjust(len(data_list_str[p]))) |
| 114 | data_list_content.append(str((byte_data >> 2) & 0x03).rjust(len(data_list_str[p + 1]))) |
| 115 | data_list_content.append(str((byte_data >> 4) & 0x0f).rjust(len(data_list_str[p + 2]))) |
| 116 | base = 2 |
| 117 | else: |
| 118 | data_list_content.append(str(byte_data).rjust(len(data_list_str[p]))) |
| 119 | debug_str = debug_str + "|".join(data_list_content) + "\n" |
| 120 | |
| 121 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], DEBUG_DATA, debug_str) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 122 | |
| 123 | def parse_acl_ha_audio_data(data, result): |
| 124 | """This function extracts HA audio data.""" |
| 125 | if len(data) < 2: |
| 126 | return |
| 127 | # Remove audio packet number |
| 128 | audio_data_b = data[1:] |
| 129 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 130 | AUDIO_DATA_B, audio_data_b) |
| 131 | |
| 132 | |
| 133 | def parse_acl_ha_audio_type(data, result): |
| 134 | """This function parses HA audio control cmd audio type.""" |
| 135 | audio_type, data = unpack_data(data, 1) |
| 136 | if audio_type is None: |
| 137 | return |
| 138 | elif audio_type == 0x01: |
| 139 | audio_type = "Ringtone" |
| 140 | elif audio_type == 0x02: |
| 141 | audio_type = "Phonecall" |
| 142 | elif audio_type == 0x03: |
| 143 | audio_type = "Media" |
| 144 | else: |
| 145 | audio_type = "Unknown" |
| 146 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 147 | AUDIO_TYPE, audio_type) |
| 148 | |
| 149 | |
| 150 | def parse_acl_ha_codec(data, result): |
| 151 | """This function parses HA audio control cmd codec and sample rate.""" |
| 152 | codec, data = unpack_data(data, 1) |
| 153 | if codec == 0x01: |
| 154 | codec = "G722" |
| 155 | sample_rate = "16KHZ" |
| 156 | elif codec == 0x02: |
| 157 | codec = "G722" |
| 158 | sample_rate = "24KHZ" |
| 159 | else: |
| 160 | codec = "Unknown" |
| 161 | sample_rate = "Unknown" |
| 162 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 163 | CODEC, codec) |
| 164 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 165 | SAMPLE_RATE, sample_rate) |
| 166 | parse_acl_ha_audio_type(data, result) |
| 167 | |
| 168 | |
| 169 | def parse_acl_ha_audio_control_cmd(data, result): |
| 170 | """This function parses HA audio control cmd is start/stop.""" |
| 171 | control_cmd, data = unpack_data(data, 1) |
| 172 | if control_cmd == 0x01: |
| 173 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 174 | START, True) |
| 175 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 176 | TIMESTAMP_STR_FORMAT, result[TIMESTAMP_STR_FORMAT]) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 177 | parse_acl_ha_codec(data, result) |
| 178 | elif control_cmd == 0x02: |
| 179 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 180 | START, False) |
| 181 | |
| 182 | |
| 183 | #----------------------------------------------------------------------- |
| 184 | # Parse ACL Packet |
| 185 | #----------------------------------------------------------------------- |
| 186 | |
| 187 | def parse_acl_att_long_uuid(data, result): |
| 188 | """This function parses ATT long UUID to get attr_handle.""" |
| 189 | # len (1 byte) + start_attr_handle (2 bytes) + properties (1 byte) + |
| 190 | # attr_handle (2 bytes) + long_uuid (16 bytes) = 22 bytes |
| 191 | if len(data) < 22: |
| 192 | return |
| 193 | # skip unpack len, start_attr_handle, properties. |
| 194 | data = data[4:] |
| 195 | attr_handle, data = unpack_data(data, 2) |
| 196 | long_uuid_list = [] |
| 197 | for p in range(0, 16): |
| 198 | long_uuid_list.append("{0:02x}".format(struct.unpack(">B", data[p])[0])) |
| 199 | long_uuid_list.reverse() |
| 200 | long_uuid = "".join(long_uuid_list) |
| 201 | # Check long_uuid is AUDIO_CONTROL_POINT uuid to get the attr_handle. |
| 202 | if long_uuid == AUDIO_CONTROL_POINT_UUID: |
| 203 | update_audio_data(CONNECTION_HANDLE, result[CONNECTION_HANDLE], |
| 204 | AUDIO_CONTROL_ATTR_HANDLE, attr_handle) |
| 205 | |
| 206 | |
| 207 | def parse_acl_opcode(data, result): |
| 208 | """This function parses acl data opcode.""" |
| 209 | # opcode (1 byte) = 1 bytes |
| 210 | if len(data) < 1: |
| 211 | return |
| 212 | opcode, data = unpack_data(data, 1) |
| 213 | # Check opcode is 0x12 (write request) and attr_handle is |
| 214 | # audio_control_attr_handle for check it is HA audio control cmd. |
| 215 | if result[IS_SENT] and opcode == 0x12: |
| 216 | if len(data) < 2: |
| 217 | return |
| 218 | attr_handle, data = unpack_data(data, 2) |
| 219 | if attr_handle == \ |
| 220 | get_audio_control_attr_handle(result[CONNECTION_HANDLE]): |
| 221 | parse_acl_ha_audio_control_cmd(data, result) |
| 222 | # Check opcode is 0x09 (read response) to parse ATT long UUID. |
| 223 | elif not result[IS_SENT] and opcode == 0x09: |
| 224 | parse_acl_att_long_uuid(data, result) |
| 225 | |
| 226 | |
| 227 | def parse_acl_handle(data, result): |
| 228 | """This function parses acl data handle.""" |
| 229 | # connection_handle (2 bytes) + total_len (2 bytes) + pdu (2 bytes) |
| 230 | # + channel_id (2 bytes) = 8 bytes |
| 231 | if len(data) < 8: |
| 232 | return |
| 233 | connection_handle, data = unpack_data(data, 2) |
| 234 | connection_handle = connection_handle & 0x0FFF |
| 235 | # skip unpack total_len |
| 236 | data = data[2:] |
| 237 | pdu, data = unpack_data(data, 2) |
| 238 | channel_id, data = unpack_data(data, 2) |
| 239 | |
| 240 | # Check ATT packet or "Coc Data Packet" to get ATT information and audio |
| 241 | # data. |
| 242 | if connection_handle <= 0x0EFF: |
| 243 | if channel_id <= 0x003F: |
| 244 | result[CONNECTION_HANDLE] = connection_handle |
| 245 | parse_acl_opcode(data, result) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 246 | elif channel_id >= 0x0040 and channel_id <= 0x007F: |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 247 | result[CONNECTION_HANDLE] = connection_handle |
| 248 | sdu, data = unpack_data(data, 2) |
| 249 | if pdu - 2 == sdu: |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 250 | if result[IS_SENT]: |
| 251 | parse_acl_ha_audio_data(data, result) |
| 252 | else: |
| 253 | if simple_debug: |
| 254 | parse_acl_ha_debug_buffer(data, result) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 255 | |
| 256 | |
| 257 | #======================================================================= |
| 258 | # Parse HCI EVT Function |
| 259 | #======================================================================= |
| 260 | |
| 261 | |
| 262 | def parse_hci_evt_peer_address(data, result): |
| 263 | """This function parses peer address from hci event.""" |
| 264 | peer_address_list = [] |
| 265 | address_empty_list = ["00", "00", "00", "00", "00", "00"] |
| 266 | for n in range(0, 3): |
| 267 | if len(data) < 6: |
| 268 | return |
| 269 | for p in range(0, 6): |
| 270 | peer_address_list.append("{0:02x}".format(struct.unpack(">B", |
| 271 | data[p])[0])) |
| 272 | # Check the address is empty or not. |
| 273 | if peer_address_list == address_empty_list: |
| 274 | del peer_address_list[:] |
| 275 | data = data[6:] |
| 276 | else: |
| 277 | break |
| 278 | peer_address_list.reverse() |
| 279 | peer_address = "_".join(peer_address_list) |
| 280 | update_audio_data("", "", PEER_ADDRESS, peer_address) |
| 281 | update_audio_data(PEER_ADDRESS, peer_address, CONNECTION_HANDLE, |
| 282 | result[CONNECTION_HANDLE]) |
| 283 | |
| 284 | |
| 285 | def parse_hci_evt_code(data, result): |
| 286 | """This function parses hci event content.""" |
| 287 | # hci_evt (1 byte) + param_total_len (1 byte) + sub_event (1 byte) |
| 288 | # + status (1 byte) + connection_handle (2 bytes) + role (1 byte) |
| 289 | # + address_type (1 byte) = 8 bytes |
| 290 | if len(data) < 8: |
| 291 | return |
| 292 | hci_evt, data = unpack_data(data, 1) |
| 293 | # skip unpack param_total_len. |
| 294 | data = data[1:] |
| 295 | sub_event, data = unpack_data(data, 1) |
| 296 | status, data = unpack_data(data, 1) |
| 297 | connection_handle, data = unpack_data(data, 2) |
| 298 | connection_handle = connection_handle & 0x0FFF |
| 299 | # skip unpack role, address_type. |
| 300 | data = data[2:] |
| 301 | # We will directly check it is LE Enhanced Connection Complete or not |
| 302 | # for get Connection Handle and Address. |
| 303 | if not result[IS_SENT] and hci_evt == 0x3E and sub_event == 0x0A \ |
| 304 | and status == 0x00 and connection_handle <= 0x0EFF: |
| 305 | result[CONNECTION_HANDLE] = connection_handle |
| 306 | parse_hci_evt_peer_address(data, result) |
| 307 | |
| 308 | |
| 309 | #======================================================================= |
| 310 | # Common Parse Function |
| 311 | #======================================================================= |
| 312 | |
| 313 | |
| 314 | def parse_packet_data(data, result): |
| 315 | """This function parses packet type.""" |
| 316 | packet_type, data = unpack_data(data, 1) |
| 317 | if packet_type == 0x02: |
| 318 | # Try to check HearingAid audio control packet and data packet. |
| 319 | parse_acl_handle(data, result) |
| 320 | elif packet_type == 0x04: |
| 321 | # Try to check HearingAid connection successful packet. |
| 322 | parse_hci_evt_code(data, result) |
| 323 | |
| 324 | |
| 325 | def parse_packet(btsnoop_file): |
| 326 | """This function parses packet len, timestamp.""" |
| 327 | packet_result = {} |
| 328 | |
| 329 | # ori_len (4 bytes) + include_len (4 bytes) + packet_flag (4 bytes) |
| 330 | # + drop (4 bytes) + timestamp (8 bytes) = 24 bytes |
| 331 | packet_header = btsnoop_file.read(24) |
| 332 | if len(packet_header) != 24: |
| 333 | return False |
| 334 | |
| 335 | ori_len, include_len, packet_flag, drop, timestamp = \ |
| 336 | struct.unpack(">IIIIq", packet_header) |
| 337 | |
| 338 | if ori_len == include_len: |
| 339 | packet_data = btsnoop_file.read(ori_len) |
| 340 | if len(packet_data) != ori_len: |
| 341 | return False |
| 342 | if packet_flag != 2 and drop == 0: |
| 343 | packet_result[IS_SENT] = (packet_flag == 0) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 344 | packet_result[TIMESTAMP_STR_FORMAT], packet_result[TIMESTAMP_TIME_FORMAT] = convert_time_str(timestamp) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 345 | parse_packet_data(packet_data, packet_result) |
| 346 | else: |
| 347 | return False |
| 348 | |
| 349 | return True |
| 350 | |
| 351 | |
| 352 | #======================================================================= |
| 353 | # Update and DumpData Function |
| 354 | #======================================================================= |
| 355 | |
| 356 | |
| 357 | def dump_audio_data(data): |
| 358 | """This function dumps audio data into file.""" |
| 359 | file_type = "." + data[CODEC] |
| 360 | file_name_list = [] |
| 361 | file_name_list.append(data[PEER_ADDRESS]) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 362 | file_name_list.append(data[TIMESTAMP_STR_FORMAT]) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 363 | file_name_list.append(data[AUDIO_TYPE]) |
| 364 | file_name_list.append(data[SAMPLE_RATE]) |
| 365 | if folder is not None: |
| 366 | if not os.path.exists(folder): |
| 367 | os.makedirs(folder) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 368 | audio_file_name = os.path.join(folder, "-".join(file_name_list) + file_type) |
| 369 | if data.has_key(DEBUG_VERSION): |
| 370 | file_prefix = "debug_ver_" + data[DEBUG_VERSION] + "-" |
| 371 | debug_file_name = os.path.join(folder, file_prefix + "-".join(file_name_list) + ".txt") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 372 | else: |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 373 | audio_file_name = "-".join(file_name_list) + file_type |
| 374 | if data.has_key(DEBUG_VERSION): |
| 375 | file_prefix = "debug_ver_" + data[DEBUG_VERSION] + "-" |
| 376 | debug_file_name = file_prefix + "-".join(file_name_list) + ".txt" |
| 377 | |
| 378 | sys.stdout.write("Start to dump Audio File : %s\n" % audio_file_name) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 379 | if data.has_key(AUDIO_DATA_B): |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 380 | with open(audio_file_name, "wb+") as audio_file: |
| 381 | audio_file.write(data[AUDIO_DATA_B]) |
| 382 | sys.stdout.write("Finished to dump Audio File: %s\n\n" % audio_file_name) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 383 | else: |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 384 | sys.stdout.write("Fail to dump Audio File: %s\n" % audio_file_name) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 385 | sys.stdout.write("There isn't any Hearing Aid audio data.\n\n") |
| 386 | |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 387 | if simple_debug: |
| 388 | sys.stdout.write("Start to dump audio %s Debug File\n" % audio_file_name) |
| 389 | if data.has_key(DEBUG_DATA): |
| 390 | with open(debug_file_name, "wb+") as debug_file: |
| 391 | debug_file.write(data[DEBUG_DATA]) |
| 392 | sys.stdout.write("Finished to dump Debug File: %s\n\n" % debug_file_name) |
| 393 | else: |
| 394 | sys.stdout.write("Fail to dump audio %s Debug File\n" % audio_file_name) |
| 395 | sys.stdout.write("There isn't any Hearing Aid debug data.\n\n") |
| 396 | |
| 397 | |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 398 | |
| 399 | def update_audio_data(relate_key, relate_value, key, value): |
| 400 | """ |
| 401 | This function records the dump audio file related information. |
| 402 | audio_data = { |
| 403 | PEER_ADDRESS:{ |
| 404 | PEER_ADDRESS: PEER_ADDRESS, |
| 405 | CONNECTION_HANDLE: CONNECTION_HANDLE, |
| 406 | AUDIO_CONTROL_ATTR_HANDLE: AUDIO_CONTROL_ATTR_HANDLE, |
| 407 | START: True or False, |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 408 | TIMESTAMP_STR_FORMAT: START_TIMESTAMP_STR_FORMAT, |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 409 | CODEC: CODEC, |
| 410 | SAMPLE_RATE: SAMPLE_RATE, |
| 411 | AUDIO_TYPE: AUDIO_TYPE, |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 412 | DEBUG_VERSION: DEBUG_VERSION, |
| 413 | DEBUG_DATA: DEBUG_DATA, |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 414 | AUDIO_DATA_B: AUDIO_DATA_B |
| 415 | }, |
| 416 | PEER_ADDRESS_2:{ |
| 417 | PEER_ADDRESS: PEER_ADDRESS, |
| 418 | CONNECTION_HANDLE: CONNECTION_HANDLE, |
| 419 | AUDIO_CONTROL_ATTR_HANDLE: AUDIO_CONTROL_ATTR_HANDLE, |
| 420 | START: True or False, |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 421 | TIMESTAMP_STR_FORMAT: START_TIMESTAMP_STR_FORMAT, |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 422 | CODEC: CODEC, |
| 423 | SAMPLE_RATE: SAMPLE_RATE, |
| 424 | AUDIO_TYPE: AUDIO_TYPE, |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 425 | DEBUG_VERSION: DEBUG_VERSION, |
| 426 | DEBUG_DATA: DEBUG_DATA, |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 427 | AUDIO_DATA_B: AUDIO_DATA_B |
| 428 | } |
| 429 | } |
| 430 | """ |
| 431 | if key == PEER_ADDRESS: |
| 432 | if audio_data.has_key(value): |
| 433 | # Dump audio data and clear previous data. |
| 434 | update_audio_data(key, value, START, False) |
| 435 | # Extra clear CONNECTION_HANDLE due to new connection create. |
| 436 | if audio_data[value].has_key(CONNECTION_HANDLE): |
| 437 | audio_data[value].pop(CONNECTION_HANDLE, "") |
| 438 | else: |
| 439 | device_audio_data = {key: value} |
| 440 | temp_audio_data = {value: device_audio_data} |
| 441 | audio_data.update(temp_audio_data) |
| 442 | else: |
| 443 | for i in audio_data: |
| 444 | if audio_data[i].has_key(relate_key) \ |
| 445 | and audio_data[i][relate_key] == relate_value: |
| 446 | if key == START: |
| 447 | if audio_data[i].has_key(key) and audio_data[i][key]: |
| 448 | dump_audio_data(audio_data[i]) |
| 449 | # Clear data except PEER_ADDRESS, CONNECTION_HANDLE and |
| 450 | # AUDIO_CONTROL_ATTR_HANDLE. |
| 451 | audio_data[i].pop(key, "") |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 452 | audio_data[i].pop(TIMESTAMP_STR_FORMAT, "") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 453 | audio_data[i].pop(CODEC, "") |
| 454 | audio_data[i].pop(SAMPLE_RATE, "") |
| 455 | audio_data[i].pop(AUDIO_TYPE, "") |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 456 | audio_data[i].pop(DEBUG_VERSION, "") |
| 457 | audio_data[i].pop(DEBUG_DATA, "") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 458 | audio_data[i].pop(AUDIO_DATA_B, "") |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 459 | elif key == AUDIO_DATA_B or key == DEBUG_DATA: |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 460 | if audio_data[i].has_key(START) and audio_data[i][START]: |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 461 | if audio_data[i].has_key(key): |
| 462 | ori_data = audio_data[i].pop(key, "") |
| 463 | value = ori_data + value |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 464 | else: |
| 465 | # Audio doesn't start, don't record. |
| 466 | return |
| 467 | device_audio_data = {key: value} |
| 468 | audio_data[i].update(device_audio_data) |
| 469 | |
| 470 | |
| 471 | #======================================================================= |
| 472 | # Tool Function |
| 473 | #======================================================================= |
| 474 | |
| 475 | |
| 476 | def get_audio_control_attr_handle(connection_handle): |
| 477 | """This function gets audio_control_attr_handle.""" |
| 478 | # If force_audio_control_attr_handle is set, will use it first. |
| 479 | if force_audio_control_attr_handle is not None: |
| 480 | return force_audio_control_attr_handle |
| 481 | |
| 482 | # Try to check the audio_control_attr_handle is record into audio_data. |
| 483 | for i in audio_data: |
| 484 | if audio_data[i].has_key(CONNECTION_HANDLE) \ |
| 485 | and audio_data[i][CONNECTION_HANDLE] == connection_handle: |
| 486 | if audio_data[i].has_key(AUDIO_CONTROL_ATTR_HANDLE): |
| 487 | return audio_data[i][AUDIO_CONTROL_ATTR_HANDLE] |
| 488 | |
| 489 | # Return default attr_handle if audio_data doesn't record it. |
| 490 | return default_audio_control_attr_handle |
| 491 | |
| 492 | |
| 493 | def unpack_data(data, byte): |
| 494 | """This function unpacks data.""" |
| 495 | if byte == 1: |
| 496 | value = struct.unpack(">B", data[0])[0] |
| 497 | elif byte == 2: |
| 498 | value = struct.unpack(">H", data[1]+data[0])[0] |
| 499 | else: |
| 500 | value = "" |
| 501 | data = data[byte:] |
| 502 | return value, data |
| 503 | |
| 504 | |
| 505 | def convert_time_str(timestamp): |
| 506 | """This function converts time to string format.""" |
| 507 | really_timestamp = float(timestamp) / SEC_CONVERT |
| 508 | local_timestamp = time.localtime(really_timestamp) |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 509 | dt = really_timestamp - long(really_timestamp) |
| 510 | ms_str = "{0:06}".format(int(round(dt * 1000000))) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 511 | |
| 512 | str_format = time.strftime("%m_%d__%H_%M_%S", local_timestamp) |
| 513 | full_str_format = str_format + "_" + ms_str |
| 514 | |
| 515 | time_format = time.strftime("%m-%d %H:%M:%S", local_timestamp) |
| 516 | full_time_format = time_format + "." + ms_str |
| 517 | return full_str_format, full_time_format |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 518 | |
| 519 | |
| 520 | def set_config(): |
| 521 | """This function is for set config by flag and check the argv is correct.""" |
| 522 | argv_parser = argparse.ArgumentParser( |
| 523 | description="Extracts Hearing Aid audio data from BTSNOOP.") |
| 524 | argv_parser.add_argument("BTSNOOP", help="BLUETOOTH BTSNOOP file.") |
| 525 | argv_parser.add_argument("-f", "--folder", help="select output folder.", |
| 526 | dest="folder") |
| 527 | argv_parser.add_argument("-c1", "--connection-handle1", |
| 528 | help="set a fake connection handle 1 to capture \ |
| 529 | audio dump.", dest="connection_handle1", type=int) |
| 530 | argv_parser.add_argument("-c2", "--connection-handle2", |
| 531 | help="set a fake connection handle 2 to capture \ |
| 532 | audio dump.", dest="connection_handle2", type=int) |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 533 | argv_parser.add_argument("-ns", "--no-start", help="No audio 'Start' cmd is \ |
| 534 | needed before extracting audio data.", |
| 535 | dest="no_start", default="False") |
| 536 | argv_parser.add_argument("-dc", "--default-codec", help="set a default \ |
| 537 | codec.", dest="codec", default="G722") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 538 | argv_parser.add_argument("-a", "--attr-handle", |
| 539 | help="force to select audio control attr handle.", |
| 540 | dest="audio_control_attr_handle", type=int) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 541 | argv_parser.add_argument("-d", "--debug", |
| 542 | help="dump full debug buffer content.", |
| 543 | dest="full_debug", default="False") |
| 544 | argv_parser.add_argument("-sd", "--simple-debug", |
| 545 | help="dump debug buffer header content.", |
| 546 | dest="simple_debug", default="False") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 547 | arg = argv_parser.parse_args() |
| 548 | |
| 549 | if arg.folder is not None: |
| 550 | global folder |
| 551 | folder = arg.folder |
| 552 | |
| 553 | if arg.connection_handle1 is not None and arg.connection_handle2 is not None \ |
| 554 | and arg.connection_handle1 == arg.connection_handle2: |
| 555 | argv_parser.error("connection_handle1 can't be same with \ |
| 556 | connection_handle2") |
| 557 | exit(1) |
| 558 | |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 559 | if not (arg.no_start.lower() == "true" or arg.no_start.lower() == "false"): |
| 560 | argv_parser.error("-ns/--no-start arg is invalid, it should be true/false.") |
| 561 | exit(1) |
| 562 | |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 563 | if arg.connection_handle1 is not None: |
| 564 | fake_name = "ConnectionHandle" + str(arg.connection_handle1) |
| 565 | update_audio_data("", "", PEER_ADDRESS, fake_name) |
| 566 | update_audio_data(PEER_ADDRESS, fake_name, CONNECTION_HANDLE, |
| 567 | arg.connection_handle1) |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 568 | if arg.no_start.lower() == "true": |
| 569 | update_audio_data(PEER_ADDRESS, fake_name, START, True) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 570 | update_audio_data(PEER_ADDRESS, fake_name, TIMESTAMP_STR_FORMAT, "Unknown") |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 571 | update_audio_data(PEER_ADDRESS, fake_name, CODEC, arg.codec) |
| 572 | update_audio_data(PEER_ADDRESS, fake_name, SAMPLE_RATE, "Unknown") |
| 573 | update_audio_data(PEER_ADDRESS, fake_name, AUDIO_TYPE, "Unknown") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 574 | |
| 575 | if arg.connection_handle2 is not None: |
| 576 | fake_name = "ConnectionHandle" + str(arg.connection_handle2) |
| 577 | update_audio_data("", "", PEER_ADDRESS, fake_name) |
| 578 | update_audio_data(PEER_ADDRESS, fake_name, CONNECTION_HANDLE, |
| 579 | arg.connection_handle2) |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 580 | if arg.no_start.lower() == "true": |
| 581 | update_audio_data(PEER_ADDRESS, fake_name, START, True) |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 582 | update_audio_data(PEER_ADDRESS, fake_name, TIMESTAMP_STR_FORMAT, "Unknown") |
weichinweng | 5b58b1f | 2019-03-07 15:25:43 +0800 | [diff] [blame] | 583 | update_audio_data(PEER_ADDRESS, fake_name, CODEC, arg.codec) |
| 584 | update_audio_data(PEER_ADDRESS, fake_name, SAMPLE_RATE, "Unknown") |
| 585 | update_audio_data(PEER_ADDRESS, fake_name, AUDIO_TYPE, "Unknown") |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 586 | |
| 587 | if arg.audio_control_attr_handle is not None: |
| 588 | global force_audio_control_attr_handle |
| 589 | force_audio_control_attr_handle = arg.audio_control_attr_handle |
| 590 | |
weichinweng | 49c1444 | 2019-04-19 17:27:32 +0800 | [diff] [blame] | 591 | global full_debug |
| 592 | global simple_debug |
| 593 | if arg.full_debug.lower() == "true": |
| 594 | full_debug = True |
| 595 | simple_debug = True |
| 596 | elif arg.simple_debug.lower() == "true": |
| 597 | simple_debug = True |
| 598 | |
weichinweng | 9bbaa09 | 2018-12-20 15:03:37 +0800 | [diff] [blame] | 599 | if os.path.isfile(arg.BTSNOOP): |
| 600 | return arg.BTSNOOP |
| 601 | else: |
| 602 | argv_parser.error("BTSNOOP file not found: %s" % arg.BTSNOOP) |
| 603 | exit(1) |
| 604 | |
| 605 | |
| 606 | def main(): |
| 607 | btsnoop_file_name = set_config() |
| 608 | |
| 609 | with open(btsnoop_file_name, "rb") as btsnoop_file: |
| 610 | identification = btsnoop_file.read(8) |
| 611 | if identification != "btsnoop\0": |
| 612 | sys.stderr.write( |
| 613 | "Check identification fail. It is not correct btsnoop file.") |
| 614 | exit(1) |
| 615 | |
| 616 | ver, data_link = struct.unpack(">II", btsnoop_file.read(4 + 4)) |
| 617 | if (ver != 1) or (data_link != 1002): |
| 618 | sys.stderr.write( |
| 619 | "Check ver or dataLink fail. It is not correct btsnoop file.") |
| 620 | exit(1) |
| 621 | |
| 622 | while True: |
| 623 | if not parse_packet(btsnoop_file): |
| 624 | break |
| 625 | |
| 626 | for i in audio_data: |
| 627 | if audio_data[i].get(START, False): |
| 628 | dump_audio_data(audio_data[i]) |
| 629 | |
| 630 | |
| 631 | if __name__ == "__main__": |
| 632 | main() |