1 // Copyright 2014 The Chromium 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 /** 6 * @fileoverview Implements a low-level gnubby driver based on chrome.usb. 7 */ 8 'use strict'; 9 10 /** 11 * Low level gnubby 'driver'. One per physical USB device. 12 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated 13 * in. 14 * @param {!chrome.usb.ConnectionHandle} dev The device. 15 * @param {number} id The device's id. 16 * @param {number} inEndpoint The device's in endpoint. 17 * @param {number} outEndpoint The device's out endpoint. 18 * @constructor 19 * @implements {llGnubby} 20 */ 21 function llUsbGnubby(gnubbies, dev, id, inEndpoint, outEndpoint) { 22 /** @private {Gnubbies} */ 23 this.gnubbies_ = gnubbies; 24 this.dev = dev; 25 this.id = id; 26 this.inEndpoint = inEndpoint; 27 this.outEndpoint = outEndpoint; 28 this.txqueue = []; 29 this.clients = []; 30 this.lockCID = 0; // channel ID of client holding a lock, if != 0. 31 this.lockMillis = 0; // current lock period. 32 this.lockTID = null; // timer id of lock timeout. 33 this.closing = false; // device to be closed by receive loop. 34 this.updating = false; // device firmware is in final stage of updating. 35 this.inTransferPending = false; 36 this.outTransferPending = false; 37 } 38 39 /** 40 * Namespace for the llUsbGnubby implementation. 41 * @const 42 */ 43 llUsbGnubby.NAMESPACE = 'usb'; 44 45 /** Destroys this low-level device instance. */ 46 llUsbGnubby.prototype.destroy = function() { 47 if (!this.dev) return; // Already dead. 48 49 this.gnubbies_.removeOpenDevice( 50 {namespace: llUsbGnubby.NAMESPACE, device: this.id}); 51 this.closing = true; 52 53 console.log(UTIL_fmt('llUsbGnubby.destroy()')); 54 55 // Synthesize a close error frame to alert all clients, 56 // some of which might be in read state. 57 // 58 // Use magic CID 0 to address all. 59 this.publishFrame_(new Uint8Array([ 60 0, 0, 0, 0, // broadcast CID 61 llGnubby.CMD_ERROR, 62 0, 1, // length 63 llGnubby.GONE]).buffer); 64 65 // Set all clients to closed status and remove them. 66 while (this.clients.length != 0) { 67 var client = this.clients.shift(); 68 if (client) client.closed = true; 69 } 70 71 if (this.lockTID) { 72 window.clearTimeout(this.lockTID); 73 this.lockTID = null; 74 } 75 76 var dev = this.dev; 77 this.dev = null; 78 79 chrome.usb.releaseInterface(dev, 0, function() { 80 console.log(UTIL_fmt('Device ' + dev.handle + ' released')); 81 chrome.usb.closeDevice(dev, function() { 82 console.log(UTIL_fmt('Device ' + dev.handle + ' closed')); 83 }); 84 }); 85 }; 86 87 /** 88 * Push frame to all clients. 89 * @param {ArrayBuffer} f Data frame 90 * @private 91 */ 92 llUsbGnubby.prototype.publishFrame_ = function(f) { 93 var old = this.clients; 94 95 var remaining = []; 96 var changes = false; 97 for (var i = 0; i < old.length; ++i) { 98 var client = old[i]; 99 if (client.receivedFrame(f)) { 100 // Client still alive; keep on list. 101 remaining.push(client); 102 } else { 103 changes = true; 104 console.log(UTIL_fmt( 105 '[' + client.cid.toString(16) + '] left?')); 106 } 107 } 108 if (changes) this.clients = remaining; 109 }; 110 111 /** 112 * @return {boolean} whether this device is open and ready to use. 113 * @private 114 */ 115 llUsbGnubby.prototype.readyToUse_ = function() { 116 if (this.closing) return false; 117 if (!this.dev) return false; 118 119 return true; 120 }; 121 122 /** 123 * Reads one reply from the low-level device. 124 * @private 125 */ 126 llUsbGnubby.prototype.readOneReply_ = function() { 127 if (!this.readyToUse_()) return; // No point in continuing. 128 if (this.updating) return; // Do not bother waiting for final update reply. 129 130 var self = this; 131 132 function inTransferComplete(x) { 133 self.inTransferPending = false; 134 135 if (!self.readyToUse_()) return; // No point in continuing. 136 137 if (chrome.runtime.lastError) { 138 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError)); 139 console.log(chrome.runtime.lastError); 140 window.setTimeout(function() { self.destroy(); }, 0); 141 return; 142 } 143 144 if (x.data) { 145 var u8 = new Uint8Array(x.data); 146 console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8))); 147 148 self.publishFrame_(x.data); 149 150 // Write another pending request, if any. 151 window.setTimeout( 152 function() { 153 self.txqueue.shift(); // Drop sent frame from queue. 154 self.writeOneRequest_(); 155 }, 156 0); 157 } else { 158 console.log(UTIL_fmt('no x.data!')); 159 console.log(x); 160 window.setTimeout(function() { self.destroy(); }, 0); 161 } 162 } 163 164 if (this.inTransferPending == false) { 165 this.inTransferPending = true; 166 chrome.usb.bulkTransfer( 167 /** @type {!chrome.usb.ConnectionHandle} */(this.dev), 168 { direction: 'in', endpoint: this.inEndpoint, length: 2048 }, 169 inTransferComplete); 170 } else { 171 throw 'inTransferPending!'; 172 } 173 }; 174 175 /** 176 * Register a client for this gnubby. 177 * @param {*} who The client. 178 */ 179 llUsbGnubby.prototype.registerClient = function(who) { 180 for (var i = 0; i < this.clients.length; ++i) { 181 if (this.clients[i] === who) return; // Already registered. 182 } 183 this.clients.push(who); 184 }; 185 186 /** 187 * De-register a client. 188 * @param {*} who The client. 189 * @return {number} The number of remaining listeners for this device, or -1 190 * Returns number of remaining listeners for this device. 191 * if this had no clients to start with. 192 */ 193 llUsbGnubby.prototype.deregisterClient = function(who) { 194 var current = this.clients; 195 if (current.length == 0) return -1; 196 this.clients = []; 197 for (var i = 0; i < current.length; ++i) { 198 var client = current[i]; 199 if (client !== who) this.clients.push(client); 200 } 201 return this.clients.length; 202 }; 203 204 /** 205 * @param {*} who The client. 206 * @return {boolean} Whether this device has who as a client. 207 */ 208 llUsbGnubby.prototype.hasClient = function(who) { 209 if (this.clients.length == 0) return false; 210 for (var i = 0; i < this.clients.length; ++i) { 211 if (who === this.clients[i]) 212 return true; 213 } 214 return false; 215 }; 216 217 /** 218 * Stuff queued frames from txqueue[] to device, one by one. 219 * @private 220 */ 221 llUsbGnubby.prototype.writeOneRequest_ = function() { 222 if (!this.readyToUse_()) return; // No point in continuing. 223 224 if (this.txqueue.length == 0) return; // Nothing to send. 225 226 var frame = this.txqueue[0]; 227 228 var self = this; 229 function OutTransferComplete(x) { 230 self.outTransferPending = false; 231 232 if (!self.readyToUse_()) return; // No point in continuing. 233 234 if (chrome.runtime.lastError) { 235 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError)); 236 console.log(chrome.runtime.lastError); 237 window.setTimeout(function() { self.destroy(); }, 0); 238 return; 239 } 240 241 window.setTimeout(function() { self.readOneReply_(); }, 0); 242 }; 243 244 var u8 = new Uint8Array(frame); 245 console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8))); 246 247 if (this.outTransferPending == false) { 248 this.outTransferPending = true; 249 chrome.usb.bulkTransfer( 250 /** @type {!chrome.usb.ConnectionHandle} */(this.dev), 251 { direction: 'out', endpoint: this.outEndpoint, data: frame }, 252 OutTransferComplete); 253 } else { 254 throw 'outTransferPending!'; 255 } 256 }; 257 258 /** 259 * Check whether channel is locked for this request or not. 260 * @param {number} cid Channel id 261 * @param {number} cmd Command to be sent 262 * @return {boolean} true if not locked for this request. 263 * @private 264 */ 265 llUsbGnubby.prototype.checkLock_ = function(cid, cmd) { 266 if (this.lockCID) { 267 // We have an active lock. 268 if (this.lockCID != cid) { 269 // Some other channel has active lock. 270 271 if (cmd != llGnubby.CMD_SYNC) { 272 // Anything but SYNC gets an immediate busy. 273 var busy = new Uint8Array( 274 [(cid >> 24) & 255, 275 (cid >> 16) & 255, 276 (cid >> 8) & 255, 277 cid & 255, 278 llGnubby.CMD_ERROR, 279 0, 1, // length 280 llGnubby.BUSY]); 281 // Log the synthetic busy too. 282 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy))); 283 this.publishFrame_(busy.buffer); 284 return false; 285 } 286 287 // SYNC gets to go to the device to flush OS tx/rx queues. 288 // The usb firmware always responds to SYNC, regardless of lock status. 289 } 290 } 291 return true; 292 }; 293 294 /** 295 * Update or grab lock. 296 * @param {number} cid Channel id 297 * @param {number} cmd Command 298 * @param {number} arg Command argument 299 * @private 300 */ 301 llUsbGnubby.prototype.updateLock_ = function(cid, cmd, arg) { 302 if (this.lockCID == 0 || this.lockCID == cid) { 303 // It is this caller's or nobody's lock. 304 if (this.lockTID) { 305 window.clearTimeout(this.lockTID); 306 this.lockTID = null; 307 } 308 309 if (cmd == llGnubby.CMD_LOCK) { 310 var nseconds = arg; 311 if (nseconds != 0) { 312 this.lockCID = cid; 313 // Set tracking time to be .1 seconds longer than usb device does. 314 this.lockMillis = nseconds * 1000 + 100; 315 } else { 316 // Releasing lock voluntarily. 317 this.lockCID = 0; 318 } 319 } 320 321 // (re)set the lock timeout if we still hold it. 322 if (this.lockCID) { 323 var self = this; 324 this.lockTID = window.setTimeout( 325 function() { 326 console.warn(UTIL_fmt( 327 'lock for CID ' + cid.toString(16) + ' expired!')); 328 self.lockTID = null; 329 self.lockCID = 0; 330 }, 331 this.lockMillis); 332 } 333 } 334 }; 335 336 /** 337 * Queue command to be sent. 338 * If queue was empty, initiate the write. 339 * @param {number} cid The client's channel ID. 340 * @param {number} cmd The command to send. 341 * @param {ArrayBuffer|Uint8Array} data Command argument data 342 */ 343 llUsbGnubby.prototype.queueCommand = function(cid, cmd, data) { 344 if (!this.dev) return; 345 if (!this.checkLock_(cid, cmd)) return; 346 347 var u8 = new Uint8Array(data); 348 var frame = new Uint8Array(u8.length + 7); 349 350 frame[0] = cid >>> 24; 351 frame[1] = cid >>> 16; 352 frame[2] = cid >>> 8; 353 frame[3] = cid; 354 frame[4] = cmd; 355 frame[5] = (u8.length >> 8); 356 frame[6] = (u8.length & 255); 357 358 frame.set(u8, 7); 359 360 var lockArg = (u8.length > 0) ? u8[0] : 0; 361 this.updateLock_(cid, cmd, lockArg); 362 363 var wasEmpty = (this.txqueue.length == 0); 364 this.txqueue.push(frame.buffer); 365 if (wasEmpty) this.writeOneRequest_(); 366 }; 367 368 /** 369 * @param {function(Array)} cb Enumerate callback 370 */ 371 llUsbGnubby.enumerate = function(cb) { 372 chrome.usb.getDevices({'vendorId': 4176, 'productId': 529}, cb); 373 }; 374 375 /** 376 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated 377 * in. 378 * @param {number} which The index of the device to open. 379 * @param {!chrome.usb.Device} dev The device to open. 380 * @param {function(number, llGnubby=)} cb Called back with the 381 * result of opening the device. 382 */ 383 llUsbGnubby.open = function(gnubbies, which, dev, cb) { 384 /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */ 385 function deviceOpened(handle) { 386 if (!handle) { 387 console.warn(UTIL_fmt('failed to open device. permissions issue?')); 388 cb(-llGnubby.NODEVICE); 389 return; 390 } 391 var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle); 392 chrome.usb.listInterfaces(nonNullHandle, function(descriptors) { 393 var inEndpoint, outEndpoint; 394 for (var i = 0; i < descriptors.length; i++) { 395 var descriptor = descriptors[i]; 396 for (var j = 0; j < descriptor.endpoints.length; j++) { 397 var endpoint = descriptor.endpoints[j]; 398 if (inEndpoint == undefined && endpoint.type == 'bulk' && 399 endpoint.direction == 'in') { 400 inEndpoint = endpoint.address; 401 } 402 if (outEndpoint == undefined && endpoint.type == 'bulk' && 403 endpoint.direction == 'out') { 404 outEndpoint = endpoint.address; 405 } 406 } 407 } 408 if (inEndpoint == undefined || outEndpoint == undefined) { 409 console.warn(UTIL_fmt('device lacking an endpoint (broken?)')); 410 chrome.usb.closeDevice(nonNullHandle); 411 cb(-llGnubby.NODEVICE); 412 return; 413 } 414 // Try getting it claimed now. 415 chrome.usb.claimInterface(nonNullHandle, 0, function() { 416 if (chrome.runtime.lastError) { 417 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError)); 418 console.log(chrome.runtime.lastError); 419 } 420 var claimed = !chrome.runtime.lastError; 421 if (!claimed) { 422 console.warn(UTIL_fmt('failed to claim interface. busy?')); 423 // Claim failed? Let the callers know and bail out. 424 chrome.usb.closeDevice(nonNullHandle); 425 cb(-llGnubby.BUSY); 426 return; 427 } 428 var gnubby = new llUsbGnubby(gnubbies, nonNullHandle, which, inEndpoint, 429 outEndpoint); 430 cb(-llGnubby.OK, gnubby); 431 }); 432 }); 433 } 434 435 if (llUsbGnubby.runningOnCrOS === undefined) { 436 llUsbGnubby.runningOnCrOS = 437 (window.navigator.appVersion.indexOf('; CrOS ') != -1); 438 } 439 if (llUsbGnubby.runningOnCrOS) { 440 chrome.usb.requestAccess(dev, 0, function(success) { 441 // Even though the argument to requestAccess is a chrome.usb.Device, the 442 // access request is for access to all devices with the same vid/pid. 443 // Curiously, if the first chrome.usb.requestAccess succeeds, a second 444 // call with a separate device with the same vid/pid fails. Since 445 // chrome.usb.openDevice will fail if a previous access request really 446 // failed, just ignore the outcome of the access request and move along. 447 chrome.usb.openDevice(dev, deviceOpened); 448 }); 449 } else { 450 chrome.usb.openDevice(dev, deviceOpened); 451 } 452 }; 453 454 /** 455 * @param {*} dev Chrome usb device 456 * @return {llGnubbyDeviceId} A device identifier for the device. 457 */ 458 llUsbGnubby.deviceToDeviceId = function(dev) { 459 var usbDev = /** @type {!chrome.usb.Device} */ (dev); 460 var deviceId = { namespace: llUsbGnubby.NAMESPACE, device: usbDev.device }; 461 return deviceId; 462 }; 463 464 /** 465 * Registers this implementation with gnubbies. 466 * @param {Gnubbies} gnubbies Gnubbies singleton instance 467 */ 468 llUsbGnubby.register = function(gnubbies) { 469 var USB_GNUBBY_IMPL = { 470 enumerate: llUsbGnubby.enumerate, 471 deviceToDeviceId: llUsbGnubby.deviceToDeviceId, 472 open: llUsbGnubby.open 473 }; 474 gnubbies.registerNamespace(llUsbGnubby.NAMESPACE, USB_GNUBBY_IMPL); 475 }; 476