1. Matthew Schinckel
  2. garmin-plugin

Commits

Matthew Schinckel  committed 5e6a952

Use promises instead of callbacks.

  • Participants
  • Parent commits 1a34217
  • Branches default

Comments (0)

Files changed (3)

File contrib/knockout.garmin.js

View file
  • Ignore whitespace
-var GarminViewModel = function GarminViewModel(options) {
-  var plugin = new Garmin.Communicator(options);
-  var devices = ko.observableArray([]);
-  var selectedDevice = ko.observable(null);
-  var busy = ko.observable(false);
+define(['knockout', 'garmin'], function(ko, Garmin) {
+  var GarminViewModel = function GarminViewModel(options) {
+    var plugin = new Garmin.Communicator(options);
+    var devices = ko.observableArray([]);
+    var selectedDevice = ko.observable(null);
+    var busy = ko.observable(false);
   
-  this.pluginNotInstalled = !plugin.installed;
+    this.pluginNotInstalled = !plugin.installed;
   
-  if (plugin.installed) {
-    plugin.findDevices(devices);
-  }
+    if (plugin.installed) {
+      plugin.findDevices().then(function(deviceList) {
+        devices(deviceList);
+      });
+    }
   
-  // Observable properties
-  this.busy = ko.computed(function(){
-    return busy();
-  });
+    // Observable properties
+    this.busy = ko.computed(function(){
+      return busy();
+    });
   
-  this.selectedDevice = ko.computed({
-    read: selectedDevice,
-    write: function(data) {
-      var index = devices.indexOf(data);
-      if (index === -1) { index = null; }
-      plugin.selectDevice(index);
-      selectedDevice(data);
+    this.selectedDevice = ko.computed({
+      read: selectedDevice,
+      write: function(data) {
+        var index = devices.indexOf(data);
+        if (index === -1) { index = null; }
+        plugin.selectDevice(index);
+        selectedDevice(data);
+      }
+    });
+  
+    this.deselectDevice = function() {
+      plugin.selectDevice(null);
+      selectedDevice(null);
     }
-  });
   
-  this.deselectDevice = function() {
-    plugin.selectDevice(null);
-    selectedDevice(null);
-  }
+    this.devices = ko.computed(function() {
+      return devices();
+    });
   
-  this.devices = ko.computed(function() {
-    return devices();
-  });
+    this.needSelection = ko.computed(function() {
+      return !selectedDevice() && devices().length > 1;
+    });
   
-  this.needSelection = ko.computed(function() {
-    return !selectedDevice() && devices().length > 1;
-  });
+    this.readActivities = plugin.readActivities;
   
-  this.readActivities = plugin.readActivities;
+    this.readWorkouts = plugin.readWorkouts;
+    this.writeWorkouts = plugin.writeWorkouts;
   
-  this.readWorkouts = plugin.readWorkouts;
-  this.writeWorkouts = plugin.writeWorkouts;
+    // Remove after debugging.
+    this._plugin = plugin;
+  };
   
-  // Remove after debugging.
-  this._plugin = plugin;
-};
+  return GarminViewModel;
+});

File src/garmin.js

View file
  • Ignore whitespace
 // Look in {{{contrib/knockout.garmin.js}}} for an example/base ViewModel
 // for use in KnockoutJS.
 
-// == Setup ==
-//
-// We want to make sure that we aren't installed into a page that already
-// has the original {{{Garmin}}} plugin handler, or something else that
-// is using the {{{Garmin}}} namespace. Just bail out with an error if
-// we have anything there (unless it is ourself, in which case just
-// exit now, as we are already installed).
-(function($, undefined){
-  if (window.Garmin) {
-    // This either means we have already been executed, or the old 
-    // JS API is installed.
-    if (window.Garmin.DevicePlugin) {
-      throw new Error(
-        "Garmin Communicator API (Prototype version) already installed."
-      );
-    }
-    if (window.Garmin.Communicator) {
-      return;
-    }
-    throw new Error(
-      "Garmin namespace appears to exist, and it's not us. Bailing out."
-    );
-  }
-  
-  var Garmin = window.Garmin = {};
+define(['jquery', 'underscore'], function($, _){
+
+  var Garmin = {};
   
   // == Plugin ==
   // 
   // == Fitness Types ==
   //
   // These are used by the dynamic function creators to add the
-  // relevant methods: for instance, there will be a method:
+  // relevant methods: for instance, there will be methods:
   //
-  // {{{readActivities(callback)}}}
+  // {{{readActivities()}}}
   //
-  // which takes a callback function that will be executed and 
-  // passed the fetched data, but also a method:
+  // {{{writeActivities(data)}}} 
   //
-  // {{{writeActivities(data, callback)}}} 
+  // These return a Promise to the result of the action: which will
+  // allow for handling of success/failure callbacks.
   
   Garmin.FITNESS_TYPES = {
     Activities: ['FitnessHistory', 'FitnessDirectory'],
       "http://localhost","45517b532362fc3149e4211ade14c9b2",
       "http://127.0.0.1","40cd4860f7988c53b15b8491693de133"
     ];
-    
-    // Data we store.
-    var devices = [];
-    
+        
     // === Device ===
     //
     // Objects of this class will be provided by the Communicator object.
       return unlocked;
     };
     
+    // Unlock the plugin (or die trying)
+    unlock(_.union(unlockCodes, options.unlockCodes || []));
+    
     var checkUnlocked = function() {
       if (plugin.Locked) {
         throw new Error("Plugin is not unlocked.");
     // These methods may accept a callback function, that
     // will be executed when the operation has completed.
     
-    // ==== findDevices(callback) ====
+    // ==== findDevices() ====
     // Look for devices that are known to the plugin.
-    // The list of devices will be passed to the 
-    // callback function, if provided.
-    this.findDevices = function(callback) {
+    // Returns a Promise to the list of devices.
+    var findDevices = function() {
+      var deferred = $.Deferred();
       checkUnlocked();
       if (inUse) {
         // communication in progress
       var finishFindDevices = function() {
         if (plugin.FinishFindDevices()) {
           var data = parseXML(plugin.DevicesXmlString());
-          devices = [];
-          _.each(data.find('Device'), function(el,i) {
-            devices.push(new Device(i));
+          var devices = $.map(data.find('Device'), function(el, i) {
+            return new Device(i);
           });
           inUse = false;
-          if (callback) {
-            callback(devices);
-          }
+          deferred.resolve(devices);
         } else {
           // Keep checking every .1 sec until we finish.
           setTimeout(finishFindDevices, 100);
       
       // Check in 100 ms if we have finished.
       setTimeout(finishFindDevices, 100);
+      return deferred.promise();
     };
     
+    var devices = findDevices();
+    var deviceList = [];
+    
+    // ==== findDevices() ====
+    this.findDevices = function() {
+      devices = findDevices();
+      devices.then(function(devs) {
+        deviceList = devs;
+      });
+      return devices.promise();
+    }
     // ==== cancelFindDevices() ====
     // Simple passthrough to the plugin.
-    // Will cancel a findDevices call
-    // if one is in progress.
-    this.cancelFindDevices = plugin.CancelFindDevices;
+    // Will cancel a findDevices call if one is in progress.
+    // Will also cause the Promise created by findDevices() to fail.
+    this.cancelFindDevices = function(){
+      devices.reject();
+      plugin.CancelFindDevices;
+    }
     
     // ==== selectDevice(d) ====
     // Select the Device with index {{{d}}}.
     //
-    // This device is also returned by the function.
+    // This method also returns a Promise to the selected device.
     this.selectDevice = function(d) {
-      // Ensure device number is 0..devices.length;
-      if (isNaN(d) || d < 0 || d >= devices.length) {
-        // Different error type?
-        currentDevice = null;
-        throw new Error("Invalid device number");
-      }
-      currentDevice = d;
-      return devices[d];
+      var device = $.Deferred();
+      
+      devices.then(
+        function(data) {
+          if (isNaN(d) || d < 0 || d > data.length) {
+            currentDevice = null;
+            throw new Error("Invalid device number");
+          }
+          currentDevice = d;
+          device.resolve(data[d]);
+        }
+      );
+      
+      return device.promise();
     };
     
     // ==== activeDevice() ====
     // Returns the {{{Device}}} object that is currently selected.
-    this.activeDevice = function() {
-      return devices[currentDevice];
+    // Note this does not return a Promise.
+    var activeDevice = this.activeDevice = function() {
+      return deviceList[currentDevice];
     };
     
     // ==== Private function: baseHandler ====
       dataType,
       pluginMethod,
       name,
-      param,
-      callback,
-      progress
+      param
     ) {
       if (devices.length === 0) {
         throw new Error("No devices found");
         throw new Error("Process '" + inUse + "' already in progress.");
         return;
       }
-      var device = devices[currentDevice];
+      var device = activeDevice();
       var toFrom;
       // Ensure readWrite is "Read" or "Write".
       if (readWrite.toLowerCase() === 'write') {
       inUse = readWrite.toLowerCase() + name;
       plugin['Start' + readWrite + pluginMethod](currentDevice, dataType, param);
       
+      var deferred = $.Deferred();
+      
       // ===== finishHandler ====
       // This handler will set a timeout and execute 
       // again if an interaction is in progress.
         var status = plugin['Finish' + readWrite + pluginMethod]();
         switch (status) {
           case Garmin.STATUS.working:
-            if (progress) {
-              progress(getProgress());
-            };
+            deferred.notify(getProgress());
             setTimeout(finishHandler, 200);
             break;
           case Garmin.STATUS.finished:
             // we want to empty the buffer.
             if (readWrite == "Write") {
               plugin.TcdXml = "";
+              plugin.DirectoryListingXml = "";
             }
-            // If we were passed in a callback function, 
-            // we call it with either the data we got from 
-            // the plugin, or true (on a write).
-            if (callback) {
-              // check both buffers (latter is used by FIT devices)
-              callback(plugin.TcdXml || plugin.DirectoryListingXml || true);
-            }
+            deferred.resolve(plugin.TcdXml || plugin.DirectoryListingXml || true);
             break;
           case Garmin.STATUS.waiting:
             // Not sure how to handle this!
             break;
           case Garmin.STATUS.idle:
+            deferred.reject();
             inUse = false;
             // This could happen if we were idle: perhaps the 
             // process was cancelled. That's cool, we'll just not set a Timeout.
             break;
           default:
+            deferred.reject();
             throw new Error("Received an unknown status: " + status);
             break;
         }
       
       // Kick the handler repeat sequence off now.
       finishHandler();
+      
+      return deferred.promise();
     };
     
     // ==== Private function: readHandler ====
         // Empty the data store, so we don't get bogus 
         // data if the event cannot complete.
         plugin.TcdXml = "";
-        baseHandler('Read', dataType, pluginMethod, type, param, callback);
+        return baseHandler('Read', dataType, pluginMethod, type, param, callback);
       };
     };
     
         // Strip out any newlines, as they break things.
         plugin.TcdXml = data.replace(/[\n\r]+/gm, '');
         plugin.FileName = "";
-        baseHandler('Write', dataType, pluginMethod, type, param, callback);
+        return baseHandler('Write', dataType, pluginMethod, type, param, callback);
       };
     };
     
     // ==== readFile ====
     // simple interface to reading FIT or TCX files, switching of base methods
     // performed according to current device support.
-    this.readFile = function(fileName, callback) {
+    this.readFile = function(fileName) {      
       if (this.activeDevice().canReadFITActivities()) {
+        var deferred = $.Deferred();
         var data = plugin.GetBinaryFile(currentDevice, fileName, false);
         var binary_data = decodeBinaryFile(data);
-        if (callback) {
-          callback(binary_data);
-        }
+        deferred.resolve(binary_data);
+        return deferred.promise();
       } else {
-        readHandler(
+        return readHandler(
           'FitnessHistory',
           'FitnessDetail',
           'FitnessHistory',
     
     this.installed = true;
     
-    // Unlock the plugin (or die trying)
-    unlock(_.union(unlockCodes, options.unlockCodes || []));
     
     // wait for other initialisation to finish, then search for devices.
-    setTimeout(this.findDevices, 0);
+    // setTimeout(this.findDevices, 0);
   };
-})(jQuery);
+  
+  return Garmin;
+});

File tests/basic_test.html

View file
  • Ignore whitespace
 <html>
-  <head></head>
+  <head>
+    <script src="../vendor/require.js"></script>
+    <script>
+    require.config({
+      paths: {
+        'jquery': '../vendor/jquery-1.7.2',
+        'underscore': '../vendor/underscore-1.3.3',
+        'garmin': '../src/garmin'
+      },
+      shim: {
+        'underscore': {exports: '_'}
+      },
+      urlArgs: "bust=" + (new Date().valueOf())
+    });
+    require(['jquery', 'garmin'], function($, Garmin) {
+      var g = new Garmin.Communicator();
+      
+      $(function() {
+        
+        g.selectDevice(0).then(
+          function() {
+            g.readActivities().then(
+              function(data){
+                $('textarea').text(data);
+              });
+          }
+        );
+        
+      });
+    });
+    </script>
+  </head>
   <body>
-    <script src="../vendor/jquery-1.7.2.js"></script>
-    <script src="../vendor/underscore-1.3.3.js"></script>
-    <script src="../src/garmin.js"></script>
-    <script>
-    var Controller = function() {
-      var plugin = new Garmin.Communicator(this, true);
-      
-      this.interactionError = function(data) {
-        throw new Error(data.message);
-      };
-      
-      this.deviceListUpdate = function(data) {
-        console.log(data.devices);
-        plugin.selectDevice(1);
-      };
-      
-      this.readActivities = plugin.readActivities;
-      
-      this.readActivitiesComplete = function(data) {
-        console.log(data.data.length);
-      };
-    };
-    var c = new Controller();
-    </script>
     <textarea>
       
     </textarea>