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