Decoder

Follows the LoRA payload decoder that transforms received bytes into SenML-formatted message ready to be processed by insigh.io backend.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*

/$$                     /$$           /$$           /$$
|__/                    |__/          | $$          |__/
/$$ /$$$$$$$   /$$$$$$$ /$$  /$$$$$$ | $$$$$$$      /$$  /$$$$$$
| $$| $$__  $$ /$$_____/| $$ /$$__  $$| $$__  $$    | $$ /$$__  $$
| $$| $$  \ $$|  $$$$$$ | $$| $$  \ $$| $$  \ $$    | $$| $$  \ $$
| $$| $$  | $$ \____  $$| $$| $$  | $$| $$  | $$    | $$| $$  | $$
| $$| $$  | $$ /$$$$$$$/| $$|  $$$$$$$| $$  | $$ /$$| $$|  $$$$$$/
|__/|__/  |__/|_______/ |__/ \____  $$|__/  |__/|__/|__/ \______/
                            /$$  \ $$
                            |  $$$$$$/
                            \______/

LoRA payload decoder for insigh.io firmware
*/

var LOCATION_DEFAULT = 0x00;
var LOCATION_INTERNAL_BOARD = 0x10;
var LOCATION_INTERNAL_CPU = 0x11;
var LOCATION_I2C = 0x20;
var LOCATION_A_P1 = 0x30;
var LOCATION_A_P2 = 0x31;
var LOCATION_AD_P1 = 0x40;
var LOCATION_AD_P2 = 0x41;
var LOCATION_SDI12 = 0x50;
var LOCATION_MODEM = 0x70;

var TYPE_DEVICE_ID = 0x01;
var TYPE_RESET_CAUSE = 0x02;
var TYPE_UPTIME = 0x03;
var TYPE_MEM_ALLOC = 0x04;
var TYPE_MEM_FREE = 0x05;
var TYPE_CURRENT = 0x07;
var TYPE_VBATT = 0x08;
var TYPE_LIGHT_LUX = 0x10;
var TYPE_TEMPERATURE = 0x11;
var TYPE_HUMIDITY = 0x12;
var TYPE_CO2 = 0x13;
var TYPE_PRESSURE = 0x14;
var TYPE_GAS = 0x15;
var TYPE_VOLTAGE = 0x16;
var TYPE_VWC = 0x17;
var TYPE_REL_PERM = 0x18;
var TYPE_SOIL_EC = 0x19;
var TYPE_PORE_WATER_CONDUCT = 0x20;
var TYPE_SAP_FLOW = 0x21;
var TYPE_HEAT_VELOCITY = 0x22;
var TYPE_LOG_RATIO = 0x23;
var TYPE_LORA_JOIN_DUR = 0xc1;
var TYPE_GENERIC = 0xe0;
var TYPE_GENERIC_MAX = 0xef;

var typeMap = {};

function init() {
  typeMap[TYPE_CO2] = { name: "co2", unit: "ppm" };
  typeMap[TYPE_CURRENT] = { name: "current", unit: "mA" };
  typeMap[TYPE_DEVICE_ID] = { name: "device_id", unit: "" };
  typeMap[TYPE_GAS] = { name: "gas", unit: "Ohm" };
  typeMap[TYPE_GENERIC] = { name: "gen", unit: "" };
  typeMap[TYPE_HEAT_VELOCITY] = { name: "hv", unit: "cm/h" };
  typeMap[TYPE_HUMIDITY] = { name: "humidity", unit: "%RH" };
  typeMap[TYPE_LOG_RATIO] = { name: "log_rt", unit: "" };
  typeMap[TYPE_LIGHT_LUX] = { name: "light_lux", unit: "lx" };
  typeMap[TYPE_LORA_JOIN_DUR] = { name: "lora_join_dur", unit: "ms" };
  typeMap[TYPE_MEM_ALLOC] = { name: "mem_alloc", unit: "B" };
  typeMap[TYPE_MEM_FREE] = { name: "mem_free", unit: "B" };
  typeMap[TYPE_PORE_WATER_CONDUCT] = {
    name: "pore_water_conduct",
    unit: "uS/cm",
  };
  typeMap[TYPE_PRESSURE] = { name: "pressure", unit: "hPa" };
  typeMap[TYPE_REL_PERM] = { name: "rel_perm", unit: "" };
  typeMap[TYPE_RESET_CAUSE] = { name: "reset_cause", unit: "" };
  typeMap[TYPE_SAP_FLOW] = { name: "sap_flow", unit: "l/h" };
  typeMap[TYPE_SOIL_EC] = { name: "soil_ec", unit: "uS/cm" };
  typeMap[TYPE_TEMPERATURE] = { name: "temperature", unit: "Cel" };
  typeMap[TYPE_UPTIME] = { name: "uptime", unit: "ms" };
  typeMap[TYPE_VBATT] = { name: "vbatt", unit: "mV" };
  typeMap[TYPE_VOLTAGE] = { name: "voltage", unit: "mV" };
  typeMap[TYPE_VWC] = { name: "vwc", unit: "" };
}

function bin32dec(bin) {
  var num = bin & 0xffffffff;
  if (0x100000000 & num) num = -(0x0100000000 - num);
  return num;
}

function bin16dec(bin) {
  var num = bin & 0xffff;
  if (0x8000 & num) num = -(0x010000 - num);
  return num;
}

function bytesToHex(byteArray, from, to) {
  var s = "";
  for (var i = from; i < to; ++i) {
    s += ("0" + (byteArray[i] & 0xff).toString(16)).slice(-2);
  }
  return s;
}

function getTypeName(typeId) {
  var typeInfo = undefined;
  if (typeId >= TYPE_GENERIC && typeId <= TYPE_GENERIC_MAX) {
    typeInfo = typeMap[TYPE_GENERIC];
    typeInfo.name = typeInfo.name + "_" + (typeId & 0x0f);
  } else typeInfo = typeMap[typeId];
  if (typeInfo === undefined) return "";
  return typeInfo.name;
}

function getTypeUnit(typeId) {
  var typeInfo = typeMap[typeId];
  if (typeInfo === undefined) return "";
  return typeInfo.unit;
}

function extract32bitInteger(bytes, startIndex) {
  return (
    (bytes[startIndex] << 24) |
    (bytes[startIndex + 1] << 16) |
    (bytes[startIndex + 2] << 8) |
    bytes[startIndex + 3]
  );
}

function getLocationName(locationId) {
  var mainLocation = locationId & 0xf0;
  var subLocation = locationId & 0x0f;
  switch (mainLocation) {
    case LOCATION_INTERNAL_BOARD:
      switch (subLocation) {
        case 0x00:
          return "board";
        case 0x01:
          return "cpu";
        default:
          return "internal";
      }
    case LOCATION_I2C:
      switch (subLocation) {
        case 0x00:
          return "tsl2561";
        case 0x01:
          return "si7021";
        case 0x02:
          return "scd30";
        case 0x03:
          return "bme680";
        default:
          return "i2c";
      }
    case LOCATION_A_P1:
      switch (subLocation) {
        case 0x00:
          return "ap1";
        case 0x01:
          return "ap2";
        default:
          return "ap";
      }
    case LOCATION_AD_P1:
      switch (subLocation) {
        case 0x00:
          return "adp1";
        case 0x01:
          return "adp2";
        default:
          return "adp";
      }
    case LOCATION_SDI12:
      return "sdi12_" + subLocation;
    case LOCATION_MODEM:
      return "modem";
    default:
      console.log(
        "location not decoded: ",
        locationId,
        ", ",
        mainLocation,
        ", ",
        subLocation
      );
      return "undefined";
  }
}

function getValidName(nameDict, original_name) {
  var j = undefined;
  var name = undefined;
  do {
    name = original_name;
    if (j !== undefined) {
      name += "_" + j;
      j++;
    } else j = 1;
  } while (nameDict[name]); //if key is used, consider it is sensor with multiple measurements
  return name;
}

function DecodeInsighioPackage(bytes) {
  try {
    init();

    var senml = [];
    var nameDict = {};
    for (var i = 6; i < bytes.length; i++) {
      var original_name =
        getLocationName(bytes[i + 1]) + "_" + getTypeName(bytes[i]);
      var name = getValidName(nameDict, original_name);
      var obj = { n: name, u: getTypeUnit(bytes[i]) };
      var processed = true;
      switch (bytes[i]) {
        case TYPE_RESET_CAUSE: // 1 byte (unsigned char)
          obj.v = bytes[i + 2];
          i += 2;
          break;
        case TYPE_TEMPERATURE: // 2 bytes (signed short)
          var temp = (bytes[i + 2] << 8) | bytes[i + 3];
          obj.v = bin16dec(temp) / 100;
          i += 3;
          break;
        case TYPE_VBATT: // 2 bytes (unsigned short)
        case TYPE_CURRENT:
        case TYPE_LIGHT_LUX:
        case TYPE_LORA_JOIN_DUR:
        case TYPE_VOLTAGE:
          obj.v = (bytes[i + 2] << 8) | bytes[i + 3];
          i += 3;
          break;
        case TYPE_HUMIDITY: // 2 bytes (unsigned short)
        case TYPE_CO2:
        case TYPE_GAS:
        case TYPE_VWC:
        case TYPE_REL_PERM:
        case TYPE_SOIL_EC:
        case TYPE_PORE_WATER_CONDUCT:
        case TYPE_SAP_FLOW:
        case TYPE_HEAT_VELOCITY:
          obj.v = ((bytes[i + 2] << 8) | bytes[i + 3]) / 100;
          i += 3;
          break;
        case TYPE_UPTIME: // 4 bytes (unsigned integer)
        case TYPE_MEM_ALLOC:
        case TYPE_MEM_FREE:
          obj.v = extract32bitInteger(bytes, i + 2);
          i += 5;
          break;
        case TYPE_LOG_RATIO:
          obj.v = extract32bitInteger(bytes, i + 2) / 100000;
          i += 5;
          break;
        case TYPE_PRESSURE: // 4 bytes (signed integer)
          var temp = extract32bitInteger(bytes, i + 2);
          obj.v = bin32dec(temp);
          i += 5;
          break;
        default:
          processed = false;
      }
      if (!processed && bytes[i] & TYPE_GENERIC) {
        var temp = extract32bitInteger(bytes, i + 2);
        obj.v = bin32dec(temp) / 100;
        i += 5;
      }

      nameDict[obj.n] = true;
      senml.push(obj);
    }
    if (senml.length > 0) {
      senml[0].bn = bytesToHex(bytes, 0, 6) + "-";
    }
  } catch (err) {
    return { e: err.stack };
  }

  return senml;
}

// Called from ChirpStack / LoRaServer
//https://www.chirpstack.io/application-server/use/device-profiles/
function Decode(fPort, bytes, variables) {
  return DecodeInsighioPackage(bytes);
}

// Called from TheThingsNetwork
function Decoder(bytes, port) {
  return DecodeInsighioPackage(bytes);
}

function decodeUplink(input) {
   return {
    data: {
      bytes: DecodeInsighioPackage(input.bytes)
    }
  }
}