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.hid. 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.hid.HidConnectInfo} dev The connection to the device. 15 * @param {number} id The device's id. 16 * @constructor 17 * @implements {GnubbyDevice} 18 */ 19function HidGnubbyDevice(gnubbies, dev, id) { 20 /** @private {Gnubbies} */ 21 this.gnubbies_ = gnubbies; 22 this.dev = dev; 23 this.id = id; 24 this.txqueue = []; 25 this.clients = []; 26 this.lockCID = 0; // channel ID of client holding a lock, if != 0. 27 this.lockMillis = 0; // current lock period. 28 this.lockTID = null; // timer id of lock timeout. 29 this.closing = false; // device to be closed by receive loop. 30 this.updating = false; // device firmware is in final stage of updating. 31} 32 33/** 34 * Namespace for the HidGnubbyDevice implementation. 35 * @const 36 */ 37HidGnubbyDevice.NAMESPACE = 'hid'; 38 39/** Destroys this low-level device instance. */ 40HidGnubbyDevice.prototype.destroy = function() { 41 if (!this.dev) return; // Already dead. 42 43 this.gnubbies_.removeOpenDevice( 44 {namespace: HidGnubbyDevice.NAMESPACE, device: this.id}); 45 this.closing = true; 46 47 console.log(UTIL_fmt('HidGnubbyDevice.destroy()')); 48 49 // Synthesize a close error frame to alert all clients, 50 // some of which might be in read state. 51 // 52 // Use magic CID 0 to address all. 53 this.publishFrame_(new Uint8Array([ 54 0, 0, 0, 0, // broadcast CID 55 GnubbyDevice.CMD_ERROR, 56 0, 1, // length 57 GnubbyDevice.GONE]).buffer); 58 59 // Set all clients to closed status and remove them. 60 while (this.clients.length != 0) { 61 var client = this.clients.shift(); 62 if (client) client.closed = true; 63 } 64 65 if (this.lockTID) { 66 window.clearTimeout(this.lockTID); 67 this.lockTID = null; 68 } 69 70 var dev = this.dev; 71 this.dev = null; 72 73 chrome.hid.disconnect(dev.connectionId, function() { 74 if (chrome.runtime.lastError) { 75 console.warn(UTIL_fmt('Device ' + dev.connectionId + 76 ' couldn\'t be disconnected:')); 77 console.warn(chrome.runtime.lastError); 78 return; 79 } 80 console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed')); 81 }); 82}; 83 84/** 85 * Push frame to all clients. 86 * @param {ArrayBuffer} f Data to push 87 * @private 88 */ 89HidGnubbyDevice.prototype.publishFrame_ = function(f) { 90 var old = this.clients; 91 92 var remaining = []; 93 var changes = false; 94 for (var i = 0; i < old.length; ++i) { 95 var client = old[i]; 96 if (client.receivedFrame(f)) { 97 // Client still alive; keep on list. 98 remaining.push(client); 99 } else { 100 changes = true; 101 console.log(UTIL_fmt( 102 '[' + client.cid.toString(16) + '] left?')); 103 } 104 } 105 if (changes) this.clients = remaining; 106}; 107 108/** 109 * Register a client for this gnubby. 110 * @param {*} who The client. 111 */ 112HidGnubbyDevice.prototype.registerClient = function(who) { 113 for (var i = 0; i < this.clients.length; ++i) { 114 if (this.clients[i] === who) return; // Already registered. 115 } 116 this.clients.push(who); 117 if (this.clients.length == 1) { 118 // First client? Kick off read loop. 119 this.readLoop_(); 120 } 121}; 122 123/** 124 * De-register a client. 125 * @param {*} who The client. 126 * @return {number} The number of remaining listeners for this device, or -1 127 * Returns number of remaining listeners for this device. 128 * if this had no clients to start with. 129 */ 130HidGnubbyDevice.prototype.deregisterClient = function(who) { 131 var current = this.clients; 132 if (current.length == 0) return -1; 133 this.clients = []; 134 for (var i = 0; i < current.length; ++i) { 135 var client = current[i]; 136 if (client !== who) this.clients.push(client); 137 } 138 return this.clients.length; 139}; 140 141/** 142 * @param {*} who The client. 143 * @return {boolean} Whether this device has who as a client. 144 */ 145HidGnubbyDevice.prototype.hasClient = function(who) { 146 if (this.clients.length == 0) return false; 147 for (var i = 0; i < this.clients.length; ++i) { 148 if (who === this.clients[i]) 149 return true; 150 } 151 return false; 152}; 153 154/** 155 * Reads all incoming frames and notifies clients of their receipt. 156 * @private 157 */ 158HidGnubbyDevice.prototype.readLoop_ = function() { 159 //console.log(UTIL_fmt('entering readLoop')); 160 if (!this.dev) return; 161 162 if (this.closing) { 163 this.destroy(); 164 return; 165 } 166 167 // No interested listeners, yet we hit readLoop(). 168 // Must be clean-up. We do this here to make sure no transfer is pending. 169 if (!this.clients.length) { 170 this.closing = true; 171 this.destroy(); 172 return; 173 } 174 175 // firmwareUpdate() sets this.updating when writing the last block before 176 // the signature. We process that reply with the already pending 177 // read transfer but we do not want to start another read transfer for the 178 // signature block, since that request will have no reply. 179 // Instead we will see the device drop and re-appear on the bus. 180 // Current libusb on some platforms gets unhappy when transfer are pending 181 // when that happens. 182 // TODO: revisit once Chrome stabilizes its behavior. 183 if (this.updating) { 184 console.log(UTIL_fmt('device updating. Ending readLoop()')); 185 return; 186 } 187 188 var self = this; 189 chrome.hid.receive( 190 this.dev.connectionId, 191 function(report_id, data) { 192 if (chrome.runtime.lastError || !data) { 193 console.log(UTIL_fmt('got lastError')); 194 console.log(chrome.runtime.lastError); 195 window.setTimeout(function() { self.destroy(); }, 0); 196 return; 197 } 198 var u8 = new Uint8Array(data); 199 console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8))); 200 201 self.publishFrame_(data); 202 203 // Read more. 204 window.setTimeout(function() { self.readLoop_(); }, 0); 205 } 206 ); 207}; 208 209/** 210 * Check whether channel is locked for this request or not. 211 * @param {number} cid Channel id 212 * @param {number} cmd Request command 213 * @return {boolean} true if not locked for this request. 214 * @private 215 */ 216HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) { 217 if (this.lockCID) { 218 // We have an active lock. 219 if (this.lockCID != cid) { 220 // Some other channel has active lock. 221 222 if (cmd != GnubbyDevice.CMD_SYNC && 223 cmd != GnubbyDevice.CMD_INIT) { 224 // Anything but SYNC|INIT gets an immediate busy. 225 var busy = new Uint8Array( 226 [(cid >> 24) & 255, 227 (cid >> 16) & 255, 228 (cid >> 8) & 255, 229 cid & 255, 230 GnubbyDevice.CMD_ERROR, 231 0, 1, // length 232 GnubbyDevice.BUSY]); 233 // Log the synthetic busy too. 234 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy))); 235 this.publishFrame_(busy.buffer); 236 return false; 237 } 238 239 // SYNC|INIT gets to go to the device to flush OS tx/rx queues. 240 // The usb firmware is to alway respond to SYNC/INIT, 241 // regardless of lock status. 242 } 243 } 244 return true; 245}; 246 247/** 248 * Update or grab lock. 249 * @param {number} cid Channel ID 250 * @param {number} cmd Command 251 * @param {number} arg Command argument 252 * @private 253 */ 254HidGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) { 255 if (this.lockCID == 0 || this.lockCID == cid) { 256 // It is this caller's or nobody's lock. 257 if (this.lockTID) { 258 window.clearTimeout(this.lockTID); 259 this.lockTID = null; 260 } 261 262 if (cmd == GnubbyDevice.CMD_LOCK) { 263 var nseconds = arg; 264 if (nseconds != 0) { 265 this.lockCID = cid; 266 // Set tracking time to be .1 seconds longer than usb device does. 267 this.lockMillis = nseconds * 1000 + 100; 268 } else { 269 // Releasing lock voluntarily. 270 this.lockCID = 0; 271 } 272 } 273 274 // (re)set the lock timeout if we still hold it. 275 if (this.lockCID) { 276 var self = this; 277 this.lockTID = window.setTimeout( 278 function() { 279 console.warn(UTIL_fmt( 280 'lock for CID ' + cid.toString(16) + ' expired!')); 281 self.lockTID = null; 282 self.lockCID = 0; 283 }, 284 this.lockMillis); 285 } 286 } 287}; 288 289/** 290 * Queue command to be sent. 291 * If queue was empty, initiate the write. 292 * @param {number} cid The client's channel ID. 293 * @param {number} cmd The command to send. 294 * @param {ArrayBuffer|Uint8Array} data Command arguments 295 */ 296HidGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) { 297 if (!this.dev) return; 298 if (!this.checkLock_(cid, cmd)) return; 299 300 var u8 = new Uint8Array(data); 301 var f = new Uint8Array(64); 302 303 HidGnubbyDevice.setCid_(f, cid); 304 f[4] = cmd; 305 f[5] = (u8.length >> 8); 306 f[6] = (u8.length & 255); 307 308 var lockArg = (u8.length > 0) ? u8[0] : 0; 309 310 // Fragment over our 64 byte frames. 311 var n = 7; 312 var seq = 0; 313 for (var i = 0; i < u8.length; ++i) { 314 f[n++] = u8[i]; 315 if (n == f.length) { 316 this.queueFrame_(f.buffer, cid, cmd, lockArg); 317 318 f = new Uint8Array(64); 319 HidGnubbyDevice.setCid_(f, cid); 320 cmd = f[4] = seq++; 321 n = 5; 322 } 323 } 324 if (n != 5) { 325 this.queueFrame_(f.buffer, cid, cmd, lockArg); 326 } 327}; 328 329/** 330 * Sets the channel id in the frame. 331 * @param {Uint8Array} frame Data frame 332 * @param {number} cid The client's channel ID. 333 * @private 334 */ 335HidGnubbyDevice.setCid_ = function(frame, cid) { 336 frame[0] = cid >>> 24; 337 frame[1] = cid >>> 16; 338 frame[2] = cid >>> 8; 339 frame[3] = cid; 340}; 341 342/** 343 * Updates the lock, and queues the frame for sending. Also begins sending if 344 * no other writes are outstanding. 345 * @param {ArrayBuffer} frame Data frame 346 * @param {number} cid The client's channel ID. 347 * @param {number} cmd The command to send. 348 * @param {number} arg Command argument 349 * @private 350 */ 351HidGnubbyDevice.prototype.queueFrame_ = function(frame, cid, cmd, arg) { 352 this.updateLock_(cid, cmd, arg); 353 var wasEmpty = (this.txqueue.length == 0); 354 this.txqueue.push(frame); 355 if (wasEmpty) this.writePump_(); 356}; 357 358/** 359 * Stuff queued frames from txqueue[] to device, one by one. 360 * @private 361 */ 362HidGnubbyDevice.prototype.writePump_ = function() { 363 if (!this.dev) return; // Ignore. 364 365 if (this.txqueue.length == 0) return; // Done with current queue. 366 367 var frame = this.txqueue[0]; 368 369 var self = this; 370 function transferComplete() { 371 if (chrome.runtime.lastError) { 372 console.log(UTIL_fmt('got lastError')); 373 console.log(chrome.runtime.lastError); 374 window.setTimeout(function() { self.destroy(); }, 0); 375 return; 376 } 377 self.txqueue.shift(); // drop sent frame from queue. 378 if (self.txqueue.length != 0) { 379 window.setTimeout(function() { self.writePump_(); }, 0); 380 } 381 }; 382 383 var u8 = new Uint8Array(frame); 384 385 // See whether this requires scrubbing before logging. 386 var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') && 387 Gnubby['redactRequestLog'](u8); 388 if (alternateLog) { 389 console.log(UTIL_fmt('>' + alternateLog)); 390 } else { 391 console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8))); 392 } 393 394 var u8f = new Uint8Array(64); 395 for (var i = 0; i < u8.length; ++i) { 396 u8f[i] = u8[i]; 397 } 398 399 chrome.hid.send( 400 this.dev.connectionId, 401 0, // report Id. Must be 0 for our use. 402 u8f.buffer, 403 transferComplete 404 ); 405}; 406 407/** 408 * @param {function(Array)} cb Enumeration callback 409 */ 410HidGnubbyDevice.enumerate = function(cb) { 411 var permittedDevs; 412 var numEnumerated = 0; 413 var allDevs = []; 414 415 function enumerated(devs) { 416 allDevs = allDevs.concat(devs); 417 if (++numEnumerated == permittedDevs.length) { 418 cb(allDevs); 419 } 420 } 421 422 try { 423 chrome.hid.getDevices({filters: [{usagePage: 0xf1d0}]}, cb); 424 } catch (e) { 425 console.log(e); 426 console.log(UTIL_fmt('falling back to vid/pid enumeration')); 427 GnubbyDevice.getPermittedUsbDevices(function(devs) { 428 permittedDevs = devs; 429 for (var i = 0; i < devs.length; i++) { 430 chrome.hid.getDevices(devs[i], enumerated); 431 } 432 }); 433 } 434}; 435 436/** 437 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated 438 * in. 439 * @param {number} which The index of the device to open. 440 * @param {!chrome.hid.HidDeviceInfo} dev The device to open. 441 * @param {function(number, GnubbyDevice=)} cb Called back with the 442 * result of opening the device. 443 */ 444HidGnubbyDevice.open = function(gnubbies, which, dev, cb) { 445 chrome.hid.connect(dev.deviceId, function(handle) { 446 if (chrome.runtime.lastError) { 447 console.log(chrome.runtime.lastError); 448 } 449 if (!handle) { 450 console.warn(UTIL_fmt('failed to connect device. permissions issue?')); 451 cb(-GnubbyDevice.NODEVICE); 452 return; 453 } 454 var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle); 455 var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which); 456 cb(-GnubbyDevice.OK, gnubby); 457 }); 458}; 459 460/** 461 * @param {*} dev A browser API device object 462 * @return {GnubbyDeviceId} A device identifier for the device. 463 */ 464HidGnubbyDevice.deviceToDeviceId = function(dev) { 465 var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev); 466 var deviceId = { 467 namespace: HidGnubbyDevice.NAMESPACE, 468 device: hidDev.deviceId 469 }; 470 return deviceId; 471}; 472 473/** 474 * Registers this implementation with gnubbies. 475 * @param {Gnubbies} gnubbies Gnubbies registry 476 */ 477HidGnubbyDevice.register = function(gnubbies) { 478 var HID_GNUBBY_IMPL = { 479 isSharedAccess: true, 480 enumerate: HidGnubbyDevice.enumerate, 481 deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId, 482 open: HidGnubbyDevice.open 483 }; 484 gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL); 485}; 486