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.
6 * @fileoverview Implements a low-level gnubby driver based on chrome.hid.
11 * Low level gnubby 'driver'. One per physical USB device.
12 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
14 * @param {!chrome.hid.HidConnectInfo} dev The connection to the device.
15 * @param {number} id The device's id.
17 * @implements {GnubbyDevice}
19 function HidGnubbyDevice(gnubbies, dev, id) {
20 /** @private {Gnubbies} */
21 this.gnubbies_ = gnubbies;
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.
34 * Namespace for the HidGnubbyDevice implementation.
37 HidGnubbyDevice.NAMESPACE = 'hid';
39 /** Destroys this low-level device instance. */
40 HidGnubbyDevice.prototype.destroy = function() {
41 if (!this.dev) return; // Already dead.
43 this.gnubbies_.removeOpenDevice(
44 {namespace: HidGnubbyDevice.NAMESPACE, device: this.id});
47 console.log(UTIL_fmt('HidGnubbyDevice.destroy()'));
49 // Synthesize a close error frame to alert all clients,
50 // some of which might be in read state.
52 // Use magic CID 0 to address all.
53 this.publishFrame_(new Uint8Array([
54 0, 0, 0, 0, // broadcast CID
55 GnubbyDevice.CMD_ERROR,
57 GnubbyDevice.GONE]).buffer);
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;
66 window.clearTimeout(this.lockTID);
73 chrome.hid.disconnect(dev.connectionId, function() {
74 console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed'));
79 * Push frame to all clients.
80 * @param {ArrayBuffer} f Data to push
83 HidGnubbyDevice.prototype.publishFrame_ = function(f) {
84 var old = this.clients;
88 for (var i = 0; i < old.length; ++i) {
90 if (client.receivedFrame(f)) {
91 // Client still alive; keep on list.
92 remaining.push(client);
96 '[' + client.cid.toString(16) + '] left?'));
99 if (changes) this.clients = remaining;
103 * Register a client for this gnubby.
104 * @param {*} who The client.
106 HidGnubbyDevice.prototype.registerClient = function(who) {
107 for (var i = 0; i < this.clients.length; ++i) {
108 if (this.clients[i] === who) return; // Already registered.
110 this.clients.push(who);
111 if (this.clients.length == 1) {
112 // First client? Kick off read loop.
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.
124 HidGnubbyDevice.prototype.deregisterClient = function(who) {
125 var current = this.clients;
126 if (current.length == 0) return -1;
128 for (var i = 0; i < current.length; ++i) {
129 var client = current[i];
130 if (client !== who) this.clients.push(client);
132 return this.clients.length;
136 * @param {*} who The client.
137 * @return {boolean} Whether this device has who as a client.
139 HidGnubbyDevice.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])
149 * Reads all incoming frames and notifies clients of their receipt.
152 HidGnubbyDevice.prototype.readLoop_ = function() {
153 //console.log(UTIL_fmt('entering readLoop'));
154 if (!this.dev) return;
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) {
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.
178 console.log(UTIL_fmt('device updating. Ending readLoop()'));
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);
192 var u8 = new Uint8Array(data);
193 console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
195 self.publishFrame_(data);
198 window.setTimeout(function() { self.readLoop_(); }, 0);
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.
210 HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
212 // We have an active lock.
213 if (this.lockCID != cid) {
214 // Some other channel has active lock.
216 if (cmd != GnubbyDevice.CMD_SYNC) {
217 // Anything but SYNC gets an immediate busy.
218 var busy = new Uint8Array(
223 GnubbyDevice.CMD_ERROR,
226 // Log the synthetic busy too.
227 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
228 this.publishFrame_(busy.buffer);
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.
240 * Update or grab lock.
241 * @param {number} cid Channel ID
242 * @param {number} cmd Command
243 * @param {number} arg Command argument
246 HidGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) {
247 if (this.lockCID == 0 || this.lockCID == cid) {
248 // It is this caller's or nobody's lock.
250 window.clearTimeout(this.lockTID);
254 if (cmd == GnubbyDevice.CMD_LOCK) {
258 // Set tracking time to be .1 seconds longer than usb device does.
259 this.lockMillis = nseconds * 1000 + 100;
261 // Releasing lock voluntarily.
266 // (re)set the lock timeout if we still hold it.
269 this.lockTID = window.setTimeout(
271 console.warn(UTIL_fmt(
272 'lock for CID ' + cid.toString(16) + ' expired!'));
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
288 HidGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {
289 if (!this.dev) return;
290 if (!this.checkLock_(cid, cmd)) return;
292 var u8 = new Uint8Array(data);
293 var f = new Uint8Array(64);
295 HidGnubbyDevice.setCid_(f, cid);
297 f[5] = (u8.length >> 8);
298 f[6] = (u8.length & 255);
300 var lockArg = (u8.length > 0) ? u8[0] : 0;
302 // Fragment over our 64 byte frames.
305 for (var i = 0; i < u8.length; ++i) {
308 this.queueFrame_(f.buffer, cid, cmd, lockArg);
310 f = new Uint8Array(64);
311 HidGnubbyDevice.setCid_(f, cid);
317 this.queueFrame_(f.buffer, cid, cmd, lockArg);
322 * Sets the channel id in the frame.
323 * @param {Uint8Array} frame Data frame
324 * @param {number} cid The client's channel ID.
327 HidGnubbyDevice.setCid_ = function(frame, cid) {
328 frame[0] = cid >>> 24;
329 frame[1] = cid >>> 16;
330 frame[2] = cid >>> 8;
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
343 HidGnubbyDevice.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_();
351 * Stuff queued frames from txqueue[] to device, one by one.
354 HidGnubbyDevice.prototype.writePump_ = function() {
355 if (!this.dev) return; // Ignore.
357 if (this.txqueue.length == 0) return; // Done with current queue.
359 var frame = this.txqueue[0];
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);
369 self.txqueue.shift(); // drop sent frame from queue.
370 if (self.txqueue.length != 0) {
371 window.setTimeout(function() { self.writePump_(); }, 0);
375 var u8 = new Uint8Array(frame);
377 // See whether this requires scrubbing before logging.
378 var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') &&
379 Gnubby['redactRequestLog'](u8);
381 console.log(UTIL_fmt('>' + alternateLog));
383 console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
386 var u8f = new Uint8Array(64);
387 for (var i = 0; i < u8.length; ++i) {
392 this.dev.connectionId,
393 0, // report Id. Must be 0 for our use.
400 * @param {function(Array)} cb Enumeration callback
402 HidGnubbyDevice.enumerate = function(cb) {
404 var numEnumerated = 0;
407 function enumerated(devs) {
408 allDevs = allDevs.concat(devs);
409 if (++numEnumerated == permittedDevs.length) {
414 GnubbyDevice.getPermittedUsbDevices(function(devs) {
415 permittedDevs = devs;
416 for (var i = 0; i < devs.length; i++) {
417 chrome.hid.getDevices(devs[i], enumerated);
423 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
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.
430 HidGnubbyDevice.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);
436 console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
437 cb(-GnubbyDevice.NODEVICE);
440 var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle);
441 var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which);
442 cb(-GnubbyDevice.OK, gnubby);
447 * @param {*} dev A browser API device object
448 * @return {GnubbyDeviceId} A device identifier for the device.
450 HidGnubbyDevice.deviceToDeviceId = function(dev) {
451 var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
453 namespace: HidGnubbyDevice.NAMESPACE,
454 device: hidDev.deviceId
460 * Registers this implementation with gnubbies.
461 * @param {Gnubbies} gnubbies Gnubbies registry
463 HidGnubbyDevice.register = function(gnubbies) {
464 var HID_GNUBBY_IMPL = {
465 isSharedAccess: true,
466 enumerate: HidGnubbyDevice.enumerate,
467 deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId,
468 open: HidGnubbyDevice.open
470 gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL);