1# Lint as: python2, python3 2# Copyright 2015 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5""" 6MBIM Data transfer module is responsible for generating valid MBIM NTB frames 7from IP packets and for extracting IP packets from received MBIM NTB frames. 8 9""" 10from __future__ import absolute_import 11from __future__ import division 12from __future__ import print_function 13 14import array 15import struct 16from collections import namedtuple 17 18import six 19 20from six.moves import range 21from six.moves import zip 22 23from autotest_lib.client.cros.cellular.mbim_compliance import mbim_constants 24from autotest_lib.client.cros.cellular.mbim_compliance import mbim_data_channel 25from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors 26 27NTH_SIGNATURE_32 = 0x686D636E # "ncmh" 28NDP_SIGNATURE_IPS_32 = 0x00737069 # "ips0" 29NDP_SIGNATURE_DSS_32 = 0x00737364 # "dss0" 30 31NTH_SIGNATURE_16 = 0x484D434E # "NCMH" 32NDP_SIGNATURE_IPS_16 = 0x00535049 # "IPS0" 33NDP_SIGNATURE_DSS_16 = 0x00535344 # "DSS0" 34 35class MBIMDataTransfer(object): 36 """ 37 MBIMDataTransfer class is the public interface for any data transfer 38 from/to the device via the MBIM data endpoints (BULK-IN/BULK-OUT). 39 40 The class encapsulates the MBIM NTB frame generation/parsing as well as 41 sending the the NTB frames to the device and vice versa. 42 Users are expected to: 43 1. Initialize the channel data transfer module by providing a valid 44 device context which holds all the required info regarding the devie under 45 test. 46 2. Use send_data_packets to send IP packets to the device. 47 3. Use receive_data_packets to receive IP packets from the device. 48 49 """ 50 def __init__(self, device_context): 51 """ 52 Initialize the Data Transfer object. The data transfer object 53 instantiates the data channel to prepare for any data transfer from/to 54 the device using the bulk pipes. 55 56 @params device_context: The device context which contains all the USB 57 descriptors, NTB params and USB handle to the device. 58 59 """ 60 self._device_context = device_context 61 mbim_data_interface = ( 62 device_context.descriptor_cache.mbim_data_interface) 63 bulk_in_endpoint = ( 64 device_context.descriptor_cache.bulk_in_endpoint) 65 bulk_out_endpoint = ( 66 device_context.descriptor_cache.bulk_out_endpoint) 67 self._data_channel = mbim_data_channel.MBIMDataChannel( 68 device=device_context.device, 69 data_interface_number=mbim_data_interface.bInterfaceNumber, 70 bulk_in_endpoint_address=bulk_in_endpoint.bEndpointAddress, 71 bulk_out_endpoint_address=bulk_out_endpoint.bEndpointAddress, 72 max_in_buffer_size=device_context.max_in_data_transfer_size) 73 74 75 def send_data_packets(self, ntb_format, data_packets): 76 """ 77 Creates an MBIM frame for the payload provided and sends it out to the 78 device using bulk out pipe. 79 80 @param ntb_format: Whether to send an NTB16 or NTB32 frame. 81 @param data_packets: Array of data packets. Each packet is a byte array 82 corresponding to the IP packet or any other payload to be sent. 83 84 """ 85 ntb_object = MBIMNtb(ntb_format) 86 ntb_frame = ntb_object.generate_ntb( 87 data_packets, 88 self._device_context.max_out_data_transfer_size, 89 self._device_context.out_data_transfer_divisor, 90 self._device_context.out_data_transfer_payload_remainder, 91 self._device_context.out_data_transfer_ndp_alignment) 92 self._data_channel.send_ntb(ntb_frame) 93 94 95 def receive_data_packets(self, ntb_format): 96 """ 97 Receives an MBIM frame from the device using the bulk in pipe, 98 deaggregates the payload from the frame and returns it to the caller. 99 100 Will return an empty tuple, if no frame is received from the device. 101 102 @param ntb_format: Whether to receive an NTB16 or NTB32 frame. 103 @returns tuple of (nth, ndp, ndp_entries, payload) where, 104 nth - NTH header object received. 105 ndp - NDP header object received. 106 ndp_entries - Array of NDP entry header objects. 107 payload - Array of packets where each packet is a byte array. 108 109 """ 110 ntb_frame = self._data_channel.receive_ntb() 111 if not ntb_frame: 112 return () 113 ntb_object = MBIMNtb(ntb_format) 114 return ntb_object.parse_ntb(ntb_frame) 115 116 117class MBIMNtb(object): 118 """ 119 MBIM NTB class used for MBIM data transfer. 120 121 This class is used to generate/parse NTB frames. 122 123 Limitations: 124 1. We currently only support a single NDP frame within an NTB. 125 2. We only support IP data payload. This can be overcome by using the DSS 126 (instead of IPS) prefix in NDP signature if required. 127 128 """ 129 _NEXT_SEQUENCE_NUMBER = 0 130 131 def __init__(self, ntb_format): 132 """ 133 Initialization of the NTB object. 134 135 We assign the appropriate header classes required based on whether 136 we are going to work with NTB16 or NTB32 data frames. 137 138 @param ntb_format: Type of NTB: 16 vs 32 139 140 """ 141 self._ntb_format = ntb_format 142 # Defining the tuples to be used for the headers. 143 if ntb_format == mbim_constants.NTB_FORMAT_16: 144 self._nth_class = Nth16 145 self._ndp_class = Ndp16 146 self._ndp_entry_class = NdpEntry16 147 self._nth_signature = NTH_SIGNATURE_16 148 self._ndp_signature = NDP_SIGNATURE_IPS_16 149 else: 150 self._nth_class = Nth32 151 self._ndp_class = Ndp32 152 self._ndp_entry_class = NdpEntry32 153 self._nth_signature = NTH_SIGNATURE_32 154 self._ndp_signature = NDP_SIGNATURE_IPS_32 155 156 157 @classmethod 158 def get_next_sequence_number(cls): 159 """ 160 Returns incrementing sequence numbers on successive calls. We start 161 the sequence numbering at 0. 162 163 @returns The sequence number for data transfers. 164 165 """ 166 # Make sure to rollover the 16 bit sequence number. 167 if MBIMNtb._NEXT_SEQUENCE_NUMBER > (0xFFFF - 2): 168 MBIMNtb._NEXT_SEQUENCE_NUMBER = 0x0000 169 sequence_number = MBIMNtb._NEXT_SEQUENCE_NUMBER 170 MBIMNtb._NEXT_SEQUENCE_NUMBER += 1 171 return sequence_number 172 173 174 @classmethod 175 def reset_sequence_number(cls): 176 """ 177 Resets the sequence number to be used for NTB's sent from host. This 178 has to be done every time the device is reset. 179 180 """ 181 cls._NEXT_SEQUENCE_NUMBER = 0x00000000 182 183 184 def get_next_payload_offset(self, 185 current_offset, 186 ntb_divisor, 187 ntb_payload_remainder): 188 """ 189 Helper function to find the offset to place the next payload 190 191 Alignment of payloads follow this formula: 192 Offset % ntb_divisor == ntb_payload_remainder. 193 194 @params current_offset: Current index offset in the frame. 195 @param ntb_divisor: Used for payload alignment within the frame. 196 @param ntb_payload_remainder: Used for payload alignment within the 197 frame. 198 @returns offset to place the next payload at. 199 200 """ 201 next_payload_offset = ((( 202 (current_offset + 203 (ntb_divisor - 1)) // ntb_divisor) * ntb_divisor) + 204 ntb_payload_remainder) 205 return next_payload_offset 206 207 208 def generate_ntb(self, 209 payload, 210 max_ntb_size, 211 ntb_divisor, 212 ntb_payload_remainder, 213 ntb_ndp_alignment): 214 """ 215 This function generates an NTB frame out of the payload provided. 216 217 @param payload: Array of packets to sent to the device. Each packet 218 contains the raw byte array of IP packet to be sent. 219 @param max_ntb_size: Max size of NTB frame supported by the device. 220 @param ntb_divisor: Used for payload alignment within the frame. 221 @param ntb_payload_remainder: Used for payload alignment within the 222 frame. 223 @param ntb_ndp_alignment : Used for NDP header alignment within the 224 frame. 225 @raises MBIMComplianceNtbError if the complete |ntb| can not fit into 226 |max_ntb_size|. 227 @returns the raw MBIM NTB byte array. 228 229 """ 230 cls = self.__class__ 231 232 # We start with the NTH header, then the payload and then finally 233 # the NDP header and the associated NDP entries. 234 ntb_curr_offset = self._nth_class.get_struct_len() 235 num_packets = len(payload) 236 nth_length = self._nth_class.get_struct_len() 237 ndp_length = self._ndp_class.get_struct_len() 238 # We need one extra ZLP NDP entry at the end, so account for it. 239 ndp_entries_length = ( 240 self._ndp_entry_class.get_struct_len() * (num_packets + 1)) 241 242 # Create the NDP header and an NDP_ENTRY header for each packet. 243 # We can create the NTH header only after we calculate the total length. 244 self.ndp = self._ndp_class( 245 signature=self._ndp_signature, 246 length=ndp_length+ndp_entries_length, 247 next_ndp_index=0) 248 self.ndp_entries = [] 249 250 # We'll also construct the payload raw data as we loop thru the packets. 251 # The padding in between the payload is added in place. 252 raw_ntb_frame_payload = array.array('B', []) 253 for packet in payload: 254 offset = self.get_next_payload_offset( 255 ntb_curr_offset, ntb_divisor, ntb_payload_remainder) 256 align_length = offset - ntb_curr_offset 257 length = len(packet) 258 # Add align zeroes, then payload, then pad zeroes 259 raw_ntb_frame_payload += array.array('B', [0] * align_length) 260 raw_ntb_frame_payload += packet 261 self.ndp_entries.append(self._ndp_entry_class( 262 datagram_index=offset, datagram_length=length)) 263 ntb_curr_offset = offset + length 264 265 # Add the ZLP entry 266 self.ndp_entries.append(self._ndp_entry_class( 267 datagram_index=0, datagram_length=0)) 268 269 # Store the NDP offset to be used in creating NTH header. 270 # NDP alignment is specified by the device with a minimum of 4 and it 271 # always a multiple of 2. 272 ndp_align_mask = ntb_ndp_alignment - 1 273 if ntb_curr_offset & ndp_align_mask: 274 pad_length = ntb_ndp_alignment - (ntb_curr_offset & ndp_align_mask) 275 raw_ntb_frame_payload += array.array('B', [0] * pad_length) 276 ntb_curr_offset += pad_length 277 ndp_offset = ntb_curr_offset 278 ntb_curr_offset += ndp_length 279 ntb_curr_offset += ndp_entries_length 280 if ntb_curr_offset > max_ntb_size: 281 mbim_errors.log_and_raise( 282 mbim_errors.MBIMComplianceNtbError, 283 'Could not fit the complete NTB of size %d into %d bytes' % 284 ntb_curr_offset, max_ntb_size) 285 # Now create the NTH header 286 self.nth = self._nth_class( 287 signature=self._nth_signature, 288 header_length=nth_length, 289 sequence_number=cls.get_next_sequence_number(), 290 block_length=ntb_curr_offset, 291 fp_index=ndp_offset) 292 293 # Create the raw bytes now, we create the raw bytes of the header and 294 # attach it to the payload raw bytes with padding already created above. 295 raw_ntb_frame = array.array('B', []) 296 raw_ntb_frame += array.array('B', self.nth.pack()) 297 raw_ntb_frame += raw_ntb_frame_payload 298 raw_ntb_frame += array.array('B', self.ndp.pack()) 299 for entry in self.ndp_entries: 300 raw_ntb_frame += array.array('B', entry.pack()) 301 302 self.payload = payload 303 self.raw_ntb_frame = raw_ntb_frame 304 305 return raw_ntb_frame 306 307 308 def parse_ntb(self, raw_ntb_frame): 309 """ 310 This function parses an NTB frame and returns the NTH header, NDP header 311 and the payload parsed which can be used to inspect the response 312 from the device. 313 314 @param raw_ntb_frame: Array of bytes of an MBIM NTB frame. 315 @raises MBIMComplianceNtbError if there is an error in parsing. 316 @returns tuple of (nth, ndp, ndp_entries, payload) where, 317 nth - NTH header object received. 318 ndp - NDP header object received. 319 ndp_entries - Array of NDP entry header objects. 320 payload - Array of packets where each packet is a byte array. 321 322 """ 323 # Read the nth header to find the ndp header index 324 self.nth = self._nth_class(raw_data=raw_ntb_frame) 325 ndp_offset = self.nth.fp_index 326 # Verify the total length field 327 if len(raw_ntb_frame) != self.nth.block_length: 328 mbim_errors.log_and_raise( 329 mbim_errors.MBIMComplianceNtbError, 330 'NTB size mismatch Total length: %x Reported: %x bytes' % ( 331 len(raw_ntb_frame), self.nth.block_length)) 332 333 # Read the NDP header to find the number of packets in the entry 334 self.ndp = self._ndp_class(raw_data=raw_ntb_frame[ndp_offset:]) 335 num_ndp_entries = ( 336 (self.ndp.length - self._ndp_class.get_struct_len()) // 337 self._ndp_entry_class.get_struct_len()) 338 ndp_entries_offset = ndp_offset + self._ndp_class.get_struct_len() 339 self.payload = [] 340 self.ndp_entries = [] 341 for _ in range(0, num_ndp_entries): 342 ndp_entry = self._ndp_entry_class( 343 raw_data=raw_ntb_frame[ndp_entries_offset:]) 344 ndp_entries_offset += self._ndp_entry_class.get_struct_len() 345 packet_start_offset = ndp_entry.datagram_index 346 packet_end_offset = ( 347 ndp_entry.datagram_index + ndp_entry.datagram_length) 348 # There is one extra ZLP NDP entry at the end, so account for it. 349 if ndp_entry.datagram_index and ndp_entry.datagram_length: 350 packet = array.array('B', raw_ntb_frame[packet_start_offset: 351 packet_end_offset]) 352 self.payload.append(packet) 353 self.ndp_entries.append(ndp_entry) 354 355 self.raw_ntb_frame = raw_ntb_frame 356 357 return (self.nth, self.ndp, self.ndp_entries, self.payload) 358 359 360def header_class_new(cls, **kwargs): 361 """ 362 Creates a header instance with either the given field name/value 363 pairs or raw data buffer. 364 365 @param kwargs: Dictionary of (field_name, field_value) pairs or 366 raw_data=Packed binary array. 367 @returns New header object created. 368 369 """ 370 field_values = [] 371 if 'raw_data' in kwargs and kwargs['raw_data']: 372 raw_data = kwargs['raw_data'] 373 data_format = cls.get_field_format_string() 374 unpack_length = cls.get_struct_len() 375 data_length = len(raw_data) 376 if data_length < unpack_length: 377 mbim_errors.log_and_raise( 378 mbim_errors.MBIMComplianceDataTransferError, 379 'Length of Data (%d) to be parsed less than header' 380 ' structure length (%d)' % 381 (data_length, unpack_length)) 382 field_values = struct.unpack_from(data_format, raw_data) 383 else: 384 field_names = cls.get_field_names() 385 for field_name in field_names: 386 if field_name not in kwargs: 387 field_value = 0 388 field_values.append(field_value) 389 else: 390 field_values.append(kwargs.pop(field_name)) 391 if kwargs: 392 mbim_errors.log_and_raise( 393 mbim_errors.MBIMComplianceDataTransferError, 394 'Unexpected fields (%s) in %s' % ( 395 list(kwargs.keys()), cls.__name__)) 396 obj = super(cls, cls).__new__(cls, *field_values) 397 return obj 398 399 400class MBIMNtbHeadersMeta(type): 401 """ 402 Metaclass for all the NTB headers. This is relatively toned down metaclass 403 to create namedtuples out of the header fields. 404 405 Header definition attributes: 406 _FIELDS: Used to define structure elements. Each element contains a format 407 specifier and the field name. 408 409 """ 410 def __new__(mcs, name, bases, attrs): 411 if object in bases: 412 return super(MBIMNtbHeadersMeta, mcs).__new__( 413 mcs, name, bases, attrs) 414 fields = attrs['_FIELDS'] 415 if not fields: 416 mbim_errors.log_and_raise( 417 mbim_errors.MBIMComplianceDataTransfer, 418 '%s header must have some fields defined' % name) 419 _, field_names = list(zip(*fields)) 420 attrs['__new__'] = header_class_new 421 header_class = namedtuple(name, field_names) 422 # Prepend the class created via namedtuple to |bases| in order to 423 # correctly resolve the __new__ method while preserving the class 424 # hierarchy. 425 cls = super(MBIMNtbHeadersMeta, mcs).__new__( 426 mcs, name, (header_class,) + bases, attrs) 427 return cls 428 429 430class MBIMNtbHeaders(six.with_metaclass(MBIMNtbHeadersMeta, object)): 431 """ 432 Base class for all NTB headers. 433 434 This class should not be instantiated on it's own. 435 436 The base class overrides namedtuple's __new__ to: 437 1. Create a tuple out of raw object. 438 2. Put value of zero for fields which are not specified by the caller, 439 For ex: reserved fields 440 441 """ 442 443 @classmethod 444 def get_fields(cls): 445 """ 446 Helper function to find all the fields of this class. 447 448 @returns Fields of the structure. 449 450 """ 451 return cls._FIELDS 452 453 454 @classmethod 455 def get_field_names(cls): 456 """ 457 Helper function to return the field names of the header. 458 459 @returns The field names of the header structure. 460 461 """ 462 _, field_names = list(zip(*cls.get_fields())) 463 return field_names 464 465 466 @classmethod 467 def get_field_formats(cls): 468 """ 469 Helper function to return the field formats of the header. 470 471 @returns The format of fields of the header structure. 472 473 """ 474 field_formats, _ = list(zip(*cls.get_fields())) 475 return field_formats 476 477 478 @classmethod 479 def get_field_format_string(cls): 480 """ 481 Helper function to return the field format string of the header. 482 483 @returns The format string of the header structure. 484 485 """ 486 format_string = '<' + ''.join(cls.get_field_formats()) 487 return format_string 488 489 490 @classmethod 491 def get_struct_len(cls): 492 """ 493 Returns the length of the structure representing the header. 494 495 @returns Length of the structure. 496 497 """ 498 return struct.calcsize(cls.get_field_format_string()) 499 500 501 def pack(self): 502 """ 503 Packs a header based on the field format specified. 504 505 @returns The packet in binary array form. 506 507 """ 508 cls = self.__class__ 509 field_names = cls.get_field_names() 510 format_string = cls.get_field_format_string() 511 field_values = [getattr(self, name) for name in field_names] 512 return array.array('B', struct.pack(format_string, *field_values)) 513 514 515class Nth16(MBIMNtbHeaders): 516 """ The class for MBIM NTH16 objects. """ 517 _FIELDS = (('I', 'signature'), 518 ('H', 'header_length'), 519 ('H', 'sequence_number'), 520 ('H', 'block_length'), 521 ('H', 'fp_index')) 522 523 524class Ndp16(MBIMNtbHeaders): 525 """ The class for MBIM NDP16 objects. """ 526 _FIELDS = (('I', 'signature'), 527 ('H', 'length'), 528 ('H', 'next_ndp_index')) 529 530 531class NdpEntry16(MBIMNtbHeaders): 532 """ The class for MBIM NDP16 objects. """ 533 _FIELDS = (('H', 'datagram_index'), 534 ('H', 'datagram_length')) 535 536 537class Nth32(MBIMNtbHeaders): 538 """ The class for MBIM NTH32 objects. """ 539 _FIELDS = (('I', 'signature'), 540 ('H', 'header_length'), 541 ('H', 'sequence_number'), 542 ('I', 'block_length'), 543 ('I', 'fp_index')) 544 545 546class Ndp32(MBIMNtbHeaders): 547 """ The class for MBIM NTH32 objects. """ 548 _FIELDS = (('I', 'signature'), 549 ('H', 'length'), 550 ('H', 'reserved_6'), 551 ('I', 'next_ndp_index'), 552 ('I', 'reserved_12')) 553 554 555class NdpEntry32(MBIMNtbHeaders): 556 """ The class for MBIM NTH32 objects. """ 557 _FIELDS = (('I', 'datagram_index'), 558 ('I', 'datagram_length')) 559