Decoding Trackio (Tabs Object Locator) GPS data with Node-RED JS

Hey there, I was wondering if anyone could help me decoding a GPS device I got. I’ve been searchig everywhere and have not been able to find out how to parse a multy-byte value that uses a fraction of those bytes for other purposes. On the other hand, I achieved retrieveng the hex.
If there is anyone who could help me I would be very happy lol
This is the documentation I have of the device, the multy byte values are represented in little-endian encoding…
image
And this is the HEX I need to get decoded: “08fe3962ff010e78baca5b”

and what’s the type and manufacturer of this device ?
maybe this is solved by another owner

Hey! It’s a Trackio device, but already searched with all that info and nothing :frowning:I forgotted to mention that signed values use two´s complement encoding :zipper_mouth_face:

First, apply a mask to ignore the high bits, like bytes[6] & 0x0F. Next, JavaScript’s bitwise operators always assume two’s complement, when handling 32 bits. But, as you’re ignoring the high bits, some sign extension for the 3 or 4 bits that you’re ignoring is still needed… Something like, very much untested:

// LSB, and bits 28-31 reserved for future use. Sign-extend to handle
// negative values; 0b00001000 = 0x08 and 0b11110000 = 0xF0:
var lat = bytes[3] | bytes[4]<<8 | bytes[5]<<16 
  | (bytes[6] & 0x0F | (bytes[6] & 0x08 ? 0xF0 : 0x00))<<24;

// LSB, and bits 29-31 used for accuracy. Sign-extend to handle
// negative values; 0b00010000 = 0x10 and 0b11100000 = 0xE0:
var lng = bytes[7] | bytes[8]<<8 | bytes[9]<<16
  | (bytes[10] & 0x1F | (bytes[10] & 0x10 ? 0xE0 : 0x00))<<24;

var accuracy = bytes[10]>>5;

Probably easier to shift 4 and 3 bytes too far to the left, and shift it back, as the bitwise operator >> is the sign-propagating right shift:

// LSB, ignoring the 4 and 3 high bits, and sign-extending to 32 bits
// to support negative values, by shifting 4 or 3 bytes too far to
// the left (discarding those bits, as only 32 bits are preserved),
// followed by a sign-propagating right shift:
var lat = bytes[3] | bytes[4]<<8 | bytes[5]<<16 | bytes[6]<<28>>4;
var lng = bytes[7] | bytes[8]<<8 | bytes[9]<<16 | bytes[10]<<27>>3;

To parse a few bits from a single byte, see:

…which you can use for:

var voltage = (25 + bits(byte[1], 0, 3)) / 10;
var capacity = 100 * (bits(byte[1], 4, 7) / 15);
var accuracy = bits(byte[10], 5, 7);
2 Likes

I want to thank you very very much for your complete and perfect explanation and answer! It worked out perfectly!!
Here is the Node Red flow! :sunglasses:

[{"id":"cbe4d8e3.b361e8","type":"inject","z":"1b040b7a.d3b095","name":"Payload GPS","topic":"","payload":"{\"topic\":\"iotlatam1/payloads_ul\",\"payload\":{\"payloads_ul\":{\"id\":1553522985733,\"deveui\":\"58a0cb0000403be3\",\"timestamp\":\"2019-03-25T14:09:45.733Z\",\"devaddr\":279538127,\"live\":false,\"dataFrame\":\"CPs/nQECDmW+yjs=\",\"fcnt\":256,\"session_id\":\"686c68e2-7e7b-410c-9d96-bd9526ef166f\",\"port\":136,\"rssi\":-87,\"snr\":9.8,\"freq\":916200012.2070312,\"sf_used\":7,\"dr_used\":\"SF7BW125\",\"cr_used\":\"4/5\",\"device_redundancy\":1,\"time_on_air_ms\":71.936,\"gtw_info\":[{\"gtw_id\":\"000800fffe4a44cd\",\"rssi\":-87,\"snr\":9.8}],\"decrypted\":true}},\"qos\":0,\"retain\":false,\"_msgid\":\"316ad2a4.6207fe\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":350,"y":220,"wires":[["8bc01482.90c508"]]},{"id":"bcd2f116.331cc","type":"function","z":"1b040b7a.d3b095","name":"DECODE","func":"// Primer paso es decodificar Base64 a Hex\n\n    var data = msg.payload.dataFrame\n    bytes = new Buffer(data,'Base64')\n    hex = bytes.toString('hex')\n\n    \n \nvar bytes = new Buffer(data, 'base64');\n    var temp = bytes[2]-37;\n   // Gets the zero-based unsigned numeric value of the given bit(s)\n  function bits(value, lsb, msb) {\n    var len = msb - lsb + 1;\n    var mask = (1<<len) - 1;\n    return value>>lsb & mask;\n  }\n\n  // Gets the boolean value of the given bit\n  function bit(value, bit) {\n    return (value & (1<<bit)) > 0;\n  } \n  \n    var voltage = (25 + bits(bytes[1], 0, 3)) / 10;\n    var capacity = 100 * (bits(bytes[1], 4, 7) / 15);\n    var accuracy = bits(bytes[10], 5, 7);\n    var status = bit(bytes[0],3)\n \n // LSB, and bits 28-31 reserved for future use. Sign-extend to handle\n// negative values; 0b00001000 = 0x08 and 0b11110000 = 0xF0:\nvar lat = bytes[3] | bytes[4]<<8 | bytes[5]<<16  | (bytes[6] & 0x0F | (bytes[6] & 0x08 ? 0xF0 : 0x00))<<24;\n\n\n// LSB, and bits 29-31 used for accuracy. Sign-extend to handle\n// negative values; 0b00010000 = 0x10 and 0b11100000 = 0xE0:\n//var lon = bytes[7] | bytes[8]<<8 | bytes[9]<<16 | (bytes[10] & 0x1F | (bytes[10] & 0x10 ? 0xE0 : 0x00))<<24;\nvar lon = bytes[7] | bytes[8]<<8 | bytes[9]<<16\n  | (bytes[10] & 0x1F | (bytes[10] & 0x10 ? 0xE0 : 0x00))<<24;\n\n \n\n   \n    \n msg.payload.data= data \n msg.payload.hex= hex\n msg.payload.lat= lat/1000000\n msg.payload.lon = lon/1000000\n msg.payload.status = status\n msg.payload.volt = voltage\n msg.payload.capacity = capacity\n msg.payload.temp = temp\n msg.payload.accu = accuracy\n    \nreturn msg;","outputs":1,"noerr":0,"x":720,"y":220,"wires":[["4573bcf5.5aa244"]]},{"id":"4573bcf5.5aa244","type":"debug","z":"1b040b7a.d3b095","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":850,"y":220,"wires":[]},{"id":"8bc01482.90c508","type":"change","z":"1b040b7a.d3b095","name":"Cambiamos Payload","rules":[{"t":"move","p":"payload.payload.payloads_ul","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":220,"wires":[["bcd2f116.331cc"]]}]

Nice. Peeking at your DECODE node, a few remarks though :wink:

var temp = bytes[2] - 37;

Your documentation shows that bit 7 of byte[2] is RFU, reserved for future use. So, while it might always be zero, it’s safer to exclude it. Also, the documentation says to subtract 32, not 37? So, I’d use one of:

// Bit 7 is RFU; exclude. 0x7F = 0b01111111
var temp = (bytes[2] & 0x7F) - 32;
// Or:
var temp = bits(bytes[2], 0, 7) - 32;

Accuracy could be converted to meters:

// Accuracy in meters; 1 << x+2 is the same as Pow(2, x+2) for x < 32
var accuracy = 1 << bits(bytes[10], 5, 7) + 2;

Also, you’re defining bytes two times.

And for future readers, a decoder for a TTN Console Payload Format could be something like:

/**
 * Decoder for Trackio GPS device.
 *
 * Test with: 08FE3962FF010E78BACA5B
 */
function Decoder(bytes, port) {
  var decoded = {};

  // Some "common status format"
  decoded.status = bits(bytes[0], 4, 7);

  decoded.battery = {
    voltage: (25 + bits(bytes[1], 0, 3)) / 10,
    capacity: 100 * (bits(bytes[1], 4, 7) / 15)
  };

  // Bit 7 is RFU; exclude
  decoded.temperature = bits(bytes[2], 0, 7) - 32;

  decoded.position = {
    gnssFix: bit(bytes[0], 3),
    // LSB, ignoring the 4 high bits (RFU) and sign-extending to 32 bits
    // to support negative values, by shifting 4 bytes too far to the
    // left (which discards those bits, as only 32 bits are preserved),
    // followed by a sign-propagating right shift:
    lat: (bytes[3] | bytes[4] << 8 | bytes[5] << 16
      | bytes[6] << 28 >> 4) / 1e6,
    // Likewise, ignoring the 3 high bits (used for accuracy):
    lng: (bytes[7] | bytes[8] << 8 | bytes[9] << 16
      | bytes[10] << 27 >> 3) / 1e6,
    // Accuracy in meters; 1 << x+2 is the same as Pow(2, x+2) for x < 32
    accuracy: 1 << bits(bytes[10], 5, 7) + 2
  };

  return decoded;
}

// Gets the zero-based unsigned numeric value of the given bit(s)
function bits(value, lsb, msb) {
  var len = msb - lsb + 1;
  var mask = (1 << len) - 1;
  return value >> lsb & mask;
}

// Gets the boolean value of the given bit
function bit(value, bit) {
  return (value & (1 << bit)) > 0;
}

For 08FE3962FF010E78BACA5B this would show:

{
  "battery": {
    "capacity": 100,
    "voltage": 3.9
  },
  "position": {
    "accuracy": 16,
    "gnssFix": true,
    "lat": -33.423518,
    "lng": -70.600072
  },
  "status": 0,
  "temperature": 25
}

If you have any specific details about a specific model, then please edit your title to add that to “Trackio”. Also, if you have details about the “common status format” from the first byte, then please add those? Thanks!

1 Like

The device seems to (also) be branded Tabs Object Locator.

status field

Tabs_ObjectLocator.pdf (789.6 KB)

This is an old thread, but hoping you can help. I tried to implement your code in Python. It doesn’t work for lat & lng.

# TABS Object Tracker
def bits(value, lsb, msb):
    len = msb - lsb + 1
    mask = (1 << len) - 1
    return value >> lsb & mask

def bit(value, bit):
    return (value & (1 << bit)) > 0

hex='08FE3962FF010E78BACA5B'
bytes=bytes.fromhex(hex)
status = bits(bytes[0], 4, 7)
voltage = (25 + bits(bytes[1], 0, 3)) / 10
capacity = 100 * (bits(bytes[1], 4, 7) / 15)
temperature = bits(bytes[2], 0, 7) - 32
lat = (bytes[3] + (bytes[4] << 8) +(bytes[5] << 16) + (bytes[6] << 28) >> 4) / 1e6
lng = (bytes[7] | bytes[8] << 8 | bytes[9] << 16 | bytes[10] << 27 >> 3) / 1e6
accuracy = 1 << bits(bytes[10], 5, 7) + 2
print(status, voltage, capacity, temperature, lat,lng, gnssFix, accuracy)

I get this result:
0 3.9 100.0 25 234.889206 1540.012664 True 16

When posting code please spend 30 seconds on formatting it properly. Makes it easier to read (especially python which depends on whitespace) for people trying to help out.