Abeeway Microtracker Decoder


(Thomassieczkowski) #1

Hi there. I’ve been struggling on getting the Abeeway Microtracker decoded. Unfortunately, the manufacturer does not share its decoder to buyers. Here is what I have in terms of material:

  1. A work in progress by @flodenh in this post

  2. This file: Abeeway Micro Tracker_Reference_Guide_FW1.7_V1.1.pdf (1.1 MB) which contains explanation on how to build a decoder;

  3. An example payload: 032CD1890900C46E1FF44B9EC5C83A355A3898A6 which should bring lat/lng close to: -29.9606817, -50.127897399999995 and it is bringing me -99.9416064,-19.6370944 (at this point I’m using @flodenh 's decoder)

It brings me the following JSON:

{
  "ack": 0,
  "age": 0,
  "battery": 3.95,
  "bytes": "AyzRiQkAxG4f9Euexcg6NVo4mKY=",
  "lattitude": -99.9416064,
  "length": 20,
  "longitude": -19.6370944,
  "mode": 1,
  "position_type": 9,
  "temperature": 25.31,
  "type": "POSITION"
}

(Arjan) #2

Looking at the documentation, the payload you’re showing for “position type” 9 does not provide GPS coordinates. Instead, it gives you up to 4 MAC addresses of WiFi access points. I guess you should also get other payloads, that do provide the GPS data. Or maybe you need to move outside to get a GPS fix.

The following has only been tested with 032CD1890900C46E1FF44B9EC5C83A355A3898A6 and 0358D895090EC46E1FF44B9EB76466B3B87454AD500959CA1ED4AD525E67DA14A1AC, the latter yielding:

{
  "ack": 0,
  "age": 112,
  "battery": 3.99,
  "data": 9,
  "position_type": "WIFI BSSIDs",
  "stations": [
    {
      "mac_address": "c4:6e:1f:f4:4b:9e",
      "rssi": -73
    },
    {
      "mac_address": "64:66:b3:b8:74:54",
      "rssi": -83
    },
    {
      "mac_address": "50:09:59:ca:1e:d4",
      "rssi": -83
    },
    {
      "mac_address": "52:5e:67:da:14:a1",
      "rssi": -84
    }
  ],
  "status": {
    "mode": {
      "code": 2,
      "message": "Permanent tracking"
    },
    "moving": 0,
    "on_demand": 0,
    "periodic": 0,
    "sos": 1,
    "tracking": 1
  },
  "temperature": 31.46,
  "type": "POSITION"
}

So, most of the following is really untested, so you’ve got a lot of testing to do now (and I’d suggest you thoroughly compare the code to the documentation as well…).

For the GPS coordinates, in function degree(bytes), you might actually need:

var d = bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100;

But we need some test payload to confirm that. I guess some payload types (and the conditions for which you get them) would be nice for future readers as well.

function Decoder(bytes, port) {

  function step_size(lo, hi, nbits, nresv) {
    return 1.0 / ((((1<<nbits) - 1) - nresv) / (hi - lo));
  }

  function mt_value_decode(value, lo, hi, nbits, nresv) {
    return ((value - nresv / 2) * step_size(lo, hi, nbits, nresv) + lo);
  }

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

  // Gets a hexadecimal representation with leading zeroes for each byte
  function hex(bytes, separator) {
    return bytes.map(function (b) {
      return ("0" + b.toString(16)).substr(-2);
    }).join(separator || "");
  }

  // Decodes 4 bytes into a signed integer, MSB
  function int32(bytes) {
    // JavaScript bitwise operators always work with 32 bits signed integers
    return bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8 | bytes[3];
  }

  // Decodes 4 bytes into an unsigned integer, MSB
  function uint32(bytes) {
    return bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100 + bytes[3];
  }

  // Decodes the decimal degree for either latitude or longitude
  function degree(bytes) {
    // TODO This is probably an unsigned number, hence needs:
    // var d = bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100;
    var d = bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8;
    if (d > 0x7FFFFFFF) {
      d = d - 0x100000000;
    }
    return d / 1e7;
  }

  // Decodes 1 to 4 MAC addresses and their RSSI
  function mac_rssi(bytes) {
    var items = [];
    for (var offset = 0; offset < bytes.length; offset += 7) {
      items.push({
        // Get hexadecimal bytes with a leading zero
        mac_address: hex(bytes.slice(offset, offset + 6), ":"),
        // Sign-extend to 32 bits to support negative values
        rssi: bytes[offset + 6]<<24>>24,
      });
    }
    return items;
  }

  function message(value, messages) {
    return {
      code: value,
      message: value > messages.length ? "UNKNOWN" : messages[value]
    };
  }

  var decoded = {};
  var i;
  var type = bytes[0];

  if (type !== 0x00) {
    // All message types, except for Frame pending messages, share the same header
    decoded.status = {
      mode: message(bits(bytes[1], 5, 7), ["Standby", "Motion tracking", "Permanent tracking",
        "Motion start/end tracking", "Activity tracking", "OFF"]),
      sos: bits(bytes[1], 4),
      tracking: bits(bytes[1], 3),
      moving: bits(bytes[1], 2),
      periodic: bits(bytes[1], 1),
      on_demand: bits(bytes[1], 0)
    };

    decoded.battery = Math.round(100 * mt_value_decode(bytes[2], 2.8, 4.2, 8, 2)) / 100;
    decoded.temperature = Math.round(100 * mt_value_decode(bytes[3], -44, 85, 8, 2)) / 100;
    decoded.ack = bits(bytes[4], 4, 7);
    decoded.data = bits(bytes[4], 0, 3);
  }

  switch (type) {
    case 0x00:
      decoded.type = "FRAME PENDING";
      decoded.token = bytes[1];
      break;

    case 0x03:
      decoded.type = "POSITION";
      switch (decoded.data) {
        case 0:
          decoded.position_type = "GPS fix";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          decoded.latitude = degree(bytes.slice[6]);
          decoded.longitude = degree(bytes.slice[9]);
          // Estimated Horizontal Position Error
          decoded.ehpe = mt_value_decode(bytes[12], 0, 1000, 8, 0);
          break;

        case 1:
          decoded.position_type = "GPS timeout";
          decoded.timeout_cause = message(bytes[5], ["User timeout cause"]);
          for (i = 0; i < 4; i++) {
            // Carrier over noise (dBm) for the i-th satellite seen
            decoded["cn" + i] = mt_value_decode(bytes[6 + i], 0, 2040, 8, 0);
          }
          break;

        case 2:
          // Documented as obsolete
          decoded.error = "UNSUPPORTED";
          break;

        case 3:
          decoded.position_type = "WIFI timeout";
          for (i = 0; i < 6; i++) {
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          break;

        case 4:
          decoded.position_type = "WIFI failure";
          for (i = 0; i < 6; i++) {
            // Most of time a WIFI timeout occurs due to a low battery condition
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          decoded.error = message(bytes[11], ["WIFI connection failure", "Scan failure",
            "Antenna unavailable", "WIFI not supported on this device"]);
          break;

        case 5:
        case 6:
          decoded.position_type = "LP-GPS data";
          // Encrypted, not described in the documentation
          decoded.error = "UNSUPPORTED";
          break;

        case 7:
          decoded.position_type = "BLE beacon scan";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 beacons
          decoded.beacons = mac_rssi(bytes.slice(6));
          break;

        case 8:
          decoded.position_type = "BLE beacon failure";
          decoded.error = message(bytes[5], ["BLE is not responding", "Internal error", "Shared antenna not available",
            "Scan already on going", "No beacon detected", "Hardware incompatibility"]);
          break;

        // Test with: 0358D895090EC46E1FF44B9EB76466B3B87454AD500959CA1ED4AD525E67DA14A1AC
        // or 032CD1890900C46E1FF44B9EC5C83A355A3898A6
        case 9:
          decoded.position_type = "WIFI BSSIDs";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 WiFi BSSIDs
          decoded.stations = mac_rssi(bytes.slice(6));
          break;

        default:
          decoded.position_type = "UNSUPPORTED";
      }
      break;

    case 0x04:
      decoded.type = "ENERGY STATUS";
      break;

    case 0x05:
      decoded.type = "HEARTBEAT";
      break;

    case 0x07:
      // Activity status message and configuration message share the same identifier
      var tag = bytes[5];
      switch (tag) {
        case 1:
          decoded.type = "Activity Status";
          decoded.activity_counter = uint32(bytes.slice(6, 10));
          break;

        case 2:
          decoded.type = "Configuration";
          for (i = 0; i < 5; i++) {
            var offset = 6 + 5 * i;
            decoded["param" + i] = {
              type: bytes[offset],
              value: uint32(bytes.slice(offset + 1, offset + 5))
            };
          }
          break;

        default:
          decoded.error = "UNSUPPORTED";
      }
      break;

    case 0x09:
      decoded.type = "Shutdown";
      break;

    case 0xFF:
      decoded.type = "Debug";
      break;

    default:
      decoded.type = "UNSUPPORTED";
  }

  // Just some redundant debug info
  decoded.debug = {
    payload: hex(bytes),
    length: bytes.length,
    port: port
  };

  return decoded;
}

Is there any documentation on payload functions?
(Thomassieczkowski) #3

@arjanvanb - Thank you so much. This totally works. I’m gonna check how to get proper downlink working in order to set GPS only mode! Thanks again!


(Arjan) #4

So did you already test GPS, or are you only getting the WiFi details? Any chance that, to test GPS, you really just need to take the device outside? Or did you already try that? Unless it’s now in some “WiFi-only” mode (which I doubt) I’d expect another uplink with the GPS details.


(Thomassieczkowski) #5

Hi @arjanvanb - Thank you so much for your help so far. I managed to get it working by using the following code:

function Decoder(bytes, port) {

  function step_size(lo, hi, nbits, nresv) {
    return 1.0 / ((((1<<nbits) - 1) - nresv) / (hi - lo));
  }

  function mt_value_decode(value, lo, hi, nbits, nresv) {
    return ((value - nresv / 2) * step_size(lo, hi, nbits, nresv) + lo);
  }

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

  // Gets a hexadecimal representation with leading zeroes for each byte
  function hex(bytes, separator) {
    return bytes.map(function (b) {
      return ("0" + b.toString(16)).substr(-2);
    }).join(separator || "");
  }

  // Decodes 4 bytes into a signed integer, MSB
  function int32(bytes) {
    // JavaScript bitwise operators always work with 32 bits signed integers
    return bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8 | bytes[3];
  }

  // Decodes 4 bytes into an unsigned integer, MSB
  function uint32(bytes) {
    return bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100 + bytes[3];
  }

  // Decodes the decimal degree for either latitude or longitude
  function degree(bytes) {
    // TODO This is probably an unsigned number, hence needs:
    //var d = bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100;
    var d = bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8;
    //var d = bytes//[0] << 8 | bytes[1] << 8 | bytes[2] << 8;
    if (d > 0x7FFFFFFF) {
      d = d - 0x100000000;
    }
    return d / 1e7;
  }

  // Decodes 1 to 4 MAC addresses and their RSSI
  function mac_rssi(bytes) {
    var items = [];
    for (var offset = 0; offset < bytes.length; offset += 7) {
      items.push({
        // Get hexadecimal bytes with a leading zero
        mac_address: hex(bytes.slice(offset, offset + 6), ":"),
        // Sign-extend to 32 bits to support negative values
        rssi: bytes[offset + 6]<<24>>24,
      });
    }
    return items;
  }

  function message(value, messages) {
    return {
      code: value,
      message: value > messages.length ? "UNKNOWN" : messages[value]
    };
  }

  var decoded = {};
  var i;
  var type = bytes[0];

  if (type !== 0x00) {
    // All message types, except for Frame pending messages, share the same header
    decoded.status = {
      mode: message(bits(bytes[1], 5, 7), ["Standby", "Motion tracking", "Permanent tracking",
        "Motion start/end tracking", "Activity tracking", "OFF"]),
      sos: bits(bytes[1], 4),
      tracking: bits(bytes[1], 3),
      moving: bits(bytes[1], 2),
      periodic: bits(bytes[1], 1),
      on_demand: bits(bytes[1], 0)
    };

    decoded.battery = Math.round(100 * mt_value_decode(bytes[2], 2.8, 4.2, 8, 2)) / 100;
    decoded.temperature = Math.round(100 * mt_value_decode(bytes[3], -44, 85, 8, 2)) / 100;
    decoded.ack = bits(bytes[4], 4, 7);
    decoded.data = bits(bytes[4], 0, 3);
  }

  switch (type) {
    case 0x00:
      decoded.type = "FRAME PENDING";
      decoded.token = bytes[1];
      break;

    case 0x03:
      decoded.type = "POSITION";
      switch (decoded.data) {
        case 0:
          decoded.position_type = "GPS fix";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          decoded.latitude = degree(bytes.slice(6,8));
          decoded.longitude = degree(bytes.slice(9,11));
          // Estimated Horizontal Position Error
          decoded.ehpe = mt_value_decode(bytes[12], 0, 1000, 8, 0);
          break;

        case 1:
          decoded.position_type = "GPS timeout";
          decoded.timeout_cause = message(bytes[5], ["User timeout cause"]);
          for (i = 0; i < 4; i++) {
            // Carrier over noise (dBm) for the i-th satellite seen
            decoded["cn" + i] = mt_value_decode(bytes[6 + i], 0, 2040, 8, 0);
          }
          break;

        case 2:
          // Documented as obsolete
          decoded.error = "UNSUPPORTED";
          break;

        case 3:
          decoded.position_type = "WIFI timeout";
          for (i = 0; i < 6; i++) {
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          break;

        case 4:
          decoded.position_type = "WIFI failure";
          for (i = 0; i < 6; i++) {
            // Most of time a WIFI timeout occurs due to a low battery condition
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          decoded.error = message(bytes[11], ["WIFI connection failure", "Scan failure",
            "Antenna unavailable", "WIFI not supported on this device"]);
          break;

        case 5:
        case 6:
          decoded.position_type = "LP-GPS data";
          // Encrypted, not described in the documentation
          decoded.error = "UNSUPPORTED";
          break;

        case 7:
          decoded.position_type = "BLE beacon scan";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 beacons
          decoded.beacons = mac_rssi(bytes.slice(6));
          break;

        case 8:
          decoded.position_type = "BLE beacon failure";
          decoded.error = message(bytes[5], ["BLE is not responding", "Internal error", "Shared antenna not available",
            "Scan already on going", "No beacon detected", "Hardware incompatibility"]);
          break;

        // Test with: 0358D895090EC46E1FF44B9EB76466B3B87454AD500959CA1ED4AD525E67DA14A1AC
        // or 032CD1890900C46E1FF44B9EC5C83A355A3898A6
        case 9:
          decoded.position_type = "WIFI BSSIDs";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 WiFi BSSIDs
          decoded.stations = mac_rssi(bytes.slice(6));
          break;

        default:
          decoded.position_type = "UNSUPPORTED";
      }
      break;

    case 0x04:
      decoded.type = "ENERGY STATUS";
      break;

    case 0x05:
      decoded.type = "HEARTBEAT";
      break;

    case 0x07:
      // Activity status message and configuration message share the same identifier
      var tag = bytes[5];
      switch (tag) {
        case 1:
          decoded.type = "Activity Status";
          decoded.activity_counter = uint32(bytes.slice(6, 10));
          break;

        case 2:
          decoded.type = "Configuration";
          for (i = 0; i < 5; i++) {
            var offset = 6 + 5 * i;
            decoded["param" + i] = {
              type: bytes[offset],
              value: uint32(bytes.slice(offset + 1, offset + 5))
            };
          }
          break;

        default:
          decoded.error = "UNSUPPORTED";
      }
      break;

    case 0x09:
      decoded.type = "Shutdown";
      break;

    case 0xFF:
      decoded.type = "Debug";
      break;

    default:
      decoded.type = "UNSUPPORTED";
  }

  // Just some redundant debug info
  decoded.debug = {
    payload: hex(bytes),
    length: bytes.length,
    port: port
  };

  return decoded;
}

I just changed the slice function.


(Arjan) #6

Ah, nice, so you changed:

decoded.latitude = degree(bytes.slice[6]);
decoded.longitude = degree(bytes.slice[9]);

…into:

decoded.latitude = degree(bytes.slice(6,8));
decoded.longitude = degree(bytes.slice(9,11));

The second parameter is not really needed (but surely cleaner as it’s more explicit) as the degree function only cares for the first 3 bytes it gets passed, and ignores the rest. However, to pass 3 bytes it would then need to read (6, 9) and (9, 12) as the end position is not included.

But using (..) instead of [..] surely helps a lot. :slight_smile:

Can you please share a GPS payload for future readers? (And any other type of payload you might get in the future? @flodenh, maybe you have some other payloads too?) I assume that the following would suffice, as JavaScript’s bitwise operators always yield signed 32 bits numbers:

function degree(bytes) {
  return (bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8) / 1e7;
}

(Arjan) #7

Seeing temperature values like 25.31, I wonder how the values are truly encoded. It is documented as:

Temperature measured in the device, expressed in degree Celsius. Encoded form using lo= - 44, hi= 85, nbits= 8, nresv= 0. It is given with a step of 0.5°C

But while decoding the step is calculated as 1 / (255 / 129) = 0.5058823529411764, yielding values such as 25.30588235294117. Here, JavaScript’s floating point numbers might be to blame partially. But even in exact math 1 / (255 / 129) will have an infinite number of digits, so I guess I would have used different low and high values, or use a different formula for the step.

One might be tempted to round the calculated step, but for the battery reading the calculated step yields 0.005533596837944666 and is documented as 5.5 mV. Or one might want to use the documented step, but even that will run into JavaScript rounding errors for (value - nresv / 2), if nresv is not zero:

function mt_value_decode2(value, lo, nresv, step_size) {
  return (value - nresv / 2) * step_size + lo;
}

Also, with the given documentation there is just no way to tell if the above should really be interpreted as 25.3 or 25.5 (not even taking the sensor’s accuracy into account)…

Or: the above could also indicate a documentation error, if different values for the high and low values are used while encoding? If the device has any option to see the actual values, then I’d compare that to the output.

All said, while looking at that code, I happened to run into a wrong value for nresv in the temperature decoding, also present in @flodenh’s version. (And there might be more: validate against the documentation!) I fixed that in the version below, and also simplified the GPS handling, though I’ve no payloads to validate that. Just to be able to refer to this example in the future, I also changed the output of single-bit values into booleans, and made the error reporting more consistent:

/**
 * Decoder for Abeeway Microtracker.
 *
 * 2019-02-09: initial release
 * 2019-02-13: fixed "nresv" for temperature reading;
 *             using booleans for single-bit values;
 *             improved error reporting
 * 2019-02-13: fixed message()
 */
function Decoder(bytes, port) {

  // nbits: number of bits used to encode
  // lo: min value that can be encoded
  // hi: max value that can be encoded
  // nresv: number of reserved values, not used for the encoding
  function step_size(lo, hi, nbits, nresv) {
    return 1.0 / ((((1<<nbits) - 1) - nresv) / (hi - lo));
  }

  function mt_value_decode(value, lo, hi, nbits, nresv) {
    return (value - nresv / 2) * step_size(lo, hi, nbits, nresv) + lo;
  }

  // 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;
  }

  // Gets a hexadecimal representation ensuring a leading zero for each byte
  function hex(bytes, separator) {
    return bytes.map(function (b) {
      return ("0" + b.toString(16)).substr(-2);
    }).join(separator || "");
  }

  // Decodes 4 bytes into a signed integer, MSB
  function int32(bytes) {
    // JavaScript bitwise operators always work with 32 bits signed integers
    return bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8 | bytes[3];
  }

  // Decodes 4 bytes into an unsigned integer, MSB
  function uint32(bytes) {
    // Or, same result:
    //   return bytes[0] * 0x1000000 + (bytes[1]<<16 | bytes[2]<<8 | bytes[3]);
    //   return bytes[0] * (1<<24) + (bytes[1]<<16 | bytes[2]<<8 | bytes[3]);
    return bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100 + bytes[3];
  }

  // Decodes 1 to 4 MAC addresses and their RSSI
  function mac_rssi(bytes) {
    var items = [];
    for (var offset = 0; offset < bytes.length; offset += 7) {
      items.push({
        mac_address: hex(bytes.slice(offset, offset + 6), ":"),
        // Sign-extend to 32 bits to support negative values; dBm
        rssi: bytes[offset + 6]<<24>>24,
      });
    }
    return items;
  }

  function message(code, descriptions) {
    return {
      code: code,
      description: code < 0 || code >= descriptions.length ? "UNKNOWN" : descriptions[code]
    };
  }

  var decoded = {};
  var i;

  var type = bytes[0];

  // All message types, except for Frame pending messages, share the same header
  if (type !== 0x00) {
    decoded.status = {
      mode: message(bits(bytes[1], 5, 7), ["Standby", "Motion tracking", "Permanent tracking",
        "Motion start/end tracking", "Activity tracking", "OFF"]),
      sos: bit(bytes[1], 4),
      tracking: bit(bytes[1], 3),
      moving: bit(bytes[1], 2),
      periodic: bit(bytes[1], 1),
      on_demand: bit(bytes[1], 0)
    };

    // Or, same result:
    //   // Unary plus-operator to cast string results of toFixed to a number:
    //   decoded.battery = +mt_value_decode(bytes[2], 2.8, 4.2, 8, 2).toFixed(2);
    //   decoded.temperature = +mt_value_decode(bytes[3], -44, 85, 8, 0).toFixed(2);
    decoded.battery = Math.round(100 * mt_value_decode(bytes[2], 2.8, 4.2, 8, 2)) / 100;
    decoded.temperature = Math.round(100 * mt_value_decode(bytes[3], -44, 85, 8, 0)) / 100;
    decoded.ack = bits(bytes[4], 4, 7);
    decoded.data = bits(bytes[4], 0, 3);
  }

  switch (type) {
    case 0x00:
      decoded.type = "FRAME PENDING";
      decoded.token = bytes[1];
      break;

    case 0x03:
      decoded.type = "POSITION";
      switch (decoded.data) {
        case 0:
          decoded.position_type = "GPS fix";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Signed 32 bits integers; LSB is always zero
          decoded.latitude = (bytes[6]<<24 | bytes[7]<<16 | bytes[8]<<8) / 1e7;
          decoded.longitude = (bytes[9]<<24 | bytes[10]<<16 | bytes[11]<<8) / 1e7;
          // Estimated Horizontal Position Error
          decoded.ehpe = mt_value_decode(bytes[12], 0, 1000, 8, 0);
          break;

        case 1:
          decoded.position_type = "GPS timeout";
          decoded.timeout_cause = message(bytes[5], ["User timeout cause"]);
          for (i = 0; i < 4; i++) {
            // Carrier over noise (dBm) for the i-th satellite seen
            decoded["cn" + i] = mt_value_decode(bytes[6 + i], 0, 2040, 8, 0);
          }
          break;

        case 2:
          // Documented as obsolete
          decoded.error = message(0, ["UNSUPPORTED POSITION TYPE " + decoded.data]);
          break;

        case 3:
          decoded.position_type = "WIFI timeout";
          for (i = 0; i < 6; i++) {
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          break;

        case 4:
          decoded.position_type = "WIFI failure";
          for (i = 0; i < 6; i++) {
            // Most of time a WIFI timeout occurs due to a low battery condition
            decoded["v_bat" + (i + 1)] = mt_value_decode(bytes[5 + i], 2.8, 4.2, 8, 2);
          }
          decoded.error = message(bytes[11], ["WIFI connection failure", "Scan failure",
            "Antenna unavailable", "WIFI not supported on this device"]);
          break;

        case 5:
        case 6:
          decoded.position_type = "LP-GPS data";
          // Encrypted; not described in the documentation
          decoded.error = message(0, ["UNSUPPORTED POSITION TYPE " + decoded.data]);
          break;

        case 7:
          decoded.position_type = "BLE beacon scan";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 beacons
          decoded.beacons = mac_rssi(bytes.slice(6));
          break;

        case 8:
          decoded.position_type = "BLE beacon failure";
          decoded.error = message(bytes[5], ["BLE is not responding", "Internal error", "Shared antenna not available",
            "Scan already on going", "No beacon detected", "Hardware incompatibility"]);
          break;

        // Test with: 0358D895090EC46E1FF44B9EB76466B3B87454AD500959CA1ED4AD525E67DA14A1AC
        // or 032CD1890900C46E1FF44B9EC5C83A355A3898A6
        case 9:
          decoded.position_type = "WIFI BSSIDs";
          decoded.age = mt_value_decode(bytes[5], 0, 2040, 8, 0);
          // Remaining data: up to 4 WiFi BSSIDs
          decoded.stations = mac_rssi(bytes.slice(6));
          break;

        default:
          decoded.error = message(0, ["UNSUPPORTED POSITION TYPE " + decoded.data]);
      }
      break;

    case 0x04:
      decoded.type = "ENERGY STATUS";
      break;

    case 0x05:
      decoded.type = "HEARTBEAT";
      break;

    case 0x07:
      // Activity status message and configuration message share the same identifier
      var tag = bytes[5];
      switch (tag) {
        case 1:
          decoded.type = "ACTIVITY STATUS";
          decoded.activity_counter = uint32(bytes.slice(6, 10));
          break;

        case 2:
          decoded.type = "CONFIGURATION";
          for (i = 0; i < 5; i++) {
            var offset = 6 + 5 * i;
            decoded["param" + i] = {
              type: bytes[offset],
              value: uint32(bytes.slice(offset + 1, offset + 5))
            };
          }
          break;

        default:
          decoded.error = message(0, ["UNSUPPORTED POSITION TYPE " + decoded.data + "/" + tag]);
      }
      break;

    case 0x09:
      decoded.type = "SHUTDOWN";
      break;

    case 0xFF:
      decoded.type = "DEBUG";
      break;

    default:
      decoded.error = message(0, ["UNSUPPORTED MESSAGE TYPE " + type]);
  }

  // Just some redundant debug info
  decoded.debug = {
    payload: hex(bytes),
    length: bytes.length,
    port: port,
    server_time: new Date().toISOString()
  };

  return decoded;
}

Still interested!


Decoding Trackio GPS data with Node-RED JS
(Flodenh) #8

Sorry - I have been busy working with something else than the Abeeway trackers. What a pleasant surprise to see this amazing job you guys have done!

Here is an example of a payload with a GPS-position: 0328D87E601523537B0AC8160B

The microtracker is configurable by sending it downlinks and I have had some success configuring it for GPS-only. Please let me know if you need some input on that @thomassieczkowski


(Arjan) #9

Does that give you the expected output? Using the simplified version, that would get you:

"latitude": 59.2673536,
"longitude": 18.0884992

And to extend on the temperature even more: a bare byte value of 137 is currently decoded as 25.31, using the calculated step of 0.5058823529411764. If the step was really 0.5 while encoding, then the decoding introduces an error of 137 * 0.0058823529411764 = 0.8 degrees, increasing for larger values. Using the documented step would indeed yield 24.5 rather than 25.31, quite a bit lower, and that still doesn’t even take the accuracy of the sensor into account…

So, depending on the use case, one might want to inquire the manufacturer about the documentation.


(Flodenh) #10

Yes that is the expected output - you all know where I live by now :slight_smile:

The temperature is of no interest in my use case but I see your point.


(system) #11

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.