UNPKG

21.5 kBJavaScriptView Raw
1var Frame = require('./frame')
2 , Hand = require('./hand')
3 , Pointable = require('./pointable')
4 , Finger = require('./finger')
5 , CircularBuffer = require("./circular_buffer")
6 , Pipeline = require("./pipeline")
7 , EventEmitter = require('events').EventEmitter
8 , gestureListener = require('./gesture').gestureListener
9 , Dialog = require('./dialog')
10 , _ = require('underscore');
11
12/**
13 * Constructs a Controller object.
14 *
15 * When creating a Controller object, you may optionally pass in options
16 * to set the host , set the port, enable gestures, or select the frame event type.
17 *
18 * ```javascript
19 * var controller = new Leap.Controller({
20 * host: '127.0.0.1',
21 * port: 6437,
22 * enableGestures: true,
23 * frameEventName: 'animationFrame'
24 * });
25 * ```
26 *
27 * @class Controller
28 * @memberof Leap
29 * @classdesc
30 * The Controller class is your main interface to the Leap Motion Controller.
31 *
32 * Create an instance of this Controller class to access frames of tracking data
33 * and configuration information. Frame data can be polled at any time using the
34 * [Controller.frame]{@link Leap.Controller#frame}() function. Call frame() or frame(0) to get the most recent
35 * frame. Set the history parameter to a positive integer to access previous frames.
36 * A controller stores up to 60 frames in its frame history.
37 *
38 * Polling is an appropriate strategy for applications which already have an
39 * intrinsic update loop, such as a game.
40 */
41
42
43var Controller = module.exports = function(opts) {
44 var inNode = (typeof(process) !== 'undefined' && process.versions && process.versions.node),
45 controller = this;
46
47 opts = _.defaults(opts || {}, {
48 inNode: inNode
49 });
50
51 this.inNode = opts.inNode;
52
53 opts = _.defaults(opts || {}, {
54 frameEventName: this.useAnimationLoop() ? 'animationFrame' : 'deviceFrame',
55 suppressAnimationLoop: !this.useAnimationLoop(),
56 loopWhileDisconnected: false,
57 useAllPlugins: false,
58 checkVersion: true
59 });
60
61 this.animationFrameRequested = false;
62 this.onAnimationFrame = function() {
63 controller.emit('animationFrame', controller.lastConnectionFrame);
64 if (controller.loopWhileDisconnected && (controller.connection.focusedState || controller.connection.opts.background) ){
65 window.requestAnimationFrame(controller.onAnimationFrame);
66 }else{
67 controller.animationFrameRequested = false;
68 }
69 }
70 this.suppressAnimationLoop = opts.suppressAnimationLoop;
71 this.loopWhileDisconnected = opts.loopWhileDisconnected;
72 this.frameEventName = opts.frameEventName;
73 this.useAllPlugins = opts.useAllPlugins;
74 this.history = new CircularBuffer(200);
75 this.lastFrame = Frame.Invalid;
76 this.lastValidFrame = Frame.Invalid;
77 this.lastConnectionFrame = Frame.Invalid;
78 this.accumulatedGestures = [];
79 this.checkVersion = opts.checkVersion;
80 if (opts.connectionType === undefined) {
81 this.connectionType = (this.inBrowser() ? require('./connection/browser') : require('./connection/node'));
82 } else {
83 this.connectionType = opts.connectionType;
84 }
85 this.connection = new this.connectionType(opts);
86 this.streamingCount = 0;
87 this.devices = {};
88 this.plugins = {};
89 this._pluginPipelineSteps = {};
90 this._pluginExtendedMethods = {};
91 if (opts.useAllPlugins) this.useRegisteredPlugins();
92 this.setupFrameEvents(opts);
93 this.setupConnectionEvents();
94}
95
96Controller.prototype.gesture = function(type, cb) {
97 var creator = gestureListener(this, type);
98 if (cb !== undefined) {
99 creator.stop(cb);
100 }
101 return creator;
102}
103
104/*
105 * @returns the controller
106 */
107Controller.prototype.setBackground = function(state) {
108 this.connection.setBackground(state);
109 return this;
110}
111
112Controller.prototype.inBrowser = function() {
113 return !this.inNode;
114}
115
116Controller.prototype.useAnimationLoop = function() {
117 return this.inBrowser() && !this.inBackgroundPage();
118}
119
120Controller.prototype.inBackgroundPage = function(){
121 // http://developer.chrome.com/extensions/extension#method-getBackgroundPage
122 return (typeof(chrome) !== "undefined") &&
123 chrome.extension &&
124 chrome.extension.getBackgroundPage &&
125 (chrome.extension.getBackgroundPage() === window)
126}
127
128/*
129 * @returns the controller
130 */
131Controller.prototype.connect = function() {
132 this.connection.connect();
133 return this;
134}
135
136Controller.prototype.streaming = function() {
137 return this.streamingCount > 0;
138}
139
140Controller.prototype.connected = function() {
141 return !!this.connection.connected;
142}
143
144Controller.prototype.runAnimationLoop = function(){
145 if (!this.suppressAnimationLoop && !this.animationFrameRequested) {
146 this.animationFrameRequested = true;
147 window.requestAnimationFrame(this.onAnimationFrame);
148 }
149}
150
151/*
152 * @returns the controller
153 */
154Controller.prototype.disconnect = function() {
155 this.connection.disconnect();
156 return this;
157}
158
159/**
160 * Returns a frame of tracking data from the Leap.
161 *
162 * Use the optional history parameter to specify which frame to retrieve.
163 * Call frame() or frame(0) to access the most recent frame; call frame(1) to
164 * access the previous frame, and so on. If you use a history value greater
165 * than the number of stored frames, then the controller returns an invalid frame.
166 *
167 * @method frame
168 * @memberof Leap.Controller.prototype
169 * @param {number} history The age of the frame to return, counting backwards from
170 * the most recent frame (0) into the past and up to the maximum age (59).
171 * @returns {Leap.Frame} The specified frame; or, if no history
172 * parameter is specified, the newest frame. If a frame is not available at
173 * the specified history position, an invalid Frame is returned.
174 **/
175Controller.prototype.frame = function(num) {
176 return this.history.get(num) || Frame.Invalid;
177}
178
179Controller.prototype.loop = function(callback) {
180 if (callback) {
181 if (typeof callback === 'function'){
182 this.on(this.frameEventName, callback);
183 }else{
184 // callback is actually of the form: {eventName: callback}
185 this.setupFrameEvents(callback);
186 }
187 }
188
189 return this.connect();
190}
191
192Controller.prototype.addStep = function(step) {
193 if (!this.pipeline) this.pipeline = new Pipeline(this);
194 this.pipeline.addStep(step);
195}
196
197// this is run on every deviceFrame
198Controller.prototype.processFrame = function(frame) {
199 if (frame.gestures) {
200 this.accumulatedGestures = this.accumulatedGestures.concat(frame.gestures);
201 }
202 // lastConnectionFrame is used by the animation loop
203 this.lastConnectionFrame = frame;
204 this.runAnimationLoop();
205 this.emit('deviceFrame', frame);
206}
207
208// on a this.deviceEventName (usually 'animationFrame' in browsers), this emits a 'frame'
209Controller.prototype.processFinishedFrame = function(frame) {
210 this.lastFrame = frame;
211 if (frame.valid) {
212 this.lastValidFrame = frame;
213 }
214 frame.controller = this;
215 frame.historyIdx = this.history.push(frame);
216 if (frame.gestures) {
217 frame.gestures = this.accumulatedGestures;
218 this.accumulatedGestures = [];
219 for (var gestureIdx = 0; gestureIdx != frame.gestures.length; gestureIdx++) {
220 this.emit("gesture", frame.gestures[gestureIdx], frame);
221 }
222 }
223 if (this.pipeline) {
224 frame = this.pipeline.run(frame);
225 if (!frame) frame = Frame.Invalid;
226 }
227 this.emit('frame', frame);
228 this.emitHandEvents(frame);
229}
230
231/**
232 * The controller will emit 'hand' events for every hand on each frame. The hand in question will be passed
233 * to the event callback.
234 *
235 * @param frame
236 */
237Controller.prototype.emitHandEvents = function(frame){
238 for (var i = 0; i < frame.hands.length; i++){
239 this.emit('hand', frame.hands[i]);
240 }
241}
242
243Controller.prototype.setupFrameEvents = function(opts){
244 if (opts.frame){
245 this.on('frame', opts.frame);
246 }
247 if (opts.hand){
248 this.on('hand', opts.hand);
249 }
250}
251
252/**
253 Controller events. The old 'deviceConnected' and 'deviceDisconnected' have been depricated -
254 use 'deviceStreaming' and 'deviceStopped' instead, except in the case of an unexpected disconnect.
255
256 There are 4 pairs of device events recently added/changed:
257 -deviceAttached/deviceRemoved - called when a device's physical connection to the computer changes
258 -deviceStreaming/deviceStopped - called when a device is paused or resumed.
259 -streamingStarted/streamingStopped - called when there is/is no longer at least 1 streaming device.
260 Always comes after deviceStreaming.
261
262 The first of all of the above event pairs is triggered as appropriate upon connection. All of
263 these events receives an argument with the most recent info about the device that triggered it.
264 These events will always be fired in the order they are listed here, with reverse ordering for the
265 matching shutdown call. (ie, deviceStreaming always comes after deviceAttached, and deviceStopped
266 will come before deviceRemoved).
267
268 -deviceConnected/deviceDisconnected - These are considered deprecated and will be removed in
269 the next revision. In contrast to the other events and in keeping with it's original behavior,
270 it will only be fired when a device begins streaming AFTER a connection has been established.
271 It is not paired, and receives no device info. Nearly identical functionality to
272 streamingStarted/Stopped if you need to port.
273*/
274Controller.prototype.setupConnectionEvents = function() {
275 var controller = this;
276 this.connection.on('frame', function(frame) {
277 controller.processFrame(frame);
278 });
279 // either deviceFrame or animationFrame:
280 this.on(this.frameEventName, function(frame) {
281 controller.processFinishedFrame(frame);
282 });
283
284
285 // here we backfill the 0.5.0 deviceEvents as best possible
286 // backfill begin streaming events
287 var backfillStreamingStartedEventsHandler = function(){
288 if (controller.connection.opts.requestProtocolVersion < 5 && controller.streamingCount == 0){
289 controller.streamingCount = 1;
290 var info = {
291 attached: true,
292 streaming: true,
293 type: 'unknown',
294 id: "Lx00000000000"
295 };
296 controller.devices[info.id] = info;
297
298 controller.emit('deviceAttached', info);
299 controller.emit('deviceStreaming', info);
300 controller.emit('streamingStarted', info);
301 controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
302 }
303 }
304
305 var backfillStreamingStoppedEvents = function(){
306 if (controller.streamingCount > 0) {
307 for (var deviceId in controller.devices){
308 controller.emit('deviceStopped', controller.devices[deviceId]);
309 controller.emit('deviceRemoved', controller.devices[deviceId]);
310 }
311 // only emit streamingStopped once, with the last device
312 controller.emit('streamingStopped', controller.devices[deviceId]);
313
314 controller.streamingCount = 0;
315
316 for (var deviceId in controller.devices){
317 delete controller.devices[deviceId];
318 }
319 }
320 }
321 // Delegate connection events
322 this.connection.on('focus', function() { controller.emit('focus'); });
323 this.connection.on('blur', function() { controller.emit('blur') });
324 this.connection.on('protocol', function(protocol) { controller.emit('protocol', protocol); });
325 this.connection.on('ready', function() {
326
327 if (controller.checkVersion && !controller.inNode){
328 // show dialog only to web users
329 controller.checkOutOfDate();
330 }
331
332 controller.emit('ready');
333 });
334
335 this.connection.on('connect', function() {
336 controller.emit('connect');
337 controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
338 controller.connection.on('frame', backfillStreamingStartedEventsHandler);
339 });
340
341 this.connection.on('disconnect', function() {
342 controller.emit('disconnect');
343 backfillStreamingStoppedEvents();
344 });
345
346 // this does not fire when the controller is manually disconnected
347 // or for Leap Service v1.2.0+
348 this.connection.on('deviceConnect', function(evt) {
349 if (evt.state){
350 controller.emit('deviceConnected');
351 controller.connection.removeListener('frame', backfillStreamingStartedEventsHandler)
352 controller.connection.on('frame', backfillStreamingStartedEventsHandler);
353 }else{
354 controller.emit('deviceDisconnected');
355 backfillStreamingStoppedEvents();
356 }
357 });
358
359 // Does not fire for Leap Service pre v1.2.0
360 this.connection.on('deviceEvent', function(evt) {
361 var info = evt.state,
362 oldInfo = controller.devices[info.id];
363
364 //Grab a list of changed properties in the device info
365 var changed = {};
366 for(var property in info) {
367 //If a property i doesn't exist the cache, or has changed...
368 if( !oldInfo || !oldInfo.hasOwnProperty(property) || oldInfo[property] != info[property] ) {
369 changed[property] = true;
370 }
371 }
372
373 //Update the device list
374 controller.devices[info.id] = info;
375
376 //Fire events based on change list
377 if(changed.attached) {
378 controller.emit(info.attached ? 'deviceAttached' : 'deviceRemoved', info);
379 }
380
381 if(!changed.streaming) return;
382
383 if(info.streaming) {
384 controller.streamingCount++;
385 controller.emit('deviceStreaming', info);
386 if( controller.streamingCount == 1 ) {
387 controller.emit('streamingStarted', info);
388 }
389 //if attached & streaming both change to true at the same time, that device was streaming
390 //already when we connected.
391 if(!changed.attached) {
392 controller.emit('deviceConnected');
393 }
394 }
395 //Since when devices are attached all fields have changed, don't send events for streaming being false.
396 else if(!(changed.attached && info.attached)) {
397 controller.streamingCount--;
398 controller.emit('deviceStopped', info);
399 if(controller.streamingCount == 0){
400 controller.emit('streamingStopped', info);
401 }
402 controller.emit('deviceDisconnected');
403 }
404
405 });
406
407
408 this.on('newListener', function(event, listener) {
409 if( event == 'deviceConnected' || event == 'deviceDisconnected' ) {
410 console.warn(event + " events are depricated. Consider using 'streamingStarted/streamingStopped' or 'deviceStreaming/deviceStopped' instead");
411 }
412 });
413
414};
415
416
417
418
419// Checks if the protocol version is the latest, if if not, shows the dialog.
420Controller.prototype.checkOutOfDate = function(){
421 console.assert(this.connection && this.connection.protocol);
422
423 var serviceVersion = this.connection.protocol.serviceVersion;
424 var protocolVersion = this.connection.protocol.version;
425 var defaultProtocolVersion = this.connectionType.defaultProtocolVersion;
426
427 if (defaultProtocolVersion > protocolVersion){
428
429 console.warn("Your Protocol Version is v" + protocolVersion +
430 ", this app was designed for v" + defaultProtocolVersion);
431
432 Dialog.warnOutOfDate({
433 sV: serviceVersion,
434 pV: protocolVersion
435 });
436 return true
437 }else{
438 return false
439 }
440
441};
442
443
444
445Controller._pluginFactories = {};
446
447/*
448 * Registers a plugin, making is accessible to controller.use later on.
449 *
450 * @member plugin
451 * @memberof Leap.Controller.prototype
452 * @param {String} name The name of the plugin (usually camelCase).
453 * @param {function} factory A factory method which will return an instance of a plugin.
454 * The factory receives an optional hash of options, passed in via controller.use.
455 *
456 * Valid keys for the object include frame, hand, finger, tool, and pointable. The value
457 * of each key can be either a function or an object. If given a function, that function
458 * will be called once for every instance of the object, with that instance injected as an
459 * argument. This allows decoration of objects with additional data:
460 *
461 * ```javascript
462 * Leap.Controller.plugin('testPlugin', function(options){
463 * return {
464 * frame: function(frame){
465 * frame.foo = 'bar';
466 * }
467 * }
468 * });
469 * ```
470 *
471 * When hand is used, the callback is called for every hand in `frame.hands`. Note that
472 * hand objects are recreated with every new frame, so that data saved on the hand will not
473 * persist.
474 *
475 * ```javascript
476 * Leap.Controller.plugin('testPlugin', function(){
477 * return {
478 * hand: function(hand){
479 * console.log('testPlugin running on hand ' + hand.id);
480 * }
481 * }
482 * });
483 * ```
484 *
485 * A factory can return an object to add custom functionality to Frames, Hands, or Pointables.
486 * The methods are added directly to the object's prototype. Finger and Tool cannot be used here, Pointable
487 * must be used instead.
488 * This is encouraged for calculations which may not be necessary on every frame.
489 * Memoization is also encouraged, for cases where the method may be called many times per frame by the application.
490 *
491 * ```javascript
492 * // This plugin allows hand.usefulData() to be called later.
493 * Leap.Controller.plugin('testPlugin', function(){
494 * return {
495 * hand: {
496 * usefulData: function(){
497 * console.log('usefulData on hand', this.id);
498 * // memoize the results on to the hand, preventing repeat work:
499 * this.x || this.x = someExpensiveCalculation();
500 * return this.x;
501 * }
502 * }
503 * }
504 * });
505 *
506 * Note that the factory pattern allows encapsulation for every plugin instance.
507 *
508 * ```javascript
509 * Leap.Controller.plugin('testPlugin', function(options){
510 * options || options = {}
511 * options.center || options.center = [0,0,0]
512 *
513 * privatePrintingMethod = function(){
514 * console.log('privatePrintingMethod - options', options);
515 * }
516 *
517 * return {
518 * pointable: {
519 * publicPrintingMethod: function(){
520 * privatePrintingMethod();
521 * }
522 * }
523 * }
524 * });
525 *
526 */
527Controller.plugin = function(pluginName, factory) {
528 if (this._pluginFactories[pluginName]) {
529 console.warn("Plugin \"" + pluginName + "\" already registered");
530 }
531 return this._pluginFactories[pluginName] = factory;
532};
533
534/*
535 * Returns a list of registered plugins.
536 * @returns {Array} Plugin Factories.
537 */
538Controller.plugins = function() {
539 return _.keys(this._pluginFactories);
540};
541
542/*
543 * Begin using a registered plugin. The plugin's functionality will be added to all frames
544 * returned by the controller (and/or added to the objects within the frame).
545 * - The order of plugin execution inside the loop will match the order in which use is called by the application.
546 * - The plugin be run for both deviceFrames and animationFrames.
547 *
548 * If called a second time, the options will be merged with those of the already instantiated plugin.
549 *
550 * @method use
551 * @memberOf Leap.Controller.prototype
552 * @param pluginName
553 * @param {Hash} Options to be passed to the plugin's factory.
554 * @returns the controller
555 */
556Controller.prototype.use = function(pluginName, options) {
557 var functionOrHash, pluginFactory, key, pluginInstance, klass;
558
559 pluginFactory = (typeof pluginName == 'function') ? pluginName : Controller._pluginFactories[pluginName];
560
561 if (!pluginFactory) {
562 throw 'Leap Plugin ' + pluginName + ' not found.';
563 }
564
565 options || (options = {});
566
567 if (this.plugins[pluginName]){
568 _.extend(this.plugins[pluginName], options)
569 return this;
570 }
571
572 this.plugins[pluginName] = options;
573
574 pluginInstance = pluginFactory.call(this, options);
575
576 for (key in pluginInstance) {
577 functionOrHash = pluginInstance[key];
578
579 if (typeof functionOrHash === 'function') {
580 if (!this.pipeline) this.pipeline = new Pipeline(this);
581 if (!this._pluginPipelineSteps[pluginName]) this._pluginPipelineSteps[pluginName] = [];
582
583 this._pluginPipelineSteps[pluginName].push( this.pipeline.addWrappedStep(key, functionOrHash) );
584 } else {
585 if (!this._pluginExtendedMethods[pluginName]) this._pluginExtendedMethods[pluginName] = [];
586
587 switch (key) {
588 case 'frame':
589 klass = Frame
590 break;
591 case 'hand':
592 klass = Hand
593 break;
594 case 'pointable':
595 klass = Pointable;
596 _.extend(Finger.prototype, functionOrHash);
597 _.extend(Finger.Invalid, functionOrHash);
598 break;
599 case 'finger':
600 klass = Finger;
601 break;
602 default:
603 throw pluginName + ' specifies invalid object type "' + key + '" for prototypical extension'
604 }
605
606 _.extend(klass.prototype, functionOrHash);
607 _.extend(klass.Invalid, functionOrHash);
608 this._pluginExtendedMethods[pluginName].push([klass, functionOrHash])
609 }
610 }
611 return this;
612};
613
614/*
615 * Stop using a used plugin. This will remove any of the plugin's pipeline methods (those called on every frame)
616 * and remove any methods which extend frame-object prototypes.
617 *
618 * @method stopUsing
619 * @memberOf Leap.Controller.prototype
620 * @param pluginName
621 * @returns the controller
622 */
623Controller.prototype.stopUsing = function (pluginName) {
624 var steps = this._pluginPipelineSteps[pluginName],
625 extMethodHashes = this._pluginExtendedMethods[pluginName],
626 i = 0, klass, extMethodHash;
627
628 if (!this.plugins[pluginName]) return;
629
630 if (steps) {
631 for (i = 0; i < steps.length; i++) {
632 this.pipeline.removeStep(steps[i]);
633 }
634 }
635
636 if (extMethodHashes){
637 for (i = 0; i < extMethodHashes.length; i++){
638 klass = extMethodHashes[i][0]
639 extMethodHash = extMethodHashes[i][1]
640 for (var methodName in extMethodHash) {
641 delete klass.prototype[methodName]
642 delete klass.Invalid[methodName]
643 }
644 }
645 }
646
647 delete this.plugins[pluginName]
648
649 return this;
650}
651
652Controller.prototype.useRegisteredPlugins = function(){
653 for (var plugin in Controller._pluginFactories){
654 this.use(plugin);
655 }
656}
657
658
659_.extend(Controller.prototype, EventEmitter.prototype);