Commits

Matthew Schinckel committed 72e637f

Documentation.

Comments (0)

Files changed (2)

 
     <script>
       $(function() {
-        var g = Garmin.Communicator();
+        var g = new Garmin.Communicator();
         g.selectDevice(0);
         // Read activities from the device.
         g.readActivities(function(data) {
       });
     </script>
     
-This will inject the plugin into the page if it isn't already there, create a new instance of the `Communicator` controller, and start looking for devices right away.
+This will inject the plugin into the page if it isn't already there, create a new instance of the `Garmin.Communicator` controller, and start looking for devices right away.
 
 If there is already an instance of the controller, it will just return that.
 
 You may pass an object to the constructor which can have a property `unlockCodes`, used to pass in the garmin unlock codes for your site:
 
-    var g = Garmin.Communicator({unlockCodes: ['foo', 'bar']});
+    var g = Garmin.Communicator({unlockCodes: ["foo", "bar"]});
+    
+You may generate Garmin Communicator unlock codes at [Garmin Communicator API Key Generator](https://my.garmin.com/api/communicator/key-generator.jsp). You will need to be registered to generate keys.
 
-There are only a handful of operations that are exposed:
+Not every method that the plugin (or the original code) has has been exposed: I've only added the bits that I needed as I went along.
 
-### findDevices
-
-Tell the plugin to search for devices. Accepts a callback function that is passed the list of devices that were found.
-
-    plugin.findDevices(function(deviceList) {
-      // Do something here with the deviceList.
-    });
-
-### selectDevice
-
-Make a device active for communication. There may only be one active device at a time. Argument is the index of the device from `findDevices` above.
-
-    plugin.selectDevice(0);
-
-TODO: allow for passing in the device object to `selectDevice()`.
-
-### activeDevice
+There is more documentation, inline in the source file, but can be viewed in a nice way by cloning the repository and opening `doc/index.html` in a browser.
 //
 // Written for jQuery, instead of Prototype.
 //
+// This plugin handler also uses a different pattern than the original
+// Garmin plugin. Instead of registering a callback to be executed when
+// a signal is generated ({{{onFinishFindDevices}}}), I have chosen to
+// allow passing in a callback function at the time the event is started.
 
 // ==== Details ====
-// Matthew Schinckel [[mailto:matt@schinckel.net|matt@schinckel.net]]
+// * Author: Matthew Schinckel [[mailto:matt@schinckel.net|matt@schinckel.net]]
+// * Website: 
 
+// == 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.");
+      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.");
+    throw new Error(
+      "Garmin namespace appears to exist, and it's not us. Bailing out."
+    );
   }
   
   var Garmin = window.Garmin = {};
   
-  // == Garmin.Plugin ==
+  // == Plugin ==
   // 
   // Return a reference to the plugin object. You probably don't need to access
   // this function directly.
   
   // == Fitness Types ==
   //
-  // The supported transfer types.
+  // These are used by the dynamic function creators to add the
+  // relevant methods: for instance, there will be a method:
   //
-  // TODO: Goals do not seem to work just yet. Data format error?
+  // {{{readActivities(callback)}}}
+  //
+  // which takes a callback function that will be executed and 
+  // passed the fetched data, but also a method:
+  //
+  // {{{writeActivities(data, callback)}}} 
   
   Garmin.FITNESS_TYPES = {
     Activities: ['FitnessHistory', 'FitnessDirectory'],
     FITActivities: ['FIT_TYPE_4', 'FITDirectory']
   };
   
-  // == Communicator ==
+  // == Garmin.Communicator ==
+  //
+  // This is probably the way you will use this code.
+  //
+  // {{{ var garmin = new Garmin.Communicator({unlockCodes: [<domain>, <code>]});}}}
+  //
+  // You then can perform actions on the unlocked plugin.
+  //
+  // {{{unlockCodes}}} is currently the only value that will be examined.
+  
   Garmin.Communicator = function Communicator(options) {
     options = options || {};
     
     var inUse = false;
     var currentDevice = null;
     var $plugin = $(plugin);
+    // === Unlock Codes ===
+    // You may generate unlock codes at
+    //
+    // https://my.garmin.com/api/communicator/key-generator.jsp
+    //
+    // These ones will do for local development.
     var unlockCodes = [
       "file:///","cb1492ae040612408d87cc53e3f7ff3c",
       "http://localhost","45517b532362fc3149e4211ade14c9b2",
     // === Device ===
     //
     // Objects of this class will be provided by the Communicator object.
+    // 
+    // You can inspect a {{{Device}}} to get some properties about it:
+    // mostly it's name, and the operations it is able to perform.
     var Device = function Device(number) {
       var deviceData = parseXML(plugin.DeviceDescription(number));
       var WRITE = 'Input';
         };
       }
       
+      // ==== read-only properties.
+      // * displayName
+      // * partNumber
+      // * softwareVersion
+      
       // Edge 500 uses Description node rather than DisplayName
       if (readData('DisplayName')() == "") {
         this.displayName = readData('Description');
       this.partNumber = readData('Model PartNumber');
       this.softwareVersion = readData('Model SoftwareVersion');
       
+      // ==== canRead<thing> / canWrite<thing> ====
+      // This enables us to do things like {{{device.canWriteWorkouts()}}},
+      // which returns a boolean value indicating if the indicated
+      // device can perform the desired action.
+      
       _.each(Garmin.FITNESS_TYPES, function(data, type) {
         this['canRead' + type] = canXY(READ, data[0]);
         this['canWrite' + type] = canXY(WRITE, data[0]);
       }, this);
     };
     
-    //************** Helpers **************//
-    
+    // === Private Helper Functions ===
+    // Nothing to see here... :)
     var parseXML = function(xml) {
       return $($.parseXML(xml));
     };
       });
       return message;
     };
-        
-    //************** Handlers **************//
     
-    this.findDevices = function(callback) {
-      checkUnlocked();
-      if (inUse) {
-        // communication in progress
-        if (inUse !== "findDevices") {
-          throw new Error("Already attempting to " + inUse);
-        }
-      } else {
-        inUse = "findDevices";
-        plugin.StartFindDevices();        
-      }
-      
-      var finishFindDevices = function() {
-        if (plugin.FinishFindDevices()) {
-          var data = parseXML(plugin.DevicesXmlString());
-          devices = [];
-          _.each(data.find('Device'), function(el,i) {
-            devices.push(new Device(i));
-          });
-          inUse = false;
-          if (callback) {
-            callback(devices);
-          }
-        } else {
-          // Keep checking every .1 sec until we finish.
-          setTimeout(finishFindDevices, 100);
-        }        
-      };
-      
-      // Check in 100 ms if we have finished.
-      setTimeout(finishFindDevices, 100);
-    };
-    
-    this.cancelFindDevices = plugin.CancelFindDevices;
-    
-    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];
-    };
-    
-    this.activeDevice = function() {
-      return devices[currentDevice];
-    };
-            
-    var baseHandler = function(readWrite, dataType, pluginMethod, name, param, callback, progress) {
-      if (devices.length === 0) {
-        throw new Error("No devices found");
-      }
-      if (currentDevice === null) {
-        throw new Error("No device selected");
-        return;
-      }
-      if (inUse) {
-        throw new Error("Process '" + inUse + "' already in progress.");
-        return;
-      }
-      var device = devices[currentDevice];
-      var toFrom;
-      // Ensure readWrite is "Read" or "Write".
-      if (readWrite.toLowerCase() === 'write') {
-        toFrom = "To";
-        readWrite = "Write";
-      } else {
-        toFrom = "From";
-        readWrite = "Read";
-      }
-      inUse = readWrite.toLowerCase() + name;
-      plugin['Start' + readWrite + pluginMethod](currentDevice, dataType, param);
-      
-      // This handler will set a timeout and execute again if an interaction is in progress.
-      var finishHandler = function finishHandler() {
-        var status = plugin['Finish' + readWrite + pluginMethod]();
-        switch (status) {
-          case Garmin.STATUS.working:
-            if (progress) {
-              progress(getProgress());
-            };
-            setTimeout(finishHandler, 200);
-            break;
-          case Garmin.STATUS.finished:
-            inUse = false;
-            // For safety, when we have done a write, we want to empty the buffer.
-            if (readWrite == "Write") {
-              plugin.TcdXml = "";
-            }
-            // 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);
-            }
-            break;
-          case Garmin.STATUS.waiting:
-            // Not sure how to handle this!
-            break;
-          case Garmin.STATUS.idle:
-            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:
-            throw new Error("Received an unknown status: " + status);
-            break;
-        }
-      };
-      
-      // Kick the handler repeat sequence off now.
-      finishHandler();
-    };
-    
-    var readHandler = function(dataType, pluginMethod, type, param) {
-      return function(callback){
-        // 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);
-      };
-    };
-    
-    var writeHandler = function(dataType, pluginMethod, type, param) {
-      return function(data, 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);
-      };
-    };
-    
-    _.each(Garmin.FITNESS_TYPES, function(data, type) {
-      this['read' + type] = readHandler(data[0], data[1], type);
-      this['write' + type] = writeHandler(data[0], data[1], type);
-    }, this);
-
-    // simple interface to reading FIT or TCX files, switching of base methods
-    // performed according to current device support
-    this.readFile = function (fileName, callback) {
-      if (this.activeDevice().canReadFITActivities()) {
-        var data = plugin.GetBinaryFile(currentDevice, fileName, false);
-        var binary_data = decodeBinaryFile(data);
-        if (callback) {
-          callback(binary_data);
-        }
-      } else {
-        readHandler('FitnessHistory', 'FitnessDetail', 'FitnessHistory', fileName)(callback);
-      }
-    }
-
     var decodeBinaryFile = function (input) {
       var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
       var output = [];
       } while (i < input.length);
 
       return new Uint8Array(output);
+    };
+    
+    // === Exposed API methods ===
+    //
+    // These methods may accept a callback function, that
+    // will be executed when the operation has completed.
+    
+    // ==== findDevices(callback) ====
+    // 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) {
+      checkUnlocked();
+      if (inUse) {
+        // communication in progress
+        if (inUse !== "findDevices") {
+          throw new Error("Already attempting to " + inUse);
+        }
+      } else {
+        inUse = "findDevices";
+        plugin.StartFindDevices();        
+      }
+      
+      // ===== finishFindDevices =====
+      //
+      // Whilst the plugin is all asynchronous and stuff, there
+      // doesn't seem to be a way to listen for events, or to provide
+      // a callback to be executed on completion for this call,
+      // so we need to set a timeout, and re-set this timeout
+      // if we have not finished at that time.
+      var finishFindDevices = function() {
+        if (plugin.FinishFindDevices()) {
+          var data = parseXML(plugin.DevicesXmlString());
+          devices = [];
+          _.each(data.find('Device'), function(el,i) {
+            devices.push(new Device(i));
+          });
+          inUse = false;
+          if (callback) {
+            callback(devices);
+          }
+        } else {
+          // Keep checking every .1 sec until we finish.
+          setTimeout(finishFindDevices, 100);
+        }        
+      };
+      
+      // Check in 100 ms if we have finished.
+      setTimeout(finishFindDevices, 100);
+    };
+    
+    // ==== cancelFindDevices() ====
+    // Simple passthrough to the plugin. Will cancel a findDevices call
+    // if one is in progress.
+    this.cancelFindDevices = plugin.CancelFindDevices;
+    
+    // ==== selectDevice(d) ====
+    // Select the Device with index {{{d}}}.
+    //
+    // This device is also returned by the function.
+    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];
+    };
+    
+    // ==== activeDevice() ====
+    // Returns the {{{Device}}} object that is currently selected.
+    this.activeDevice = function() {
+      return devices[currentDevice];
+    };
+    
+    // ==== Private function: baseHandler ====
+    // This code probably hurts to look at. It allows us to encapsulate
+    // the functionality of the reading and writing processes into a
+    // single function, and dynamically create methods based on this.
+    //
+    // A fair chunk of this is just sanity checking (making sure we have
+    // a "Read" or a "Write", for instance).
+    var baseHandler = function(
+      readWrite,
+      dataType,
+      pluginMethod,
+      name,
+      param,
+      callback,
+      progress
+    ) {
+      if (devices.length === 0) {
+        throw new Error("No devices found");
+      }
+      if (currentDevice === null) {
+        throw new Error("No device selected");
+        return;
+      }
+      if (inUse) {
+        throw new Error("Process '" + inUse + "' already in progress.");
+        return;
+      }
+      var device = devices[currentDevice];
+      var toFrom;
+      // Ensure readWrite is "Read" or "Write".
+      if (readWrite.toLowerCase() === 'write') {
+        toFrom = "To";
+        readWrite = "Write";
+      } else {
+        toFrom = "From";
+        readWrite = "Read";
+      }
+      inUse = readWrite.toLowerCase() + name;
+      plugin['Start' + readWrite + pluginMethod](currentDevice, dataType, param);
+      
+      // ===== finishHandler ====
+      // This handler will set a timeout and execute again if an interaction is in progress.
+      var finishHandler = function finishHandler() {
+        var status = plugin['Finish' + readWrite + pluginMethod]();
+        switch (status) {
+          case Garmin.STATUS.working:
+            if (progress) {
+              progress(getProgress());
+            };
+            setTimeout(finishHandler, 200);
+            break;
+          case Garmin.STATUS.finished:
+            inUse = false;
+            // For safety, when we have done a write, we want to empty the buffer.
+            if (readWrite == "Write") {
+              plugin.TcdXml = "";
+            }
+            // 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);
+            }
+            break;
+          case Garmin.STATUS.waiting:
+            // Not sure how to handle this!
+            break;
+          case Garmin.STATUS.idle:
+            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:
+            throw new Error("Received an unknown status: " + status);
+            break;
+        }
+      };
+      
+      // Kick the handler repeat sequence off now.
+      finishHandler();
+    };
+    
+    // ==== Private function: readHandler ====
+    // Even more metaprogramming. This function creates a function
+    // that will read one data type. See below for how it is used.
+    // The created function will take a callback function, that will
+    // be executed with the relevant data when the read operation is
+    // complete.
+    var readHandler = function(dataType, pluginMethod, type, param) {
+      return function(callback){
+        // 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);
+      };
+    };
+    
+    // ==== Private function: writeHandler ====
+    // Very similar to readHandler, but the function that is created takes
+    // a required argument: {{{data}}}, as well as a possible callback
+    // function.
+    var writeHandler = function(dataType, pluginMethod, type, param) {
+      return function(data, 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);
+      };
+    };
+    
+    // ==== Generate those dynamic methods ====
+    _.each(Garmin.FITNESS_TYPES, function(data, type) {
+      this['read' + type] = readHandler(data[0], data[1], type);
+      this['write' + type] = writeHandler(data[0], data[1], type);
+    }, this);
+
+    // ==== readFile ====
+    // simple interface to reading FIT or TCX files, switching of base methods
+    // performed according to current device support.
+    this.readFile = function(fileName, callback) {
+      if (this.activeDevice().canReadFITActivities()) {
+        var data = plugin.GetBinaryFile(currentDevice, fileName, false);
+        var binary_data = decodeBinaryFile(data);
+        if (callback) {
+          callback(binary_data);
+        }
+      } else {
+        readHandler('FitnessHistory', 'FitnessDetail', 'FitnessHistory', fileName)(callback);
+      }
     }
-
-    // See if the plugin is actually installed.
+    
+    // === Initialisation ===
+    // 
+    // See if the plugin is actually installed. If not, then...
+    //
+    // If it is, then we attempt to unlock it with whatever codes
+    // we have access to.
+    //
+    // Finally, we will kick off a {{{findDevices()}}} call.
     if (!plugin.Unlock) {
       this.installed = false;
       this.installationInstructions = {
         heading: "Plugin not installed",
         message: [
           "The Garmin Communicator plugin could not be loaded.",
-          "<a href='http://www.garmin.com/products/communicator'>Install the plugin</a>."
+          "<a href='http://www.garmin.com/products/communicator'>" + 
+          "Install the plugin</a>."
         ]
       };
       // throw new Error("Could not find Communicator Plugin");