Home | History | Annotate | Download | only in mbim_compliance
      1 # Copyright 2015 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 import Queue
      7 import signal
      8 import struct
      9 import time
     10 import numpy
     11 
     12 from collections import namedtuple
     13 from usb import core
     14 
     15 import common
     16 from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors
     17 
     18 
     19 USBNotificationPacket = namedtuple(
     20         'USBNotificationPacket',
     21         ['bmRequestType', 'bNotificationCode', 'wValue', 'wIndex',
     22          'wLength'])
     23 
     24 
     25 class MBIMChannelEndpoint(object):
     26     """
     27     An object dedicated to interacting with the MBIM capable USB device.
     28 
     29     This object interacts with the USB devies in a forever loop, servicing
     30     command requests from |MBIMChannel| as well as surfacing any notifications
     31     from the modem.
     32 
     33     """
     34     USB_PACKET_HEADER_FORMAT = '<BBHHH'
     35     # Sleeping for 0 seconds *may* hint for the schedular to relinquish CPU.
     36     QUIET_TIME_MS = 0
     37     INTERRUPT_READ_TIMEOUT_MS = 1  # We don't really want to wait.
     38     GET_ENCAPSULATED_RESPONSE_TIMEOUT_MS = 50
     39     SEND_ENCAPSULATED_REQUEST_TIMEOUT_MS = 50
     40     GET_ENCAPSULATED_RESPONSE_ARGS = {
     41             'bmRequestType' : 0b10100001,
     42             'bRequest' : 0b00000001,
     43             'wValue' : 0x0000}
     44     SEND_ENCAPSULATED_COMMAND_ARGS = {
     45             'bmRequestType' : 0b00100001,
     46             'bRequest' : 0b00000000,
     47             'wValue' : 0x0000}
     48 
     49     def __init__(self,
     50                  device,
     51                  interface_number,
     52                  interrupt_endpoint_address,
     53                  in_buffer_size,
     54                  request_queue,
     55                  response_queue,
     56                  stop_request_event,
     57                  strict=True):
     58         """
     59         @param device: Device handle returned by PyUSB for the modem to test.
     60         @param interface_number: |bInterfaceNumber| of the MBIM interface.
     61         @param interrupt_endpoint_address: |bEndpointAddress| for the usb
     62                 INTERRUPT IN endpoint for notifications.
     63         @param in_buffer_size: The (fixed) buffer size to use for in control
     64                 transfers.
     65         @param request_queue: A process safe queue where we expect commands
     66                 to send be be enqueued.
     67         @param response_queue: A process safe queue where we enqueue
     68                 non-notification responses from the device.
     69         @param strict: In strict mode (default), any unexpected error causes an
     70                 abort. Otherwise, we merely warn.
     71 
     72         """
     73         self._device = device
     74         self._interface_number = interface_number
     75         self._interrupt_endpoint_address = interrupt_endpoint_address
     76         self._in_buffer_size = in_buffer_size
     77         self._request_queue = request_queue
     78         self._response_queue = response_queue
     79         self._stop_requested = stop_request_event
     80         self._strict = strict
     81 
     82         self._num_outstanding_responses = 0
     83         self._response_available_packet = USBNotificationPacket(
     84                 bmRequestType=0b10100001,
     85                 bNotificationCode=0b00000001,
     86                 wValue=0x0000,
     87                 wIndex=self._interface_number,
     88                 wLength=0x0000)
     89 
     90         # SIGINT recieved by the parent process is forwarded to this process.
     91         # Exit graciously when that happens.
     92         signal.signal(signal.SIGINT,
     93                       lambda signum, frame: self._stop_requested.set())
     94         self.start()
     95 
     96 
     97     def start(self):
     98         """ Start the busy-loop that periodically interacts with the modem. """
     99         while not self._stop_requested.is_set():
    100             try:
    101                 self._tick()
    102             except mbim_errors.MBIMComplianceChannelError as e:
    103                 if self._strict:
    104                     raise
    105 
    106             time.sleep(self.QUIET_TIME_MS / 1000)
    107 
    108 
    109     def _tick(self):
    110         """ Work done in one time slice. """
    111         self._check_response()
    112         response = self._get_response()
    113         self._check_response()
    114         if response is not None:
    115             try:
    116                 self._response_queue.put_nowait(response)
    117             except Queue.Full:
    118                 mbim_errors.log_and_raise(
    119                         mbim_errors.MBIMComplianceChannelError,
    120                         'Response queue full.')
    121 
    122         self._check_response()
    123         try:
    124             request = self._request_queue.get_nowait()
    125             if request:
    126                 self._send_request(request)
    127         except Queue.Empty:
    128             pass
    129 
    130         self._check_response()
    131 
    132 
    133     def _check_response(self):
    134         """
    135         Check if there is a response available.
    136 
    137         If a response is available, increment |outstanding_responses|.
    138 
    139         This method is kept separate from |_get_response| because interrupts are
    140         time critical. A separate method underscores this point. It also opens
    141         up the possibility of giving this method higher priority wherever
    142         possible.
    143 
    144         """
    145         try:
    146             in_data = self._device.read(
    147                     self._interrupt_endpoint_address,
    148                     struct.calcsize(self.USB_PACKET_HEADER_FORMAT),
    149                     self._interface_number,
    150                     self.INTERRUPT_READ_TIMEOUT_MS)
    151         except core.USBError:
    152             # If there is no response available, the modem will response with
    153             # STALL messages, and pyusb will raise an exception.
    154             return
    155 
    156         if len(in_data) != struct.calcsize(self.USB_PACKET_HEADER_FORMAT):
    157             mbim_errors.log_and_raise(
    158                     mbim_errors.MBIMComplianceChannelError,
    159                     'Received unexpected notification (%s) of length %d.' %
    160                     (in_data, len(in_data)))
    161 
    162         in_packet = USBNotificationPacket(
    163                 *struct.unpack(self.USB_PACKET_HEADER_FORMAT, in_data))
    164         if in_packet != self._response_available_packet:
    165             mbim_errors.log_and_raise(
    166                     mbim_errors.MBIMComplianceChannelError,
    167                     'Received unexpected notification (%s).' % in_data)
    168 
    169         self._num_outstanding_responses += 1
    170 
    171 
    172     def _get_response(self):
    173         """
    174         Get the outstanding response from the device.
    175 
    176         @returns: The MBIM payload, if any. None otherwise.
    177 
    178         """
    179         if self._num_outstanding_responses == 0:
    180             return None
    181 
    182         # We count all failed cases also as an attempt.
    183         self._num_outstanding_responses -= 1
    184         response = self._device.ctrl_transfer(
    185                 wIndex=self._interface_number,
    186                 data_or_wLength=self._in_buffer_size,
    187                 timeout=self.GET_ENCAPSULATED_RESPONSE_TIMEOUT_MS,
    188                 **self.GET_ENCAPSULATED_RESPONSE_ARGS)
    189         numpy.set_printoptions(formatter={'int':lambda x: hex(int(x))},
    190                                linewidth=1000)
    191         logging.debug('Control Channel: Received %d bytes response. Payload:%s',
    192                       len(response), numpy.array(response))
    193         return response
    194 
    195 
    196     def _send_request(self, payload):
    197         """
    198         Send payload (one fragment) down to the device.
    199 
    200         @raises MBIMComplianceGenericError if the complete |payload| could not
    201                 be sent.
    202 
    203         """
    204         actual_written = self._device.ctrl_transfer(
    205                 wIndex=self._interface_number,
    206                 data_or_wLength=payload,
    207                 timeout=self.SEND_ENCAPSULATED_REQUEST_TIMEOUT_MS,
    208                 **self.SEND_ENCAPSULATED_COMMAND_ARGS)
    209         numpy.set_printoptions(formatter={'int':lambda x: hex(int(x))},
    210                                linewidth=1000)
    211         logging.debug('Control Channel: Sent %d bytes out of %d bytes '
    212                       'requested. Payload:%s',
    213                       actual_written, len(payload), numpy.array(payload))
    214         if actual_written < len(payload):
    215             mbim_errors.log_and_raise(
    216                     mbim_errors.MBIMComplianceGenericError,
    217                     'Could not send the complete packet (%d/%d bytes sent)' %
    218                     actual_written, len(payload))
    219