• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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