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