added TTNv3 converters (#issue 736)
This commit is contained in:
parent
2e6a82f432
commit
6dde1d4bf4
@ -594,6 +594,7 @@ Thanks to
|
|||||||
- [Oliver Brandmüller](https://github.com/spmrider) for idea and initial setup of this project
|
- [Oliver Brandmüller](https://github.com/spmrider) for idea and initial setup of this project
|
||||||
- [Charles Hallard](https://github.com/hallard) for major code contributions to this project
|
- [Charles Hallard](https://github.com/hallard) for major code contributions to this project
|
||||||
- [robbi5](https://github.com/robbi5) for the payload converter
|
- [robbi5](https://github.com/robbi5) for the payload converter
|
||||||
|
- [Caspar Armster](https://www.dasdigidings.de/) for the The Things Stack V3 payload converter
|
||||||
- [terrillmoore](https://github.com/mcci-catena) for maintaining the LMIC for arduino LoRaWAN stack
|
- [terrillmoore](https://github.com/mcci-catena) for maintaining the LMIC for arduino LoRaWAN stack
|
||||||
- [sbamueller](https://github.com/sbamueller) for writing the tutorial in Make Magazine
|
- [sbamueller](https://github.com/sbamueller) for writing the tutorial in Make Magazine
|
||||||
- [Stefan](https://github.com/nerdyscout) for paxcounter opensensebox integration
|
- [Stefan](https://github.com/nerdyscout) for paxcounter opensensebox integration
|
||||||
|
330
src/TTNv3/packed_decodeUplink.js
Normal file
330
src/TTNv3/packed_decodeUplink.js
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
// Decoder for device payload encoder "PACKED"
|
||||||
|
// copy&paste to TTN Console V3 -> Applications -> Payload formatters -> Uplink -> Javascript
|
||||||
|
// modified for The Things Stack V3 by Caspar Armster, dasdigidings e.V.
|
||||||
|
|
||||||
|
function decodeUplink(input) {
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (input.fPort === 1) {
|
||||||
|
// only wifi counter data, no gps
|
||||||
|
if (input.bytes.length === 2) {
|
||||||
|
data = decode(input.bytes, [uint16], ['wifi']);
|
||||||
|
}
|
||||||
|
// wifi + ble counter data, no gps
|
||||||
|
if (input.bytes.length === 4) {
|
||||||
|
data = decode(input.bytes, [uint16, uint16], ['wifi', 'ble']);
|
||||||
|
}
|
||||||
|
// combined wifi + ble + SDS011
|
||||||
|
if (input.bytes.length === 8) {
|
||||||
|
data = decode(input.bytes, [uint16, uint16, uint16, uint16], ['wifi', 'ble', 'PM10', 'PM25']);
|
||||||
|
}
|
||||||
|
// combined wifi counter and gps data, used by https://opensensemap.org
|
||||||
|
if (input.bytes.length === 10) {
|
||||||
|
data = decode(input.bytes, [latLng, latLng, uint16], ['latitude', 'longitude', 'wifi']);
|
||||||
|
}
|
||||||
|
// combined wifi + ble counter and gps data, used by https://opensensemap.org
|
||||||
|
if (input.bytes.length === 12) {
|
||||||
|
data = decode(input.bytes, [latLng, latLng, uint16, uint16], ['latitude', 'longitude', 'wifi', 'ble']);
|
||||||
|
}
|
||||||
|
// combined wifi counter and gps data
|
||||||
|
if (input.bytes.length === 15) {
|
||||||
|
data = decode(input.bytes, [uint16, latLng, latLng, uint8, hdop, altitude], ['wifi', 'latitude', 'longitude', 'sats', 'hdop', 'altitude']);
|
||||||
|
}
|
||||||
|
// combined wifi + ble counter and gps data
|
||||||
|
if (input.bytes.length === 17) {
|
||||||
|
data = decode(input.bytes, [uint16, uint16, latLng, latLng, uint8, hdop, altitude], ['wifi', 'ble', 'latitude', 'longitude', 'sats', 'hdop', 'altitude']);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.pax = 0;
|
||||||
|
if ('wifi' in data) {
|
||||||
|
data.pax += data.wifi;
|
||||||
|
}
|
||||||
|
if ('ble' in data) {
|
||||||
|
data.pax += data.ble;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 2) {
|
||||||
|
// device status data
|
||||||
|
if (input.bytes.length === 17) {
|
||||||
|
data = decode(input.bytes, [uint16, uptime, uint8, uint32, uint8, uint8], ['voltage', 'uptime', 'cputemp', 'memory', 'reset0', 'reset1']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 3) {
|
||||||
|
// device config data
|
||||||
|
data = decode(input.bytes, [uint8, uint8, int16, uint8, uint8, uint8, uint8, bitmap1, bitmap2, version], ['loradr', 'txpower', 'rssilimit', 'sendcycle', 'wifichancycle', 'blescantime', 'rgblum', 'flags', 'payloadmask', 'version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 4) {
|
||||||
|
// gps data
|
||||||
|
if (input.bytes.length === 8) {
|
||||||
|
data = decode(input.bytes, [latLng, latLng], ['latitude', 'longitude']);
|
||||||
|
} else {
|
||||||
|
data = decode(input.bytes, [latLng, latLng, uint8, hdop, altitude], ['latitude', 'longitude', 'sats', 'hdop', 'altitude']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 5) {
|
||||||
|
// button pressed
|
||||||
|
data = decode(input.bytes, [uint8], ['button']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 6) {
|
||||||
|
// beacon proximity alarm
|
||||||
|
data = decode(input.bytes, [int8, uint8], ['rssi', 'beacon']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 7) {
|
||||||
|
// BME680 sensor data
|
||||||
|
data = decode(input.bytes, [float, pressure, ufloat, ufloat], ['temperature', 'pressure', 'humidity', 'air']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 8) {
|
||||||
|
// battery voltage
|
||||||
|
data = decode(input.bytes, [uint16], ['voltage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 9) {
|
||||||
|
// timesync request
|
||||||
|
if (input.bytes.length === 1) {
|
||||||
|
data.timesync_seqno = input.bytes[0];
|
||||||
|
}
|
||||||
|
// epoch time answer
|
||||||
|
if (input.bytes.length === 5) {
|
||||||
|
data = decode(input.bytes, [uint32, uint8], ['time', 'timestatus']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 10) {
|
||||||
|
// ENS count
|
||||||
|
data = decode(input.bytes, [uint16], ['ens']);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.bytes = input.bytes; // comment out if you do not want to include the original payload
|
||||||
|
data.port = input.fPort; // comment out if you do not want to inlude the port
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
warnings: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- contents of /src/decoder.js --------------------------------------------
|
||||||
|
// https://github.com/thesolarnomad/lora-serialization/blob/master/src/decoder.js
|
||||||
|
|
||||||
|
var bytesToInt = function (bytes) {
|
||||||
|
var i = 0;
|
||||||
|
for (var x = 0; x < bytes.length; x++) {
|
||||||
|
i |= (bytes[x] << (x * 8));
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
};
|
||||||
|
|
||||||
|
var version = function (bytes) {
|
||||||
|
if (bytes.length !== version.BYTES) {
|
||||||
|
throw new Error('version must have exactly 10 bytes');
|
||||||
|
}
|
||||||
|
return String.fromCharCode.apply(null, bytes).split('\u0000')[0];
|
||||||
|
};
|
||||||
|
version.BYTES = 10;
|
||||||
|
|
||||||
|
var uint8 = function (bytes) {
|
||||||
|
if (bytes.length !== uint8.BYTES) {
|
||||||
|
throw new Error('uint8 must have exactly 1 byte');
|
||||||
|
}
|
||||||
|
return bytesToInt(bytes);
|
||||||
|
};
|
||||||
|
uint8.BYTES = 1;
|
||||||
|
|
||||||
|
var uint16 = function (bytes) {
|
||||||
|
if (bytes.length !== uint16.BYTES) {
|
||||||
|
throw new Error('uint16 must have exactly 2 bytes');
|
||||||
|
}
|
||||||
|
return bytesToInt(bytes);
|
||||||
|
};
|
||||||
|
uint16.BYTES = 2;
|
||||||
|
|
||||||
|
var uint32 = function (bytes) {
|
||||||
|
if (bytes.length !== uint32.BYTES) {
|
||||||
|
throw new Error('uint32 must have exactly 4 bytes');
|
||||||
|
}
|
||||||
|
return bytesToInt(bytes);
|
||||||
|
};
|
||||||
|
uint32.BYTES = 4;
|
||||||
|
|
||||||
|
var uint64 = function (bytes) {
|
||||||
|
if (bytes.length !== uint64.BYTES) {
|
||||||
|
throw new Error('uint64 must have exactly 8 bytes');
|
||||||
|
}
|
||||||
|
return bytesToInt(bytes);
|
||||||
|
};
|
||||||
|
uint64.BYTES = 8;
|
||||||
|
|
||||||
|
var int8 = function (bytes) {
|
||||||
|
if (bytes.length !== int8.BYTES) {
|
||||||
|
throw new Error('int8 must have exactly 1 byte');
|
||||||
|
}
|
||||||
|
var value = +(bytesToInt(bytes));
|
||||||
|
if (value > 127) {
|
||||||
|
value -= 256;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
int8.BYTES = 1;
|
||||||
|
|
||||||
|
var int16 = function (bytes) {
|
||||||
|
if (bytes.length !== int16.BYTES) {
|
||||||
|
throw new Error('int16 must have exactly 2 bytes');
|
||||||
|
}
|
||||||
|
var value = +(bytesToInt(bytes));
|
||||||
|
if (value > 32767) {
|
||||||
|
value -= 65536;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
int16.BYTES = 2;
|
||||||
|
|
||||||
|
var int32 = function (bytes) {
|
||||||
|
if (bytes.length !== int32.BYTES) {
|
||||||
|
throw new Error('int32 must have exactly 4 bytes');
|
||||||
|
}
|
||||||
|
var value = +(bytesToInt(bytes));
|
||||||
|
if (value > 2147483647) {
|
||||||
|
value -= 4294967296;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
int32.BYTES = 4;
|
||||||
|
|
||||||
|
var latLng = function (bytes) {
|
||||||
|
return +(int32(bytes) / 1e6).toFixed(6);
|
||||||
|
};
|
||||||
|
latLng.BYTES = int32.BYTES;
|
||||||
|
|
||||||
|
var uptime = function (bytes) {
|
||||||
|
return uint64(bytes);
|
||||||
|
};
|
||||||
|
uptime.BYTES = uint64.BYTES;
|
||||||
|
|
||||||
|
var hdop = function (bytes) {
|
||||||
|
return +(uint16(bytes) / 100).toFixed(2);
|
||||||
|
};
|
||||||
|
hdop.BYTES = uint16.BYTES;
|
||||||
|
|
||||||
|
var altitude = function (bytes) {
|
||||||
|
// Option to increase altitude resolution (also on encoder side)
|
||||||
|
// return +(int16(bytes) / 4 - 1000).toFixed(1);
|
||||||
|
return +(int16(bytes));
|
||||||
|
};
|
||||||
|
altitude.BYTES = int16.BYTES;
|
||||||
|
|
||||||
|
|
||||||
|
var float = function (bytes) {
|
||||||
|
if (bytes.length !== float.BYTES) {
|
||||||
|
throw new Error('Float must have exactly 2 bytes');
|
||||||
|
}
|
||||||
|
var isNegative = bytes[0] & 0x80;
|
||||||
|
var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8)
|
||||||
|
+ ('00000000' + Number(bytes[1]).toString(2)).slice(-8);
|
||||||
|
if (isNegative) {
|
||||||
|
var arr = b.split('').map(function (x) { return !Number(x); });
|
||||||
|
for (var i = arr.length - 1; i > 0; i--) {
|
||||||
|
arr[i] = !arr[i];
|
||||||
|
if (arr[i]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = arr.map(Number).join('');
|
||||||
|
}
|
||||||
|
var t = parseInt(b, 2);
|
||||||
|
if (isNegative) {
|
||||||
|
t = -t;
|
||||||
|
}
|
||||||
|
return +(t / 100).toFixed(2);
|
||||||
|
};
|
||||||
|
float.BYTES = 2;
|
||||||
|
|
||||||
|
var ufloat = function (bytes) {
|
||||||
|
return +(uint16(bytes) / 100).toFixed(2);
|
||||||
|
};
|
||||||
|
ufloat.BYTES = uint16.BYTES;
|
||||||
|
|
||||||
|
var pressure = function (bytes) {
|
||||||
|
return +(uint16(bytes) / 10).toFixed(1);
|
||||||
|
};
|
||||||
|
pressure.BYTES = uint16.BYTES;
|
||||||
|
|
||||||
|
var bitmap1 = function (byte) {
|
||||||
|
if (byte.length !== bitmap1.BYTES) {
|
||||||
|
throw new Error('Bitmap must have exactly 1 byte');
|
||||||
|
}
|
||||||
|
var i = bytesToInt(byte);
|
||||||
|
var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean);
|
||||||
|
return ['adr', 'screensaver', 'screen', 'countermode', 'blescan', 'antenna', 'filter', 'alarm']
|
||||||
|
.reduce(function (obj, pos, index) {
|
||||||
|
obj[pos] = +bm[index];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
bitmap1.BYTES = 1;
|
||||||
|
|
||||||
|
var bitmap2 = function (byte) {
|
||||||
|
if (byte.length !== bitmap2.BYTES) {
|
||||||
|
throw new Error('Bitmap must have exactly 1 byte');
|
||||||
|
}
|
||||||
|
var i = bytesToInt(byte);
|
||||||
|
var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean);
|
||||||
|
return ['battery', 'sensor3', 'sensor2', 'sensor1', 'gps', 'bme', 'alarm', 'counter']
|
||||||
|
.reduce(function (obj, pos, index) {
|
||||||
|
obj[pos] = +bm[index];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
bitmap2.BYTES = 1;
|
||||||
|
|
||||||
|
var decode = function (bytes, mask, names) {
|
||||||
|
|
||||||
|
var maskLength = mask.reduce(function (prev, cur) {
|
||||||
|
return prev + cur.BYTES;
|
||||||
|
}, 0);
|
||||||
|
if (bytes.length < maskLength) {
|
||||||
|
throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
names = names || [];
|
||||||
|
var offset = 0;
|
||||||
|
return mask
|
||||||
|
.map(function (decodeFn) {
|
||||||
|
var current = bytes.slice(offset, offset += decodeFn.BYTES);
|
||||||
|
return decodeFn(current);
|
||||||
|
})
|
||||||
|
.reduce(function (prev, cur, idx) {
|
||||||
|
prev[names[idx] || idx] = cur;
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof module === 'object' && typeof module.exports !== 'undefined') {
|
||||||
|
module.exports = {
|
||||||
|
uint8: uint8,
|
||||||
|
uint16: uint16,
|
||||||
|
uint32: uint32,
|
||||||
|
int8: int8,
|
||||||
|
int16: int16,
|
||||||
|
int32: int32,
|
||||||
|
uptime: uptime,
|
||||||
|
float: float,
|
||||||
|
ufloat: ufloat,
|
||||||
|
pressure: pressure,
|
||||||
|
latLng: latLng,
|
||||||
|
hdop: hdop,
|
||||||
|
altitude: altitude,
|
||||||
|
bitmap1: bitmap1,
|
||||||
|
bitmap2: bitmap2,
|
||||||
|
version: version,
|
||||||
|
decode: decode
|
||||||
|
};
|
||||||
|
}
|
114
src/TTNv3/plain_decodeUplink.js
Normal file
114
src/TTNv3/plain_decodeUplink.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// Decoder for device payload encoder "PLAIN"
|
||||||
|
// copy&paste to TTN Console V3 -> Applications -> Payload formatters -> Uplink -> Javascript
|
||||||
|
// modified for The Things Stack V3 by Caspar Armster, dasdigidings e.V.
|
||||||
|
|
||||||
|
function decodeUplink(input) {
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (input.fPort === 1) {
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
if (input.bytes.length >= 2) {
|
||||||
|
data.wifi = (input.bytes[i++] << 8) | input.bytes[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.bytes.length === 4 || input.bytes.length > 15) {
|
||||||
|
data.ble = (input.bytes[i++] << 8) | input.bytes[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.bytes.length > 4) {
|
||||||
|
data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.sats = input.bytes[i++];
|
||||||
|
data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
|
||||||
|
data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
|
||||||
|
}
|
||||||
|
data.pax = 0;
|
||||||
|
if ('wifi' in data) {
|
||||||
|
data.pax += data.wifi;
|
||||||
|
}
|
||||||
|
if ('ble' in data) {
|
||||||
|
data.pax += data.ble;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 2) {
|
||||||
|
var i = 0;
|
||||||
|
data.voltage = ((input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.uptime = ((input.bytes[i++] << 56) | (input.bytes[i++] << 48) | (input.bytes[i++] << 40) | (input.bytes[i++] << 32) | (input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.cputemp = input.bytes[i++];
|
||||||
|
data.memory = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.reset0 = input.bytes[i++];
|
||||||
|
data.reset1 = input.bytes[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 4) {
|
||||||
|
var i = 0;
|
||||||
|
data.latitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.longitude = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.sats = input.bytes[i++];
|
||||||
|
data.hdop = (input.bytes[i++] << 8) | (input.bytes[i++]);
|
||||||
|
data.altitude = ((input.bytes[i++] << 8) | (input.bytes[i++]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 5) {
|
||||||
|
var i = 0;
|
||||||
|
data.button = input.bytes[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 6) {
|
||||||
|
var i = 0;
|
||||||
|
data.rssi = input.bytes[i++];
|
||||||
|
data.beacon = input.bytes[i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 7) {
|
||||||
|
var i = 0;
|
||||||
|
data.temperature = ((input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.pressure = ((input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.humidity = ((input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.air = ((input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 8) {
|
||||||
|
var i = 0;
|
||||||
|
if (input.bytes.length >= 2) {
|
||||||
|
data.voltage = (input.bytes[i++] << 8) | input.bytes[i++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 9) {
|
||||||
|
// timesync request
|
||||||
|
if (input.bytes.length === 1) {
|
||||||
|
data.timesync_seqno = input.bytes[0];
|
||||||
|
}
|
||||||
|
// epoch time answer
|
||||||
|
if (input.bytes.length === 5) {
|
||||||
|
var i = 0;
|
||||||
|
data.time = ((input.bytes[i++] << 24) | (input.bytes[i++] << 16) | (input.bytes[i++] << 8) | input.bytes[i++]);
|
||||||
|
data.timestatus = input.bytes[i++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.fPort === 10) {
|
||||||
|
var i = 0;
|
||||||
|
if (input.bytes.length >= 2) {
|
||||||
|
data.ens = (input.bytes[i++] << 8) | input.bytes[i++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hdop) {
|
||||||
|
data.hdop /= 100;
|
||||||
|
data.latitude /= 1000000;
|
||||||
|
data.longitude /= 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.bytes = input.bytes; // comment out if you do not want to include the original payload
|
||||||
|
data.port = input.fPort; // comment out if you do not want to inlude the port
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
warnings: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user