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 Queue
7import signal
8import struct
9import time
10import numpy
11
12from collections import namedtuple
13from usb import core
14
15import common
16from autotest_lib.client.cros.cellular.mbim_compliance import mbim_errors
17
18
19USBNotificationPacket = namedtuple(
20        'USBNotificationPacket',
21        ['bmRequestType', 'bNotificationCode', 'wValue', 'wIndex',
22         'wLength'])
23
24
25class 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