Skip to content

Instantly share code, notes, and snippets.

@iPAS
Last active June 20, 2024 16:24
Show Gist options
  • Save iPAS/e24970a91463a4a8177f9806d1ef14b8 to your computer and use it in GitHub Desktop.
Save iPAS/e24970a91463a4a8177f9806d1ef14b8 to your computer and use it in GitHub Desktop.
Cayenne Low-power Payload Decoder (javascript / node.js)
/**
* Cayenne Low-power Payload Library
*
* @author Pasakorn Tiwatthanont
* @email [email protected]
*
* @link https://htmlcheatsheet.com/js/
*/
// console.log('Node.js version: ' + process.version)
format = require('string-format')
/**
* Byte stream to fixed-point decimal word.
*
* @param stream: array of bytes.
* @return: word of the bytes in big-endian format.
*/
function arrayToDecimal(stream, is_signed=false, decimal_point=0) {
var value = 0;
for (var i = 0; i < stream.length; i++) {
if (stream[i] > 0xFF)
throw 'Byte value overflow!';
value = (value<<8) | stream[i];
}
if (is_signed) {
edge = 1 << (stream.length )*8; // 0x1000..
max = (edge-1) >> 1; // 0x0FFF.. >> 1
value = (value > max) ? value - edge : value;
}
if (decimal_point) {
value /= Math.pow(10, decimal_point);
}
return value;
}
/**
* Cayenne Low-power Payload Decoder
*
* @param payload: array of bytes.
* @return: JSON
*/
function decode(payload) {
if (payload == null)
payload = [
0x01, 0x67, 0x00, 0xE1, // "temperature_1" : 22.5 (fixed point)
0x02, 0x73, 0x29, 0xEC, // "barometric_pressure_2": 1073.2 (fixed point)
0x03, 0x88, // "gps_3":
0x02, 0xDD, 0xFC, // { "latitude" : 18.79,
0x0F, 0x1A, 0x68, // "longitude": 98.98,
0x00, 0x79, 0x18, // "altitude" : 310 }
]
/**
* @reference https://github.com/myDevicesIoT/cayenne-docs/blob/master/docs/LORA.md
*
* Type IPSO LPP Hex Data Size Data Resolution per bit
* Digital Input 3200 0 0 1 1
* Digital Output 3201 1 1 1 1
* Analog Input 3202 2 2 2 0.01 Signed
* Analog Output 3203 3 3 2 0.01 Signed
* Illuminance Sensor 3301 101 65 2 1 Lux Unsigned MSB
* Presence Sensor 3302 102 66 1 1
* Temperature Sensor 3303 103 67 2 0.1 °C Signed MSB
* Humidity Sensor 3304 104 68 1 0.5 % Unsigned
* Accelerometer 3313 113 71 6 0.001 G Signed MSB per axis
* Barometer 3315 115 73 2 0.1 hPa Unsigned MSB
* Gyrometer 3334 134 86 6 0.01 °/s Signed MSB per axis
* GPS Location 3336 136 88 9 Latitude : 0.0001 ° Signed MSB
* Longitude : 0.0001 ° Signed MSB
* Altitude : 0.01 meter Signed MSB
*/
var sensor_types = {
0 : {'size': 1, 'name': 'Digital Input' , 'signed': false, 'decimal_point': 0,},
1 : {'size': 1, 'name': 'Digital Output' , 'signed': false, 'decimal_point': 0,},
2 : {'size': 2, 'name': 'Analog Input' , 'signed': true , 'decimal_point': 2,},
3 : {'size': 2, 'name': 'Analog Output' , 'signed': true , 'decimal_point': 2,},
101: {'size': 2, 'name': 'Illuminance Sensor' , 'signed': false, 'decimal_point': 0,},
102: {'size': 1, 'name': 'Presence Sensor' , 'signed': false, 'decimal_point': 0,},
103: {'size': 2, 'name': 'Temperature Sensor' , 'signed': true , 'decimal_point': 1,},
104: {'size': 1, 'name': 'Humidity Sensor' , 'signed': false, 'decimal_point': 1,},
113: {'size': 6, 'name': 'Accelerometer' , 'signed': true , 'decimal_point': 3,},
115: {'size': 2, 'name': 'Barometer' , 'signed': false, 'decimal_point': 1,},
134: {'size': 6, 'name': 'Gyrometer' , 'signed': true , 'decimal_point': 2,},
136: {'size': 9, 'name': 'GPS Location' , 'signed': false, 'decimal_point': [4,4,2], },};
var sensors = {};
var i = 0;
while (i < payload.length) {
// console.log(i);
// console.log(typeof payload[i])
// console.log(payload[i].toString())
s_no = payload[i++];
s_type = payload[i++];
if (typeof sensor_types[s_type] == 'undefined')
throw format('Sensor type error!: {}', s_type);
s_size = sensor_types[s_type].size;
s_name = sensor_types[s_type].name;
switch (s_type) {
case 0 : // Digital Input
case 1 : // Digital Output
case 2 : // Analog Input
case 3 : // Analog Output
case 101: // Illuminance Sensor
case 102: // Presence Sensor
case 103: // Temperature Sensor
case 104: // Humidity Sensor
case 113: // Accelerometer
case 115: // Barometer
case 134: // Gyrometer
s_value = arrayToDecimal(payload.slice(i, i+s_size),
is_signed = sensor_types[s_type].signed,
decimal_point = sensor_types[s_type].decimal_point);
//console.log(format('no:{} size:{} type:{} value:{}', s_no, s_size, s_name, s_value));
break;
case 136: // GPS Location
s_value = {
'latitude': arrayToDecimal(payload.slice(i+0, i+3), is_signed=sensor_types[s_type].signed, decimal_point=sensor_types[s_type].decimal_point[0]),
'longitude': arrayToDecimal(payload.slice(i+3, i+6), is_signed=sensor_types[s_type].signed, decimal_point=sensor_types[s_type].decimal_point[1]),
'altitude': arrayToDecimal(payload.slice(i+6, i+9), is_signed=sensor_types[s_type].signed, decimal_point=sensor_types[s_type].decimal_point[2]),};
//console.log(format('no:{} size:{} type:{} lat:{} lon:{} alt:{}', s_no, s_size, s_name, s_value.lat, s_value.lon, s_value.alt));
break;
}
sensors[s_no] = {'type': s_type, 'type_name': s_name, 'value': s_value };
i += s_size;
}
return sensors;
}
/**
* Test
*/
function test() {
// Test arrayToDecimal()
arrTest = [ 0xFF ];
console.log(arrayToDecimal(arrTest) + ' -> ' + arrayToDecimal(arrTest, is_signed=true)); // -1
arrTest = [ 0xFF, 0xFF ];
console.log(arrayToDecimal(arrTest) + ' -> ' + arrayToDecimal(arrTest, is_signed=true)); // -1
// Test decode()
console.log(decode());
}
test()
@hjmhardsoft
Copy link

The script decode Humidity incorrectly, due resolution for humidity is 0,5% per bit and no 0,1% per bit.
So, the code need to be modified, replacing

case 134: // Gyrometer
s_value = arrayToDecimal(payload.slice(i, i+s_size),
is_signed = sensor_types[s_type].signed,
decimal_point = sensor_types[s_type].decimal_point);
//console.log(format('no:{} size:{} type:{} value:{}', s_no, s_size, s_name, s_value));
break;

with

case 134: // Gyrometer
s_value = arrayToDecimal(payload.slice(i, i+s_size),
is_signed = sensor_types[s_type].signed,
decimal_point = sensor_types[s_type].decimal_point);
//console.log(format('no:{} size:{} type:{} value:{}', s_no, s_size, s_name, s_value));
if (s_type == 104) { s_value = s_value * 5}; // Humidity Data Resolution is 0,5% per bit, not 0,1% per bit
break;

@avbentem
Copy link

avbentem commented Jan 21, 2020

Alternatively, rather than adding a single exception, one could also replace the decimals handling. Like maybe make arrayToDecimal use some resolution parameter, rather than decimal_point:

function arrayToDecimal(stream, is_signed = false, resolution = 1) {
  ...
  value *= resolution;
  // Ensure the JavaScript IEEE 754 floating point calculations don't
  // return too many decimals, like for 111 * 0.1 = 11.100000000000001.
  // Unary plus-operator to cast string result of toFixed to number.
  return +value.toFixed(10);
}

...along with fixing all definitions to match the table from the comments:

  var sensor_types = {
    0: {size: 1, name: "Digital Input", signed: false, resolution: 1},
    1: {size: 1, name: "Digital Output", signed: false, resolution: 1},
    2: {size: 2, name: "Analog Input", signed: true, resolution: 0.01}
    ...
    104: {size: 1, name: "humidity", signed: false, resolution: 0.5},
    ...

...and the invocations, such as:

  while (i < payload.length) {
    ...
    switch (s_type) {
      ...
      case 104:  // Humidity Sensor
      case 113:  // Accelerometer
      case 115: // Barometer
      case 134: // Gyrometer
        s_value = arrayToDecimal(
          payload.slice(i, i + s_size),
          sensor_types[s_type].signed,
          sensor_types[s_type].resolution
        );
        break;
  ...

As for the additional use of toFixed, beware that JavaScript's 0.1 cannot be represented in an exact JavaScript IEEE 754 floating point, yielding funny results like 0.1 * 111= 11.100000000000001, while 111 / 10 = 11.1 and 0.01 * 111 = 1.11 are just fine.

Finally: lacking named parameters in JavaScript, using is_signed in is_signed = sensor_types[s_type].signed really just creates a temporary variable that is not used anywhere in the code, so can be omitted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment