GlobalSat LT-20L and TagoIO GPS Decoder

Hi Guys,

I am struggling with the decoder for the GlobeSAT LT20-L Tracker. I am using TagoIO but I need TTN to post the decoded data to TagoIO.

Here is a preview of a String that comes from the device.
800248F813398DDD423C00
800247F813398DDD423C00
800246F8133B8DDD423C00

Here are the parameters for the bits.

lt20 parameters

This is also in the file they gave me.

lt20 gps

If anyone could please help me with a decoder that would be fantastic.

Regards
G

1 Like

Have you looked at some of the decoders already written? How good is your JavaScript?

Hy Gerhardt
I have more or less the same problem with the GlobeSat LT-20 tracker. Have you been able to fix the problem?

1 Like

We didn’t hear anymore on this from @gerhardtsiebert. I hinted there were decoders to be searched for and we can help with tweaking any JavaScript

I’ve been working on recycling these…

https://docs.microshare.io/docs/2/technical/microshare-platform-advanced/robots-libraries/globalsat-lt-100/

Will need to be adapted…
Otherwise…

function Decoder(bytes) {
var Battery = bytes[2];
var d = (bytes[3]<<24) | (bytes[4] <<16) | (bytes[5]<<8) | (bytes[6]);
var e = (bytes[7]<<24) | (bytes[8] <<16) | (bytes[9]<<8) | (bytes[10]);
var Longitude = parseInt(e) * 0.000001;
var Latitude = parseInt(d) * 0.000001;

return {Battery:Battery,Latitude:Latitude,Longitude:Longitude};

}

Ive made the longitude decode work… but struggling for a correct latitude. also Im southern hemisphere… so the negative is likely causing issues with the decode…

i tried subtracting 180 and still not right.

 // Sydney: -33.8688, 151.2092 = MSB FAD500 17129C, LSB 00D5FA 9C1217
 // 8002 2C3252 64EFD2 42 4B 00
 // Perth: -31.95224, 115.8614 == LSB 2D3252 66EFD2 
 // Stirling -31.896675, 115.815534
/*
80022B325261EFD2424100

8002 protocol and command
2B3252 longitude
61EFD2 latitude
42 gps
41 batt
00 preserved

  "frm_payload": "gAIrMlJh79JCQQA=",
  "decoded_payload": {
    "Battery": 65,
    "LatiHex": "D2EF61",
    "Latitude": 149.2974828,   ** still not right ???
    "LongHex": "52322B",
    "Longitude": 115.8160925

 */


function Decoder(bytes) {
//protocol version
var a = bytes[0];
//command id
var b = bytes[1];
//longitude - little endian
var c = (bytes[2] | bytes[3]<<8 | bytes[4]<<16 ) ;
//latitude - little endian
var d = (bytes[5] | bytes[6]<<8 | bytes[7]<<16 ) ; 
//gps fix status and report type
var e = bytes[8];
//battery capacity
var f = bytes[9];
//preserved
var g = bytes[10];
//convert as per guide - little endian and then multiple/divide etc
var Longitude = parseInt(c) * 215 / 10 * 0.000001;
var Latitude = parseInt(d) *108 / 10 * 0.000001;
//check hex little endian is swapping correctly
var Long = c.toString(16).toUpperCase();
var Lati = d.toString(16).toUpperCase();
//display battery level - little endian
var Battery = parseInt(f)

return {LongHex:Long,LatiHex:Lati, Longitude:Longitude, Latitude:Latitude, Battery:Battery};
}

function decodeUplink(input) {
  var data = input.bytes;
  var valid = true;

  if (typeof Decoder === "function") {
    data = Decoder(data, input.fPort);
  }

  if (typeof Converter === "function") {
    data = Converter(data, input.fPort);
  }

  if (typeof Validator === "function") {
    valid = Validator(data, input.fPort);
  }

  if (valid) {
    return {
      data: data
    };
  } else {
    return {
      data: {},
      errors: ["Invalid data received"]
    };
  }
}

I have come across the following kinda interesting code for the LT-20. Ive not yet made it work in TTN but its got some interesting (for me as a nonproficient codehacker manipulator).
I have questioned the github owner around the validity of the calculation as it doesnt match the PDF manual (or the image at the top of this thread).

akenza-io device-type-library globalsat LT-20 uplink.js

function toLittleEndian(hex) {
var data = hex.match(/…/g);
// Create a buffer
var buf = new ArrayBuffer(4);
// Create a data view of it
var view = new DataView(buf);
// set bytes
data.forEach(function (b, i) {
view.setUint8(i, parseInt(b, 16));
});
// get an int32 with little endian
var num = view.getInt32(0, 1);
return num;
}

function consume(event) {
var payload = event.data.payload_hex;
var bits = Bits.hexToBits(payload);
var data = {};
var protocolVersion = Bits.bitsToUnsigned(bits.substr(0, 8));
var commandID = Bits.bitsToUnsigned(bits.substr(8, 8));

data.longitude = (toLittleEndian(payload.substr(4, 6)) * 215) / 10 * 0.000001;
data.latitude = (toLittleEndian(payload.substr(10, 6)) * 215) / 10 * 0.000001;

var reportType = Math.round(Bits.bitsToUnsigned(bits.substr(64, 8)) / 32);
var gpsFix = Math.round(Bits.bitsToUnsigned(bits.substr(64, 8)) / 32);

if (gpsFix == 0) {
data.gpsFix = “Not fix”;
} else if (gpsFix == 1) {
data.gpsFix = “2D fix”;
} else if (gpsFix == 2) {
data.gpsFix = “3D fix”;
}

if (reportType == 2) {
data.reportType = “Periodic mode report”;
} else if (reportType == 4) {
data.reportType = “Motion mode static report”;
} else if (reportType == 5) {
data.reportType = “Motion mode moving report”;
} else if (reportType == 6) {
data.reportType = “Motion mode static to moving report”;
} else if (reportType == 7) {
data.reportType = “Motion mode moving to static report”;
} else if (reportType == 15) {
data.reportType = “Low battery alarm report”;
}
var batteryPercent = Bits.bitsToUnsigned(bits.substr(72, 8));

emit(‘sample’, { “data”: { “batteryPercent”: batteryPercent }, “topic”: “lifecycle” });
emit(‘sample’, { “data”: data, “topic”: “default” });
}

I seem to (with some help from AH [a friend], and the akenza.io team) made the following work within TTNv3.
I have been given some additional improvements, so might try those in the not too distant future. But for now this works for me.

//Functions Convert HEX to Bits
function hexToBits(hex){
  var out = "";
  for (var c of hex) {
    switch (c) {
      case '0': out += "0000"; break;
      case '1': out += "0001"; break;
      case '2': out += "0010"; break;
      case '3': out += "0011"; break;
      case '4': out += "0100"; break;
      case '5': out += "0101"; break;
      case '6': out += "0110"; break;
      case '7': out += "0111"; break;
      case '8': out += "1000"; break;
      case '9': out += "1001"; break;
      case 'a': out += "1010"; break;
      case 'b': out += "1011"; break;
      case 'c': out += "1100"; break;
      case 'd': out += "1101"; break;
      case 'e': out += "1110"; break;
      case 'f': out += "1111"; break;
      default: return "";
    }
  }
  return out;
}
//Convert Bits to Signed INT
function bitsToSigned(bits){
  var value = parseInt(bits, 2);
  var limit = 1 << (bits.length - 1);
  if (value >= limit) {
    // Value is negative; calculate two's complement.
    value = value - limit - limit;
  }
  return value;
}
function bitsToUnsigned(bits){
  return parseInt(bits, 2);
}
function toLittleEndianSigned(hex) {
  // Creating little endian hex DCBA
  var hexArray = [];
  while (hex.length >= 2) {
    hexArray.push(hex.substring(0, 2));
    hex = hex.substring(2, hex.length);
  }
  // seems to already be reversed so not needed 
  //hexArray.reverse();
  hex = hexArray.join('');

  // Hex to Bits
  var hex2bits = hexToBits(hex);

  // To signed int
  var signedInt = bitsToSigned(hex2bits);
  
  return signedInt;
}
function decodeUplink(input) {
  var data = {};
  //protocol version
  //data.protocolVersion = (input.bytes[0] << 8);
  //command id
  //data.commandID = (input.bytes[1] << 8);
  //longitude - little endian
  //note - old method (works for positive only)
  //data.longitude = (((input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16)) * 215) / 10 * 0.000001;
  //note - new method (works for positive and negative)
  data.longitude = ((toLittleEndianSigned(((input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16)).toString(16).toLowerCase())) * 215) / 10 * 0.000001;
  //data.longraw = (input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16);
  //data.longhexupper = ((input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16)).toString(16).toUpperCase();
  //data.longhexlower = ((input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16)).toString(16).toLowerCase();
  //data.longsignedint = (toLittleEndianSigned(((input.bytes[2] ) + (input.bytes[3] << 8) + (input.bytes[4] << 16)).toString(16).toLowerCase()));
  //help from AH & akenza.io - with thanks!
  //latitude - little endian
  data.latitude = ((toLittleEndianSigned(((input.bytes[5] ) + (input.bytes[6] << 8) + (input.bytes[7] << 16)).toString(16).toLowerCase())) * 108) / 10 * 0.000001;
  //data.latraw = (input.bytes[5] ) + (input.bytes[6] << 8) + (input.bytes[7] << 16);
  //data.lathexupper = ((input.bytes[5] ) + (input.bytes[6] << 8) + (input.bytes[7] << 16)).toString(16).toUpperCase();
  //data.lathexlower = ((input.bytes[5] ) + (input.bytes[6] << 8) + (input.bytes[7] << 16)).toString(16).toLowerCase();
  //data.latisignedint = (toLittleEndianSigned(((input.bytes[5] ) + (input.bytes[6] << 8) + (input.bytes[7] << 16)).toString(16).toLowerCase()));
  //gps fix status and report type
  //data.reportType = (input.bytes[8] << 8);
  var gps = Math.round((bitsToUnsigned(hexToBits((input.bytes[8] << 8).toString(16).toLowerCase()))/256)/32);
  if (gps === 0) {
    data.gps = "No fix";
  } else if (gps == 1) {
    data.gps = "2D fix";
  } else if (gps == 2) {
    data.gps = "3D fix";
  }
//battery capacity
  data.batterypercent = bitsToUnsigned(hexToBits((input.bytes[9] << 8).toString(16).toLowerCase()))/256;
//preserved
  //data.preserved = (input.bytes[10] << 8);
  var warnings = [];
/*  if (data.battery < 30) {
    warnings.push("low battery");
  } */
  return {
    data: data,
    warnings: warnings
  };
}

Thanks for the script. But it has one major flaw. if the longitude or latitude has a leading zero in it’s hex-string, the “.toString(16)” would delete it. And this would lead to a wrong GPS coordinate.
I edited the script and added it to Github Gist.

I life in northern hemisphere, and this works for me. would be happy if anyone would give their feedback.

1 Like