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.usb.
11 * Low level gnubby 'driver'. One per physical USB device.
12 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
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.
19 * @implements {llGnubby}
21 function llUsbGnubby(gnubbies, dev, id, inEndpoint, outEndpoint) {
22 /** @private {Gnubbies} */
23 this.gnubbies_ = gnubbies;
26 this.inEndpoint = inEndpoint;
27 this.outEndpoint = outEndpoint;
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;
40 * Namespace for the llUsbGnubby implementation.
43 llUsbGnubby.NAMESPACE = 'usb';
45 /** Destroys this low-level device instance. */
46 llUsbGnubby.prototype.destroy = function() {
47 if (!this.dev) return; // Already dead.
51 console.log(UTIL_fmt('llUsbGnubby.destroy()'));
53 // Synthesize a close error frame to alert all clients,
54 // some of which might be in read state.
56 // Use magic CID 0 to address all.
57 this.publishFrame_(new Uint8Array([
58 0, 0, 0, 0, // broadcast CID
61 llGnubby.GONE]).buffer);
63 // Set all clients to closed status and remove them.
64 while (this.clients.length != 0) {
65 var client = this.clients.shift();
66 if (client) client.closed = true;
70 window.clearTimeout(this.lockTID);
80 console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
81 self.gnubbies_.removeOpenDevice(
82 {namespace: llUsbGnubby.NAMESPACE, device: self.id});
86 chrome.usb.releaseInterface(dev, 0, function() {
87 console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
88 chrome.usb.closeDevice(dev, onClosed);
93 * Push frame to all clients.
94 * @param {ArrayBuffer} f Data frame
97 llUsbGnubby.prototype.publishFrame_ = function(f) {
98 var old = this.clients;
102 for (var i = 0; i < old.length; ++i) {
104 if (client.receivedFrame(f)) {
105 // Client still alive; keep on list.
106 remaining.push(client);
109 console.log(UTIL_fmt(
110 '[' + client.cid.toString(16) + '] left?'));
113 if (changes) this.clients = remaining;
117 * @return {boolean} whether this device is open and ready to use.
120 llUsbGnubby.prototype.readyToUse_ = function() {
121 if (this.closing) return false;
122 if (!this.dev) return false;
128 * Reads one reply from the low-level device.
131 llUsbGnubby.prototype.readOneReply_ = function() {
132 if (!this.readyToUse_()) return; // No point in continuing.
133 if (this.updating) return; // Do not bother waiting for final update reply.
137 function inTransferComplete(x) {
138 self.inTransferPending = false;
140 if (!self.readyToUse_()) return; // No point in continuing.
142 if (chrome.runtime.lastError) {
143 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
144 console.log(chrome.runtime.lastError);
145 window.setTimeout(function() { self.destroy(); }, 0);
150 var u8 = new Uint8Array(x.data);
151 console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
153 self.publishFrame_(x.data);
155 // Write another pending request, if any.
158 self.txqueue.shift(); // Drop sent frame from queue.
159 self.writeOneRequest_();
163 console.log(UTIL_fmt('no x.data!'));
165 window.setTimeout(function() { self.destroy(); }, 0);
169 if (this.inTransferPending == false) {
170 this.inTransferPending = true;
171 chrome.usb.bulkTransfer(
172 /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
173 { direction: 'in', endpoint: this.inEndpoint, length: 2048 },
176 throw 'inTransferPending!';
181 * Register a client for this gnubby.
182 * @param {*} who The client.
184 llUsbGnubby.prototype.registerClient = function(who) {
185 for (var i = 0; i < this.clients.length; ++i) {
186 if (this.clients[i] === who) return; // Already registered.
188 this.clients.push(who);
192 * De-register a client.
193 * @param {*} who The client.
194 * @return {number} The number of remaining listeners for this device, or -1
195 * Returns number of remaining listeners for this device.
196 * if this had no clients to start with.
198 llUsbGnubby.prototype.deregisterClient = function(who) {
199 var current = this.clients;
200 if (current.length == 0) return -1;
202 for (var i = 0; i < current.length; ++i) {
203 var client = current[i];
204 if (client !== who) this.clients.push(client);
206 return this.clients.length;
210 * @param {*} who The client.
211 * @return {boolean} Whether this device has who as a client.
213 llUsbGnubby.prototype.hasClient = function(who) {
214 if (this.clients.length == 0) return false;
215 for (var i = 0; i < this.clients.length; ++i) {
216 if (who === this.clients[i])
223 * Stuff queued frames from txqueue[] to device, one by one.
226 llUsbGnubby.prototype.writeOneRequest_ = function() {
227 if (!this.readyToUse_()) return; // No point in continuing.
229 if (this.txqueue.length == 0) return; // Nothing to send.
231 var frame = this.txqueue[0];
234 function OutTransferComplete(x) {
235 self.outTransferPending = false;
237 if (!self.readyToUse_()) return; // No point in continuing.
239 if (chrome.runtime.lastError) {
240 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
241 console.log(chrome.runtime.lastError);
242 window.setTimeout(function() { self.destroy(); }, 0);
246 window.setTimeout(function() { self.readOneReply_(); }, 0);
249 var u8 = new Uint8Array(frame);
250 console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
252 if (this.outTransferPending == false) {
253 this.outTransferPending = true;
254 chrome.usb.bulkTransfer(
255 /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
256 { direction: 'out', endpoint: this.outEndpoint, data: frame },
257 OutTransferComplete);
259 throw 'outTransferPending!';
264 * Check whether channel is locked for this request or not.
265 * @param {number} cid Channel id
266 * @param {number} cmd Command to be sent
267 * @return {boolean} true if not locked for this request.
270 llUsbGnubby.prototype.checkLock_ = function(cid, cmd) {
272 // We have an active lock.
273 if (this.lockCID != cid) {
274 // Some other channel has active lock.
276 if (cmd != llGnubby.CMD_SYNC) {
277 // Anything but SYNC gets an immediate busy.
278 var busy = new Uint8Array(
286 // Log the synthetic busy too.
287 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
288 this.publishFrame_(busy.buffer);
292 // SYNC gets to go to the device to flush OS tx/rx queues.
293 // The usb firmware always responds to SYNC, regardless of lock status.
300 * Update or grab lock.
301 * @param {number} cid Channel id
302 * @param {number} cmd Command
303 * @param {number} arg Command argument
306 llUsbGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
307 if (this.lockCID == 0 || this.lockCID == cid) {
308 // It is this caller's or nobody's lock.
310 window.clearTimeout(this.lockTID);
314 if (cmd == llGnubby.CMD_LOCK) {
318 // Set tracking time to be .1 seconds longer than usb device does.
319 this.lockMillis = nseconds * 1000 + 100;
321 // Releasing lock voluntarily.
326 // (re)set the lock timeout if we still hold it.
329 this.lockTID = window.setTimeout(
331 console.warn(UTIL_fmt(
332 'lock for CID ' + cid.toString(16) + ' expired!'));
342 * Queue command to be sent.
343 * If queue was empty, initiate the write.
344 * @param {number} cid The client's channel ID.
345 * @param {number} cmd The command to send.
346 * @param {ArrayBuffer} data Command argument data
348 llUsbGnubby.prototype.queueCommand = function(cid, cmd, data) {
349 if (!this.dev) return;
350 if (!this.checkLock_(cid, cmd)) return;
352 var u8 = new Uint8Array(data);
353 var frame = new Uint8Array(u8.length + 7);
355 frame[0] = cid >>> 24;
356 frame[1] = cid >>> 16;
357 frame[2] = cid >>> 8;
360 frame[5] = (u8.length >> 8);
361 frame[6] = (u8.length & 255);
365 var lockArg = (u8.length > 0) ? u8[0] : 0;
366 this.updateLock_(cid, cmd, lockArg);
368 var wasEmpty = (this.txqueue.length == 0);
369 this.txqueue.push(frame.buffer);
370 if (wasEmpty) this.writeOneRequest_();
374 * @param {function(Array)} cb Enumerate callback
376 llUsbGnubby.enumerate = function(cb) {
377 chrome.usb.getDevices({'vendorId': 4176, 'productId': 529}, cb);
381 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
383 * @param {number} which The index of the device to open.
384 * @param {!chrome.usb.Device} dev The device to open.
385 * @param {function(number, llGnubby=)} cb Called back with the
386 * result of opening the device.
388 llUsbGnubby.open = function(gnubbies, which, dev, cb) {
389 /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */
390 function deviceOpened(handle) {
392 console.warn(UTIL_fmt('failed to open device. permissions issue?'));
393 cb(-llGnubby.NODEVICE);
396 var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
397 chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
398 var inEndpoint, outEndpoint;
399 for (var i = 0; i < descriptors.length; i++) {
400 var descriptor = descriptors[i];
401 for (var j = 0; j < descriptor.endpoints.length; j++) {
402 var endpoint = descriptor.endpoints[j];
403 if (inEndpoint == undefined && endpoint.type == 'bulk' &&
404 endpoint.direction == 'in') {
405 inEndpoint = endpoint.address;
407 if (outEndpoint == undefined && endpoint.type == 'bulk' &&
408 endpoint.direction == 'out') {
409 outEndpoint = endpoint.address;
413 if (inEndpoint == undefined || outEndpoint == undefined) {
414 console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
415 chrome.usb.closeDevice(nonNullHandle);
416 cb(-llGnubby.NODEVICE);
419 // Try getting it claimed now.
420 chrome.usb.claimInterface(nonNullHandle, 0, function() {
421 if (chrome.runtime.lastError) {
422 console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
423 console.log(chrome.runtime.lastError);
425 var claimed = !chrome.runtime.lastError;
427 console.warn(UTIL_fmt('failed to claim interface. busy?'));
428 // Claim failed? Let the callers know and bail out.
429 chrome.usb.closeDevice(nonNullHandle);
433 var gnubby = new llUsbGnubby(gnubbies, nonNullHandle, which, inEndpoint,
435 cb(-llGnubby.OK, gnubby);
440 if (llUsbGnubby.runningOnCrOS === undefined) {
441 llUsbGnubby.runningOnCrOS =
442 (window.navigator.appVersion.indexOf('; CrOS ') != -1);
444 if (llUsbGnubby.runningOnCrOS) {
445 chrome.usb.requestAccess(dev, 0, function(success) {
446 // Even though the argument to requestAccess is a chrome.usb.Device, the
447 // access request is for access to all devices with the same vid/pid.
448 // Curiously, if the first chrome.usb.requestAccess succeeds, a second
449 // call with a separate device with the same vid/pid fails. Since
450 // chrome.usb.openDevice will fail if a previous access request really
451 // failed, just ignore the outcome of the access request and move along.
452 chrome.usb.openDevice(dev, deviceOpened);
455 chrome.usb.openDevice(dev, deviceOpened);
460 * @param {*} dev Chrome usb device
461 * @return {llGnubbyDeviceId} A device identifier for the device.
463 llUsbGnubby.deviceToDeviceId = function(dev) {
464 var usbDev = /** @type {!chrome.usb.Device} */ (dev);
465 var deviceId = { namespace: llUsbGnubby.NAMESPACE, device: usbDev.device };
470 * Registers this implementation with gnubbies.
471 * @param {Gnubbies} gnubbies Gnubbies singleton instance
473 llUsbGnubby.register = function(gnubbies) {
474 var USB_GNUBBY_IMPL = {
475 enumerate: llUsbGnubby.enumerate,
476 deviceToDeviceId: llUsbGnubby.deviceToDeviceId,
477 open: llUsbGnubby.open
479 gnubbies.registerNamespace(llUsbGnubby.NAMESPACE, USB_GNUBBY_IMPL);