Decoder library

For your uplinks to appear on the map, GPS coordinates must be present as latitude / longitude in the decoded payload. Here you find ready-made payload formatters to paste into the TTS Console: End Devices → [Device] → Payload formatters → Uplink → Custom Javascript.

Dragino LGT-92 (GPS-Tracker)
Classic asset-tracking device. Payload contains position, altitude, battery and acceleration.
function decodeUplink(input) {
 var b = input.bytes;
 var data = {};

 // Lat / Lon (signed int32, /1e6)
 var lat = (b[0]<<24) | (b[1]<<16) | (b[2]<<8) | b[3];
 if (lat & 0x80000000) lat -= 0x100000000;
 var lon = (b[4]<<24) | (b[5]<<16) | (b[6]<<8) | b[7];
 if (lon & 0x80000000) lon -= 0x100000000;
 data.latitude = lat / 1e6;
 data.longitude = lon / 1e6;

 // Battery (10-bit, in volt)
 data.battery = (((b[8] & 0x3F) << 8) | b[9]) / 1000;
 data.alarm = (b[8] & 0x40) ? true : false;

 // Optional: roll / pitch / hdop / altitude (only when GPS valid & full payload)
 if (b.length >= 11) {
 data.motion = ["Disable","Move","Collide","User"][(b[10] >> 6) & 0x03];
 }
 if (b.length >= 19) {
 var roll = (b[11]<<8) | b[12]; if (roll & 0x8000) roll -= 0x10000;
 var pitch = (b[13]<<8) | b[14]; if (pitch & 0x8000) pitch -= 0x10000;
 var alt = (b[17]<<8) | b[18]; if (alt & 0x8000) alt -= 0x10000;
 data.roll = roll / 100;
 data.pitch = pitch / 100;
 data.altitude = alt;
 data.hdop = ((b[15]<<8) | b[16]) / 100;
 }
 return { data: data };
}
Dragino LHT65 (Temperatur & Luftfeuchte)
Classic indoor/outdoor sensor with built-in SHT sensor plus optional external port. Outputs TempC_SHT and Hum_SHT — automatically recognised by the server.
// Dragino LHT65 — Temperatur + Feuchte (intern) plus externer Sensor.
// Standard-FPort: 2. 11 Byte Payload.

function decodeUplink(input) {
  var b = input.bytes;
  if (b.length < 7) return { data: {} };

  // Battery (14-bit) + status (2-bit)
  var batValue   = ((b[0] & 0x3F) << 8) | b[1];
  var batV       = batValue / 1000;
  var batStatus  = (b[0] >> 6) & 0x03;
  var batLabel   = ["Ultra Low","Low","OK","Good"][batStatus];

  // Internal SHT temperature (int16 / 100)
  var t   = (b[2] << 8) | b[3];
  if (t & 0x8000) t -= 0x10000;
  var TempC_SHT = t / 100;

  // Internal humidity (uint16 / 10)
  var Hum_SHT = (((b[4] << 8) | b[5])) / 10;

  // External sensor type
  var extId    = b[6] & 0x7F;
  var extLabel = {
    1: "Temperature Sensor", 4: "Interrupt Sensor", 5: "ADC + Interrupt",
    6: "Temp DS18B20 + Counter", 7: "Counter", 8: "Liquid Level",
    9: "Temperature + Counter"
  }[extId] || "Unknown";

  var data = {
    BatV:       batV,
    Bat_status: batStatus,
    Bat_label:  batLabel,
    TempC_SHT:  TempC_SHT,
    Hum_SHT:    Hum_SHT,
    Ext:        extId,
    Ext_sensor: extLabel,
  };

  // External temperature (DS18B20) if ext = 1 or 6 or 9
  if (extId === 1 || extId === 6 || extId === 9) {
    var ext = (b[7] << 8) | b[8];
    if (ext & 0x8000) ext -= 0x10000;
    var TempC_DS = ext / 100;
    if (TempC_DS !== 327.67) data.TempC_DS = TempC_DS; // 327.67 = "no sensor"
  }

  // Canonical aliases — so the Coverage-Map kann nach Temperatur /
  // Luftfeuchte färben ohne weitere Konfiguration.
  data.temperature = TempC_SHT;
  data.humidity    = Hum_SHT;
  data.battery     = batV;

  return { data: data };
}

If your existing formatter already outputs TempC_SHT and Hum_SHT, you don't need to change anything — the server recognises these keys automatically and colours the map accordingly.

RAK7200 / RAK7201 (WisTrio Tracker)
RAK Wireless tracker with GPS, accelerometer & magnetometer. Sends Cayenne-LPP (see below) — you can also use the Cayenne decoder.
// Vereinfachter Decoder für RAK7200 GPS-Frames (FPort 8)
function decodeUplink(input) {
 var b = input.bytes;
 if (input.fPort !== 8 || b.length < 11) return { data: {} };

 var data = {};
 data.gps_valid = (b[0] === 0x09);

 var lat = (b[1]<<24) | (b[2]<<16) | (b[3]<<8) | b[4];
 if (lat & 0x80000000) lat -= 0x100000000;
 var lon = (b[5]<<24) | (b[6]<<16) | (b[7]<<8) | b[8];
 if (lon & 0x80000000) lon -= 0x100000000;

 data.latitude = lat / 1e4 / 100; // RAK uses 0.0001 deg per LSB
 data.longitude = lon / 1e4 / 100;

 // Speed (km/h) and direction (deg)
 if (b.length >= 13) {
 data.speed = (b[9]<<8 | b[10]) / 100;
 data.heading = (b[11]<<8 | b[12]) / 100;
 }
 return { data: data };
}
Seeed SenseCAP T1000 (Asset-Tracker)
Compact multi-mode tracker (GPS + WiFi + BLE). Payload is TLV-based. Works for T1000-A / T1000-B / T1000-C / T1000-E.
// Seeed SenseCAP T1000 — vereinfachter Decoder.
// Extrahiert GPS-Position, Batterie, Temperatur und Motion-Status.
// Für vollständige Sensor-Decoder siehe
// https://github.com/Seeed-Solution/SenseCAP-Decoder

function decodeUplink(input) {
 var b = input.bytes;
 var data = {};
 if (b.length < 1) return { data: data };

 // Frame-Header: erstes Byte enthält Event-Mask & Power-Type
 var header = b[0];
 data.battery_event = !!(header & 0x80);
 data.motion = !!(header & 0x40);

 // Ab Byte 1: aneinandergereihte TLV-Blöcke [type(1)][value(varlen)]
 var i = 1;
 function s16(o) { var v=(b[o]<<8)|b[o+1]; return v&0x8000 ? v-0x10000 : v; }
 function s32(o) {
 var v = (b[o]<<24)|(b[o+1]<<16)|(b[o+2]<<8)|b[o+3];
 return v >> 0; // signed in JS
 }

 while (i < b.length) {
 var t = b[i++];
 switch (t) {
 case 0x01: { // GPS: lat(4) + lon(4) + acc(1)
 if (i + 9 > b.length) return { data: data };
 data.latitude = s32(i) / 1e7;
 data.longitude = s32(i + 4) / 1e7;
 data.accuracy = b[i + 8];
 i += 9; break;
 }
 case 0x02: { // WiFi MAC list (3-4 BSSIDs à 6+1 Byte) — skip
 var count = b[i++];
 i += count * 7;
 break;
 }
 case 0x03: { // BLE MAC list
 var n = b[i++];
 i += n * 7;
 break;
 }
 case 0x04: { // Battery in %
 data.battery = b[i++]; break;
 }
 case 0x05: { // Temperature × 10 (signed int16)
 data.temperature = s16(i) / 10; i += 2; break;
 }
 case 0x06: { // Humidity × 10 (uint16)
 data.humidity = ((b[i]<<8) | b[i+1]) / 10; i += 2; break;
 }
 case 0x07: { // Light (uint16)
 data.light = (b[i]<<8) | b[i+1]; i += 2; break;
 }
 case 0x08: { // Air pressure / 10
 data.pressure = ((b[i]<<8) | b[i+1]) / 10; i += 2; break;
 }
 default: return { data: data, warnings: ["unknown TLV 0x" + t.toString(16) + " at " + (i-1)] };
 }
 }
 return { data: data };
}

Switch the tracker to Plain LoRaWAN mode in the SenseCAP app (not "SenseCAP Cloud"), otherwise uplinks won't reach your TTS app.

Seeed SenseCAP T2000 (Asset-Tracker)
Industrial asset tracker (T1000 successor) with GNSS, WiFi and BLE positioning. Payload is a multi-frame TLV format with its own frame IDs instead of a fixed byte layout.
// Seeed SenseCAP T2000 (Asset-Tracker A/B/C) — vereinfachter Decoder.
// Deckt die GPS-tragenden Frame-Typen ab: 0x2E (periodischer Location-Report)
// und 0x2B (Motion+Location). Reine Settings-/Heartbeat-Frames (0x27, 0x2A,
// 0x31, 0x32) liefern nur Batterie. WiFi/BLE-Scan-Frames (0x2C/0x2D/0x2F/0x30)
// haben variable Länge und keine direkten Koordinaten (brauchen einen
// externen Geolocation-Service) — werden übersprungen. Vollständiger
// offizieller Decoder (alle Frame-Typen inkl. Settings, Firmware-Version):
// https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/sensecap/sensecapt2000-tracker-abc-decoder.js

function decodeUplink(input) {
 var b = input.bytes;
 var data = {};
 var i = 0;

 function u32(o) { return ((b[o]<<24)|(b[o+1]<<16)|(b[o+2]<<8)|b[o+3]) >>> 0; }
 function s32(o) { return u32(o) | 0; } // reinterpret as two's complement

 while (i < b.length) {
 var id = b[i];
 if (id === 0x0c) {
 i += 1; // Time-Sync-Ping, kein Payload
 } else if (id === 0x0d && i + 5 <= b.length) {
 i += 5; // GNSS-Fehlercode, keine Position
 } else if (id === 0x2e && i + 17 <= b.length) {
 // Event(2) MotionId(1) Timestamp(4) Lon(4) Lat(4) Battery(1)
 data.timestamp = u32(i + 4) * 1000;
 data.longitude = s32(i + 8) / 1e6;
 data.latitude = s32(i + 12) / 1e6;
 data.battery = b[i + 16];
 i += 17;
 } else if (id === 0x2b && i + 23 <= b.length) {
 // Event(2) MotionId(1) Timestamp(4) Accel-xyz(6) Lon(4) Lat(4) Battery(1)
 data.timestamp = u32(i + 4) * 1000;
 data.longitude = s32(i + 14) / 1e6;
 data.latitude = s32(i + 18) / 1e6;
 data.battery = b[i + 22];
 i += 23;
 } else if (id === 0x27 && i + 46 <= b.length) {
 data.battery = b[i + 1]; // Registrierungs-/Settings-Frame, keine GPS-Daten
 i += 46;
 } else if (id === 0x2a && i + 6 <= b.length) {
 data.battery = b[i + 1];
 i += 6;
 } else if (id === 0x31 && i + 15 <= b.length) {
 data.battery = b[i + 14];
 i += 15;
 } else if (id === 0x32 && i + 9 <= b.length) {
 data.battery = b[i + 8];
 i += 9;
 } else {
 break; // WiFi/BLE-Scan oder unbekannter Frame-Typ
 }
 }
 return { data: data };
}

Only covers the two GPS-bearing frame types (0x2E periodic, 0x2B on motion). WiFi/BLE scan frames have no direct coordinates (need an external geolocation service) and are skipped — see the link in the code comment for the full official decoder.

RAK7200 v2 (WisTrio Tracker, neue Firmware)
The new firmware sends an extended frame with GPS, acceleration, temperature and battery. Default FPort: 8.
// RAK7200 v2 — neuer Frame mit GPS + Temp + Bat (FPort 8).
function decodeUplink(input) {
  var b = input.bytes;
  if (input.fPort !== 8 || b.length < 11) return { data: {} };

  var data = { gps_valid: (b[0] === 0x09) };

  // Lat / Lon — int32, /1e7 (neue Firmware)
  var lat = (b[1]<<24) | (b[2]<<16) | (b[3]<<8) | b[4];
  if (lat & 0x80000000) lat -= 0x100000000;
  var lon = (b[5]<<24) | (b[6]<<16) | (b[7]<<8) | b[8];
  if (lon & 0x80000000) lon -= 0x100000000;
  data.latitude  = lat / 1e7;
  data.longitude = lon / 1e7;

  // Battery (uint16 mV)
  if (b.length >= 11) data.battery = ((b[9]<<8) | b[10]) / 1000;

  // Optional: temperature (int16 / 10) + accelerometer
  if (b.length >= 13) {
    var t = (b[11]<<8) | b[12];
    if (t & 0x8000) t -= 0x10000;
    data.temperature = t / 10;
  }
  return { data: data };
}
Browan TBHV110 (Healthy Home Sensor)
Indoor sensor with temperature, humidity, VOC, light and motion counter. No GPS — position comes from the receiving gateways.
// Browan TBHV110 (Healthy Home Sensor).
// Payload: 10 byte fix. FPort: 103.
function decodeUplink(input) {
  var b = input.bytes;
  if (b.length < 10) return { data: {} };

  // Byte 0: status — Bit 0..3 base-ID, Bit 4..7 battery (×0.1V + 1.8)
  var batRaw = (b[0] >> 4) & 0x0F;
  var batV   = 1.8 + batRaw * 0.1;

  // Byte 1..2: temperature, signed int16 / 10
  var t = (b[1]<<8) | b[2];
  if (t & 0x8000) t -= 0x10000;

  // Byte 3: humidity 0..100 %
  var hum = b[3];

  // Byte 4..5: VOC index (uint16)
  var voc = (b[4]<<8) | b[5];

  // Byte 6..7: IAQ index (uint16)
  var iaq = (b[6]<<8) | b[7];

  // Byte 8..9: light / brightness lux (uint16)
  var light = (b[8]<<8) | b[9];

  return {
    data: {
      battery:     batV,
      temperature: t / 10,
      humidity:    hum,
      voc:         voc,
      iaq:         iaq,
      light:       light,
    },
  };
}
Adeunis Field Test Device (FTD)
Classic coverage-measurement tool. Sends status, temperature and GPS position (BCD-encoded). FPort 1 or 2.
// Adeunis FTD (Field Test Device).
// Status byte am Anfang sagt, welche Felder folgen.
function decodeUplink(input) {
  var b = input.bytes;
  if (b.length < 1) return { data: {} };
  var status = b[0];
  var i = 1;
  var data = {
    has_temp: !!(status & 0x80),
    has_gps:  !!(status & 0x40),
    trigger_acc:    !!(status & 0x20),
    trigger_push:   !!(status & 0x10),
    has_uplink_cnt: !!(status & 0x08),
    has_downlink_cnt: !!(status & 0x04),
    has_battery:    !!(status & 0x02),
    has_rssi_snr:   !!(status & 0x01),
  };

  if (data.has_temp && i < b.length) {
    var t = b[i++];
    if (t & 0x80) t -= 0x100;
    data.temperature = t;
  }

  if (data.has_gps && i + 8 <= b.length) {
    // Lat / Lon in BCD: ddmmmmmmh (degrees, decimal-minutes ×1e4, hemisphere)
    function bcd(o, len) {
      var s = "";
      for (var k = 0; k < len; k++) {
        s += ((b[o + k] >> 4) & 0x0F).toString() + (b[o + k] & 0x0F).toString();
      }
      return s;
    }
    var latStr = bcd(i, 4);
    var lonStr = bcd(i + 4, 4);
    // Format: DDMMmmmmH — degrees + minutes.fraction + N/S, E/W hemisphere
    var latDeg = parseInt(latStr.substr(0, 2), 10);
    var latMin = parseFloat(latStr.substr(2, 2) + "." + latStr.substr(4, 4));
    var latHem = latStr.substr(8, 1) === "0" ? 1 : -1; // 0=N, 1=S
    var lonDeg = parseInt(lonStr.substr(0, 3), 10);
    var lonMin = parseFloat(lonStr.substr(3, 2) + "." + lonStr.substr(5, 3));
    var lonHem = lonStr.substr(8, 1) === "0" ? 1 : -1; // 0=E, 1=W
    data.latitude  = latHem * (latDeg + latMin / 60);
    data.longitude = lonHem * (lonDeg + lonMin / 60);
    i += 8;
    if (i < b.length) data.gps_quality = b[i++];
  }

  if (data.has_uplink_cnt && i < b.length)   data.uplink_count   = b[i++];
  if (data.has_downlink_cnt && i < b.length) data.downlink_count = b[i++];
  if (data.has_battery && i + 2 <= b.length) {
    data.battery = ((b[i]<<8) | b[i + 1]) / 1000;
    i += 2;
  }
  if (data.has_rssi_snr && i + 2 <= b.length) {
    data.rssi = -b[i]; data.snr = b[i + 1];
    if (data.snr & 0x80) data.snr -= 0x100;
    i += 2;
  }
  return { data: data };
}

Adeunis FTD also works without GPS fix (status-only frames). These won't appear on the map — that's expected.

ELSYS (ERS / EMS / ELT generisch)
ELSYS sensors use TLV format. Decoder recognises temperature, humidity, light, motion, CO₂, battery and GPS.
// ELSYS generic — TLV-Decoder für ERS, ELT, EMS Familie.
function decodeUplink(input) {
  var b = input.bytes, i = 0, data = {};
  function u16(o) { return (b[o]<<8) | b[o + 1]; }
  function s16(o) { var v = u16(o); return v & 0x8000 ? v - 0x10000 : v; }
  function u32(o) {
    return (b[o]<<24 >>> 0) + (b[o + 1]<<16) + (b[o + 2]<<8) + b[o + 3];
  }

  while (i < b.length) {
    var t = b[i++];
    switch (t) {
      case 0x01: data.temperature = s16(i) / 10;        i += 2; break; // °C
      case 0x02: data.humidity    = b[i++];                     break; // %
      case 0x03: { // accelerometer: x, y, z (int8)
        var x = b[i] & 0x80 ? b[i] - 256 : b[i];
        var y = b[i + 1] & 0x80 ? b[i + 1] - 256 : b[i + 1];
        var z = b[i + 2] & 0x80 ? b[i + 2] - 256 : b[i + 2];
        data.accel = { x: x, y: y, z: z }; i += 3; break;
      }
      case 0x04: data.light       = u16(i);             i += 2; break; // lux
      case 0x05: data.motion      = b[i++];                     break; // PIR count
      case 0x06: data.co2         = u16(i);             i += 2; break; // ppm
      case 0x07: data.battery     = u16(i) / 1000;      i += 2; break; // V
      case 0x08: data.analog1     = u16(i);             i += 2; break;
      case 0x09: { // GPS: lat int32 / 10000, lon int32 / 10000
        var lat = u32(i); if (lat & 0x80000000) lat -= 0x100000000;
        var lon = u32(i + 4); if (lon & 0x80000000) lon -= 0x100000000;
        data.latitude  = lat / 10000;
        data.longitude = lon / 10000;
        i += 8; break;
      }
      case 0x0A: data.pulse1      = u16(i);             i += 2; break;
      case 0x0B: data.pulse1_abs  = u32(i);             i += 4; break;
      case 0x0E: data.temp_ext    = s16(i) / 10;        i += 2; break;
      case 0x0F: data.occupancy   = b[i++];                     break;
      case 0x14: data.pressure    = u32(i) / 1000;      i += 4; break; // hPa
      default:
        return {
          data: data,
          warnings: ["unknown ELSYS type 0x" + t.toString(16)],
        };
    }
  }
  return { data: data };
}
Cayenne LPP (Standard-Format)
Generic low-power payload format used by many trackers (RAK, Adeunis, ELSYS, …). Automatically detects GPS, temperature, voltage, etc.
function decodeUplink(input) {
 var b = input.bytes;
 var data = {};
 var i = 0;
 while (i + 2 <= b.length) {
 var ch = b[i++];
 var t = b[i++];
 switch (t) {
 case 0x00: data["digital_in_" + ch] = b[i++]; break;
 case 0x01: data["digital_out_" + ch] = b[i++]; break;
 case 0x02: { // analog input
 var v = (b[i]<<8) | b[i+1]; if (v & 0x8000) v -= 0x10000;
 data["analog_in_" + ch] = v / 100; i += 2; break;
 }
 case 0x03: { // analog output
 var v = (b[i]<<8) | b[i+1]; if (v & 0x8000) v -= 0x10000;
 data["analog_out_" + ch] = v / 100; i += 2; break;
 }
 case 0x65: data["luminosity_" + ch] = (b[i]<<8) | b[i+1]; i += 2; break;
 case 0x66: data["presence_" + ch] = b[i++]; break;
 case 0x67: { // temperature
 var v = (b[i]<<8) | b[i+1]; if (v & 0x8000) v -= 0x10000;
 data["temperature_" + ch] = v / 10; i += 2; break;
 }
 case 0x68: data["humidity_" + ch] = b[i++] / 2; break;
 case 0x71: i += 6; break; // accelerometer
 case 0x73: data["barometer_" + ch] = ((b[i]<<8) | b[i+1]) / 10; i += 2; break;
 case 0x74: data["voltage_" + ch] = ((b[i]<<8) | b[i+1]) / 100; i += 2; break;
 case 0x86: i += 6; break; // gyrometer
 case 0x88: { // gps
 var lat = (b[i]<<16) | (b[i+1]<<8) | b[i+2]; if (lat & 0x800000) lat -= 0x1000000;
 var lon = (b[i+3]<<16) | (b[i+4]<<8) | b[i+5]; if (lon & 0x800000) lon -= 0x1000000;
 var alt = (b[i+6]<<16) | (b[i+7]<<8) | b[i+8]; if (alt & 0x800000) alt -= 0x1000000;
 data.latitude = lat / 10000;
 data.longitude = lon / 10000;
 data.altitude = alt / 100;
 i += 9; break;
 }
 default: return { data: data, warnings: ["unknown type 0x" + t.toString(16) + " at " + (i-1)] };
 }
 }
 return { data: data };
}
How to add the decoder in TTS
  1. Open TTS Console → Applications → your app
  2. End Devices → your device → Payload formatters tab
  3. Uplink: Formatter type → Custom JavaScript formatter
  4. Paste the code from above → Save changes
  5. Check on next uplink: Live data tab must show "latitude" and "longitude" in decoded payload
  6. The uplink will then automatically appear on the coverage map

Decoders for other devices can also be found in the official lorawan-devices repository .