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.ConnectionHandle} dev The device.
15 * @param {number} id The device's id.
17 * @implements {llGnubby}
19 function llHidGnubby(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 llHidGnubby implementation.
37 llHidGnubby.NAMESPACE = 'hid';
39 /** Destroys this low-level device instance. */
40 llHidGnubby.prototype.destroy = function() {
41 if (!this.dev) return; // Already dead.
43 this.gnubbies_.removeOpenDevice(
44 {namespace: llHidGnubby.NAMESPACE, device: this.id});
47 console.log(UTIL_fmt('llHidGnubby.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
57 llGnubby.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.handle + ' closed'));
79 * Push frame to all clients.
80 * @param {ArrayBuffer} f Data to push
83 llHidGnubby.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 llHidGnubby.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 llHidGnubby.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 llHidGnubby.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 llHidGnubby.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,
187 if (chrome.runtime.lastError || !x) {
188 console.log(UTIL_fmt('got lastError'));
189 console.log(chrome.runtime.lastError);
190 window.setTimeout(function() { self.destroy(); }, 0);
193 var u8 = new Uint8Array(x);
194 //console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
196 self.publishFrame_(x);
199 window.setTimeout(function() { self.readLoop_(); }, 0);
205 * Check whether channel is locked for this request or not.
206 * @param {number} cid Channel id
207 * @param {number} cmd Request command
208 * @return {boolean} true if not locked for this request.
211 llHidGnubby.prototype.checkLock_ = function(cid, cmd) {
213 // We have an active lock.
214 if (this.lockCID != cid) {
215 // Some other channel has active lock.
217 if (cmd != llGnubby.CMD_SYNC) {
218 // Anything but SYNC gets an immediate busy.
219 var busy = new Uint8Array(
227 // Log the synthetic busy too.
228 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
229 this.publishFrame_(busy.buffer);
233 // SYNC gets to go to the device to flush OS tx/rx queues.
234 // The usb firmware always responds to SYNC, regardless of lock status.
241 * Update or grab lock.
242 * @param {number} cid Channel ID
243 * @param {number} cmd Command
244 * @param {number} arg Command argument
247 llHidGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
248 if (this.lockCID == 0 || this.lockCID == cid) {
249 // It is this caller's or nobody's lock.
251 window.clearTimeout(this.lockTID);
255 if (cmd == llGnubby.CMD_LOCK) {
259 // Set tracking time to be .1 seconds longer than usb device does.
260 this.lockMillis = nseconds * 1000 + 100;
262 // Releasing lock voluntarily.
267 // (re)set the lock timeout if we still hold it.
270 this.lockTID = window.setTimeout(
272 console.warn(UTIL_fmt(
273 'lock for CID ' + cid.toString(16) + ' expired!'));
283 * Queue command to be sent.
284 * If queue was empty, initiate the write.
285 * @param {number} cid The client's channel ID.
286 * @param {number} cmd The command to send.
287 * @param {ArrayBuffer|Uint8Array} data Command arguments
289 llHidGnubby.prototype.queueCommand = function(cid, cmd, data) {
290 if (!this.dev) return;
291 if (!this.checkLock_(cid, cmd)) return;
293 var u8 = new Uint8Array(data);
294 var f = new Uint8Array(64);
296 llHidGnubby.setCid_(f, cid);
298 f[5] = (u8.length >> 8);
299 f[6] = (u8.length & 255);
301 var lockArg = (u8.length > 0) ? u8[0] : 0;
303 // Fragment over our 64 byte frames.
306 for (var i = 0; i < u8.length; ++i) {
309 this.queueFrame_(f.buffer, cid, cmd, lockArg);
311 f = new Uint8Array(64);
312 llHidGnubby.setCid_(f, cid);
318 this.queueFrame_(f.buffer, cid, cmd, lockArg);
323 * Sets the channel id in the frame.
324 * @param {Uint8Array} frame Data frame
325 * @param {number} cid The client's channel ID.
328 llHidGnubby.setCid_ = function(frame, cid) {
329 frame[0] = cid >>> 24;
330 frame[1] = cid >>> 16;
331 frame[2] = cid >>> 8;
336 * Updates the lock, and queues the frame for sending. Also begins sending if
337 * no other writes are outstanding.
338 * @param {ArrayBuffer} frame Data frame
339 * @param {number} cid The client's channel ID.
340 * @param {number} cmd The command to send.
341 * @param {number} arg Command argument
344 llHidGnubby.prototype.queueFrame_ = function(frame, cid, cmd, arg) {
345 this.updateLock_(cid, cmd, arg);
346 var wasEmpty = (this.txqueue.length == 0);
347 this.txqueue.push(frame);
348 if (wasEmpty) this.writePump_();
352 * Stuff queued frames from txqueue[] to device, one by one.
355 llHidGnubby.prototype.writePump_ = function() {
356 if (!this.dev) return; // Ignore.
358 if (this.txqueue.length == 0) return; // Done with current queue.
360 var frame = this.txqueue[0];
363 function transferComplete(x) {
364 if (chrome.runtime.lastError) {
365 console.log(UTIL_fmt('got lastError'));
366 console.log(chrome.runtime.lastError);
367 window.setTimeout(function() { self.destroy(); }, 0);
370 self.txqueue.shift(); // drop sent frame from queue.
371 if (self.txqueue.length != 0) {
372 window.setTimeout(function() { self.writePump_(); }, 0);
376 var u8 = new Uint8Array(frame);
377 //console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
379 var u8f = new Uint8Array(64);
380 for (var i = 0; i < u8.length; ++i) {
385 this.dev.connectionId,
392 * @param {function(Array)} cb Enumeration callback
394 llHidGnubby.enumerate = function(cb) {
395 chrome.hid.getDevices({'vendorId': 4176, 'productId': 512}, cb);
399 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
401 * @param {number} which The index of the device to open.
402 * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
403 * @param {function(number, llGnubby=)} cb Called back with the
404 * result of opening the device.
406 llHidGnubby.open = function(gnubbies, which, dev, cb) {
407 chrome.hid.connect(dev.deviceId, function(handle) {
408 if (chrome.runtime.lastError) {
409 console.log(chrome.runtime.lastError);
412 console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
413 cb(-llGnubby.NODEVICE);
416 var nonNullHandle = /** @type {!chrome.hid.HidConnection} */ (handle);
417 var gnubby = new llHidGnubby(gnubbies, nonNullHandle, which);
418 cb(-llGnubby.OK, gnubby);
423 * @param {*} dev A browser API device object
424 * @return {llGnubbyDeviceId} A device identifier for the device.
426 llHidGnubby.deviceToDeviceId = function(dev) {
427 var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
428 var deviceId = { namespace: llHidGnubby.NAMESPACE, device: hidDev.deviceId };
433 * Registers this implementation with gnubbies.
434 * @param {Gnubbies} gnubbies Gnubbies registry
436 llHidGnubby.register = function(gnubbies) {
437 var HID_GNUBBY_IMPL = {
438 enumerate: llHidGnubby.enumerate,
439 deviceToDeviceId: llHidGnubby.deviceToDeviceId,
440 open: llHidGnubby.open
442 gnubbies.registerNamespace(llHidGnubby.NAMESPACE, HID_GNUBBY_IMPL);