• File not uploaded, so I copied the code here.
    Please find here the example firmware

    //======================================­========================================­====
    // Copyright (c) 2014 Giovanni Varasano.
    // v4 - last update 06/06/2014
    // ========================================­========================================­==
    
    var fs = require("fs");
    
    var  ST = { IDLE: 0, FIX:1, RUN:2 };
    tmrId=0;
    file_ok=0, gps_ok = 0;
    gps_path="";
    gps_date= "", gps_time=""; gps_msec=0;
    gps_lat=0, gps_lon=0, gps_speed =0; gps_course=0;
    ir_red=0.0,ir_nir=0.0,irr_edg=0.0,ir_ndr­e=0.0,ir_ndvi=0.0;
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    var WORK    = LED2; 
    var FIX     = LED3;
    var STOP    = LED4; 
    
    // ----------------------------------------­-----------------
    // ----------------------------------------­-----------------
    btn_state = false;
    var btn_id = setWatch(function(e) { btn_state = !btn_state; } ,B6,{ repeat : true, edge : "both", debounce : 350});
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function onInit() {
      setTimeout(function(){
        file_ok=0, gps_ok = 0;
        Serial6.setup(19200, {rx:C7,tx:C6});
        Serial6.print("$JBAUD,115200\r\n");
        
        setTimeout(function(){
            Serial6.setup(115200, {rx:C7,tx:C6});
            Serial4.setup(38400, {rx:C11,tx:C10});
            tracking("RMC");
            tracking("GGA");
            tmrId = go_ahead();
           }, 3000 );
        }, 2000 );
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function go_ahead() {
        state=ST.IDLE
        return  setInterval( function() {
             switch(state) {
    
                case ST.IDLE: file_ok=0;STOP.set();FIX.reset();WORK.re­set();
                             if (btn_state ) { STOP.reset(); state=ST.FIX; }
    	     	         break;
    
                case ST.FIX: if (!btn_state ) {  state=ST.IDLE }
                             FIX.set();
                             if(file_ok === 1) {FIX.reset();state=ST.RUN;work=WORK.star­tFlashing(250);}
    	                 break;
    
                case ST.RUN: if (!btn_state ) {state=ST.IDLE; WORK.stopFlashing(work);}
                             save_data();
    	                 break;
            }
        },50); 
    }
    
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function save_data() {
       if ( gps_ok ) {
         var s_data  = gps_date+","+gps_time+"."+gps_msec+","+g­ps_speed+","+gps_course+",";
         s_data += utm.toString()+","+ir_red+","+ir_nir+","­+irr_edg+","+ir_ndre+","+ir_ndvi+"\r\n";­
         fs.appendFileSync(gps_path,s_data);
         gps_ok=0;
       }
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function LatLng(lat,lng,alt) {
      this.lat = lat;
      this.lng = lng;
      this.alt = alt;
      this.toUTMRef = LatLngToUTMRef;
      this.toString = LatLngToString;
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function LatLngToString() {
      return "(" + this.lat + ", " + this.lng + ")";
    }
    
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function UTMRef(easting, northing, altitude, latZone, lngZone) {
      this.easting  = Number(easting).toFixed(3);   
      this.northing = Number(northing).toFixed(3);  
      this.altitude = Number(altitude).toFixed(3); 
      this.latZone = latZone;
      this.lngZone = lngZone;
     
      this.toString = UTMRefToString;
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function UTMRefToString() {
      return this.easting + "," + this.northing+ ","+this.altitude+","+this.latZone+this.­lngZone+"";
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function RefEll(maj, min) {
      this.maj = maj;
      this.min = min;
      this.ecc = ((maj * maj) - (min * min)) / (maj * maj);
    }
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function LatLngToUTMRef() {
      var wgs84 = new RefEll(6378137, 6356752.314);
      var UTM_F0   = 0.9996;
      var a = wgs84.maj;
      var eSquared  = wgs84.ecc;
      var longitude = this.lng;
      var latitude  = this.lat;
      var altitude  = this.alt;
    
      var latitudeRad   = latitude  * (Math.PI / 180.0);
      var longitudeRad  = longitude * (Math.PI / 180.0);
      var longitudeZone = Math.floor((Number(longitude) + 180.0) / 6.0) + 1;
      
      if (latitude >= 56.0  && latitude < 64.0 && longitude >= 3.0  && longitude < 12.0) {
        longitudeZone = 32;
      }
    
      if (latitude >= 72.0 && latitude < 84.0) {
        if (longitude >= 0.0 && longitude < 9.0) {
          longitudeZone = 31;
        } else if (longitude >= 9.0 && longitude < 21.0) {
          longitudeZone = 33;
        } else if (longitude >= 21.0 && longitude < 33.0) {
          longitudeZone = 35;
        } else if (longitude >= 33.0 && longitude < 42.0) {
          longitudeZone = 37;
        }
      }
      
      var longitudeOrigin = (longitudeZone - 1) * 6 - 180 + 3;
      var longitudeOriginRad = longitudeOrigin * (Math.PI / 180.0);
    
      
      var UTMZone = getUTMLatitudeZoneLetter(latitude);
      
      ePrimeSquared = (eSquared) / (1 - eSquared);
    
      var n = a / Math.sqrt(1 - eSquared * Math.sin(latitudeRad) * Math.sin(latitudeRad));
      var t = Math.tan(latitudeRad) * Math.tan(latitudeRad);
      var c = ePrimeSquared * Math.cos(latitudeRad) * Math.cos(latitudeRad);
      var A = Math.cos(latitudeRad) * (longitudeRad - longitudeOriginRad);
    
     var M = a * (
        (1  - eSquared / 4 - 
         3 * eSquared * eSquared / 64  - 
         5 * eSquared * eSquared * eSquared / 256)  * latitudeRad  - 
       (3 * eSquared / 8  + 3 * eSquared * eSquared / 32 + 45 * eSquared * eSquared * eSquared / 1024)  * 
       Math.sin(2 * latitudeRad) + (15 * eSquared * eSquared / 256   + 45 * eSquared * eSquared * eSquared / 1024)  * 
       Math.sin(4 * latitudeRad)  - (35 * eSquared * eSquared * eSquared / 3072)* Math.sin(6 * latitudeRad));
    
     var UTMEasting =
        (UTM_F0   * n * 
         (A  + (1 - t + c) * Math.pow(A, 3.0) / 6 + 
          (5 - 18 * t + t * t + 72 * c - 58 * ePrimeSquared) * 
          Math.pow(A, 5.0)  / 120)   + 500000.0);
     
      var UTMNorthing = (UTM_F0 * (M   + n * Math.tan(latitudeRad) * 
                  (A * A / 2 + (5 - t + (9 * c) + (4 * c * c)) *
              Math.pow(A, 4.0) / 24 + (61 - (58 * t) + (t * t) + (600 * c) - 
         (330 * ePrimeSquared)) * Math.pow(A, 6.0) / 720))
       );
    
      if (latitude < 0) { UTMNorthing += 10000000.0; }
    
      return new UTMRef(UTMEasting, UTMNorthing, altitude, UTMZone, longitudeZone);
    }
    
    
    //--------------------------------------­------------------------------------
    //--------------------------------------­------------------------------------
    function getUTMLatitudeZoneLetter(latitude) {
      if ((84 >= latitude) && (latitude >= 72)) return "X";
      else if (( 72 > latitude) && (latitude >=  64)) return "W";
      else if (( 64 > latitude) && (latitude >=  56)) return "V";
      else if (( 56 > latitude) && (latitude >=  48)) return "U";
      else if (( 48 > latitude) && (latitude >=  40)) return "T";
      else if (( 40 > latitude) && (latitude >=  32)) return "S";
      else if (( 32 > latitude) && (latitude >=  24)) return "R";
      else if (( 24 > latitude) && (latitude >=  16)) return "Q";
      else if (( 16 > latitude) && (latitude >=   8)) return "P";
      else if ((  8 > latitude) && (latitude >=   0)) return "N";
      else if ((  0 > latitude) && (latitude >=  -8)) return "M";
      else if (( -8 > latitude) && (latitude >= -16)) return "L";
      else if ((-16 > latitude) && (latitude >= -24)) return "K";
      else if ((-24 > latitude) && (latitude >= -32)) return "J";
      else if ((-32 > latitude) && (latitude >= -40)) return "H";
      else if ((-40 > latitude) && (latitude >= -48)) return "G";
      else if ((-48 > latitude) && (latitude >= -56)) return "F";
      else if ((-56 > latitude) && (latitude >= -64)) return "E";
      else if ((-64 > latitude) && (latitude >= -72)) return "D";
      else if ((-72 > latitude) && (latitude >= -80)) return "C";
      else return 'Z';
    }
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    var Checksum = function(sentence, checksum) {
          var q, c1, c2, i;
          i = 1; 
          c1 = sentence.charCodeAt(i); 
          for( i = 2; i < sentence.length; ++i) {
            c1 = c1 ^ sentence.charCodeAt(i);
          }
          c2 = parseInt(checksum, 16);
          return (c1  === c2); 
    };
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    var m_hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    var toHexString = function(v) {
          var lsn, msn;
          msn = (v >> 4) & 0x0f;
          lsn = (v >> 0) & 0x0f;
          return m_hex[msn] + m_hex[lsn];
    };
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    var computeChecksum = function(sentence) {
          var c1;
          var i;
          i = 1; 
          c1 = sentence.charCodeAt(i);
          for( i = 2; i < sentence.length; ++i) {
            c1 = c1 ^ sentence.charCodeAt(i);
          }
          return '*' + toHexString(c1);
    };
    
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­-------------------
    function handleGPSLine(line ,callback) {
       var c = line.split("*");
      if ( Checksum(c[0],c[1]) ) {
          var tag = line.substr(3,3);
          var d = line.split(",");
          if (tag=="GGA") {  
             var dlat = d[2].indexOf(".");
             var dlon = d[4].indexOf(".");
             callback({
                tag    : tag,
                time : d[1].substr(0,2)+":"+d[1].substr(2,2)+":­"+d[1].substr(4,2),
                msec : (parseInt(d[1].substr(7,2),10)*10),
                lat : ((parseInt(d[2].substr(0,dlat-2),10)+par­seFloat(d[2].substr(dlat-2))/60)*(d[3]==­"S"?-1:1)).toFixed(9),
                lon : ((parseInt(d[4].substr(0,dlon-2),10)+par­seFloat(d[4].substr(dlon-2))/60)*(d[5]==­"W"?-1:1)).toFixed(9),
                fix : parseInt(d[6],10),
                alt : parseFloat(d[9])
             });
          }
          if (tag=="RMC") { 
             callback({
              tag    : tag,
              time   : d[1].substr(0,2)+":"+d[1].substr(2,2)+":­"+d[1].substr(4,2),
              valid  : d[2],
              speed  : parseFloat(d[7])*1.852,
              course : parseFloat(d[8]),
              date   : d[9].substr(0,2)+"/"+d[9].substr(2,2)+"/­"+d[9].substr(4,2),
              path   : d[9].substr(4,2)+d[9].substr(2,2)+d[9].s­ubstr(0,2)+"_"+d[1].substr(0,2)+d[1].sub­str(2,2)+".txt"
          		});
         }
      }
    }
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    function fix_gps_data(data) {
       if ( data.fix > 0 ) {
            gps_ok = 1;
            gps_time=data.time;
            gps_msec=data.msec;
            gps_lat=data.lat;
            gps_lon=data.lon;
            var ll = new LatLng(data.lat,data.lon,data.alt);
            utm = ll.toUTMRef();
       } else {
            gps_ok = 0;
       } 
    }
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    function get_gps_data(data) {
        if(data.tag == "GGA" ) {  fix_gps_data(data); }
        if(data.tag == "RMC" ) {  fix_data(data); }
    }
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­-------------------
    function fix_data(data) {
        if ( data.valid=="A" ) {
          if ( file_ok === 0) {
    	    file_ok=1;
        	    gps_date = data.date;
        	    gps_path = data.path;
                var head = "# File: "+gps_path+" created on "+data.date+" "+data.time+"\r\n";
        	    fs.appendFileSync(gps_path,head);
          }
          gps_speed  = data.speed;
          gps_course = data.course;
        }
    }
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    function fix_irs_data(data) {
        ir_red   = data.red;
        ir_nir   = data.nir;
        irr_edg  = data.red_edg;
        ir_ndre  = data.ndre;
        ir_ndvi  = data.ndvi; 
    }
      
    
    // ----------------------------------------­-----------
    //
    
    // ----------------------------------------­----------
    function handleIRSLine(line ,callback) {
         var d = line.split(",");
         callback({ red     : parseFloat(d[0]),
    	        nir     : parseFloat(d[1]),
                    red_edg : parseFloat(d[2]),
                    ndre    : parseFloat(d[3]),
                    ndvi    : parseFloat(d[4]) });
    }
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    Serial6.onData(function (e) {  
        if (e.data=="\n") { 
          var line = gpsLine;
          gpsLine="";
          handleGPSLine(line ,function(data) { get_gps_data(data); } );
        } else gpsLine+=e.data;
      } 
    );
    
    // ----------------------------------------­-------------------
    // ----------------------------------------­----------
    Serial4.onData(function (e) {  
        if (e.data=="\n") { 
          var line = irsLine;
          irsLine="";
          handleIRSLine(line ,function(data) {  fix_irs_data(data); } );
        } else irsLine+=e.data;
      } 
    );
    
    // --------------------------------------
    // --------------------------------------
    var tracking = function(cmd ) {
      var str = "$J";
      if (cmd =="GGA" ) { 
          str += "ASC,GP"+cmd+",10";
      } else if ( cmd == "RMC" ) {
           str += "ASC,GP"+cmd+",1";
      } else { }
      if (cmd == "OFF") {  str += cmd; }
      str += "\r\n";
      Serial6.print(str);
    };
    
    // ----------------------------------------­------
    // ----------------------------------------­------
    Pin.prototype.startFlashing = function(period) { 
      var on = false;
      var pin = this;
      return setInterval(function() { on = !on; digitalWrite(pin, on); }, period);
    };
    
    // ----------------------------------------­------
    // ----------------------------------------­------
    Pin.prototype.stopFlashing = function(stop ) { 
        var pin = this;
        clearInterval(stop);digitalWrite(pin,0);­
    };
    
    onInit();
    
  • @user6350, working on getting Espruino to understand my 'old' marine #GPS, I read through your code in post http://forum.espruino.com/conversations/­1578/#comment26928. I tried to understand getUTMLatitudeZoneLetter(latitude) {... #latitudezone function. Analyzing the implied algorithm and underlaying data values and structure, a How about:... popped up in my head:

    How about arithmetically coming up with an index pointing at a character in a string of the UTM latitude zone letters:

    And this would be the code:

    function getUTMLatitudeZoneLetter4(latitude) { 
     return ( 
      ((latitude >= -80) && (latitude <= 84)) // defined, valid range
       ? "CDEFGHJKLMNPQRSTUVWX".charAt(Math.floor­((latitude * 1 + 80) / 8)) // valid
       : "Z" // undefined and invalid
      );
    }
    

    The two-termed logical AND expressions, for example, ((84 >= latitude) && (latitude >= 72))
    threw me off. Two-termed logical expressions are usually only used for dealing with non-contiguous ranges.

    To verify the How about, I first morphed the given, quite redundant code into a more concise form - of which one you find below.

    function getUTMLatitudeZoneLetter3(latitude) {
      return (                     // interval:
          (latitude >   84) ? "Z" // invalid
        : (latitude >=  72) ? "X" // [72..84] - incl 72N .. incl 84N
        : (latitude >=  64) ? "W" // [64..72) - incl 64N .. excl 72N
        : (latitude >=  56) ? "V"
        : (latitude >=  48) ? "U"
        : (latitude >=  40) ? "T"
        : (latitude >=  32) ? "S"
        : (latitude >=  24) ? "R"
        : (latitude >=  16) ? "Q"
        : (latitude >=   8) ? "P"
        : (latitude >=   0) ? "N"
        : (latitude >=  -8) ? "M"
        : (latitude >= -16) ? "L"
        : (latitude >= -24) ? "K"
        : (latitude >= -32) ? "J"
        : (latitude >= -40) ? "H"
        : (latitude >= -48) ? "G"
        : (latitude >= -56) ? "F"
        : (latitude >= -64) ? "E"
        : (latitude >= -72) ? "D"
        : (latitude >= -80) ? "C" // [-80..-72) - iexcl 72S .. incl 80S
        :                     "Z" // invalid
        );
    }
    

    After that it became clear how the arithmetic version would look like for the computational approach. Separating the defined range from the undefined ones is though still best done with the comparison approach. The values in the defined range are converted by a formula into range of 0..18, just parallel matching the "CDE...X" string. Note that letters I and O are 'skipped'.

    Luckily, the irregular interval [72..84] N still works for the divide by 8, because the top value is less than 2 * 8 and collapse with Math.floor() to the the range of [72..80).

    I'd like to point out a nice, genuine JavaScript language behaviors: #operatoroverloading, and #typeconversion. You may wonder about the multiplication by 1 in the correction term (latitude * 1 + 80) to base the defined range to begin with 0. JavaScript has the nice feature - Java-ists call it flaw - of going further with auto type conversion than many other languages - especially (strongly, static) typed ones. When the function argument latitude is as a string - which it most likely is when reading plainly from the wire) - then types matter expressions operator overloading.

    Therefore, the value of (latitude + 80) with latitude having the string value "-80", the expression ("-80" + 80) evaluates to "-8080" string, because the "-80" is a string type and defines the type precedence of the operator overloading: + operator becomes string concatenation and hence the result is a string too. JavaScript will convert the number 80 into a string to accommodate the concatenation. Reordering and taking the number 80 first - (80 + latitude) - does not help either. The number 80 is operator overloading happens again to accommodate the "-80" string. Multiplying the latitude by 1 - (latitude * 1 + 80) - solves the issue with no (real) harm or value effect, even when latitude is (already) a number. Multiplication has no operator overloading and numeric is the only option. The "-80" string is converted to the number -80 to accommodate it and the expression evaluates to 0 - just as expected ;-).

    Furthermore, notice the opening parenthesis on the same line of the return statement. This is because of another JavaScript genuine 'feature', or #returnpitfall, #jspitfall: If a line feed follows a return, the return is considered as complete and the function returns #undefined, no matter what keeps going on in the next line(s). I could have of course put something relevant after the return keyword to avoid it, for example, the logical expression for separating the valid from the invalid ranges:

    function getUTMLatitudeZoneLetter4(latitude) { 
     return ((latitude >= -80) && (latitude <= 84)) // defined, valid range
      ? "CDEFGHJKLMNPQRSTUVWX".charAt(Math.floor­((latitude * 1 + 80) / 8)) // valid
      : "Z"; // undefined and invalid(?)
    }
    

    Try it yourself with the uploaded .html file... ;-).

    For some (random and visualizing) unit testing I like to have a visual UI environment that I can drive with HTML5/JavaScript. Therefore I created a simple file-protocol served .html file. See uploaded screenshot and .html file. You can download the .html file, look at the various implementations, open it in a browser (with no harm) and play with it.


    2 Attachments

About

Avatar for allObjects @allObjects started