Abeeway Microtracker Decoder

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"
}

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.

Click for old version of the decoder, or see posts below for updates
//
// SEE UPDATED VERSION(S) IN FORUM POSTS BELOW
//
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) {
    // Force an unsigned 32 bits integer using zero-fill right shift:
    return (bytes[0]<<24 | bytes[1]<<16 | bytes[2]<< 8 | bytes[3])>>>0;
    // Alternatively, don't use bitwise operators at all:
    // 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]<<24 | bytes[1]<<16 | bytes[2]<<8)>>>0
    // or:
    //   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;
}

@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!

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.

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.

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;
}
1 Like

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()
 * 2020-06-16: simplified uint32()
 *             added warning about Data Storage Integration
 */
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] * 0x10000 + bytes[2] * 0x100 + bytes[3];
    //   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 int32(bytes)>>>0;

    // JavaScript bitwise operators always work with 32 bits signed integers;
    // force conversion to unsigned 32 bits value using zero-fill right shift
    return (bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8 | bytes[3])>>>0;    
  }

  // 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) {
    // Note: the Data Storage Integration stores nested objects as text
    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!

1 Like

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

1 Like

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.

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.

2 Likes

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

I used the above decoder, but it didn’t give a satisfying result. So I contacted Abeeway again. The statement, that they don’t provide a decoder is wrong. They sent me the file ands I’ll post it here. My problem is, that I can’t see location data on Cayenne. According to Abeeway the problem is not on TTN but on Cayenne. So i post it there too.
Now, here is the current offical decoder file from Abeeway. i think some other people will need that in future too:

function Decoder(bytes, port) {
/*
 * Example decoder for some Netvox sensors with The Things Network
 * FOR TESTING PURPOSES ONLY
 * Paul Hayes - paul@alliot.co.uk
 */

  var decoded = {};
  
  // decode common header
  decoded.battery_voltage = bytes[2]*0.0055+2.8;
  decoded.battery_percentage = parseInt((bytes[2]/255)*100);
  decoded.temperature = (bytes[3]*0.5)-44;
  decoded.ack_token = bytes[4] >> 4;
  // decode status
  if (bytes[1] & 0x10) decoded.sos_mode = true;
  if (bytes[1] & 0x08) decoded.tracking_state = true;
  if (bytes[1] & 0x04) decoded.moving = true;
  if (bytes[1] & 0x02) decoded.periodic_pos = true;
  if (bytes[1] & 0x01) decoded.pos_on_demand = true;
  decoded.operating_mode = bytes[1] >> 5;
  
  // decode rest of message
  if ((bytes[0] === 0x03) && ((bytes[4] & 0x0F) === 0x00)) { // position message & GPS type
    var lat_raw = ((bytes[6] << 16) | (bytes[7] << 8) | bytes[8]);
    lat_raw = lat_raw << 8;
    if (lat_raw > 0x7FFFFFFF) {
      lat_raw = lat_raw - 0x100000000;
    }
    decoded.latitude = lat_raw/10000000;
    var lng_raw = ((bytes[9] << 16) | (bytes[10] << 8) | bytes[11]);
    lng_raw = lng_raw << 8;
    if (lng_raw > 0x7FFFFFFF) {
      lng_raw = lng_raw - 0x100000000;
    }
    decoded.longitude = lng_raw/10000000;
    decoded.accuracy = bytes[12]*3.9;
    decoded.age = bytes[5]*8;
  } else if ((bytes[0] === 0x03) && ((bytes[4] & 0x0F) === 0x09)) { // position message & wifi bssid type
    decoded.bssid0 = bytes.slice(6, 12).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.bssid1 = bytes.slice(13, 19).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.bssid2 = bytes.slice(20, 26).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.bssid3 = bytes.slice(27, 33).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    
    decoded.rssi0 = (bytes[12] > 127 ? bytes[12] -256 : bytes[12]);
    decoded.rssi1 = (bytes[19] > 127 ? bytes[19] -256 : bytes[19]);
    decoded.rssi2 = (bytes[26] > 127 ? bytes[26] -256 : bytes[26]);
    decoded.rssi3 = (bytes[33] > 127 ? bytes[33] -256 : bytes[33]);
  } else if ((bytes[0] === 0x03) && ((bytes[4] & 0x0F) === 0x07)) { // position message & BLE macaddr type
    decoded.macadr0 = bytes.slice(6, 12).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.macadr1 = bytes.slice(13, 19).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.macadr2 = bytes.slice(20, 26).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.macadr3 = bytes.slice(27, 33).map(function(b) { return ("0" + b.toString(16)).substr(-2); }).join(":");
    decoded.rssi0 = (bytes[12] > 127 ? bytes[12] -256 : bytes[12]);
    decoded.rssi1 = (bytes[19] > 127 ? bytes[19] -256 : bytes[19]);
    decoded.rssi2 = (bytes[26] > 127 ? bytes[26] -256 : bytes[26]);
    decoded.rssi3 = (bytes[33] > 127 ? bytes[33] -256 : bytes[33]);
  } else if ((bytes[0] === 0x03) && ((bytes[4] & 0x0F) === 0x01)) { // position message & GPS timeout (failure)
    decoded.gpstimeout = true;
  } else if (bytes[0] === 0x09) { // shutdown message
    decoded.shutdown = true;
  } else if (bytes[0] === 0x0A) {
    decoded.geoloc_start = true; 
  } else if (bytes[0] === 0x05) {
    decoded.heartbeat = true;
    decoded.reset_cause = bytes[5];
    decoded.firmware_ver = bytes.slice(6, 9);
  }
  return decoded;
}

For the people using that decoder: can you tell us what details were off? And for which payload?

@thomassieczkowski and @flodenh, the new decoder shows a different value for temperature, for 0358D895090EC46E1FF44B9EB76466B3B87454AD500959CA1ED4AD525E67DA14A1AC. The new one shows 30.5 using:

decoded.temperature = (bytes[3]*0.5)-44;

…where the earlier one showed 31.38 using:

decoded.temperature 
  = Math.round(100 * mt_value_decode(bytes[3], -44, 85, 8, 0)) / 100;

For 032CD1890900C46E1FF44B9EC5C83A355A3898A6 the new one shows 24.5 while earlier we got 25.31.

For the coordinates both show the same results, for 0328D87E601523537B0AC8160B, again with a different temperature.

It seems Alliot is a reseller, not the manufacturer?

Aside, they provide a more recent version of the documentation. I’ve not looked at it.

Indeed, Alliot claims:

Alliot Technologies are official Hardware Distributors and Resellers of Abeeway products.

So, @JoergHu, did you get the decoder from Abeeway (in which case one might say it’s official), or from Alliot (in which case it’s simply not clear if their simplified approach is valid)?

To summarize the issues already mentioned above: when relying on the notes about encoding in the official documentation, then decoding uses non-exact conversion factors, such as 0.5058823529411764 instead of 0.5. Using those non-exact numbers is actually required if the encoding uses the same non-exact numbers. Assuming the firmware is closed source, and if the device provides no logging of its own (to compare sensor readings to the LoRaWAN payload) then one may want to ask Abeeway.

1 Like

Changes in the battery value - it’s now expressed for all rechargeable Abeeway devices as a %, also added MCU and BLE fwVersion

/**
 * Decoder for Abeeway Microtracker.
 *From Abeeway Tracker Reference Guide FW 2.1 V1.3
 */
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) {
   
    return (bytes[0]<<24 | bytes[1]<<16 | bytes[2]<<8 | bytes[3])>>>0;    
  }

  // 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) {
    // Note: the Data Storage Integration stores nested objects as text
    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)
    };

    // Trackers with a rechargeable battery:the percentage reflects the actual value
    decoded.batteryPersentage = bytes[2];
    //Temperature
    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);
    //MCU and BLE fwVersion
    decoded.lastResetCause = "lastResetCause: " + bytes[5];
    decoded.mcuFirmware = "fwVersion: " + bytes[6] + "." + bytes[7] + "." + bytes[8];
    decoded.bleFirmware = "bleFwVersion" + bytes[9] + "." + bytes[10] + "." + bytes[11];

  }

  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;
}
1 Like

Getting an error description": “UNSUPPORTED MESSAGE TYPE 10”
Sample packet below –

0A2849770002FFFDFFFB040D

From a terminal “system info” gives the following:

Versions
 Application: AT2 2.3-1 - built Jun 14 2022 10:22:04
 MTOS: MTOSv4/CT0/2022.03.21 built on Mar 21 2022 at 17:33:14
 FW Specific: 00FF0144 10001F40 00002660

Information at this link
https://docs.abeeway.com/thingpark-location/AbeewayRefGuide/uplink-messages/

Your type is bytes[0]

Is this the only uplink message you get?

NO – I get all the other values – The device operates as you would expect. Intermittently I get this value (0xA) as per the documentation here:

https://docs.abeeway.com/thingpark-location/AbeewayRefGuide/uplink-messages/

Which looks to be decoded here:

https://docs.abeeway.com/thingpark-location/AbeewayRefGuide/uplink-messages/event/#event-value

If no one else does it, I will adjust the code and post a link to a github repo. There seems to be some other message types that are missing too - I will try to add these.

1 Like