|
| 1 | +/** |
| 2 | + * @reference https://github.com/myDevicesIoT/cayenne-docs/blob/master/docs/LORA.md |
| 3 | + * @reference http://openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html#extlabel |
| 4 | + * |
| 5 | + * Adapted for lora-app-server from https://gist.github.com/iPAS/e24970a91463a4a8177f9806d1ef14b8 |
| 6 | + * |
| 7 | + * Type IPSO LPP Hex Data Size Data Resolution per bit |
| 8 | + * Digital Input 3200 0 0 1 1 |
| 9 | + * Digital Output 3201 1 1 1 1 |
| 10 | + * Analog Input 3202 2 2 2 0.01 Signed |
| 11 | + * Analog Output 3203 3 3 2 0.01 Signed |
| 12 | + * Illuminance Sensor 3301 101 65 2 1 Lux Unsigned MSB |
| 13 | + * Presence Sensor 3302 102 66 1 1 |
| 14 | + * Temperature Sensor 3303 103 67 2 0.1 °C Signed MSB |
| 15 | + * Humidity Sensor 3304 104 68 1 0.5 % Unsigned |
| 16 | + * Accelerometer 3313 113 71 6 0.001 G Signed MSB per axis |
| 17 | + * Barometer 3315 115 73 2 0.1 hPa Unsigned MSB |
| 18 | + * Time 3333 133 85 4 Unix time MSB |
| 19 | + * Gyrometer 3334 134 86 6 0.01 °/s Signed MSB per axis |
| 20 | + * GPS Location 3336 136 88 9 Latitude : 0.0001 ° Signed MSB |
| 21 | + * Longitude : 0.0001 ° Signed MSB |
| 22 | + * Altitude : 0.01 meter Signed MSB |
| 23 | + * |
| 24 | + * Additional types |
| 25 | + * Generic Sensor 3300 100 64 4 Unsigned integer MSB |
| 26 | + * Voltage 3316 116 74 2 0.01 V Unsigned MSB |
| 27 | + * Current 3317 117 75 2 0.001 A Unsigned MSB |
| 28 | + * Frequency 3318 118 76 4 1 Hz Unsigned MSB |
| 29 | + * Percentage 3320 120 78 1 1% Unsigned |
| 30 | + * Altitude 3321 121 79 2 1m Signed MSB |
| 31 | + * Concentration 3325 125 7D 2 1 PPM unsigned : 1pmm = 1 * 10 ^-6 = 0.000 001 |
| 32 | + * Power 3328 128 80 2 1 W Unsigned MSB |
| 33 | + * Distance 3330 130 82 4 0.001m Unsigned MSB |
| 34 | + * Energy 3331 131 83 4 0.001kWh Unsigned MSB |
| 35 | + * Colour 3335 135 87 3 R: 255 G: 255 B: 255 |
| 36 | + * Direction 3332 132 84 2 1º Unsigned MSB |
| 37 | + * Switch 3342 142 8E 1 0/1 |
| 38 | + * |
| 39 | + * RAKwireless specific types |
| 40 | + * GPS Location 3337 137 89 11 Higher precision location information |
| 41 | + * Latitude : 0.000001 ° Signed MSB |
| 42 | + * Longitude : 0.000001 ° Signed MSB |
| 43 | + * Altitude : 0.01 meter Signed MSB |
| 44 | + * VOC index 3338 138 8A 1 VOC index |
| 45 | + * Wind Speed 3390 190 BE 2 Wind speed 0.01 m/s |
| 46 | + * Wind Direction 3391 191 BF 2 Wind direction 1º Unsigned MSB |
| 47 | + * Light Level 3403 203 CB 1 0 0-5 lux, 1 6-50 lux, 2 51-100 lux, 3 101-500 lux, 4 501-2000 lux, 6 >2000 lux |
| 48 | + * Soil Moisture 3388 188 BC 2 0.1 % in 0~100% (m3/m3) |
| 49 | + * Soil EC 3392 192 C0 2 0.001, mS/cm |
| 50 | + * Soil pH high prec. 3393 193 C1 2 0.01 pH |
| 51 | + * Soil pH low prec. 3394 194 C2 2 0.1 pH |
| 52 | + * Pyranometer 3395 195 C3 2 1 unsigned MSB (W/m2) |
| 53 | + * Precise Humidity 3312 112 70 2 0.1 %RH |
| 54 | + * |
| 55 | + */ |
| 56 | + |
| 57 | +// lppDecode decodes an array of bytes into an array of ojects, |
| 58 | +// each one with the channel, the data type and the value. |
| 59 | +function lppDecode(bytes) { |
| 60 | + |
| 61 | + var sensor_types = { |
| 62 | + 0: { 'size': 1, 'name': 'digital_in', 'signed': false, 'divisor': 1 }, |
| 63 | + 1: { 'size': 1, 'name': 'digital_out', 'signed': false, 'divisor': 1 }, |
| 64 | + 2: { 'size': 2, 'name': 'analog_in', 'signed': true, 'divisor': 100 }, |
| 65 | + 3: { 'size': 2, 'name': 'analog_out', 'signed': true, 'divisor': 100 }, |
| 66 | + 100: { 'size': 4, 'name': 'generic', 'signed': false, 'divisor': 1 }, |
| 67 | + 101: { 'size': 2, 'name': 'illuminance', 'signed': false, 'divisor': 1 }, |
| 68 | + 102: { 'size': 1, 'name': 'presence', 'signed': false, 'divisor': 1 }, |
| 69 | + 103: { 'size': 2, 'name': 'temperature', 'signed': true, 'divisor': 10 }, |
| 70 | + 104: { 'size': 1, 'name': 'humidity', 'signed': false, 'divisor': 2 }, |
| 71 | + 112: { 'size': 2, 'name': 'humidity_prec', 'signed': true, 'divisor': 10 }, |
| 72 | + 113: { 'size': 6, 'name': 'accelerometer', 'signed': true, 'divisor': 1000 }, |
| 73 | + 115: { 'size': 2, 'name': 'barometer', 'signed': false, 'divisor': 10 }, |
| 74 | + 116: { 'size': 2, 'name': 'voltage', 'signed': false, 'divisor': 100 }, |
| 75 | + 117: { 'size': 2, 'name': 'current', 'signed': false, 'divisor': 1000 }, |
| 76 | + 118: { 'size': 4, 'name': 'frequency', 'signed': false, 'divisor': 1 }, |
| 77 | + 120: { 'size': 1, 'name': 'percentage', 'signed': false, 'divisor': 1 }, |
| 78 | + 121: { 'size': 2, 'name': 'altitude', 'signed': true, 'divisor': 1 }, |
| 79 | + 125: { 'size': 2, 'name': 'concentration', 'signed': false, 'divisor': 1 }, |
| 80 | + 128: { 'size': 2, 'name': 'power', 'signed': false, 'divisor': 1 }, |
| 81 | + 130: { 'size': 4, 'name': 'distance', 'signed': false, 'divisor': 1000 }, |
| 82 | + 131: { 'size': 4, 'name': 'energy', 'signed': false, 'divisor': 1000 }, |
| 83 | + 132: { 'size': 2, 'name': 'direction', 'signed': false, 'divisor': 1 }, |
| 84 | + 133: { 'size': 4, 'name': 'time', 'signed': false, 'divisor': 1 }, |
| 85 | + 134: { 'size': 6, 'name': 'gyrometer', 'signed': true, 'divisor': 100 }, |
| 86 | + 135: { 'size': 3, 'name': 'colour', 'signed': false, 'divisor': 1 }, |
| 87 | + 136: { 'size': 9, 'name': 'gps', 'signed': true, 'divisor': [10000, 10000, 100] }, |
| 88 | + 137: { 'size': 11, 'name': 'gps', 'signed': true, 'divisor': [1000000, 1000000, 100] }, |
| 89 | + 138: { 'size': 2, 'name': 'voc', 'signed': false, 'divisor': 1 }, |
| 90 | + 142: { 'size': 1, 'name': 'switch', 'signed': false, 'divisor': 1 }, |
| 91 | + 188: { 'size': 2, 'name': 'soil_moist', 'signed': false, 'divisor': 10 }, |
| 92 | + 190: { 'size': 2, 'name': 'wind_speed', 'signed': false, 'divisor': 100 }, |
| 93 | + 191: { 'size': 2, 'name': 'wind_direction', 'signed': false, 'divisor': 1 }, |
| 94 | + 192: { 'size': 2, 'name': 'soil_ec', 'signed': false, 'divisor': 1000 }, |
| 95 | + 193: { 'size': 2, 'name': 'soil_ph_h', 'signed': false, 'divisor': 100 }, |
| 96 | + 194: { 'size': 2, 'name': 'soil_ph_l', 'signed': false, 'divisor': 10 }, |
| 97 | + 195: { 'size': 2, 'name': 'pyranometer', 'signed': false, 'divisor': 1 }, |
| 98 | + 203: { 'size': 1, 'name': 'light', 'signed': false, 'divisor': 1 }, |
| 99 | + }; |
| 100 | + |
| 101 | + function arrayToDecimal(stream, is_signed, divisor) { |
| 102 | + |
| 103 | + var value = 0; |
| 104 | + for (var i = 0; i < stream.length; i++) { |
| 105 | + if (stream[i] > 0xFF) |
| 106 | + throw 'Byte value overflow!'; |
| 107 | + value = (value << 8) | stream[i]; |
| 108 | + } |
| 109 | + |
| 110 | + if (is_signed) { |
| 111 | + var edge = 1 << (stream.length) * 8; // 0x1000.. |
| 112 | + var max = (edge - 1) >> 1; // 0x0FFF.. >> 1 |
| 113 | + value = (value > max) ? value - edge : value; |
| 114 | + } |
| 115 | + |
| 116 | + value /= divisor; |
| 117 | + |
| 118 | + return value; |
| 119 | + |
| 120 | + } |
| 121 | + |
| 122 | + var sensors = []; |
| 123 | + var i = 0; |
| 124 | + while (i < bytes.length) { |
| 125 | + |
| 126 | + var s_no = bytes[i++]; |
| 127 | + var s_type = bytes[i++]; |
| 128 | + if (typeof sensor_types[s_type] == 'undefined') { |
| 129 | + throw 'Sensor type error!: ' + s_type; |
| 130 | + } |
| 131 | + |
| 132 | + var s_value = 0; |
| 133 | + var type = sensor_types[s_type]; |
| 134 | + switch (s_type) { |
| 135 | + |
| 136 | + case 113: // Accelerometer |
| 137 | + case 134: // Gyrometer |
| 138 | + s_value = { |
| 139 | + 'x': arrayToDecimal(bytes.slice(i + 0, i + 2), type.signed, type.divisor), |
| 140 | + 'y': arrayToDecimal(bytes.slice(i + 2, i + 4), type.signed, type.divisor), |
| 141 | + 'z': arrayToDecimal(bytes.slice(i + 4, i + 6), type.signed, type.divisor) |
| 142 | + }; |
| 143 | + break; |
| 144 | + case 136: // GPS Location |
| 145 | + s_value = { |
| 146 | + 'latitude': arrayToDecimal(bytes.slice(i + 0, i + 3), type.signed, type.divisor[0]), |
| 147 | + 'longitude': arrayToDecimal(bytes.slice(i + 3, i + 6), type.signed, type.divisor[1]), |
| 148 | + 'altitude': arrayToDecimal(bytes.slice(i + 6, i + 9), type.signed, type.divisor[2]) |
| 149 | + }; |
| 150 | + break; |
| 151 | + case 137: // Precise GPS Location |
| 152 | + s_value = { |
| 153 | + 'latitude': arrayToDecimal(bytes.slice(i + 0, i + 4), type.signed, type.divisor[0]), |
| 154 | + 'longitude': arrayToDecimal(bytes.slice(i + 4, i + 8), type.signed, type.divisor[1]), |
| 155 | + 'altitude': arrayToDecimal(bytes.slice(i + 8, i + 11), type.signed, type.divisor[2]) |
| 156 | + }; |
| 157 | + sensors.push({ |
| 158 | + 'channel': s_no, |
| 159 | + 'type': s_type, |
| 160 | + 'name': 'location', |
| 161 | + 'value': "(" + s_value.latitude + "," + s_value.longitude + ")" |
| 162 | + }); |
| 163 | + sensors.push({ |
| 164 | + 'channel': s_no, |
| 165 | + 'type': s_type, |
| 166 | + 'name': 'altitude', |
| 167 | + 'value': s_value.altitude |
| 168 | + }); |
| 169 | + break; |
| 170 | + case 135: // Colour |
| 171 | + s_value = { |
| 172 | + 'r': arrayToDecimal(bytes.slice(i + 0, i + 1), type.signed, type.divisor), |
| 173 | + 'g': arrayToDecimal(bytes.slice(i + 1, i + 2), type.signed, type.divisor), |
| 174 | + 'b': arrayToDecimal(bytes.slice(i + 2, i + 3), type.signed, type.divisor) |
| 175 | + }; |
| 176 | + break; |
| 177 | + |
| 178 | + default: // All the rest |
| 179 | + s_value = arrayToDecimal(bytes.slice(i, i + type.size), type.signed, type.divisor); |
| 180 | + break; |
| 181 | + } |
| 182 | + |
| 183 | + sensors.push({ |
| 184 | + 'channel': s_no, |
| 185 | + 'type': s_type, |
| 186 | + 'name': type.name, |
| 187 | + 'value': s_value |
| 188 | + }); |
| 189 | + |
| 190 | + i += type.size; |
| 191 | + |
| 192 | + } |
| 193 | + |
| 194 | + return sensors; |
| 195 | + |
| 196 | +} |
| 197 | + |
| 198 | +// For TTN, Helium and Datacake |
| 199 | +function Decoder(bytes, fport) { |
| 200 | + |
| 201 | + // bytes = input.bytes; |
| 202 | + // fPort = input.fPort; |
| 203 | + |
| 204 | + // flat output (like original decoder): |
| 205 | + var response = {}; |
| 206 | + lppDecode(bytes, 1).forEach(function (field) { |
| 207 | + response[field['name'] + '_' + field['channel']] = field['value']; |
| 208 | + }); |
| 209 | + |
| 210 | + // Enable only for Datacake |
| 211 | + // response['LORA_RSSI'] = (!!normalizedPayload.gateways && !!normalizedPayload.gateways[0] && normalizedPayload.gateways[0].rssi) || 0; |
| 212 | + // response['LORA_SNR'] = (!!normalizedPayload.gateways && !!normalizedPayload.gateways[0] && normalizedPayload.gateways[0].snr) || 0; |
| 213 | + // response['LORA_DATARATE'] = normalizedPayload.data_rate; |
| 214 | + |
| 215 | + return response; |
| 216 | +} |
| 217 | + |
| 218 | +// For Chirpstack V3 |
| 219 | +function Decode(fPort, bytes, variables) { |
| 220 | + |
| 221 | + // bytes = input.bytes; |
| 222 | + // fPort = input.fPort; |
| 223 | + |
| 224 | + // flat output (like original decoder): |
| 225 | + var response = {}; |
| 226 | + lppDecode(bytes, 1).forEach(function (field) { |
| 227 | + response[field['name'] + '_' + field['channel']] = field['value']; |
| 228 | + }); |
| 229 | + return response; |
| 230 | +} |
| 231 | + |
| 232 | +// Chirpstack v3 to v4 compatibility wrapper |
| 233 | +function decodeUplink(input) { |
| 234 | + return { |
| 235 | + data: Decode(input.fPort, input.bytes, input.variables) |
| 236 | + }; |
| 237 | +} |
| 238 | + |
| 239 | +function encodeDownlink(input) { |
| 240 | + return { |
| 241 | + bytes: [input.data.temp] |
| 242 | + }; |
| 243 | +} |
0 commit comments