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