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