When you want to to get data from your device over an LPWAN network (such as LoRaWAN), you cannot use a plain JSON format over HTTP(S) or MQTT. Given their constrained nature, LPWAN networks expect that your device sends over a binary payload. The payload should be as compact as possible to minimise the Time-on-Air and reduce power consumption (for battery operated) devices.
This guide will help you to encode and decode different types of data in as little bytes as possible, using JavaScript.
The goal of the conversion is to translate the state which device reports in binary payload into a format understandable to AllThingstTalk Platform.
For example, a device reports its battery and force states like this:
Example payload: 1B 5D | ||
Byte | Hex | |
---|---|---|
0 | 1B | Set force to 27 |
1 | 5D | Set battery to 93 |
The device state in AllThingsTalk should look like this:
{
"force": {
"value": 27
},
"battery": {
"value": 93
}
}
Ultimately, you should write a JavaScript conversion which:
force
battery
.You can provide a conversion for uplink messages, and optionally another conversion for downlink messages.
Once you provide JS conversion, the platform will validate it against external outputs, malicious code etc. and apply it to the device object. From then on, every payload will be processed by the conversion.
Debug tab shows both successful and unsuccessful conversions.
Elsys ERS are devices for advanced indoor environment monitoring. ERS device reports the state of all of its assets in a binary payload.
More so, the payloads are dynamic, e.g. depending on the current real world situation, it might report just CO2 and Light readings, while at other times it might report the states of all the assets. This makes it ideal candidate to a JavaScript conversion.
Uplink conversion
Here’s the example of the Uplink conversion:
function converter(code) {
const TYPE_TEMP = 0x01; //temp 2 bytes -3276.8°C -->3276.7°C
const TYPE_RH = 0x02; //Humidity 1 byte 0-100%
const TYPE_ACC = 0x03; //acceleration 3 bytes X,Y,Z -128 --> 127 +/-63=1G
const TYPE_LIGHT = 0x04; //Light 2 bytes 0-->65535 Lux
const TYPE_MOTION = 0x05; //No of motion 1 byte 0-255
const TYPE_CO2 = 0x06; //Co2 2 bytes 0-65535 ppm
const TYPE_VDD = 0x07; //VDD 2byte 0-65535mV
const TYPE_ANALOG1 = 0x08; //VDD 2byte 0-65535mV
const TYPE_GPS = 0x09; //3bytes lat 3bytes long binary
const TYPE_PULSE1 = 0x0A; //2bytes relative pulse count
const TYPE_PULSE1_ABS = 0x0B; //4bytes no 0->0xFFFFFFFF
const TYPE_EXT_TEMP1 = 0x0C; //2bytes -3276.5C-->3276.5C
const TYPE_EXT_DIGITAL = 0x0D; //1bytes value 1 or 0
const TYPE_EXT_DISTANCE = 0x0E; //2bytes distance in mm
const TYPE_ACC_MOTION = 0x0F; //1byte number of vibration/motion
const TYPE_IR_TEMP = 0x10; //2bytes internal temp 2bytes external temp -3276.5C-->3276.5C
const TYPE_OCCUPANCY = 0x11; //1byte data
const TYPE_WATERLEAK = 0x12; //1byte data 0-255
const TYPE_GRIDEYE = 0x13; //65byte temperature data 1byte ref+64byte external temp
const TYPE_PRESSURE = 0x14; //4byte pressure data (hPa)
const TYPE_SOUND = 0x15; //2byte sound data (peak/avg)
const TYPE_PULSE2 = 0x16; //2bytes 0-->0xFFFF
const TYPE_PULSE2_ABS = 0x17; //4bytes no 0->0xFFFFFFFF
const TYPE_ANALOG2 = 0x18; //2bytes voltage in mV
const TYPE_EXT_TEMP2 = 0x19; //2bytes -3276.5C-->3276.5C
const TYPE_EXT_DIGITAL2 = 0x1A; // 1bytes value 1 or 0
const TYPE_EXT_ANALOG_UV = 0x1B; // 4 bytes signed int (uV)
const TYPE_TVOC = 0x1C; // 2 bytes (ppb)
const TYPE_DEBUG = 0x3D; // 4bytes debug
function bin16dec(bin) {
var num = bin & 0xFFFF;
if (0x8000 & num)
num = -(0x010000 - num);
return num;
}
function bin8dec(bin) {
var num = bin & 0xFF;
if (0x80 & num)
num = -(0x0100 - num);
return num;
}
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
function DecodeElsysPayload(data) {
var obj = new Object();
for (i = 0; i < data.length; i++) {
//console.log(data[i]);
switch (data[i]) {
case TYPE_TEMP: //Temperature
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
obj.temperature = {value: temp / 10};
i += 2;
break
case TYPE_RH: //Humidity
var rh = (data[i + 1]);
obj.humidity = {value: rh};
i += 1;
break
case TYPE_ACC: //Acceleration
obj.acceleration = {
x: bin8dec(data[i + 1]),
y: bin8dec(data[i + 2]),
z: bin8dec(data[i + 3])
}
i += 3;
break
case TYPE_LIGHT: //Light
obj.light = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_MOTION: //Motion sensor(PIR)
obj.motion = {value: (data[i + 1])};
i += 1;
break
case TYPE_CO2: //CO2
obj.co2 = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_VDD: //Battery level
obj.vdd = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_ANALOG1: //Analog input 1
obj.analog1 = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_GPS: //gps
i++;
obj.location = {
lat: (data[i + 0] | data[i + 1] << 8 | data[i + 2] << 16 | (data[i + 2] & 0x80 ? 0xFF << 24 : 0)) / 10000,
long: (data[i + 3] | data[i + 4] << 8 | data[i + 5] << 16 | (data[i + 5] & 0x80 ? 0xFF << 24 : 0)) / 10000
}
i += 5;
break
case TYPE_PULSE1: //Pulse input 1
obj.pulse1 = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_PULSE1_ABS: //Pulse input 1 absolute value
var pulseAbs = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
obj.pulseAbs = {value: pulseAbs};
i += 4;
break
case TYPE_EXT_TEMP1: //External temp
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
obj.externalTemperature = {value: temp / 10};
i += 2;
break
case TYPE_EXT_DIGITAL: //Digital input
obj.digital = {value: (data[i + 1])};
i += 1;
break
case TYPE_EXT_DISTANCE: //Distance sensor input
obj.distance = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_ACC_MOTION: //Acc motion
obj.accMotion = {value: (data[i + 1])};
i += 1;
break
case TYPE_IR_TEMP: //IR temperature
var iTemp = (data[i + 1] << 8) | (data[i + 2]);
iTemp = bin16dec(iTemp);
var eTemp = (data[i + 3] << 8) | (data[i + 4]);
eTemp = bin16dec(eTemp);
obj.irInternalTemperature = {value: iTemp / 10}
obj.irExternalTemperature = {value: eTemp / 10}
i += 4;
break
case TYPE_OCCUPANCY: //Body occupancy
obj.occupancy = {value: (data[i + 1])};
i += 1;
break
case TYPE_WATERLEAK: //Water leak
obj.waterleak = {value: (data[i + 1])};
i += 1;
break
case TYPE_GRIDEYE: //Grideye data
var ref = data[i+1];
i++;
obj.grideye = [];
for(var j = 0; j < 64; j++) {
obj.grideye[j] = ref + (data[1+i+j] / 10.0);
}
obj.grideye = {
value: obj.grideye
}
i += 64;
break
case TYPE_PRESSURE: //External Pressure
var temp = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]);
obj.pressure = {value: temp / 1000};
i += 4;
break
case TYPE_SOUND: //Sound
obj.soundPeak = {value: data[i + 1]};
obj.soundAvg = {value: data[i + 2]};
i += 2;
break
case TYPE_PULSE2: //Pulse 2
obj.pulse2 = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_PULSE2_ABS: //Pulse input 2 absolute value
obj.pulseAbs2 = {value: (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4])};
i += 4;
break
case TYPE_ANALOG2: //Analog input 2
obj.analog2 = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
case TYPE_EXT_TEMP2: //External temp 2
var temp = (data[i + 1] << 8) | (data[i + 2]);
temp = bin16dec(temp);
if(typeof obj.externalTemperature2 === "number") {
obj.externalTemperature2 = [obj.externalTemperature2];
}
if(typeof obj.externalTemperature2 === "object") {
obj.externalTemperature2.push(temp / 10);
} else {
obj.externalTemperature2 = temp / 10;
}
obj.externalTemperature2 = {
value: obj.externalTemperature2
}
i += 2;
break
case TYPE_EXT_DIGITAL2: //Digital input 2
obj.digital2 = {value: (data[i + 1])};
i += 1;
break
case TYPE_EXT_ANALOG_UV: //Load cell analog uV
obj.analogUv = {value: (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4])};
i += 4;
break
case TYPE_TVOC:
obj.tvoc = {value: (data[i + 1] << 8) | (data[i + 2])};
i += 2;
break
default: //somthing is wrong with data
i = data.length;
break
}
}
return obj;
}
return JSON.stringify(DecodeElsysPayload(hexToBytes(code)));
}
Now, for an example payload of 0100e202290400270506060308070d62
this would be converted and parsed like this (output of the DEBUG log):
Parsed {"co2": {"value": 776}, "humidity": {"value": 41}, "light": {"value": 39}, "motion": {"value": 6}, "temperature": {"value": 22.6}, "vdd": {"value": 3426}} from payload 01 00 E2 02 29 04 00 27 05 06 06 03 08 07 0D 62 using JS
Downlink conversion
Here’s the example of the downlink conversion:
function converter(code) {
var given_obj = JSON.parse(code);
var given_value = given_obj[Object.keys(given_obj)[0]].value;
return "0x018A03000" + Number(given_value).toString(16).toUpperCase();}
This would enable you to send a command from the actuator
asset and schedule a downlink for your device:
Parsed {"actuator": {"at": "2022-03-02T09:13:50.422881Z", "value": 13}} to payload 30 78 30 31 38 41 30 33 30 30 30 44 using JS
To specify port for your downlink you can do this:
function converter(code){
var data = {data: "AA00", meta: {
port: 9
}
};
return JSON.stringify(data)
}
To specify that your downlink should be confirmed, do this:
function converter(code){
var data = {data: "AA00", meta: {
confirmed: true
}
};
return JSON.stringify(data)
}