├── .babelrc ├── .gitignore ├── README.md ├── SenseCAP_K1100_Decoder.js ├── SenseCAP_LoRaWAN_V2_Decoder.js ├── SenseCAP_LoRaWAN_V2_Decoder_For_ChripstackV4.js ├── SenseCAP_LoRaWAN_V2_Decoder_For_Helium.js ├── SenseCAP_LoRaWAN_V2_Decoder_For_TTN.js ├── SenseCAP_LoRaWAN_V4_Decoder_For_Datacake.js ├── SenseCAP_LoRaWAN_V4_Decoder_For_Helium.js ├── SenseCAP_LoRaWAN_V4_Decoder_For_TTN.js ├── SenseCAP_S2120_Weather_Station_Decoder.js ├── datacake └── decoder.js ├── decoder.js ├── decoder_for_aws_iot_core.js ├── decoder_new-v3-uglifyjs.js ├── decoder_new-v3.js ├── package.json └── uglifyjs_decoder_new-v3.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TTN Payload Decoder 2 | 3 | TTN payload decoding script for SenseCAP LoRaWAN messages 4 | This script is used for decoding the LoRaWAN messages sent from the SenseCAP nodes. After decoding, users' applications will get more friendly and readable messages from TTN data API. 5 | 6 | ## How to Use 7 | 8 | #### 1. Prerequisites 9 | 10 | We assume that you already setup your gateway and SenseCAP nodes correctly, and the SenseCAP nodes can send messages to your TTN application. You can already observe the data transmisstion through TTN console. 11 | 12 | ![image](https://user-images.githubusercontent.com/5130185/76604764-48b49180-654a-11ea-992b-761bf8179590.png) 13 | 14 | #### 2. Configure the Payload Decoder 15 | 16 | - Please navigate to the `Payload Formats` tab of your application to which your SenseCAP nodes send data. 17 | - Select `Custom` for `Payload Format` 18 | - Copy and paste the whole contents of `decoder.js` to the `decoder` textarea. 19 | > If you are using the latest version of TTN v3, you may encounter an error that exceeds the character limit. In this case, you will need to use `decoder_new-v3-uglifyjs.js` instead. 20 | - Click `save payload functions` 21 | 22 | ![image](https://user-images.githubusercontent.com/5130185/76605545-954c9c80-654b-11ea-984e-272c58492e85.png) 23 | 24 | 25 | #### 3. Check the Decoded Messages 26 | 27 | You may test the decoding script with sample payload first. To do this, just copy `01 01 10 98 53 00 00 01 02 10 A8 7A 00 00 AF 51` into the `Payload` text input, and click `Test` button. If succeeded, you will see a successfully parsed JSON structure below. 28 | 29 | ![image](https://user-images.githubusercontent.com/5130185/76605914-43584680-654c-11ea-9074-6e78d93059a7.png) 30 | 31 | Then let's check out the magic of the script. We navigate to the `Data` tab, and you can expand any uploaded message to check the `Fields` in the payload. These fields are just populated by the script. 32 | 33 | ![image](https://user-images.githubusercontent.com/5130185/76606268-ec9f3c80-654c-11ea-90e0-332f186475f3.png) 34 | 35 | If you're subscribing the messages with TTN's MQTT Data API, you will also get parsed JSON payload fields. 36 | 37 | ``` 38 | Client mosq-TCSlhYcKaRCn3cIePE received PUBLISH (d0, q0, r0, m0, 'lorawan868/devices/2cf7f12010700041/up', ... (719 bytes)) 39 | lorawan868/devices/2cf7f12010700041/up {"app_id":"lorawan868","dev_id":"2cf7f12010700041","hardware_serial":"2CF7F12010700041","port":2,"counter":1119,"confirmed":true,"payload_raw":"AQEQYG0AAAECEOj9AACWSA==","payload_fields":{"err":0,"messages":[{"measurementId":4097,"measurementValue":28,"type":"report_telemetry"},{"measurementId":4098,"measurementValue":65,"type":"report_telemetry"}],"payload":"010110606D0000010210E8FD00009648","valid":true},"metadata":{"time":"2020-03-13T09:09:45.834032725Z","frequency":867.3,"modulation":"LORA","data_rate":"SF7BW125","airtime":66816000,"coding_rate":"4/5","gateways":[{"gtw_id":"eui-2cf7f11014300001","timestamp":1779605971,"time":"2020-03-13T09:09:45.672666033Z","channel":4,"rssi":-66,"snr":8.8,"rf_chain":0}]}} 40 | ``` 41 | 42 | ## Reference 43 | 44 | How many message types and what's the structure of each message type? 45 | 46 | We will document the latest message types in the comment header of the script. Please check out [here](https://github.com/Seeed-Solution/TTN-Payload-Decoder/blob/master/decoder.js#L1). 47 | -------------------------------------------------------------------------------- /SenseCAP_K1100_Decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | 5 | function Decoder(bytes, port) { 6 | // data split 7 | let splitArray = dataSplit(bytes); 8 | console.log('end data split >>>>>>>>>>>>>>>>', splitArray) 9 | // data decoder 10 | let measurementResultArray = [] 11 | for (let i = 0; i < splitArray.length; i++) { 12 | let item = splitArray[i] 13 | let dataId = item.dataId 14 | let dataValue = item.dataValue 15 | let measurementArray = dataIdAndDataValueJudge(dataId, dataValue) 16 | measurementResultArray.push(measurementArray) 17 | } 18 | console.log(`end data decode >>>>>>>>>>>>${JSON.stringify(measurementResultArray)}`) 19 | return measurementResultArray 20 | } 21 | 22 | /** 23 | * data splits 24 | * @param bytes 25 | * @returns {*[]} 26 | */ 27 | function dataSplit(bytes) { 28 | let frameArray = [] 29 | for (let i = 0; i < bytes.length; i++) { 30 | let remainingValue = bytes 31 | let dataId = remainingValue.substring(0, 2) 32 | let dataValue 33 | let dataObj = {} 34 | switch (dataId) { 35 | case '40' : 36 | case '41' : 37 | case '42' : 38 | case '43' : 39 | case '44' : 40 | case '45' : 41 | dataValue = remainingValue.substring(2, 22) 42 | bytes = remainingValue.substring(22) 43 | dataObj = { 44 | 'dataId': dataId, 'dataValue': dataValue 45 | } 46 | break 47 | default: 48 | dataValue = '9' 49 | console.log(`!!!!!Illegal Data Id`) 50 | break 51 | } 52 | if (dataValue.length < 2) { 53 | break 54 | } 55 | frameArray.push(dataObj) 56 | } 57 | return frameArray 58 | } 59 | 60 | function dataIdAndDataValueJudge(dataId, dataValue) { 61 | let measurementArray = [] 62 | switch (dataId) { 63 | case '40': 64 | case '41': 65 | // lightIntensity 66 | let lightIntensity = dataValue.substring(0, 4) 67 | // loudness 68 | let loudness = dataValue.substring(4, 8) 69 | // X 70 | let accelerateX = dataValue.substring(8, 12) 71 | // Y 72 | let accelerateY = dataValue.substring(12, 16) 73 | // Z 74 | let accelerateZ = dataValue.substring(16, 20) 75 | measurementArray = [{ 76 | value: loraWANV2DataFormat(lightIntensity), measurementId: '4193' 77 | }, { 78 | value: loraWANV2DataFormat(loudness), measurementId: '4192' 79 | }, { 80 | value: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150' 81 | }, { 82 | value: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151' 83 | }, { 84 | value: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152' 85 | }] 86 | break 87 | case '42': 88 | // airTemperature 89 | let airTemperature = dataValue.substring(0, 4) 90 | // AirHumidity 91 | let AirHumidity = dataValue.substring(4, 8) 92 | // tVOC 93 | let tVOC = dataValue.substring(8, 12) 94 | // CO2eq 95 | let CO2eq = dataValue.substring(12, 16) 96 | // Z 97 | let soilMoisture = dataValue.substring(16, 20) 98 | measurementArray = [{ 99 | value: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097' 100 | }, { 101 | value: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098' 102 | }, { 103 | value: loraWANV2DataFormat(tVOC), measurementId: '4200' 104 | }, { 105 | value: loraWANV2DataFormat(CO2eq), measurementId: '4201' 106 | }, { 107 | value: loraWANV2DataFormat(soilMoisture), measurementId: '4103' 108 | }] 109 | break 110 | case '43': 111 | case '44': 112 | // Vision AI: 113 | // AI the first frame 114 | console.log(`!!! AI the first frame ${dataValue}`) 115 | let initDevkitMeasurementId = 4175 116 | for (let i = 0; i < dataValue.length; i += 4) { 117 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 118 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 119 | let aiHeadValues = `${modelId}.${detectionType}` 120 | measurementArray.push({ 121 | value: aiHeadValues, measurementId: initDevkitMeasurementId 122 | }) 123 | initDevkitMeasurementId++ 124 | } 125 | break 126 | case '45': 127 | // Vision AI: 128 | // AI output frame 129 | console.log(`!!! AI output frame ${dataValue}`) 130 | let initTailDevKitMeasurementId = 4180 131 | for (let i = 0; i < dataValue.length; i += 4) { 132 | console.log(`version ai pending info ${dataValue}`) 133 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 134 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 135 | let aiTailValues = `${modelId}.${detectionType}` 136 | measurementArray.push({ 137 | value: aiTailValues, measurementId: initTailDevKitMeasurementId 138 | }) 139 | initTailDevKitMeasurementId++ 140 | } 141 | break 142 | default: 143 | console.error(`!!!!!!!!!illegal request`) 144 | break 145 | } 146 | return measurementArray 147 | } 148 | 149 | /** 150 | * 151 | * data formatting 152 | * @param str 153 | * @param divisor 154 | * @returns {string|number} 155 | */ 156 | function loraWANV2DataFormat(str, divisor = 1) { 157 | let strReverse = bigEndianTransform(str) 158 | let str2 = toBinary(strReverse) 159 | if (str2.substring(0, 1) === '1') { 160 | let arr = str2.split('') 161 | let reverseArr = arr.map((item) => { 162 | if (parseInt(item) === 1) { 163 | return 0 164 | } else { 165 | return 1 166 | } 167 | }) 168 | str2 = parseInt(reverseArr.join(''), 2) + 1 169 | return '-' + str2 / divisor 170 | } 171 | return parseInt(str2, 2) / divisor 172 | } 173 | 174 | /** 175 | * Handling big-endian data formats 176 | * @param data 177 | * @returns {*[]} 178 | */ 179 | function bigEndianTransform(data) { 180 | let dataArray = [] 181 | for (let i = 0; i < data.length; i += 2) { 182 | dataArray.push(data.substring(i, i + 2)) 183 | } 184 | // array of hex 185 | return dataArray 186 | } 187 | 188 | /** 189 | * Convert to an 8-digit binary number with 0s in front of the number 190 | * @param arr 191 | * @returns {string} 192 | */ 193 | function toBinary(arr) { 194 | let binaryData = arr.map((item) => { 195 | let data = parseInt(item, 16) 196 | .toString(2) 197 | let dataLength = data.length 198 | if (data.length !== 8) { 199 | for (let i = 0; i < 8 - dataLength; i++) { 200 | data = `0` + data 201 | } 202 | } 203 | return data 204 | }) 205 | let ret = binaryData.toString() 206 | .replace(/,/g, '') 207 | return ret 208 | } 209 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V2_Decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | function decodeUplink (input, port) { 5 | // data split 6 | 7 | var bytes = input['bytes'] 8 | // init 9 | bytes = bytes2HexString(bytes) 10 | .toLocaleUpperCase() 11 | let result = { 12 | 'err': 0, 'payload': bytes, 'valid': true, messages: [] 13 | } 14 | let splitArray = dataSplit(bytes) 15 | // data decoder 16 | let decoderArray = [] 17 | for (let i = 0; i < splitArray.length; i++) { 18 | let item = splitArray[i] 19 | let dataId = item.dataId 20 | let dataValue = item.dataValue 21 | let messages = dataIdAndDataValueJudge(dataId, dataValue) 22 | decoderArray.push(messages) 23 | } 24 | result.messages = decoderArray 25 | return { data: result } 26 | } 27 | 28 | /** 29 | * data splits 30 | * @param bytes 31 | * @returns {*[]} 32 | */ 33 | function dataSplit (bytes) { 34 | let frameArray = [] 35 | 36 | for (let i = 0; i < bytes.length; i++) { 37 | let remainingValue = bytes 38 | let dataId = remainingValue.substring(0, 2) 39 | let dataValue 40 | let dataObj = {} 41 | switch (dataId) { 42 | case '01' : 43 | case '20' : 44 | case '21' : 45 | case '30' : 46 | case '31' : 47 | case '33' : 48 | case '40' : 49 | case '41' : 50 | case '42' : 51 | case '43' : 52 | case '44' : 53 | case '45' : 54 | dataValue = remainingValue.substring(2, 22) 55 | bytes = remainingValue.substring(22) 56 | dataObj = { 57 | 'dataId': dataId, 'dataValue': dataValue 58 | } 59 | break 60 | case '02': 61 | dataValue = remainingValue.substring(2, 18) 62 | bytes = remainingValue.substring(18) 63 | dataObj = { 64 | 'dataId': '02', 'dataValue': dataValue 65 | } 66 | break 67 | case '03' : 68 | case '06': 69 | dataValue = remainingValue.substring(2, 4) 70 | bytes = remainingValue.substring(4) 71 | dataObj = { 72 | 'dataId': dataId, 'dataValue': dataValue 73 | } 74 | break 75 | case '05' : 76 | case '34': 77 | dataValue = bytes.substring(2, 10) 78 | bytes = remainingValue.substring(10) 79 | dataObj = { 80 | 'dataId': dataId, 'dataValue': dataValue 81 | } 82 | break 83 | case '04': 84 | case '10': 85 | case '32': 86 | case '35': 87 | case '36': 88 | case '37': 89 | case '38': 90 | case '39': 91 | dataValue = bytes.substring(2, 20) 92 | bytes = remainingValue.substring(20) 93 | dataObj = { 94 | 'dataId': dataId, 'dataValue': dataValue 95 | } 96 | break 97 | default: 98 | dataValue = '9' 99 | break 100 | } 101 | if (dataValue.length < 2) { 102 | break 103 | } 104 | frameArray.push(dataObj) 105 | } 106 | return frameArray 107 | } 108 | 109 | function dataIdAndDataValueJudge (dataId, dataValue) { 110 | let messages = [] 111 | switch (dataId) { 112 | case '01': 113 | let temperature = dataValue.substring(0, 4) 114 | let humidity = dataValue.substring(4, 6) 115 | let illumination = dataValue.substring(6, 14) 116 | let uv = dataValue.substring(14, 16) 117 | let windSpeed = dataValue.substring(16, 20) 118 | messages = [{ 119 | measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' 120 | }, { 121 | measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' 122 | }, { 123 | measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' 124 | }, { 125 | measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' 126 | }, { 127 | measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' 128 | }] 129 | break 130 | case '02': 131 | let windDirection = dataValue.substring(0, 4) 132 | let rainfall = dataValue.substring(4, 12) 133 | let airPressure = dataValue.substring(12, 16) 134 | messages = [{ 135 | measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Speed Sensor' 136 | }, { 137 | measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'RainFall Hourly' 138 | }, { 139 | 140 | measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' 141 | }] 142 | break 143 | case '03': 144 | let Electricity = dataValue 145 | messages = [{ 146 | '3000': loraWANV2DataFormat(Electricity) 147 | }] 148 | break 149 | case '04': 150 | let electricityWhether = dataValue.substring(0, 2) 151 | let hwv = dataValue.substring(2, 6) 152 | let bdv = dataValue.substring(6, 10) 153 | let sensorAcquisitionInterval = dataValue.substring(10, 14) 154 | let gpsAcquisitionInterval = dataValue.substring(14, 18) 155 | messages = [{ 156 | '3000': loraWANV2DataFormat(electricityWhether), 157 | '3001': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, 158 | '3502': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, 159 | '3900': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, 160 | '3911': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 161 | }] 162 | break 163 | case '05': 164 | let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) 165 | let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) 166 | messages = [{ 167 | '3900': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, 168 | '3911': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 169 | }] 170 | break 171 | case '06': 172 | let errorCode = dataValue 173 | let descZh 174 | switch (errorCode) { 175 | case '00': 176 | descZh = 'CCL_SENSOR_ERROR_NONE' 177 | break 178 | case '01': 179 | descZh = 'CCL_SENSOR_NOT_FOUND' 180 | break 181 | case '02': 182 | descZh = 'CCL_SENSOR_WAKEUP_ERROR' 183 | break 184 | case '03': 185 | descZh = 'CCL_SENSOR_NOT_RESPONSE' 186 | break 187 | case '04': 188 | descZh = 'CCL_SENSOR_DATA_EMPTY' 189 | break 190 | case '05': 191 | descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' 192 | break 193 | case '06': 194 | descZh = 'CCL_SENSOR_DATA_CRC_ERROR' 195 | break 196 | case '07': 197 | descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' 198 | break 199 | case '08': 200 | descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' 201 | break 202 | case '09': 203 | descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' 204 | break 205 | case '0A': 206 | descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' 207 | break 208 | case '0B': 209 | descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' 210 | break 211 | case '0C': 212 | descZh = 'CCL_SENSOR_DATA_VALUE_HI' 213 | break 214 | case '0D': 215 | descZh = 'CCL_SENSOR_DATA_VALUE_LOW' 216 | break 217 | case '0E': 218 | descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' 219 | break 220 | case '0F': 221 | descZh = 'CCL_SENSOR_ARG_INVAILD' 222 | break 223 | case '10': 224 | descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' 225 | break 226 | case '11': 227 | descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' 228 | break 229 | case '12': 230 | descZh = 'CCL_SENSOR_RS485_REG_MISSED' 231 | break 232 | case '13': 233 | descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' 234 | break 235 | case '14': 236 | descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' 237 | break 238 | case '15': 239 | descZh = 'CCL_SENSOR_CONFIG_ERROR' 240 | break 241 | case 'FF': 242 | descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' 243 | break 244 | default: 245 | descZh = 'CC_OTHER_FAILED' 246 | break 247 | } 248 | messages = [{ 249 | measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh 250 | }] 251 | break 252 | case '10': 253 | let statusValue = dataValue.substring(0, 2) 254 | let { status, type } = loraWANV2BitDataFormat(statusValue) 255 | let sensecapId = dataValue.substring(2) 256 | messages = [{ 257 | status: status, channelType: type, sensorEui: sensecapId 258 | }] 259 | break 260 | case '20': 261 | let initmeasurementId = 4175 262 | let sensor = [] 263 | for (let i = 0; i < dataValue.length; i += 4) { 264 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 265 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 266 | let aiHeadValues = `${modelId}.${detectionType}` 267 | sensor.push({ 268 | measurementValue: aiHeadValues, measurementId: initmeasurementId 269 | }) 270 | initmeasurementId++ 271 | } 272 | messages = sensor 273 | break 274 | case '21': 275 | // Vision AI: 276 | // AI 识别输出帧 277 | let tailValueArray = [] 278 | let initTailmeasurementId = 4180 279 | for (let i = 0; i < dataValue.length; i += 4) { 280 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 281 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 282 | let aiTailValues = `${modelId}.${detectionType}` 283 | tailValueArray.push({ 284 | measurementValue: aiTailValues, measurementId: initTailmeasurementId, type: `AI Detection ${i}` 285 | }) 286 | initTailmeasurementId++ 287 | } 288 | messages = tailValueArray 289 | break 290 | case '30': 291 | case '31': 292 | // 首帧或者首帧输出帧 293 | let channelInfoOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 294 | let dataOne = { 295 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 296 | measurementId: parseInt(channelInfoOne.one), 297 | type: 'Measurement' 298 | } 299 | let dataTwo = { 300 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 301 | measurementId: parseInt(channelInfoOne.two), 302 | type: 'Measurement' 303 | } 304 | let cacheArrayInfo = [] 305 | if (parseInt(channelInfoOne.one)) { 306 | cacheArrayInfo.push(dataOne) 307 | } 308 | if (parseInt(channelInfoOne.two)) { 309 | cacheArrayInfo.push(dataTwo) 310 | } 311 | cacheArrayInfo.forEach(item => { 312 | messages.push(item) 313 | }) 314 | break 315 | case '32': 316 | let channelInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 317 | let dataThree = { 318 | measurementValue: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 319 | measurementId: 4164 + parseInt(channelInfoTwo.one), 320 | type: 'Measurement' 321 | } 322 | let dataFour = { 323 | measurementValue: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), 324 | measurementId: 4164 + parseInt(channelInfoTwo.two), 325 | type: 'Measurement' 326 | } 327 | if (parseInt(channelInfoTwo.one)) { 328 | messages.push(dataThree) 329 | } 330 | if (parseInt(channelInfoTwo.two)) { 331 | messages.push(dataFour) 332 | } 333 | break 334 | case '33': 335 | let channelInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 336 | let dataFive = { 337 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 338 | measurementId: parseInt(channelInfoThree.one), 339 | type: 'Measurement' 340 | } 341 | let dataSix = { 342 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 343 | measurementId: parseInt(channelInfoThree.two), 344 | type: 'Measurement' 345 | } 346 | if (parseInt(channelInfoThree.one)) { 347 | messages.push(dataFive) 348 | } 349 | if (parseInt(channelInfoThree.two)) { 350 | messages.push(dataSix) 351 | } 352 | 353 | break 354 | case '34': 355 | let model = loraWANV2DataFormat(dataValue.substring(0, 2)) 356 | let GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)) 357 | let simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)) 358 | let simulationInterface = loraWANV2DataFormat(dataValue.substring(6, 8)) 359 | messages = [{ 360 | dataId: 34, '3570': model, '3571': GPIOInput, '3572': simulationModel, '3573': simulationInterface 361 | }] 362 | break 363 | case '35': 364 | case '36': 365 | let channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 366 | let channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2 367 | let channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2 368 | messages = [{ 369 | [channelSortTDOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 370 | [channelSortTDTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 371 | }] 372 | break 373 | case '37': 374 | let channelTDInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 375 | let channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2 376 | let channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2 377 | messages = [{ 378 | [channelSortOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 379 | [channelSortTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 380 | }] 381 | break 382 | case '38': 383 | let channelTDInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 384 | let channelSortThreeOne = 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2 385 | let channelSortThreeTWO = 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2 386 | messages = [{ 387 | [channelSortThreeOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 388 | [channelSortThreeTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 389 | }] 390 | break 391 | case '39': 392 | let electricityWhetherTD = dataValue.substring(0, 2) 393 | let hwvTD = dataValue.substring(2, 6) 394 | let bdvTD = dataValue.substring(6, 10) 395 | let sensorAcquisitionIntervalTD = dataValue.substring(10, 14) 396 | let gpsAcquisitionIntervalTD = dataValue.substring(14, 18) 397 | messages = [{ 398 | dataId: parseInt(dataId), 399 | '3000': loraWANV2DataFormat(electricityWhetherTD), 400 | '3001': `${loraWANV2DataFormat(hwvTD.substring(0, 2))}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, 401 | '3502': `${loraWANV2DataFormat(bdvTD.substring(0, 2))}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, 402 | '3900': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, 403 | '3912': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)) 404 | }] 405 | break 406 | case '40': 407 | case '41': 408 | let lightIntensity = dataValue.substring(0, 4) 409 | let loudness = dataValue.substring(4, 8) 410 | // X 411 | let accelerateX = dataValue.substring(8, 12) 412 | // Y 413 | let accelerateY = dataValue.substring(12, 16) 414 | // Z 415 | let accelerateZ = dataValue.substring(16, 20) 416 | messages = [{ 417 | measurementValue: loraWANV2DataFormat(lightIntensity), measurementId: '4193', type: 'Light Intensity' 418 | }, { 419 | measurementValue: loraWANV2DataFormat(loudness), measurementId: '4192', type: 'Sound Intensity' 420 | }, { 421 | 422 | measurementValue: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150', type: 'AccelerometerX' 423 | }, { 424 | 425 | measurementValue: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151', type: 'AccelerometerY' 426 | }, { 427 | 428 | measurementValue: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152', type: 'AccelerometerZ' 429 | }] 430 | break 431 | case '42': 432 | let airTemperature = dataValue.substring(0, 4) 433 | let AirHumidity = dataValue.substring(4, 8) 434 | let tVOC = dataValue.substring(8, 12) 435 | let CO2eq = dataValue.substring(12, 16) 436 | let soilMoisture = dataValue.substring(16, 20) 437 | messages = [{ 438 | measurementValue: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097', type: 'Air Temperature' 439 | }, { 440 | measurementValue: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098', type: 'Air Humidity' 441 | }, { 442 | measurementValue: loraWANV2DataFormat(tVOC), measurementId: '4195', type: 'Total Volatile Organic Compounds' 443 | }, { 444 | measurementValue: loraWANV2DataFormat(CO2eq), measurementId: '4100', type: 'CO2' 445 | }, { 446 | measurementValue: loraWANV2DataFormat(soilMoisture), measurementId: '4196', type: 'Soil moisture intensity' 447 | }] 448 | break 449 | case '43': 450 | case '44': 451 | let headerDevKitValueArray = [] 452 | let initDevkitmeasurementId = 4175 453 | for (let i = 0; i < dataValue.length; i += 4) { 454 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 455 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 456 | let aiHeadValues = `${modelId}.${detectionType}` 457 | headerDevKitValueArray.push({ 458 | measurementValue: aiHeadValues, measurementId: initDevkitmeasurementId, type: `AI Detection ${i}` 459 | }) 460 | initDevkitmeasurementId++ 461 | } 462 | messages = headerDevKitValueArray 463 | break 464 | case '45': 465 | let initTailDevKitmeasurementId = 4180 466 | for (let i = 0; i < dataValue.length; i += 4) { 467 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 468 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 469 | let aiTailValues = `${modelId}.${detectionType}` 470 | messages.push({ 471 | measurementValue: aiTailValues, measurementId: initTailDevKitmeasurementId, type: `AI Detection ${i}` 472 | }) 473 | initTailDevKitmeasurementId++ 474 | } 475 | break 476 | default: 477 | break 478 | } 479 | return messages 480 | } 481 | 482 | /** 483 | * 484 | * data formatting 485 | * @param str 486 | * @param divisor 487 | * @returns {string|number} 488 | */ 489 | function loraWANV2DataFormat (str, divisor = 1) { 490 | let strReverse = bigEndianTransform(str) 491 | let str2 = toBinary(strReverse) 492 | if (str2.substring(0, 1) === '1') { 493 | let arr = str2.split('') 494 | let reverseArr = arr.map((item) => { 495 | if (parseInt(item) === 1) { 496 | return 0 497 | } else { 498 | return 1 499 | } 500 | }) 501 | str2 = parseInt(reverseArr.join(''), 2) + 1 502 | return '-' + str2 / divisor 503 | } 504 | return parseInt(str2, 2) / divisor 505 | } 506 | 507 | /** 508 | * Handling big-endian data formats 509 | * @param data 510 | * @returns {*[]} 511 | */ 512 | function bigEndianTransform (data) { 513 | let dataArray = [] 514 | for (let i = 0; i < data.length; i += 2) { 515 | dataArray.push(data.substring(i, i + 2)) 516 | } 517 | // array of hex 518 | return dataArray 519 | } 520 | 521 | /** 522 | * Convert to an 8-digit binary number with 0s in front of the number 523 | * @param arr 524 | * @returns {string} 525 | */ 526 | function toBinary (arr) { 527 | let binaryData = arr.map((item) => { 528 | let data = parseInt(item, 16) 529 | .toString(2) 530 | let dataLength = data.length 531 | if (data.length !== 8) { 532 | for (let i = 0; i < 8 - dataLength; i++) { 533 | data = `0` + data 534 | } 535 | } 536 | return data 537 | }) 538 | let ret = binaryData.toString() 539 | .replace(/,/g, '') 540 | return ret 541 | } 542 | 543 | /** 544 | * sensor 545 | * @param str 546 | * @returns {{channel: number, type: number, status: number}} 547 | */ 548 | function loraWANV2BitDataFormat (str) { 549 | let strReverse = bigEndianTransform(str) 550 | let str2 = toBinary(strReverse) 551 | let channel = parseInt(str2.substring(0, 4), 2) 552 | let status = parseInt(str2.substring(4, 5), 2) 553 | let type = parseInt(str2.substring(5), 2) 554 | return { channel, status, type } 555 | } 556 | 557 | /** 558 | * channel info 559 | * @param str 560 | * @returns {{channelTwo: number, channelOne: number}} 561 | */ 562 | function loraWANV2ChannelBitFormat (str) { 563 | let strReverse = bigEndianTransform(str) 564 | let str2 = toBinary(strReverse) 565 | let one = parseInt(str2.substring(0, 4), 2) 566 | let two = parseInt(str2.substring(4, 8), 2) 567 | let resultInfo = { 568 | one: one, two: two 569 | } 570 | return resultInfo 571 | } 572 | 573 | /** 574 | * data log status bit 575 | * @param str 576 | * @returns {{total: number, level: number, isTH: number}} 577 | */ 578 | function loraWANV2DataLogBitFormat (str) { 579 | let strReverse = bigEndianTransform(str) 580 | let str2 = toBinary(strReverse) 581 | let isTH = parseInt(str2.substring(0, 1), 2) 582 | let total = parseInt(str2.substring(1, 5), 2) 583 | let left = parseInt(str2.substring(5), 2) 584 | let resultInfo = { 585 | isTH: isTH, total: total, left: left 586 | } 587 | return resultInfo 588 | } 589 | 590 | function bytes2HexString (arrBytes) { 591 | var str = '' 592 | for (var i = 0; i < arrBytes.length; i++) { 593 | var tmp 594 | var num = arrBytes[i] 595 | if (num < 0) { 596 | tmp = (255 + num + 1).toString(16) 597 | } else { 598 | tmp = num.toString(16) 599 | } 600 | if (tmp.length === 1) { 601 | tmp = '0' + tmp 602 | } 603 | str += tmp 604 | } 605 | return str 606 | } 607 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V2_Decoder_For_ChripstackV4.js: -------------------------------------------------------------------------------- 1 | // Decode uplink function. 2 | // 3 | // Input is an object with the following fields: 4 | // - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] 5 | // - fPort = Uplink fPort. 6 | // - variables = Object containing the configured device variables. 7 | // 8 | // Output must be an object with the following fields: 9 | // - data = Object representing the decoded payload. 10 | function decodeUplink(input) { 11 | // data split 12 | 13 | var bytes = input['bytes'] 14 | // init 15 | bytes = bytes2HexString(bytes) 16 | .toLocaleUpperCase() 17 | 18 | let result = { 19 | 'err': 0, 'payload': bytes, 'valid': true, messages: [] 20 | } 21 | let splitArray = dataSplit(bytes) 22 | // data decoder 23 | let decoderArray = [] 24 | for (let i = 0; i < splitArray.length; i++) { 25 | let item = splitArray[i] 26 | let dataId = item.dataId 27 | let dataValue = item.dataValue 28 | let messages = dataIdAndDataValueJudge(dataId, dataValue) 29 | decoderArray.push(messages) 30 | } 31 | result.messages = decoderArray 32 | return { data: result } 33 | } 34 | 35 | /** 36 | * data splits 37 | * @param bytes 38 | * @returns {*[]} 39 | */ 40 | function dataSplit (bytes) { 41 | let frameArray = [] 42 | 43 | for (let i = 0; i < bytes.length; i++) { 44 | let remainingValue = bytes 45 | let dataId = remainingValue.substring(0, 2) 46 | let dataValue 47 | let dataObj = {} 48 | switch (dataId) { 49 | case '01' : 50 | case '20' : 51 | case '21' : 52 | case '30' : 53 | case '31' : 54 | case '33' : 55 | case '40' : 56 | case '41' : 57 | case '42' : 58 | case '43' : 59 | case '44' : 60 | case '45' : 61 | dataValue = remainingValue.substring(2, 22) 62 | bytes = remainingValue.substring(22) 63 | dataObj = { 64 | 'dataId': dataId, 'dataValue': dataValue 65 | } 66 | break 67 | case '02': 68 | dataValue = remainingValue.substring(2, 18) 69 | bytes = remainingValue.substring(18) 70 | dataObj = { 71 | 'dataId': '02', 'dataValue': dataValue 72 | } 73 | break 74 | case '03' : 75 | case '06': 76 | dataValue = remainingValue.substring(2, 4) 77 | bytes = remainingValue.substring(4) 78 | dataObj = { 79 | 'dataId': dataId, 'dataValue': dataValue 80 | } 81 | break 82 | case '05' : 83 | case '34': 84 | dataValue = bytes.substring(2, 10) 85 | bytes = remainingValue.substring(10) 86 | dataObj = { 87 | 'dataId': dataId, 'dataValue': dataValue 88 | } 89 | break 90 | case '04': 91 | case '10': 92 | case '32': 93 | case '35': 94 | case '36': 95 | case '37': 96 | case '38': 97 | case '39': 98 | dataValue = bytes.substring(2, 20) 99 | bytes = remainingValue.substring(20) 100 | dataObj = { 101 | 'dataId': dataId, 'dataValue': dataValue 102 | } 103 | break 104 | default: 105 | dataValue = '9' 106 | break 107 | } 108 | if (dataValue.length < 2) { 109 | break 110 | } 111 | frameArray.push(dataObj) 112 | } 113 | return frameArray 114 | } 115 | 116 | function dataIdAndDataValueJudge (dataId, dataValue) { 117 | let messages = [] 118 | switch (dataId) { 119 | case '01': 120 | let temperature = dataValue.substring(0, 4) 121 | let humidity = dataValue.substring(4, 6) 122 | let illumination = dataValue.substring(6, 14) 123 | let uv = dataValue.substring(14, 16) 124 | let windSpeed = dataValue.substring(16, 20) 125 | messages = [{ 126 | measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' 127 | }, { 128 | measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' 129 | }, { 130 | measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' 131 | }, { 132 | measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' 133 | }, { 134 | measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' 135 | }] 136 | break 137 | case '02': 138 | let windDirection = dataValue.substring(0, 4) 139 | let rainfall = dataValue.substring(4, 12) 140 | let airPressure = dataValue.substring(12, 16) 141 | messages = [{ 142 | measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Direction Sensor' 143 | }, { 144 | measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'Rain Gauge' 145 | }, { 146 | 147 | measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' 148 | }] 149 | break 150 | case '03': 151 | let Electricity = dataValue 152 | messages = [{ 153 | 'Battery(%)': loraWANV2DataFormat(Electricity) 154 | }] 155 | break 156 | case '04': 157 | let electricityWhether = dataValue.substring(0, 2) 158 | let hwv = dataValue.substring(2, 6) 159 | let bdv = dataValue.substring(6, 10) 160 | let sensorAcquisitionInterval = dataValue.substring(10, 14) 161 | let gpsAcquisitionInterval = dataValue.substring(14, 18) 162 | messages = [{ 163 | 'Battery(%)': loraWANV2DataFormat(electricityWhether), 164 | 'Hardware Version': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, 165 | 'Firmware Version': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, 166 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, 167 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 168 | }] 169 | break 170 | case '05': 171 | let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) 172 | let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) 173 | messages = [{ 174 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, 175 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 176 | }] 177 | break 178 | case '06': 179 | let errorCode = dataValue 180 | let descZh 181 | switch (errorCode) { 182 | case '00': 183 | descZh = 'CCL_SENSOR_ERROR_NONE' 184 | break 185 | case '01': 186 | descZh = 'CCL_SENSOR_NOT_FOUND' 187 | break 188 | case '02': 189 | descZh = 'CCL_SENSOR_WAKEUP_ERROR' 190 | break 191 | case '03': 192 | descZh = 'CCL_SENSOR_NOT_RESPONSE' 193 | break 194 | case '04': 195 | descZh = 'CCL_SENSOR_DATA_EMPTY' 196 | break 197 | case '05': 198 | descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' 199 | break 200 | case '06': 201 | descZh = 'CCL_SENSOR_DATA_CRC_ERROR' 202 | break 203 | case '07': 204 | descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' 205 | break 206 | case '08': 207 | descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' 208 | break 209 | case '09': 210 | descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' 211 | break 212 | case '0A': 213 | descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' 214 | break 215 | case '0B': 216 | descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' 217 | break 218 | case '0C': 219 | descZh = 'CCL_SENSOR_DATA_VALUE_HI' 220 | break 221 | case '0D': 222 | descZh = 'CCL_SENSOR_DATA_VALUE_LOW' 223 | break 224 | case '0E': 225 | descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' 226 | break 227 | case '0F': 228 | descZh = 'CCL_SENSOR_ARG_INVAILD' 229 | break 230 | case '10': 231 | descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' 232 | break 233 | case '11': 234 | descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' 235 | break 236 | case '12': 237 | descZh = 'CCL_SENSOR_RS485_REG_MISSED' 238 | break 239 | case '13': 240 | descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' 241 | break 242 | case '14': 243 | descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' 244 | break 245 | case '15': 246 | descZh = 'CCL_SENSOR_CONFIG_ERROR' 247 | break 248 | case 'FF': 249 | descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' 250 | break 251 | default: 252 | descZh = 'CC_OTHER_FAILED' 253 | break 254 | } 255 | messages = [{ 256 | measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh 257 | }] 258 | break 259 | case '10': 260 | let statusValue = dataValue.substring(0, 2) 261 | let { status, type } = loraWANV2BitDataFormat(statusValue) 262 | let sensecapId = dataValue.substring(2) 263 | messages = [{ 264 | status: status, channelType: type, sensorEui: sensecapId 265 | }] 266 | break 267 | case '20': 268 | let initmeasurementId = 4175 269 | let sensor = [] 270 | for (let i = 0; i < dataValue.length; i += 4) { 271 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 272 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 273 | let aiHeadValues = `${modelId}.${detectionType}` 274 | sensor.push({ 275 | measurementValue: aiHeadValues, measurementId: initmeasurementId 276 | }) 277 | initmeasurementId++ 278 | } 279 | messages = sensor 280 | break 281 | case '21': 282 | // Vision AI: 283 | // AI 识别输出帧 284 | let tailValueArray = [] 285 | let initTailmeasurementId = 4180 286 | for (let i = 0; i < dataValue.length; i += 4) { 287 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 288 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 289 | let aiTailValues = `${modelId}.${detectionType}` 290 | tailValueArray.push({ 291 | measurementValue: aiTailValues, measurementId: initTailmeasurementId, type: `AI Detection ${i}` 292 | }) 293 | initTailmeasurementId++ 294 | } 295 | messages = tailValueArray 296 | break 297 | case '30': 298 | case '31': 299 | // 首帧或者首帧输出帧 300 | let channelInfoOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 301 | let dataOne = { 302 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 303 | measurementId: parseInt(channelInfoOne.one), 304 | type: 'Measurement' 305 | } 306 | let dataTwo = { 307 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 308 | measurementId: parseInt(channelInfoOne.two), 309 | type: 'Measurement' 310 | } 311 | let cacheArrayInfo = [] 312 | if (parseInt(channelInfoOne.one)) { 313 | cacheArrayInfo.push(dataOne) 314 | } 315 | if (parseInt(channelInfoOne.two)) { 316 | cacheArrayInfo.push(dataTwo) 317 | } 318 | cacheArrayInfo.forEach(item => { 319 | messages.push(item) 320 | }) 321 | break 322 | case '32': 323 | let channelInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 324 | let dataThree = { 325 | measurementValue: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 326 | measurementId: parseInt(channelInfoTwo.one), 327 | type: 'Measurement' 328 | } 329 | let dataFour = { 330 | measurementValue: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), 331 | measurementId: parseInt(channelInfoTwo.two), 332 | type: 'Measurement' 333 | } 334 | if (parseInt(channelInfoTwo.one)) { 335 | messages.push(dataThree) 336 | } 337 | if (parseInt(channelInfoTwo.two)) { 338 | messages.push(dataFour) 339 | } 340 | break 341 | case '33': 342 | let channelInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 343 | let dataFive = { 344 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 345 | measurementId: parseInt(channelInfoThree.one), 346 | type: 'Measurement' 347 | } 348 | let dataSix = { 349 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 350 | measurementId: parseInt(channelInfoThree.two), 351 | type: 'Measurement' 352 | } 353 | if (parseInt(channelInfoThree.one)) { 354 | messages.push(dataFive) 355 | } 356 | if (parseInt(channelInfoThree.two)) { 357 | messages.push(dataSix) 358 | } 359 | 360 | break 361 | case '34': 362 | let model = loraWANV2DataFormat(dataValue.substring(0, 2)) 363 | let GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)) 364 | let simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)) 365 | let simulationInterface = loraWANV2DataFormat(dataValue.substring(6, 8)) 366 | messages = [{ 367 | 'dataloggerProtocol': model, 368 | 'dataloggerGPIOInput': GPIOInput, 369 | 'dataloggerAnalogType': simulationModel, 370 | 'dataloggerAnalogInterface': simulationInterface 371 | }] 372 | break 373 | case '35': 374 | case '36': 375 | let channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 376 | let channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2 377 | let channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2 378 | messages = [{ 379 | [channelSortTDOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 380 | [channelSortTDTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 381 | }] 382 | break 383 | case '37': 384 | let channelTDInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 385 | let channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2 386 | let channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2 387 | messages = [{ 388 | [channelSortOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 389 | [channelSortTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 390 | }] 391 | break 392 | case '38': 393 | let channelTDInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 394 | let channelSortThreeOne = 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2 395 | let channelSortThreeTWO = 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2 396 | messages = [{ 397 | [channelSortThreeOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 398 | [channelSortThreeTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 399 | }] 400 | break 401 | case '39': 402 | let electricityWhetherTD = dataValue.substring(0, 2) 403 | let hwvTD = dataValue.substring(2, 6) 404 | let bdvTD = dataValue.substring(6, 10) 405 | let sensorAcquisitionIntervalTD = dataValue.substring(10, 14) 406 | let gpsAcquisitionIntervalTD = dataValue.substring(14, 18) 407 | messages = [{ 408 | 'Battery(%)': loraWANV2DataFormat(electricityWhetherTD), 409 | 'Hardware Version': `${loraWANV2DataFormat(hwvTD.substring(0, 2))}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, 410 | 'Firmware Version': `${loraWANV2DataFormat(bdvTD.substring(0, 2))}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, 411 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, 412 | 'thresholdMeasureInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)) 413 | }] 414 | break 415 | case '40': 416 | case '41': 417 | let lightIntensity = dataValue.substring(0, 4) 418 | let loudness = dataValue.substring(4, 8) 419 | // X 420 | let accelerateX = dataValue.substring(8, 12) 421 | // Y 422 | let accelerateY = dataValue.substring(12, 16) 423 | // Z 424 | let accelerateZ = dataValue.substring(16, 20) 425 | messages = [{ 426 | measurementValue: loraWANV2DataFormat(lightIntensity), measurementId: '4193', type: 'Light Intensity' 427 | }, { 428 | measurementValue: loraWANV2DataFormat(loudness), measurementId: '4192', type: 'Sound Intensity' 429 | }, { 430 | 431 | measurementValue: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150', type: 'AccelerometerX' 432 | }, { 433 | 434 | measurementValue: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151', type: 'AccelerometerY' 435 | }, { 436 | 437 | measurementValue: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152', type: 'AccelerometerZ' 438 | }] 439 | break 440 | case '42': 441 | let airTemperature = dataValue.substring(0, 4) 442 | let AirHumidity = dataValue.substring(4, 8) 443 | let tVOC = dataValue.substring(8, 12) 444 | let CO2eq = dataValue.substring(12, 16) 445 | let soilMoisture = dataValue.substring(16, 20) 446 | messages = [{ 447 | measurementValue: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097', type: 'Air Temperature' 448 | }, { 449 | measurementValue: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098', type: 'Air Humidity' 450 | }, { 451 | measurementValue: loraWANV2DataFormat(tVOC), measurementId: '4195', type: 'Total Volatile Organic Compounds' 452 | }, { 453 | measurementValue: loraWANV2DataFormat(CO2eq), measurementId: '4100', type: 'CO2' 454 | }, { 455 | measurementValue: loraWANV2DataFormat(soilMoisture), measurementId: '4196', type: 'Soil moisture intensity' 456 | }] 457 | break 458 | case '43': 459 | case '44': 460 | let headerDevKitValueArray = [] 461 | let initDevkitmeasurementId = 4175 462 | for (let i = 0; i < dataValue.length; i += 4) { 463 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 464 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 465 | let aiHeadValues = `${modelId}.${detectionType}` 466 | headerDevKitValueArray.push({ 467 | measurementValue: aiHeadValues, measurementId: initDevkitmeasurementId, type: `AI Detection ${i}` 468 | }) 469 | initDevkitmeasurementId++ 470 | } 471 | messages = headerDevKitValueArray 472 | break 473 | case '45': 474 | let initTailDevKitmeasurementId = 4180 475 | for (let i = 0; i < dataValue.length; i += 4) { 476 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 477 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 478 | let aiTailValues = `${modelId}.${detectionType}` 479 | messages.push({ 480 | measurementValue: aiTailValues, measurementId: initTailDevKitmeasurementId, type: `AI Detection ${i}` 481 | }) 482 | initTailDevKitmeasurementId++ 483 | } 484 | break 485 | default: 486 | break 487 | } 488 | return messages 489 | } 490 | 491 | /** 492 | * 493 | * data formatting 494 | * @param str 495 | * @param divisor 496 | * @returns {string|number} 497 | */ 498 | function loraWANV2DataFormat (str, divisor = 1) { 499 | let strReverse = bigEndianTransform(str) 500 | let str2 = toBinary(strReverse) 501 | if (str2.substring(0, 1) === '1') { 502 | let arr = str2.split('') 503 | let reverseArr = arr.map((item) => { 504 | if (parseInt(item) === 1) { 505 | return 0 506 | } else { 507 | return 1 508 | } 509 | }) 510 | str2 = parseInt(reverseArr.join(''), 2) + 1 511 | return '-' + str2 / divisor 512 | } 513 | return parseInt(str2, 2) / divisor 514 | } 515 | 516 | /** 517 | * Handling big-endian data formats 518 | * @param data 519 | * @returns {*[]} 520 | */ 521 | function bigEndianTransform (data) { 522 | let dataArray = [] 523 | for (let i = 0; i < data.length; i += 2) { 524 | dataArray.push(data.substring(i, i + 2)) 525 | } 526 | // array of hex 527 | return dataArray 528 | } 529 | 530 | /** 531 | * Convert to an 8-digit binary number with 0s in front of the number 532 | * @param arr 533 | * @returns {string} 534 | */ 535 | function toBinary (arr) { 536 | let binaryData = arr.map((item) => { 537 | let data = parseInt(item, 16) 538 | .toString(2) 539 | let dataLength = data.length 540 | if (data.length !== 8) { 541 | for (let i = 0; i < 8 - dataLength; i++) { 542 | data = `0` + data 543 | } 544 | } 545 | return data 546 | }) 547 | let ret = binaryData.toString() 548 | .replace(/,/g, '') 549 | return ret 550 | } 551 | 552 | /** 553 | * sensor 554 | * @param str 555 | * @returns {{channel: number, type: number, status: number}} 556 | */ 557 | function loraWANV2BitDataFormat (str) { 558 | let strReverse = bigEndianTransform(str) 559 | let str2 = toBinary(strReverse) 560 | let channel = parseInt(str2.substring(0, 4), 2) 561 | let status = parseInt(str2.substring(4, 5), 2) 562 | let type = parseInt(str2.substring(5), 2) 563 | return { channel, status, type } 564 | } 565 | 566 | /** 567 | * channel info 568 | * @param str 569 | * @returns {{channelTwo: number, channelOne: number}} 570 | */ 571 | function loraWANV2ChannelBitFormat (str) { 572 | let strReverse = bigEndianTransform(str) 573 | let str2 = toBinary(strReverse) 574 | let one = parseInt(str2.substring(0, 4), 2) 575 | let two = parseInt(str2.substring(4, 8), 2) 576 | let resultInfo = { 577 | one: one, two: two 578 | } 579 | return resultInfo 580 | } 581 | 582 | /** 583 | * data log status bit 584 | * @param str 585 | * @returns {{total: number, level: number, isTH: number}} 586 | */ 587 | function loraWANV2DataLogBitFormat (str) { 588 | let strReverse = bigEndianTransform(str) 589 | let str2 = toBinary(strReverse) 590 | let isTH = parseInt(str2.substring(0, 1), 2) 591 | let total = parseInt(str2.substring(1, 5), 2) 592 | let left = parseInt(str2.substring(5), 2) 593 | let resultInfo = { 594 | isTH: isTH, total: total, left: left 595 | } 596 | return resultInfo 597 | } 598 | 599 | function bytes2HexString (arrBytes) { 600 | var str = '' 601 | for (var i = 0; i < arrBytes.length; i++) { 602 | var tmp 603 | var num = arrBytes[i] 604 | if (num < 0) { 605 | tmp = (255 + num + 1).toString(16) 606 | } else { 607 | tmp = num.toString(16) 608 | } 609 | if (tmp.length === 1) { 610 | tmp = '0' + tmp 611 | } 612 | str += tmp 613 | } 614 | return str 615 | } 616 | 617 | 618 | // Encode downlink function. 619 | // 620 | // Input is an object with the following fields: 621 | // - data = Object representing the payload that must be encoded. 622 | // - variables = Object containing the configured device variables. 623 | // 624 | // Output must be an object with the following fields: 625 | // - bytes = Byte array containing the downlink payload. 626 | function encodeDownlink(input) { 627 | return { 628 | bytes: [225, 230, 255, 0] 629 | }; 630 | } 631 | 632 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V2_Decoder_For_Helium.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | function Decoder (bytes, port) { 5 | // data split 6 | 7 | bytes = bytes2HexString(bytes) 8 | .toLocaleUpperCase() 9 | 10 | let result = { 11 | 'err': 0, 'payload': bytes, 'valid': true, messages: [] 12 | } 13 | let splitArray = dataSplit(bytes) 14 | // data decoder 15 | let decoderArray = [] 16 | for (let i = 0; i < splitArray.length; i++) { 17 | let item = splitArray[i] 18 | let dataId = item.dataId 19 | let dataValue = item.dataValue 20 | let messages = dataIdAndDataValueJudge(dataId, dataValue) 21 | decoderArray.push(messages) 22 | } 23 | result.messages = decoderArray 24 | return { data: result } 25 | } 26 | 27 | /** 28 | * data splits 29 | * @param bytes 30 | * @returns {*[]} 31 | */ 32 | function dataSplit (bytes) { 33 | let frameArray = [] 34 | 35 | for (let i = 0; i < bytes.length; i++) { 36 | let remainingValue = bytes 37 | let dataId = remainingValue.substring(0, 2) 38 | let dataValue 39 | let dataObj = {} 40 | switch (dataId) { 41 | case '01' : 42 | case '20' : 43 | case '21' : 44 | case '30' : 45 | case '31' : 46 | case '33' : 47 | case '40' : 48 | case '41' : 49 | case '42' : 50 | case '43' : 51 | case '44' : 52 | case '45' : 53 | dataValue = remainingValue.substring(2, 22) 54 | bytes = remainingValue.substring(22) 55 | dataObj = { 56 | 'dataId': dataId, 'dataValue': dataValue 57 | } 58 | break 59 | case '02': 60 | dataValue = remainingValue.substring(2, 18) 61 | bytes = remainingValue.substring(18) 62 | dataObj = { 63 | 'dataId': '02', 'dataValue': dataValue 64 | } 65 | break 66 | case '03' : 67 | case '06': 68 | dataValue = remainingValue.substring(2, 4) 69 | bytes = remainingValue.substring(4) 70 | dataObj = { 71 | 'dataId': dataId, 'dataValue': dataValue 72 | } 73 | break 74 | case '05' : 75 | case '34': 76 | dataValue = bytes.substring(2, 10) 77 | bytes = remainingValue.substring(10) 78 | dataObj = { 79 | 'dataId': dataId, 'dataValue': dataValue 80 | } 81 | break 82 | case '04': 83 | case '10': 84 | case '32': 85 | case '35': 86 | case '36': 87 | case '37': 88 | case '38': 89 | case '39': 90 | dataValue = bytes.substring(2, 20) 91 | bytes = remainingValue.substring(20) 92 | dataObj = { 93 | 'dataId': dataId, 'dataValue': dataValue 94 | } 95 | break 96 | default: 97 | dataValue = '9' 98 | break 99 | } 100 | if (dataValue.length < 2) { 101 | break 102 | } 103 | frameArray.push(dataObj) 104 | } 105 | return frameArray 106 | } 107 | 108 | function dataIdAndDataValueJudge (dataId, dataValue) { 109 | let messages = [] 110 | switch (dataId) { 111 | case '01': 112 | let temperature = dataValue.substring(0, 4) 113 | let humidity = dataValue.substring(4, 6) 114 | let illumination = dataValue.substring(6, 14) 115 | let uv = dataValue.substring(14, 16) 116 | let windSpeed = dataValue.substring(16, 20) 117 | messages = [{ 118 | measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' 119 | }, { 120 | measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' 121 | }, { 122 | measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' 123 | }, { 124 | measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' 125 | }, { 126 | measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' 127 | }] 128 | break 129 | case '02': 130 | let windDirection = dataValue.substring(0, 4) 131 | let rainfall = dataValue.substring(4, 12) 132 | let airPressure = dataValue.substring(12, 16) 133 | messages = [{ 134 | measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Direction Sensor' 135 | }, { 136 | measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'Rain Gauge' 137 | }, { 138 | 139 | measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' 140 | }] 141 | break 142 | case '03': 143 | let Electricity = dataValue 144 | messages = [{ 145 | 'Battery(%)': loraWANV2DataFormat(Electricity) 146 | }] 147 | break 148 | case '04': 149 | let electricityWhether = dataValue.substring(0, 2) 150 | let hwv = dataValue.substring(2, 6) 151 | let bdv = dataValue.substring(6, 10) 152 | let sensorAcquisitionInterval = dataValue.substring(10, 14) 153 | let gpsAcquisitionInterval = dataValue.substring(14, 18) 154 | messages = [{ 155 | 'Battery(%)': loraWANV2DataFormat(electricityWhether), 156 | 'Hardware Version': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, 157 | 'Firmware Version': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, 158 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, 159 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 160 | }] 161 | break 162 | case '05': 163 | let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) 164 | let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) 165 | messages = [{ 166 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, 167 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 168 | }] 169 | break 170 | case '06': 171 | let errorCode = dataValue 172 | let descZh 173 | switch (errorCode) { 174 | case '00': 175 | descZh = 'CCL_SENSOR_ERROR_NONE' 176 | break 177 | case '01': 178 | descZh = 'CCL_SENSOR_NOT_FOUND' 179 | break 180 | case '02': 181 | descZh = 'CCL_SENSOR_WAKEUP_ERROR' 182 | break 183 | case '03': 184 | descZh = 'CCL_SENSOR_NOT_RESPONSE' 185 | break 186 | case '04': 187 | descZh = 'CCL_SENSOR_DATA_EMPTY' 188 | break 189 | case '05': 190 | descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' 191 | break 192 | case '06': 193 | descZh = 'CCL_SENSOR_DATA_CRC_ERROR' 194 | break 195 | case '07': 196 | descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' 197 | break 198 | case '08': 199 | descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' 200 | break 201 | case '09': 202 | descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' 203 | break 204 | case '0A': 205 | descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' 206 | break 207 | case '0B': 208 | descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' 209 | break 210 | case '0C': 211 | descZh = 'CCL_SENSOR_DATA_VALUE_HI' 212 | break 213 | case '0D': 214 | descZh = 'CCL_SENSOR_DATA_VALUE_LOW' 215 | break 216 | case '0E': 217 | descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' 218 | break 219 | case '0F': 220 | descZh = 'CCL_SENSOR_ARG_INVAILD' 221 | break 222 | case '10': 223 | descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' 224 | break 225 | case '11': 226 | descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' 227 | break 228 | case '12': 229 | descZh = 'CCL_SENSOR_RS485_REG_MISSED' 230 | break 231 | case '13': 232 | descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' 233 | break 234 | case '14': 235 | descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' 236 | break 237 | case '15': 238 | descZh = 'CCL_SENSOR_CONFIG_ERROR' 239 | break 240 | case 'FF': 241 | descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' 242 | break 243 | default: 244 | descZh = 'CC_OTHER_FAILED' 245 | break 246 | } 247 | messages = [{ 248 | measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh 249 | }] 250 | break 251 | case '10': 252 | let statusValue = dataValue.substring(0, 2) 253 | let { status, type } = loraWANV2BitDataFormat(statusValue) 254 | let sensecapId = dataValue.substring(2) 255 | messages = [{ 256 | status: status, channelType: type, sensorEui: sensecapId 257 | }] 258 | break 259 | case '20': 260 | let initmeasurementId = 4175 261 | let sensor = [] 262 | for (let i = 0; i < dataValue.length; i += 4) { 263 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 264 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 265 | let aiHeadValues = `${modelId}.${detectionType}` 266 | sensor.push({ 267 | measurementValue: aiHeadValues, measurementId: initmeasurementId 268 | }) 269 | initmeasurementId++ 270 | } 271 | messages = sensor 272 | break 273 | case '21': 274 | // Vision AI: 275 | // AI 识别输出帧 276 | let tailValueArray = [] 277 | let initTailmeasurementId = 4180 278 | for (let i = 0; i < dataValue.length; i += 4) { 279 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 280 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 281 | let aiTailValues = `${modelId}.${detectionType}` 282 | tailValueArray.push({ 283 | measurementValue: aiTailValues, measurementId: initTailmeasurementId, type: `AI Detection ${i}` 284 | }) 285 | initTailmeasurementId++ 286 | } 287 | messages = tailValueArray 288 | break 289 | case '30': 290 | case '31': 291 | // 首帧或者首帧输出帧 292 | let channelInfoOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 293 | let dataOne = { 294 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 295 | measurementId: parseInt(channelInfoOne.one), 296 | type: 'Measurement' 297 | } 298 | let dataTwo = { 299 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 300 | measurementId: parseInt(channelInfoOne.two), 301 | type: 'Measurement' 302 | } 303 | let cacheArrayInfo = [] 304 | if (parseInt(channelInfoOne.one)) { 305 | cacheArrayInfo.push(dataOne) 306 | } 307 | if (parseInt(channelInfoOne.two)) { 308 | cacheArrayInfo.push(dataTwo) 309 | } 310 | cacheArrayInfo.forEach(item => { 311 | messages.push(item) 312 | }) 313 | break 314 | case '32': 315 | let channelInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 316 | let dataThree = { 317 | measurementValue: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 318 | measurementId: parseInt(channelInfoTwo.one), 319 | type: 'Measurement' 320 | } 321 | let dataFour = { 322 | measurementValue: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), 323 | measurementId: parseInt(channelInfoTwo.two), 324 | type: 'Measurement' 325 | } 326 | if (parseInt(channelInfoTwo.one)) { 327 | messages.push(dataThree) 328 | } 329 | if (parseInt(channelInfoTwo.two)) { 330 | messages.push(dataFour) 331 | } 332 | break 333 | case '33': 334 | let channelInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 335 | let dataFive = { 336 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 337 | measurementId: parseInt(channelInfoThree.one), 338 | type: 'Measurement' 339 | } 340 | let dataSix = { 341 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 342 | measurementId: parseInt(channelInfoThree.two), 343 | type: 'Measurement' 344 | } 345 | if (parseInt(channelInfoThree.one)) { 346 | messages.push(dataFive) 347 | } 348 | if (parseInt(channelInfoThree.two)) { 349 | messages.push(dataSix) 350 | } 351 | 352 | break 353 | case '34': 354 | let model = loraWANV2DataFormat(dataValue.substring(0, 2)) 355 | let GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)) 356 | let simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)) 357 | let simulationInterface = loraWANV2DataFormat(dataValue.substring(6, 8)) 358 | messages = [{ 359 | 'dataloggerProtocol': model, 'dataloggerGPIOInput': GPIOInput, 'dataloggerAnalogType': simulationModel, 'dataloggerAnalogInterface': simulationInterface 360 | }] 361 | break 362 | case '35': 363 | case '36': 364 | let channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 365 | let channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2 366 | let channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2 367 | messages = [{ 368 | [channelSortTDOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 369 | [channelSortTDTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 370 | }] 371 | break 372 | case '37': 373 | let channelTDInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 374 | let channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2 375 | let channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2 376 | messages = [{ 377 | [channelSortOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 378 | [channelSortTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 379 | }] 380 | break 381 | case '38': 382 | let channelTDInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 383 | let channelSortThreeOne = 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2 384 | let channelSortThreeTWO = 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2 385 | messages = [{ 386 | [channelSortThreeOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 387 | [channelSortThreeTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 388 | }] 389 | break 390 | case '39': 391 | let electricityWhetherTD = dataValue.substring(0, 2) 392 | let hwvTD = dataValue.substring(2, 6) 393 | let bdvTD = dataValue.substring(6, 10) 394 | let sensorAcquisitionIntervalTD = dataValue.substring(10, 14) 395 | let gpsAcquisitionIntervalTD = dataValue.substring(14, 18) 396 | messages = [{ 397 | 'Battery(%)': loraWANV2DataFormat(electricityWhetherTD), 398 | 'Hardware Version': `${loraWANV2DataFormat(hwvTD.substring(0, 2))}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, 399 | 'Firmware Version': `${loraWANV2DataFormat(bdvTD.substring(0, 2))}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, 400 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, 401 | 'thresholdMeasureInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)) 402 | }] 403 | break 404 | case '40': 405 | case '41': 406 | let lightIntensity = dataValue.substring(0, 4) 407 | let loudness = dataValue.substring(4, 8) 408 | // X 409 | let accelerateX = dataValue.substring(8, 12) 410 | // Y 411 | let accelerateY = dataValue.substring(12, 16) 412 | // Z 413 | let accelerateZ = dataValue.substring(16, 20) 414 | messages = [{ 415 | measurementValue: loraWANV2DataFormat(lightIntensity), measurementId: '4193', type: 'Light Intensity' 416 | }, { 417 | measurementValue: loraWANV2DataFormat(loudness), measurementId: '4192', type: 'Sound Intensity' 418 | }, { 419 | 420 | measurementValue: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150', type: 'AccelerometerX' 421 | }, { 422 | 423 | measurementValue: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151', type: 'AccelerometerY' 424 | }, { 425 | 426 | measurementValue: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152', type: 'AccelerometerZ' 427 | }] 428 | break 429 | case '42': 430 | let airTemperature = dataValue.substring(0, 4) 431 | let AirHumidity = dataValue.substring(4, 8) 432 | let tVOC = dataValue.substring(8, 12) 433 | let CO2eq = dataValue.substring(12, 16) 434 | let soilMoisture = dataValue.substring(16, 20) 435 | messages = [{ 436 | measurementValue: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097', type: 'Air Temperature' 437 | }, { 438 | measurementValue: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098', type: 'Air Humidity' 439 | }, { 440 | measurementValue: loraWANV2DataFormat(tVOC), measurementId: '4195', type: 'Total Volatile Organic Compounds' 441 | }, { 442 | measurementValue: loraWANV2DataFormat(CO2eq), measurementId: '4100', type: 'CO2' 443 | }, { 444 | measurementValue: loraWANV2DataFormat(soilMoisture), measurementId: '4196', type: 'Soil moisture intensity' 445 | }] 446 | break 447 | case '43': 448 | case '44': 449 | let headerDevKitValueArray = [] 450 | let initDevkitmeasurementId = 4175 451 | for (let i = 0; i < dataValue.length; i += 4) { 452 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 453 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 454 | let aiHeadValues = `${modelId}.${detectionType}` 455 | headerDevKitValueArray.push({ 456 | measurementValue: aiHeadValues, measurementId: initDevkitmeasurementId, type: `AI Detection ${i}` 457 | }) 458 | initDevkitmeasurementId++ 459 | } 460 | messages = headerDevKitValueArray 461 | break 462 | case '45': 463 | let initTailDevKitmeasurementId = 4180 464 | for (let i = 0; i < dataValue.length; i += 4) { 465 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 466 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 467 | let aiTailValues = `${modelId}.${detectionType}` 468 | messages.push({ 469 | measurementValue: aiTailValues, measurementId: initTailDevKitmeasurementId, type: `AI Detection ${i}` 470 | }) 471 | initTailDevKitmeasurementId++ 472 | } 473 | break 474 | default: 475 | break 476 | } 477 | return messages 478 | } 479 | 480 | /** 481 | * 482 | * data formatting 483 | * @param str 484 | * @param divisor 485 | * @returns {string|number} 486 | */ 487 | function loraWANV2DataFormat (str, divisor = 1) { 488 | let strReverse = bigEndianTransform(str) 489 | let str2 = toBinary(strReverse) 490 | if (str2.substring(0, 1) === '1') { 491 | let arr = str2.split('') 492 | let reverseArr = arr.map((item) => { 493 | if (parseInt(item) === 1) { 494 | return 0 495 | } else { 496 | return 1 497 | } 498 | }) 499 | str2 = parseInt(reverseArr.join(''), 2) + 1 500 | return '-' + str2 / divisor 501 | } 502 | return parseInt(str2, 2) / divisor 503 | } 504 | 505 | /** 506 | * Handling big-endian data formats 507 | * @param data 508 | * @returns {*[]} 509 | */ 510 | function bigEndianTransform (data) { 511 | let dataArray = [] 512 | for (let i = 0; i < data.length; i += 2) { 513 | dataArray.push(data.substring(i, i + 2)) 514 | } 515 | // array of hex 516 | return dataArray 517 | } 518 | 519 | /** 520 | * Convert to an 8-digit binary number with 0s in front of the number 521 | * @param arr 522 | * @returns {string} 523 | */ 524 | function toBinary (arr) { 525 | let binaryData = arr.map((item) => { 526 | let data = parseInt(item, 16) 527 | .toString(2) 528 | let dataLength = data.length 529 | if (data.length !== 8) { 530 | for (let i = 0; i < 8 - dataLength; i++) { 531 | data = `0` + data 532 | } 533 | } 534 | return data 535 | }) 536 | let ret = binaryData.toString() 537 | .replace(/,/g, '') 538 | return ret 539 | } 540 | 541 | /** 542 | * sensor 543 | * @param str 544 | * @returns {{channel: number, type: number, status: number}} 545 | */ 546 | function loraWANV2BitDataFormat (str) { 547 | let strReverse = bigEndianTransform(str) 548 | let str2 = toBinary(strReverse) 549 | let channel = parseInt(str2.substring(0, 4), 2) 550 | let status = parseInt(str2.substring(4, 5), 2) 551 | let type = parseInt(str2.substring(5), 2) 552 | return { channel, status, type } 553 | } 554 | 555 | /** 556 | * channel info 557 | * @param str 558 | * @returns {{channelTwo: number, channelOne: number}} 559 | */ 560 | function loraWANV2ChannelBitFormat (str) { 561 | let strReverse = bigEndianTransform(str) 562 | let str2 = toBinary(strReverse) 563 | let one = parseInt(str2.substring(0, 4), 2) 564 | let two = parseInt(str2.substring(4, 8), 2) 565 | let resultInfo = { 566 | one: one, two: two 567 | } 568 | return resultInfo 569 | } 570 | 571 | /** 572 | * data log status bit 573 | * @param str 574 | * @returns {{total: number, level: number, isTH: number}} 575 | */ 576 | function loraWANV2DataLogBitFormat (str) { 577 | let strReverse = bigEndianTransform(str) 578 | let str2 = toBinary(strReverse) 579 | let isTH = parseInt(str2.substring(0, 1), 2) 580 | let total = parseInt(str2.substring(1, 5), 2) 581 | let left = parseInt(str2.substring(5), 2) 582 | let resultInfo = { 583 | isTH: isTH, total: total, left: left 584 | } 585 | return resultInfo 586 | } 587 | 588 | function bytes2HexString (arrBytes) { 589 | var str = '' 590 | for (var i = 0; i < arrBytes.length; i++) { 591 | var tmp 592 | var num = arrBytes[i] 593 | if (num < 0) { 594 | tmp = (255 + num + 1).toString(16) 595 | } else { 596 | tmp = num.toString(16) 597 | } 598 | if (tmp.length === 1) { 599 | tmp = '0' + tmp 600 | } 601 | str += tmp 602 | } 603 | return str 604 | } 605 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V2_Decoder_For_TTN.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | function decodeUplink (input, port) { 5 | // data split 6 | 7 | var bytes = input['bytes'] 8 | // init 9 | bytes = bytes2HexString(bytes) 10 | .toLocaleUpperCase() 11 | 12 | let result = { 13 | 'err': 0, 'payload': bytes, 'valid': true, messages: [] 14 | } 15 | let splitArray = dataSplit(bytes) 16 | // data decoder 17 | let decoderArray = [] 18 | for (let i = 0; i < splitArray.length; i++) { 19 | let item = splitArray[i] 20 | let dataId = item.dataId 21 | let dataValue = item.dataValue 22 | let messages = dataIdAndDataValueJudge(dataId, dataValue) 23 | decoderArray.push(messages) 24 | } 25 | result.messages = decoderArray 26 | return { data: result } 27 | } 28 | 29 | /** 30 | * data splits 31 | * @param bytes 32 | * @returns {*[]} 33 | */ 34 | function dataSplit (bytes) { 35 | let frameArray = [] 36 | 37 | for (let i = 0; i < bytes.length; i++) { 38 | let remainingValue = bytes 39 | let dataId = remainingValue.substring(0, 2) 40 | let dataValue 41 | let dataObj = {} 42 | switch (dataId) { 43 | case '01' : 44 | case '20' : 45 | case '21' : 46 | case '30' : 47 | case '31' : 48 | case '33' : 49 | case '40' : 50 | case '41' : 51 | case '42' : 52 | case '43' : 53 | case '44' : 54 | case '45' : 55 | dataValue = remainingValue.substring(2, 22) 56 | bytes = remainingValue.substring(22) 57 | dataObj = { 58 | 'dataId': dataId, 'dataValue': dataValue 59 | } 60 | break 61 | case '02': 62 | dataValue = remainingValue.substring(2, 18) 63 | bytes = remainingValue.substring(18) 64 | dataObj = { 65 | 'dataId': '02', 'dataValue': dataValue 66 | } 67 | break 68 | case '03' : 69 | case '06': 70 | dataValue = remainingValue.substring(2, 4) 71 | bytes = remainingValue.substring(4) 72 | dataObj = { 73 | 'dataId': dataId, 'dataValue': dataValue 74 | } 75 | break 76 | case '05' : 77 | case '34': 78 | dataValue = bytes.substring(2, 10) 79 | bytes = remainingValue.substring(10) 80 | dataObj = { 81 | 'dataId': dataId, 'dataValue': dataValue 82 | } 83 | break 84 | case '04': 85 | case '10': 86 | case '32': 87 | case '35': 88 | case '36': 89 | case '37': 90 | case '38': 91 | case '39': 92 | dataValue = bytes.substring(2, 20) 93 | bytes = remainingValue.substring(20) 94 | dataObj = { 95 | 'dataId': dataId, 'dataValue': dataValue 96 | } 97 | break 98 | default: 99 | dataValue = '9' 100 | break 101 | } 102 | if (dataValue.length < 2) { 103 | break 104 | } 105 | frameArray.push(dataObj) 106 | } 107 | return frameArray 108 | } 109 | 110 | function dataIdAndDataValueJudge (dataId, dataValue) { 111 | let messages = [] 112 | switch (dataId) { 113 | case '01': 114 | let temperature = dataValue.substring(0, 4) 115 | let humidity = dataValue.substring(4, 6) 116 | let illumination = dataValue.substring(6, 14) 117 | let uv = dataValue.substring(14, 16) 118 | let windSpeed = dataValue.substring(16, 20) 119 | messages = [{ 120 | measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' 121 | }, { 122 | measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' 123 | }, { 124 | measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' 125 | }, { 126 | measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' 127 | }, { 128 | measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' 129 | }] 130 | break 131 | case '02': 132 | let windDirection = dataValue.substring(0, 4) 133 | let rainfall = dataValue.substring(4, 12) 134 | let airPressure = dataValue.substring(12, 16) 135 | messages = [{ 136 | measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Direction Sensor' 137 | }, { 138 | measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'Rain Gauge' 139 | }, { 140 | 141 | measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' 142 | }] 143 | break 144 | case '03': 145 | let Electricity = dataValue 146 | messages = [{ 147 | 'Battery(%)': loraWANV2DataFormat(Electricity) 148 | }] 149 | break 150 | case '04': 151 | let electricityWhether = dataValue.substring(0, 2) 152 | let hwv = dataValue.substring(2, 6) 153 | let bdv = dataValue.substring(6, 10) 154 | let sensorAcquisitionInterval = dataValue.substring(10, 14) 155 | let gpsAcquisitionInterval = dataValue.substring(14, 18) 156 | messages = [{ 157 | 'Battery(%)': loraWANV2DataFormat(electricityWhether), 158 | 'Hardware Version': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, 159 | 'Firmware Version': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, 160 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, 161 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 162 | }] 163 | break 164 | case '05': 165 | let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) 166 | let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) 167 | messages = [{ 168 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, 169 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 170 | }] 171 | break 172 | case '06': 173 | let errorCode = dataValue 174 | let descZh 175 | switch (errorCode) { 176 | case '00': 177 | descZh = 'CCL_SENSOR_ERROR_NONE' 178 | break 179 | case '01': 180 | descZh = 'CCL_SENSOR_NOT_FOUND' 181 | break 182 | case '02': 183 | descZh = 'CCL_SENSOR_WAKEUP_ERROR' 184 | break 185 | case '03': 186 | descZh = 'CCL_SENSOR_NOT_RESPONSE' 187 | break 188 | case '04': 189 | descZh = 'CCL_SENSOR_DATA_EMPTY' 190 | break 191 | case '05': 192 | descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' 193 | break 194 | case '06': 195 | descZh = 'CCL_SENSOR_DATA_CRC_ERROR' 196 | break 197 | case '07': 198 | descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' 199 | break 200 | case '08': 201 | descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' 202 | break 203 | case '09': 204 | descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' 205 | break 206 | case '0A': 207 | descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' 208 | break 209 | case '0B': 210 | descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' 211 | break 212 | case '0C': 213 | descZh = 'CCL_SENSOR_DATA_VALUE_HI' 214 | break 215 | case '0D': 216 | descZh = 'CCL_SENSOR_DATA_VALUE_LOW' 217 | break 218 | case '0E': 219 | descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' 220 | break 221 | case '0F': 222 | descZh = 'CCL_SENSOR_ARG_INVAILD' 223 | break 224 | case '10': 225 | descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' 226 | break 227 | case '11': 228 | descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' 229 | break 230 | case '12': 231 | descZh = 'CCL_SENSOR_RS485_REG_MISSED' 232 | break 233 | case '13': 234 | descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' 235 | break 236 | case '14': 237 | descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' 238 | break 239 | case '15': 240 | descZh = 'CCL_SENSOR_CONFIG_ERROR' 241 | break 242 | case 'FF': 243 | descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' 244 | break 245 | default: 246 | descZh = 'CC_OTHER_FAILED' 247 | break 248 | } 249 | messages = [{ 250 | measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh 251 | }] 252 | break 253 | case '10': 254 | let statusValue = dataValue.substring(0, 2) 255 | let { status, type } = loraWANV2BitDataFormat(statusValue) 256 | let sensecapId = dataValue.substring(2) 257 | messages = [{ 258 | status: status, channelType: type, sensorEui: sensecapId 259 | }] 260 | break 261 | case '20': 262 | let initmeasurementId = 4175 263 | let sensor = [] 264 | for (let i = 0; i < dataValue.length; i += 4) { 265 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 266 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 267 | let aiHeadValues = `${modelId}.${detectionType}` 268 | sensor.push({ 269 | measurementValue: aiHeadValues, measurementId: initmeasurementId 270 | }) 271 | initmeasurementId++ 272 | } 273 | messages = sensor 274 | break 275 | case '21': 276 | // Vision AI: 277 | // AI 识别输出帧 278 | let tailValueArray = [] 279 | let initTailmeasurementId = 4180 280 | for (let i = 0; i < dataValue.length; i += 4) { 281 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 282 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 283 | let aiTailValues = `${modelId}.${detectionType}` 284 | tailValueArray.push({ 285 | measurementValue: aiTailValues, measurementId: initTailmeasurementId, type: `AI Detection ${i}` 286 | }) 287 | initTailmeasurementId++ 288 | } 289 | messages = tailValueArray 290 | break 291 | case '30': 292 | case '31': 293 | // 首帧或者首帧输出帧 294 | let channelInfoOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 295 | let dataOne = { 296 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 297 | measurementId: parseInt(channelInfoOne.one), 298 | type: 'Measurement' 299 | } 300 | let dataTwo = { 301 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 302 | measurementId: parseInt(channelInfoOne.two), 303 | type: 'Measurement' 304 | } 305 | let cacheArrayInfo = [] 306 | if (parseInt(channelInfoOne.one)) { 307 | cacheArrayInfo.push(dataOne) 308 | } 309 | if (parseInt(channelInfoOne.two)) { 310 | cacheArrayInfo.push(dataTwo) 311 | } 312 | cacheArrayInfo.forEach(item => { 313 | messages.push(item) 314 | }) 315 | break 316 | case '32': 317 | let channelInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 318 | let dataThree = { 319 | measurementValue: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 320 | measurementId: parseInt(channelInfoTwo.one), 321 | type: 'Measurement' 322 | } 323 | let dataFour = { 324 | measurementValue: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), 325 | measurementId: parseInt(channelInfoTwo.two), 326 | type: 'Measurement' 327 | } 328 | if (parseInt(channelInfoTwo.one)) { 329 | messages.push(dataThree) 330 | } 331 | if (parseInt(channelInfoTwo.two)) { 332 | messages.push(dataFour) 333 | } 334 | break 335 | case '33': 336 | let channelInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 337 | let dataFive = { 338 | measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), 339 | measurementId: parseInt(channelInfoThree.one), 340 | type: 'Measurement' 341 | } 342 | let dataSix = { 343 | measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), 344 | measurementId: parseInt(channelInfoThree.two), 345 | type: 'Measurement' 346 | } 347 | if (parseInt(channelInfoThree.one)) { 348 | messages.push(dataFive) 349 | } 350 | if (parseInt(channelInfoThree.two)) { 351 | messages.push(dataSix) 352 | } 353 | 354 | break 355 | case '34': 356 | let model = loraWANV2DataFormat(dataValue.substring(0, 2)) 357 | let GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)) 358 | let simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)) 359 | let simulationInterface = loraWANV2DataFormat(dataValue.substring(6, 8)) 360 | messages = [{ 361 | 'dataloggerProtocol': model, 362 | 'dataloggerGPIOInput': GPIOInput, 363 | 'dataloggerAnalogType': simulationModel, 364 | 'dataloggerAnalogInterface': simulationInterface 365 | }] 366 | break 367 | case '35': 368 | case '36': 369 | let channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 370 | let channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2 371 | let channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2 372 | messages = [{ 373 | [channelSortTDOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 374 | [channelSortTDTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 375 | }] 376 | break 377 | case '37': 378 | let channelTDInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 379 | let channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2 380 | let channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2 381 | messages = [{ 382 | [channelSortOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 383 | [channelSortTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 384 | }] 385 | break 386 | case '38': 387 | let channelTDInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) 388 | let channelSortThreeOne = 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2 389 | let channelSortThreeTWO = 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2 390 | messages = [{ 391 | [channelSortThreeOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), 392 | [channelSortThreeTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) 393 | }] 394 | break 395 | case '39': 396 | let electricityWhetherTD = dataValue.substring(0, 2) 397 | let hwvTD = dataValue.substring(2, 6) 398 | let bdvTD = dataValue.substring(6, 10) 399 | let sensorAcquisitionIntervalTD = dataValue.substring(10, 14) 400 | let gpsAcquisitionIntervalTD = dataValue.substring(14, 18) 401 | messages = [{ 402 | 'Battery(%)': loraWANV2DataFormat(electricityWhetherTD), 403 | 'Hardware Version': `${loraWANV2DataFormat(hwvTD.substring(0, 2))}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, 404 | 'Firmware Version': `${loraWANV2DataFormat(bdvTD.substring(0, 2))}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, 405 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, 406 | 'thresholdMeasureInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)) 407 | }] 408 | break 409 | case '40': 410 | case '41': 411 | let lightIntensity = dataValue.substring(0, 4) 412 | let loudness = dataValue.substring(4, 8) 413 | // X 414 | let accelerateX = dataValue.substring(8, 12) 415 | // Y 416 | let accelerateY = dataValue.substring(12, 16) 417 | // Z 418 | let accelerateZ = dataValue.substring(16, 20) 419 | messages = [{ 420 | measurementValue: loraWANV2DataFormat(lightIntensity), measurementId: '4193', type: 'Light Intensity' 421 | }, { 422 | measurementValue: loraWANV2DataFormat(loudness), measurementId: '4192', type: 'Sound Intensity' 423 | }, { 424 | 425 | measurementValue: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150', type: 'AccelerometerX' 426 | }, { 427 | 428 | measurementValue: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151', type: 'AccelerometerY' 429 | }, { 430 | 431 | measurementValue: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152', type: 'AccelerometerZ' 432 | }] 433 | break 434 | case '42': 435 | let airTemperature = dataValue.substring(0, 4) 436 | let AirHumidity = dataValue.substring(4, 8) 437 | let tVOC = dataValue.substring(8, 12) 438 | let CO2eq = dataValue.substring(12, 16) 439 | let soilMoisture = dataValue.substring(16, 20) 440 | messages = [{ 441 | measurementValue: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097', type: 'Air Temperature' 442 | }, { 443 | measurementValue: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098', type: 'Air Humidity' 444 | }, { 445 | measurementValue: loraWANV2DataFormat(tVOC), measurementId: '4195', type: 'Total Volatile Organic Compounds' 446 | }, { 447 | measurementValue: loraWANV2DataFormat(CO2eq), measurementId: '4100', type: 'CO2' 448 | }, { 449 | measurementValue: loraWANV2DataFormat(soilMoisture), measurementId: '4196', type: 'Soil moisture intensity' 450 | }] 451 | break 452 | case '43': 453 | case '44': 454 | let headerDevKitValueArray = [] 455 | let initDevkitmeasurementId = 4175 456 | for (let i = 0; i < dataValue.length; i += 4) { 457 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 458 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 459 | let aiHeadValues = `${modelId}.${detectionType}` 460 | headerDevKitValueArray.push({ 461 | measurementValue: aiHeadValues, measurementId: initDevkitmeasurementId, type: `AI Detection ${i}` 462 | }) 463 | initDevkitmeasurementId++ 464 | } 465 | messages = headerDevKitValueArray 466 | break 467 | case '45': 468 | let initTailDevKitmeasurementId = 4180 469 | for (let i = 0; i < dataValue.length; i += 4) { 470 | let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) 471 | let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) 472 | let aiTailValues = `${modelId}.${detectionType}` 473 | messages.push({ 474 | measurementValue: aiTailValues, measurementId: initTailDevKitmeasurementId, type: `AI Detection ${i}` 475 | }) 476 | initTailDevKitmeasurementId++ 477 | } 478 | break 479 | default: 480 | break 481 | } 482 | return messages 483 | } 484 | 485 | /** 486 | * 487 | * data formatting 488 | * @param str 489 | * @param divisor 490 | * @returns {string|number} 491 | */ 492 | function loraWANV2DataFormat (str, divisor = 1) { 493 | let strReverse = bigEndianTransform(str) 494 | let str2 = toBinary(strReverse) 495 | if (str2.substring(0, 1) === '1') { 496 | let arr = str2.split('') 497 | let reverseArr = arr.map((item) => { 498 | if (parseInt(item) === 1) { 499 | return 0 500 | } else { 501 | return 1 502 | } 503 | }) 504 | str2 = parseInt(reverseArr.join(''), 2) + 1 505 | return '-' + str2 / divisor 506 | } 507 | return parseInt(str2, 2) / divisor 508 | } 509 | 510 | /** 511 | * Handling big-endian data formats 512 | * @param data 513 | * @returns {*[]} 514 | */ 515 | function bigEndianTransform (data) { 516 | let dataArray = [] 517 | for (let i = 0; i < data.length; i += 2) { 518 | dataArray.push(data.substring(i, i + 2)) 519 | } 520 | // array of hex 521 | return dataArray 522 | } 523 | 524 | /** 525 | * Convert to an 8-digit binary number with 0s in front of the number 526 | * @param arr 527 | * @returns {string} 528 | */ 529 | function toBinary (arr) { 530 | let binaryData = arr.map((item) => { 531 | let data = parseInt(item, 16) 532 | .toString(2) 533 | let dataLength = data.length 534 | if (data.length !== 8) { 535 | for (let i = 0; i < 8 - dataLength; i++) { 536 | data = `0` + data 537 | } 538 | } 539 | return data 540 | }) 541 | let ret = binaryData.toString() 542 | .replace(/,/g, '') 543 | return ret 544 | } 545 | 546 | /** 547 | * sensor 548 | * @param str 549 | * @returns {{channel: number, type: number, status: number}} 550 | */ 551 | function loraWANV2BitDataFormat (str) { 552 | let strReverse = bigEndianTransform(str) 553 | let str2 = toBinary(strReverse) 554 | let channel = parseInt(str2.substring(0, 4), 2) 555 | let status = parseInt(str2.substring(4, 5), 2) 556 | let type = parseInt(str2.substring(5), 2) 557 | return { channel, status, type } 558 | } 559 | 560 | /** 561 | * channel info 562 | * @param str 563 | * @returns {{channelTwo: number, channelOne: number}} 564 | */ 565 | function loraWANV2ChannelBitFormat (str) { 566 | let strReverse = bigEndianTransform(str) 567 | let str2 = toBinary(strReverse) 568 | let one = parseInt(str2.substring(0, 4), 2) 569 | let two = parseInt(str2.substring(4, 8), 2) 570 | let resultInfo = { 571 | one: one, two: two 572 | } 573 | return resultInfo 574 | } 575 | 576 | /** 577 | * data log status bit 578 | * @param str 579 | * @returns {{total: number, level: number, isTH: number}} 580 | */ 581 | function loraWANV2DataLogBitFormat (str) { 582 | let strReverse = bigEndianTransform(str) 583 | let str2 = toBinary(strReverse) 584 | let isTH = parseInt(str2.substring(0, 1), 2) 585 | let total = parseInt(str2.substring(1, 5), 2) 586 | let left = parseInt(str2.substring(5), 2) 587 | let resultInfo = { 588 | isTH: isTH, total: total, left: left 589 | } 590 | return resultInfo 591 | } 592 | 593 | function bytes2HexString (arrBytes) { 594 | var str = '' 595 | for (var i = 0; i < arrBytes.length; i++) { 596 | var tmp 597 | var num = arrBytes[i] 598 | if (num < 0) { 599 | tmp = (255 + num + 1).toString(16) 600 | } else { 601 | tmp = num.toString(16) 602 | } 603 | if (tmp.length === 1) { 604 | tmp = '0' + tmp 605 | } 606 | str += tmp 607 | } 608 | return str 609 | } 610 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V4_Decoder_For_Datacake.js: -------------------------------------------------------------------------------- 1 | function Decoder(bytes, port) { 2 | var bytesString = bytes2HexString(bytes).toLocaleUpperCase(); 3 | var datacakeFields = [] 4 | var measurement = messageAnalyzed(bytesString); 5 | datacakeFields = measurement; 6 | return datacakeFields; 7 | } 8 | 9 | function messageAnalyzed(messageValue) { 10 | try { 11 | var frames = unpack(messageValue); 12 | var measurementResultArray = []; 13 | for (var i = 0; i < frames.length; i++) { 14 | var item = frames[i]; 15 | var dataId = item.dataId; 16 | var dataValue = item.dataValue; 17 | var measurementArray = deserialize(dataId, dataValue); 18 | measurementResultArray = measurementResultArray.concat(measurementArray); 19 | } 20 | return measurementResultArray; 21 | } catch (e) { 22 | return e.toString(); 23 | } 24 | } 25 | 26 | function unpack(messageValue) { 27 | var frameArray = []; 28 | 29 | for (var i = 0; i < messageValue.length; i++) { 30 | var remainMessage = messageValue; 31 | var dataId = remainMessage.substring(0, 2); 32 | var dataValue; 33 | var dataObj = {}; 34 | switch (dataId) { 35 | case '01': 36 | dataValue = remainMessage.substring(2, 94); 37 | messageValue = remainMessage.substring(94); 38 | dataObj = { 39 | 'dataId': dataId, 'dataValue': dataValue 40 | }; 41 | break; 42 | case '02': 43 | dataValue = remainMessage.substring(2, 32); 44 | messageValue = remainMessage.substring(32); 45 | dataObj = { 46 | 'dataId': dataId, 'dataValue': dataValue 47 | }; 48 | break; 49 | case '03': 50 | break; 51 | case '04': 52 | dataValue = remainMessage.substring(2, 20); 53 | messageValue = remainMessage.substring(20); 54 | dataObj = { 55 | 'dataId': dataId, 'dataValue': dataValue 56 | }; 57 | break; 58 | case '05': 59 | dataValue = remainMessage.substring(2, 10); 60 | messageValue = remainMessage.substring(10); 61 | dataObj = { 62 | 'dataId': dataId, 'dataValue': dataValue 63 | }; 64 | break; 65 | case '06': 66 | dataValue = remainMessage.substring(2, 44); 67 | messageValue = remainMessage.substring(44); 68 | dataObj = { 69 | 'dataId': dataId, 'dataValue': dataValue 70 | }; 71 | break; 72 | case '07': 73 | dataValue = remainMessage.substring(2, 76); 74 | messageValue = remainMessage.substring(76); 75 | dataObj = { 76 | 'dataId': dataId, 'dataValue': dataValue 77 | }; 78 | break; 79 | case '08': 80 | dataValue = remainMessage.substring(2, 70); 81 | messageValue = remainMessage.substring(70); 82 | dataObj = { 83 | 'dataId': dataId, 'dataValue': dataValue 84 | }; 85 | break; 86 | case '09': 87 | dataValue = remainMessage.substring(2, 36); 88 | messageValue = remainMessage.substring(36); 89 | dataObj = { 90 | 'dataId': dataId, 'dataValue': dataValue 91 | }; 92 | break; 93 | case '0A': 94 | dataValue = remainMessage.substring(2, 68); 95 | messageValue = remainMessage.substring(68); 96 | dataObj = { 97 | 'dataId': dataId, 'dataValue': dataValue 98 | }; 99 | break; 100 | case '0B': 101 | dataValue = remainMessage.substring(2, 62); 102 | messageValue = remainMessage.substring(62); 103 | dataObj = { 104 | 'dataId': dataId, 'dataValue': dataValue 105 | }; 106 | break; 107 | case '0C': 108 | break; 109 | default: 110 | dataValue = ''; 111 | break; 112 | } 113 | if (dataValue.length < 2) { 114 | break; 115 | } 116 | frameArray.push(dataObj); 117 | } 118 | return frameArray; 119 | } 120 | 121 | function deserialize(dataId, dataValue) { 122 | var measurementArray = []; 123 | switch (dataId) { 124 | case '01': 125 | measurementArray = getUpShortInfo(dataValue); 126 | break; 127 | case '02': 128 | measurementArray = getUpShortInfo(dataValue); 129 | break; 130 | case '05': 131 | measurementArray = [{ field: 'BATTERY', value: getBattery(dataValue.substring(0, 2)) }]; 132 | break; 133 | case '06': 134 | measurementArray = [{ field: 'DEVICE_LOCATION', value: '(' + getSensorValue(dataValue.substring(24, 32), 1000000) + ',' + getSensorValue(dataValue.substring(16, 24), 1000000) + ')' }, { field: 'AIR_TEMPERATURE', value: getSensorValue(dataValue.substring(32, 36), 10) }, { field: 'LIGHT', value: getSensorValue(dataValue.substring(36, 40)) }, { field: 'Battery', value: getBattery(dataValue.substring(40, 42)) }]; 135 | break; 136 | case '09': 137 | measurementArray = [{ field: 'DEVICE_LOCATION', value: '(' + getSensorValue(dataValue.substring(24, 32), 1000000) + ',' + getSensorValue(dataValue.substring(16, 24), 1000000) + ')' }, { field: 'Battery', value: getBattery(dataValue.substring(32, 34)) }]; 138 | break; 139 | } 140 | return measurementArray; 141 | } 142 | 143 | function getUpShortInfo(messageValue) { 144 | return [{ 145 | field: 'BATTERY', value: getBattery(messageValue.substring(0, 2)) 146 | }, { 147 | field: 'HARDWARE_VERSION', value: getSoftVersion(messageValue.substring(2, 6)) 148 | }, { 149 | field: 'FIRMWARE_VERSION', value: getHardVersion(messageValue.substring(6, 10)) 150 | }, { 151 | field: 'HEARTBEAT_INTERVAL', value: getOneWeekInterval(messageValue.substring(14, 18)) 152 | }, { 153 | field: 'UPLINK_INTERVAL', value: getOneWeekInterval(messageValue.substring(18, 22)) 154 | }]; 155 | } 156 | function getBattery(batteryStr) { 157 | return loraWANV2DataFormat(batteryStr); 158 | } 159 | function getSoftVersion(softVersion) { 160 | return loraWANV2DataFormat(softVersion.substring(0, 2)) + '.' + loraWANV2DataFormat(softVersion.substring(2, 4)); 161 | } 162 | function getHardVersion(hardVersion) { 163 | return loraWANV2DataFormat(hardVersion.substring(0, 2)) + '.' + loraWANV2DataFormat(hardVersion.substring(2, 4)); 164 | } 165 | 166 | function getOneWeekInterval(str) { 167 | return loraWANV2DataFormat(str); 168 | } 169 | function getSensorValue(str, dig) { 170 | if (str === '8000') { 171 | return null; 172 | } else { 173 | return loraWANV2DataFormat(str, dig); 174 | } 175 | } 176 | 177 | function bytes2HexString(arrBytes) { 178 | var str = ''; 179 | for (var i = 0; i < arrBytes.length; i++) { 180 | var tmp; 181 | var num = arrBytes[i]; 182 | if (num < 0) { 183 | tmp = (255 + num + 1).toString(16); 184 | } else { 185 | tmp = num.toString(16); 186 | } 187 | if (tmp.length === 1) { 188 | tmp = '0' + tmp; 189 | } 190 | str += tmp; 191 | } 192 | return str; 193 | } 194 | function loraWANV2DataFormat(str) { 195 | var divisor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; 196 | 197 | var strReverse = bigEndianTransform(str); 198 | var str2 = toBinary(strReverse); 199 | if (str2.substring(0, 1) === '1') { 200 | var arr = str2.split(''); 201 | var reverseArr = arr.map(function (item) { 202 | if (parseInt(item) === 1) { 203 | return 0; 204 | } else { 205 | return 1; 206 | } 207 | }); 208 | str2 = parseInt(reverseArr.join(''), 2) + 1; 209 | return '-' + str2 / divisor; 210 | } 211 | return parseInt(str2, 2) / divisor; 212 | } 213 | 214 | function bigEndianTransform(data) { 215 | var dataArray = []; 216 | for (var i = 0; i < data.length; i += 2) { 217 | dataArray.push(data.substring(i, i + 2)); 218 | } 219 | return dataArray; 220 | } 221 | 222 | function toBinary(arr) { 223 | var binaryData = arr.map(function (item) { 224 | var data = parseInt(item, 16).toString(2); 225 | var dataLength = data.length; 226 | if (data.length !== 8) { 227 | for (var i = 0; i < 8 - dataLength; i++) { 228 | data = '0' + data; 229 | } 230 | } 231 | return data; 232 | }); 233 | var ret = binaryData.toString().replace(/,/g, ''); 234 | return ret; 235 | } 236 | -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V4_Decoder_For_Helium.js: -------------------------------------------------------------------------------- 1 | function Decoder (bytes, port) { 2 | var bytes = bytes2HexString(bytes) 3 | .toLocaleUpperCase() 4 | var decoded = { 5 | valid: true, 6 | err: 0, 7 | payload: bytes, 8 | messages: [] 9 | } 10 | let measurement = messageAnalyzed(bytes) 11 | decoded.messages = measurement 12 | return { data: decoded } 13 | } 14 | 15 | function messageAnalyzed (messageValue) { 16 | try { 17 | let frames = unpack(messageValue) 18 | let measurementResultArray = [] 19 | for (let i = 0; i < frames.length; i++) { 20 | let item = frames[i] 21 | let dataId = item.dataId 22 | let dataValue = item.dataValue 23 | let measurementArray = deserialize(dataId, dataValue) 24 | measurementResultArray.push(measurementArray) 25 | } 26 | return measurementResultArray 27 | } catch (e) { 28 | return e.toString() 29 | } 30 | } 31 | 32 | function unpack (messageValue) { 33 | let frameArray = [] 34 | 35 | for (let i = 0; i < messageValue.length; i++) { 36 | let remainMessage = messageValue 37 | let dataId = remainMessage.substring(0, 2).toUpperCase() 38 | let dataValue 39 | let dataObj = {} 40 | let packageLen 41 | switch (dataId) { 42 | case '01': 43 | packageLen = 94 44 | if (remainMessage.length < packageLen) { 45 | return frameArray 46 | } 47 | dataValue = remainMessage.substring(2, packageLen) 48 | messageValue = remainMessage.substring(packageLen) 49 | dataObj = { 50 | 'dataId': dataId, 'dataValue': dataValue 51 | } 52 | break 53 | case '02': 54 | packageLen = 32 55 | if (remainMessage.length < packageLen) { 56 | return frameArray 57 | } 58 | dataValue = remainMessage.substring(2, packageLen) 59 | messageValue = remainMessage.substring(packageLen) 60 | dataObj = { 61 | 'dataId': dataId, 'dataValue': dataValue 62 | } 63 | break 64 | case '03': 65 | packageLen = 64 66 | if (remainMessage.length < packageLen) { 67 | return frameArray 68 | } 69 | break 70 | case '04': 71 | packageLen = 20 72 | if (remainMessage.length < packageLen) { 73 | return frameArray 74 | } 75 | dataValue = remainMessage.substring(2, packageLen) 76 | messageValue = remainMessage.substring(packageLen) 77 | dataObj = { 78 | 'dataId': dataId, 'dataValue': dataValue 79 | } 80 | break 81 | case '05': 82 | packageLen = 10 83 | if (remainMessage.length < packageLen) { 84 | return frameArray 85 | } 86 | dataValue = remainMessage.substring(2, packageLen) 87 | messageValue = remainMessage.substring(packageLen) 88 | dataObj = { 89 | 'dataId': dataId, 'dataValue': dataValue 90 | } 91 | break 92 | case '06': 93 | packageLen = 44 94 | if (remainMessage.length < packageLen) { 95 | return frameArray 96 | } 97 | dataValue = remainMessage.substring(2, packageLen) 98 | messageValue = remainMessage.substring(packageLen) 99 | dataObj = { 100 | 'dataId': dataId, 'dataValue': dataValue 101 | } 102 | break 103 | case '07': 104 | packageLen = 84 105 | if (remainMessage.length < packageLen) { 106 | return frameArray 107 | } 108 | dataValue = remainMessage.substring(2, packageLen) 109 | messageValue = remainMessage.substring(packageLen) 110 | dataObj = { 111 | 'dataId': dataId, 'dataValue': dataValue 112 | } 113 | break 114 | case '08': 115 | packageLen = 70 116 | if (remainMessage.length < packageLen) { 117 | return frameArray 118 | } 119 | dataValue = remainMessage.substring(2, packageLen) 120 | messageValue = remainMessage.substring(packageLen) 121 | dataObj = { 122 | 'dataId': dataId, 'dataValue': dataValue 123 | } 124 | break 125 | case '09': 126 | packageLen = 36 127 | if (remainMessage.length < packageLen) { 128 | return frameArray 129 | } 130 | dataValue = remainMessage.substring(2, packageLen) 131 | messageValue = remainMessage.substring(packageLen) 132 | dataObj = { 133 | 'dataId': dataId, 'dataValue': dataValue 134 | } 135 | break 136 | case '0A': 137 | packageLen = 76 138 | if (remainMessage.length < packageLen) { 139 | return frameArray 140 | } 141 | dataValue = remainMessage.substring(2, packageLen) 142 | messageValue = remainMessage.substring(packageLen) 143 | dataObj = { 144 | 'dataId': dataId, 'dataValue': dataValue 145 | } 146 | break 147 | case '0B': 148 | packageLen = 62 149 | if (remainMessage.length < packageLen) { 150 | return frameArray 151 | } 152 | dataValue = remainMessage.substring(2, packageLen) 153 | messageValue = remainMessage.substring(packageLen) 154 | dataObj = { 155 | 'dataId': dataId, 'dataValue': dataValue 156 | } 157 | break 158 | case '0C': 159 | packageLen = 2 160 | if (remainMessage.length < packageLen) { 161 | return frameArray 162 | } 163 | break 164 | case '0D': 165 | packageLen = 10 166 | if (remainMessage.length < packageLen) { 167 | return frameArray 168 | } 169 | dataValue = remainMessage.substring(2, packageLen) 170 | messageValue = remainMessage.substring(packageLen) 171 | dataObj = { 172 | 'dataId': dataId, 'dataValue': dataValue 173 | } 174 | break 175 | default: 176 | return frameArray 177 | } 178 | if (dataValue.length < 2) { 179 | break 180 | } 181 | frameArray.push(dataObj) 182 | } 183 | return frameArray 184 | } 185 | 186 | function deserialize (dataId, dataValue) { 187 | let measurementArray = [] 188 | let eventList = [] 189 | let collectTime = 0 190 | switch (dataId) { 191 | case '01': 192 | measurementArray = getUpShortInfo(dataValue) 193 | break 194 | case '02': 195 | measurementArray = getUpShortInfo(dataValue) 196 | break 197 | case '03': 198 | break 199 | case '04': 200 | measurementArray = [ 201 | {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(0, 2))}, 202 | {measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getOneWeekInterval(dataValue.substring(4, 8))}, 203 | {measurementId: '3943', type: 'Periodic Interval', measurementValue: getOneWeekInterval(dataValue.substring(8, 12))}, 204 | {measurementId: '3944', type: 'Event Interval', measurementValue: getOneWeekInterval(dataValue.substring(12, 16))}, 205 | {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(16, 18))} 206 | ] 207 | break; 208 | case '05': 209 | measurementArray = [ 210 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(0, 2))}, 211 | {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(2, 4))}, 212 | {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(6, 8))} 213 | ] 214 | break 215 | case '06': 216 | eventList = getEventStatus(dataValue.substring(0, 6)) 217 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 218 | measurementArray = [ 219 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 220 | {measurementId: '4197', type: 'Longitude', measurementValue: getSensorValue(dataValue.substring(16, 24), 1000000)}, 221 | {measurementId: '4198', type: 'Latitude', measurementValue: getSensorValue(dataValue.substring(24, 32), 1000000)}, 222 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, 223 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, 224 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(40, 42))}, 225 | {type: 'Timestamp', measurementValue: collectTime} 226 | ] 227 | break 228 | case '07': 229 | eventList = getEventStatus(dataValue.substring(0, 6)) 230 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 231 | measurementArray = [ 232 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 233 | {measurementId: '5001', type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, 234 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, 235 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, 236 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(80, 82))}, 237 | {type: 'Timestamp', measurementValue: collectTime} 238 | ] 239 | break 240 | case '08': 241 | eventList = getEventStatus(dataValue.substring(0, 6)) 242 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 243 | measurementArray = [ 244 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 245 | {measurementId: '5002', type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, 246 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, 247 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, 248 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(66, 68))}, 249 | {type: 'Timestamp', measurementValue: collectTime} 250 | ] 251 | break 252 | case '09': 253 | eventList = getEventStatus(dataValue.substring(0, 6)) 254 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 255 | measurementArray = [ 256 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 257 | {measurementId: '4197', type: 'Longitude', measurementValue: getSensorValue(dataValue.substring(16, 24), 1000000)}, 258 | {measurementId: '4198', type: 'Latitude', measurementValue: getSensorValue(dataValue.substring(24, 32), 1000000)}, 259 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(32, 34))}, 260 | {type: 'Timestamp', measurementValue: collectTime} 261 | ] 262 | break 263 | case '0A': 264 | eventList = getEventStatus(dataValue.substring(0, 6)) 265 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 266 | measurementArray = [ 267 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 268 | {measurementId: '5001', type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, 269 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(72, 74))}, 270 | {type: 'Timestamp', measurementValue: collectTime} 271 | ] 272 | break 273 | case '0B': 274 | eventList = getEventStatus(dataValue.substring(0, 6)) 275 | collectTime = getUTCTimestamp(dataValue.substring(8, 16)) 276 | measurementArray = [ 277 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 278 | {measurementId: '5002', type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, 279 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(58, 60))}, 280 | {type: 'Timestamp', measurementValue: collectTime} 281 | ] 282 | break 283 | case '0D': 284 | let errorCode = getInt(dataValue) 285 | let error = '' 286 | switch (errorCode) { 287 | case 0: 288 | error = 'THE GNSS SCAN TIME OUT' 289 | break 290 | case 1: 291 | error = 'THE WI-FI SCAN TIME OUT' 292 | break 293 | case 2: 294 | error = 'THE WI-FI+GNSS SCAN TIME OUT' 295 | break 296 | case 3: 297 | error = 'THE GNSS+WI-FI SCAN TIME OUT' 298 | break 299 | case 4: 300 | error = 'THE BEACON SCAN TIME OUT' 301 | break 302 | case 5: 303 | error = 'THE BEACON+WI-FI SCAN TIME OUT' 304 | break 305 | case 6: 306 | error = 'THE BEACON+GNSS SCAN TIME OUT' 307 | break 308 | case 7: 309 | error = 'THE BEACON+WI-FI+GNSS SCAN TIME OUT' 310 | break 311 | case 8: 312 | error = 'FAILED TO OBTAIN THE UTC TIMESTAMP' 313 | break 314 | } 315 | measurementArray.push({errorCode, error}) 316 | } 317 | return measurementArray 318 | } 319 | 320 | function getUpShortInfo (messageValue) { 321 | return [ 322 | { 323 | measurementId: '3000', type: 'Battery', measurementValue: getBattery(messageValue.substring(0, 2)) 324 | }, { 325 | measurementId: '3502', type: 'Firmware Version', measurementValue: getSoftVersion(messageValue.substring(2, 6)) 326 | }, { 327 | measurementId: '3001', type: 'Hardware Version', measurementValue: getHardVersion(messageValue.substring(6, 10)) 328 | }, { 329 | measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(messageValue.substring(10, 12)) 330 | }, { 331 | measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getOneWeekInterval(messageValue.substring(14, 18)) 332 | }, { 333 | measurementId: '3943', type: 'Periodic Interval', measurementValue: getOneWeekInterval(messageValue.substring(18, 22)) 334 | }, { 335 | measurementId: '3944', type: 'Event Interval', measurementValue: getOneWeekInterval(messageValue.substring(22, 26)) 336 | }, { 337 | measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(messageValue.substring(28, 30)) 338 | } 339 | ] 340 | } 341 | function getBattery (batteryStr) { 342 | return loraWANV2DataFormat(batteryStr) 343 | } 344 | function getSoftVersion (softVersion) { 345 | return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` 346 | } 347 | function getHardVersion (hardVersion) { 348 | return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` 349 | } 350 | 351 | function getOneWeekInterval (str) { 352 | return loraWANV2DataFormat(str) * 60 353 | } 354 | function getSensorValue (str, dig) { 355 | if (str === '8000') { 356 | return null 357 | } else { 358 | return loraWANV2DataFormat(str, dig) 359 | } 360 | } 361 | 362 | function bytes2HexString (arrBytes) { 363 | var str = '' 364 | for (var i = 0; i < arrBytes.length; i++) { 365 | var tmp 366 | var num = arrBytes[i] 367 | if (num < 0) { 368 | tmp = (255 + num + 1).toString(16) 369 | } else { 370 | tmp = num.toString(16) 371 | } 372 | if (tmp.length === 1) { 373 | tmp = '0' + tmp 374 | } 375 | str += tmp 376 | } 377 | return str 378 | } 379 | function loraWANV2DataFormat (str, divisor = 1) { 380 | let strReverse = bigEndianTransform(str) 381 | let str2 = toBinary(strReverse) 382 | if (str2.substring(0, 1) === '1') { 383 | let arr = str2.split('') 384 | let reverseArr = arr.map((item) => { 385 | if (parseInt(item) === 1) { 386 | return 0 387 | } else { 388 | return 1 389 | } 390 | }) 391 | str2 = parseInt(reverseArr.join(''), 2) + 1 392 | return '-' + str2 / divisor 393 | } 394 | return parseInt(str2, 2) / divisor 395 | } 396 | 397 | function bigEndianTransform (data) { 398 | let dataArray = [] 399 | for (let i = 0; i < data.length; i += 2) { 400 | dataArray.push(data.substring(i, i + 2)) 401 | } 402 | return dataArray 403 | } 404 | 405 | function toBinary (arr) { 406 | let binaryData = arr.map((item) => { 407 | let data = parseInt(item, 16) 408 | .toString(2) 409 | let dataLength = data.length 410 | if (data.length !== 8) { 411 | for (let i = 0; i < 8 - dataLength; i++) { 412 | data = `0` + data 413 | } 414 | } 415 | return data 416 | }) 417 | return binaryData.toString().replace(/,/g, '') 418 | } 419 | 420 | function getSOSMode (str) { 421 | return loraWANV2DataFormat(str) 422 | } 423 | 424 | function getMacAndRssiObj (pair) { 425 | let pairs = [] 426 | if (pair.length % 14 === 0) { 427 | for (let i = 0; i < pair.length; i += 14) { 428 | let mac = getMacAddress(pair.substring(i, i + 12)) 429 | if (mac) { 430 | let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) 431 | pairs.push({mac: mac, rssi: rssi}) 432 | } else { 433 | continue 434 | } 435 | } 436 | } 437 | return pairs 438 | } 439 | 440 | function getMacAddress (str) { 441 | if (str.toLowerCase() === 'ffffffffffff') { 442 | return null 443 | } 444 | let macArr = [] 445 | for (let i = 1; i < str.length; i++) { 446 | if (i % 2 === 1) { 447 | macArr.push(str.substring(i - 1, i + 1)) 448 | } 449 | } 450 | let mac = '' 451 | for (let i = 0; i < macArr.length; i++) { 452 | mac = mac + macArr[i] 453 | if (i < macArr.length - 1) { 454 | mac = mac + ':' 455 | } 456 | } 457 | return mac 458 | } 459 | 460 | function getInt8RSSI (str) { 461 | return loraWANV2DataFormat(str) 462 | } 463 | 464 | function getInt (str) { 465 | return parseInt(str) 466 | } 467 | 468 | /** 469 | * 1.MOVING_STARTING 470 | * 2.MOVING_END 471 | * 3.DEVICE_STATIC 472 | * 4.SHOCK_EVENT 473 | * 5.TEMP_EVENT 474 | * 6.LIGHTING_EVENT 475 | * 7.SOS_EVENT 476 | * 8.CUSTOMER_EVENT 477 | * */ 478 | function getEventStatus (str) { 479 | let bitStr = getByteArray(str) 480 | let event = [] 481 | for (let i = bitStr.length; i >= 0; i--) { 482 | if (i === 0) { 483 | event[i] = bitStr.substring(0) 484 | } else { 485 | event[i] = bitStr.substring(i - 1, i) 486 | } 487 | } 488 | return event.reverse() 489 | } 490 | 491 | function getByteArray (str) { 492 | let bytes = [] 493 | for (let i = 0; i < str.length; i += 2) { 494 | bytes.push(str.substring(i, i + 2)) 495 | } 496 | return toBinary(bytes) 497 | } 498 | 499 | function getWorkingMode (workingMode) { 500 | return getInt(workingMode) 501 | } 502 | 503 | function getUTCTimestamp(str){ 504 | return parseInt(loraWANV2PositiveDataFormat(str)) * 1000 505 | } 506 | 507 | function loraWANV2PositiveDataFormat (str, divisor = 1) { 508 | let strReverse = bigEndianTransform(str) 509 | let str2 = toBinary(strReverse) 510 | return parseInt(str2, 2) / divisor 511 | } -------------------------------------------------------------------------------- /SenseCAP_LoRaWAN_V4_Decoder_For_TTN.js: -------------------------------------------------------------------------------- 1 | function decodeUplink (input) { 2 | var bytes = input['bytes'] 3 | var bytesString = bytes2HexString(bytes).toLocaleUpperCase() 4 | var decoded = { 5 | valid: true, 6 | err: 0, 7 | payload: bytesString, 8 | messages: [] 9 | } 10 | let measurement = messageAnalyzed(bytesString) 11 | decoded.messages = measurement 12 | return { data: decoded } 13 | } 14 | 15 | function messageAnalyzed (messageValue) { 16 | try { 17 | let frames = unpack(messageValue) 18 | let measurementResultArray = [] 19 | for (let i = 0; i < frames.length; i++) { 20 | let item = frames[i] 21 | let dataId = item.dataId 22 | let dataValue = item.dataValue 23 | let measurementArray = deserialize(dataId, dataValue) 24 | measurementResultArray.push(measurementArray) 25 | } 26 | return measurementResultArray 27 | } catch (e) { 28 | return e.toString() 29 | } 30 | } 31 | 32 | function unpack (messageValue) { 33 | let frameArray = [] 34 | 35 | for (let i = 0; i < messageValue.length; i++) { 36 | let remainMessage = messageValue 37 | let dataId = remainMessage.substring(0, 2).toUpperCase() 38 | let dataValue 39 | let dataObj = {} 40 | let packageLen 41 | switch (dataId) { 42 | case '01': 43 | packageLen = 94 44 | if (remainMessage.length < packageLen) { 45 | return frameArray 46 | } 47 | dataValue = remainMessage.substring(2, packageLen) 48 | messageValue = remainMessage.substring(packageLen) 49 | dataObj = { 50 | 'dataId': dataId, 'dataValue': dataValue 51 | } 52 | break 53 | case '02': 54 | packageLen = 32 55 | if (remainMessage.length < packageLen) { 56 | return frameArray 57 | } 58 | dataValue = remainMessage.substring(2, packageLen) 59 | messageValue = remainMessage.substring(packageLen) 60 | dataObj = { 61 | 'dataId': dataId, 'dataValue': dataValue 62 | } 63 | break 64 | case '03': 65 | packageLen = 64 66 | if (remainMessage.length < packageLen) { 67 | return frameArray 68 | } 69 | break 70 | case '04': 71 | packageLen = 20 72 | if (remainMessage.length < packageLen) { 73 | return frameArray 74 | } 75 | dataValue = remainMessage.substring(2, packageLen) 76 | messageValue = remainMessage.substring(packageLen) 77 | dataObj = { 78 | 'dataId': dataId, 'dataValue': dataValue 79 | } 80 | break 81 | case '05': 82 | packageLen = 10 83 | if (remainMessage.length < packageLen) { 84 | return frameArray 85 | } 86 | dataValue = remainMessage.substring(2, packageLen) 87 | messageValue = remainMessage.substring(packageLen) 88 | dataObj = { 89 | 'dataId': dataId, 'dataValue': dataValue 90 | } 91 | break 92 | case '06': 93 | packageLen = 44 94 | if (remainMessage.length < packageLen) { 95 | return frameArray 96 | } 97 | dataValue = remainMessage.substring(2, packageLen) 98 | messageValue = remainMessage.substring(packageLen) 99 | dataObj = { 100 | 'dataId': dataId, 'dataValue': dataValue 101 | } 102 | break 103 | case '07': 104 | packageLen = 84 105 | if (remainMessage.length < packageLen) { 106 | return frameArray 107 | } 108 | dataValue = remainMessage.substring(2, packageLen) 109 | messageValue = remainMessage.substring(packageLen) 110 | dataObj = { 111 | 'dataId': dataId, 'dataValue': dataValue 112 | } 113 | break 114 | case '08': 115 | packageLen = 70 116 | if (remainMessage.length < packageLen) { 117 | return frameArray 118 | } 119 | dataValue = remainMessage.substring(2, packageLen) 120 | messageValue = remainMessage.substring(packageLen) 121 | dataObj = { 122 | 'dataId': dataId, 'dataValue': dataValue 123 | } 124 | break 125 | case '09': 126 | packageLen = 36 127 | if (remainMessage.length < packageLen) { 128 | return frameArray 129 | } 130 | dataValue = remainMessage.substring(2, packageLen) 131 | messageValue = remainMessage.substring(packageLen) 132 | dataObj = { 133 | 'dataId': dataId, 'dataValue': dataValue 134 | } 135 | break 136 | case '0A': 137 | packageLen = 76 138 | if (remainMessage.length < packageLen) { 139 | return frameArray 140 | } 141 | dataValue = remainMessage.substring(2, packageLen) 142 | messageValue = remainMessage.substring(packageLen) 143 | dataObj = { 144 | 'dataId': dataId, 'dataValue': dataValue 145 | } 146 | break 147 | case '0B': 148 | packageLen = 62 149 | if (remainMessage.length < packageLen) { 150 | return frameArray 151 | } 152 | dataValue = remainMessage.substring(2, packageLen) 153 | messageValue = remainMessage.substring(packageLen) 154 | dataObj = { 155 | 'dataId': dataId, 'dataValue': dataValue 156 | } 157 | break 158 | case '0C': 159 | packageLen = 2 160 | if (remainMessage.length < packageLen) { 161 | return frameArray 162 | } 163 | break 164 | case '0D': 165 | packageLen = 10 166 | if (remainMessage.length < packageLen) { 167 | return frameArray 168 | } 169 | dataValue = remainMessage.substring(2, packageLen) 170 | messageValue = remainMessage.substring(packageLen) 171 | dataObj = { 172 | 'dataId': dataId, 'dataValue': dataValue 173 | } 174 | break 175 | default: 176 | return frameArray 177 | } 178 | if (dataValue.length < 2) { 179 | break 180 | } 181 | frameArray.push(dataObj) 182 | } 183 | return frameArray 184 | } 185 | 186 | function deserialize (dataId, dataValue) { 187 | let measurementArray = [] 188 | let eventList = [] 189 | let collectTime = 0 190 | switch (dataId) { 191 | case '01': 192 | measurementArray = getUpShortInfo(dataValue) 193 | break 194 | case '02': 195 | measurementArray = getUpShortInfo(dataValue) 196 | break 197 | case '03': 198 | break 199 | case '04': 200 | measurementArray = [ 201 | {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(0, 2))}, 202 | {measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getOneWeekInterval(dataValue.substring(4, 8))}, 203 | {measurementId: '3943', type: 'Periodic Interval', measurementValue: getOneWeekInterval(dataValue.substring(8, 12))}, 204 | {measurementId: '3944', type: 'Event Interval', measurementValue: getOneWeekInterval(dataValue.substring(12, 16))}, 205 | {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(16, 18))} 206 | ] 207 | break; 208 | case '05': 209 | measurementArray = [ 210 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(0, 2))}, 211 | {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(2, 4))}, 212 | {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(6, 8))} 213 | ] 214 | break 215 | case '06': 216 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 217 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 218 | measurementArray = [ 219 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 220 | {measurementId: '4197', type: 'Longitude', measurementValue: getSensorValue(dataValue.substring(16, 24), 1000000)}, 221 | {measurementId: '4198', type: 'Latitude', measurementValue: getSensorValue(dataValue.substring(24, 32), 1000000)}, 222 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, 223 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, 224 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(40, 42))}, 225 | {type: 'Timestamp', measurementValue: collectTime} 226 | ] 227 | break 228 | case '07': 229 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 230 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 231 | measurementArray = [ 232 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 233 | {measurementId: '5001', type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, 234 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, 235 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, 236 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(80, 82))}, 237 | {type: 'Timestamp', measurementValue: collectTime} 238 | ] 239 | break 240 | case '08': 241 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 242 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 243 | measurementArray = [ 244 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 245 | {measurementId: '5002', type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, 246 | {measurementId: '4097', type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, 247 | {measurementId: '4199', type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, 248 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(66, 68))}, 249 | {type: 'Timestamp', measurementValue: collectTime} 250 | ] 251 | break 252 | case '09': 253 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 254 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 255 | measurementArray = [ 256 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 257 | {measurementId: '4197', type: 'Longitude', measurementValue: getSensorValue(dataValue.substring(16, 24), 1000000)}, 258 | {measurementId: '4198', type: 'Latitude', measurementValue: getSensorValue(dataValue.substring(24, 32), 1000000)}, 259 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(32, 34))}, 260 | {type: 'Timestamp', measurementValue: collectTime} 261 | ] 262 | break 263 | case '0A': 264 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 265 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 266 | measurementArray = [ 267 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 268 | {measurementId: '5001', type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, 269 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(72, 74))}, 270 | {type: 'Timestamp', measurementValue: collectTime} 271 | ] 272 | break 273 | case '0B': 274 | eventList = this.getEventStatus(dataValue.substring(0, 6)) 275 | collectTime = this.getUTCTimestamp(dataValue.substring(8, 16)) 276 | measurementArray = [ 277 | {measurementId: '4200', type: 'SOS Event', measurementValue: eventList[6]}, 278 | {measurementId: '5002', type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, 279 | {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(58, 60))}, 280 | {type: 'Timestamp', measurementValue: collectTime} 281 | ] 282 | break 283 | case '0D': 284 | let errorCode = this.getInt(dataValue) 285 | let error = '' 286 | switch (errorCode) { 287 | case 0: 288 | error = 'THE GNSS SCAN TIME OUT' 289 | break 290 | case 1: 291 | error = 'THE WI-FI SCAN TIME OUT' 292 | break 293 | case 2: 294 | error = 'THE WI-FI+GNSS SCAN TIME OUT' 295 | break 296 | case 3: 297 | error = 'THE GNSS+WI-FI SCAN TIME OUT' 298 | break 299 | case 4: 300 | error = 'THE BEACON SCAN TIME OUT' 301 | break 302 | case 5: 303 | error = 'THE BEACON+WI-FI SCAN TIME OUT' 304 | break 305 | case 6: 306 | error = 'THE BEACON+GNSS SCAN TIME OUT' 307 | break 308 | case 7: 309 | error = 'THE BEACON+WI-FI+GNSS SCAN TIME OUT' 310 | break 311 | case 8: 312 | error = 'FAILED TO OBTAIN THE UTC TIMESTAMP' 313 | break 314 | } 315 | measurementArray.push({errorCode, error}) 316 | } 317 | return measurementArray 318 | } 319 | 320 | function getUpShortInfo (messageValue) { 321 | return [ 322 | { 323 | measurementId: '3000', type: 'Battery', measurementValue: getBattery(messageValue.substring(0, 2)) 324 | }, { 325 | measurementId: '3502', type: 'Firmware Version', measurementValue: getSoftVersion(messageValue.substring(2, 6)) 326 | }, { 327 | measurementId: '3001', type: 'Hardware Version', measurementValue: getHardVersion(messageValue.substring(6, 10)) 328 | }, { 329 | measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(messageValue.substring(10, 12)) 330 | }, { 331 | measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getOneWeekInterval(messageValue.substring(14, 18)) 332 | }, { 333 | measurementId: '3943', type: 'Periodic Interval', measurementValue: getOneWeekInterval(messageValue.substring(18, 22)) 334 | }, { 335 | measurementId: '3944', type: 'Event Interval', measurementValue: getOneWeekInterval(messageValue.substring(22, 26)) 336 | }, { 337 | measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(messageValue.substring(28, 30)) 338 | } 339 | ] 340 | } 341 | function getBattery (batteryStr) { 342 | return loraWANV2DataFormat(batteryStr) 343 | } 344 | function getSoftVersion (softVersion) { 345 | return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` 346 | } 347 | function getHardVersion (hardVersion) { 348 | return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` 349 | } 350 | 351 | function getOneWeekInterval (str) { 352 | return loraWANV2DataFormat(str) * 60 353 | } 354 | function getSensorValue (str, dig) { 355 | if (str === '8000') { 356 | return null 357 | } else { 358 | return loraWANV2DataFormat(str, dig) 359 | } 360 | } 361 | 362 | function bytes2HexString (arrBytes) { 363 | var str = '' 364 | for (var i = 0; i < arrBytes.length; i++) { 365 | var tmp 366 | var num = arrBytes[i] 367 | if (num < 0) { 368 | tmp = (255 + num + 1).toString(16) 369 | } else { 370 | tmp = num.toString(16) 371 | } 372 | if (tmp.length === 1) { 373 | tmp = '0' + tmp 374 | } 375 | str += tmp 376 | } 377 | return str 378 | } 379 | function loraWANV2DataFormat (str, divisor = 1) { 380 | let strReverse = bigEndianTransform(str) 381 | let str2 = toBinary(strReverse) 382 | if (str2.substring(0, 1) === '1') { 383 | let arr = str2.split('') 384 | let reverseArr = arr.map((item) => { 385 | if (parseInt(item) === 1) { 386 | return 0 387 | } else { 388 | return 1 389 | } 390 | }) 391 | str2 = parseInt(reverseArr.join(''), 2) + 1 392 | return '-' + str2 / divisor 393 | } 394 | return parseInt(str2, 2) / divisor 395 | } 396 | 397 | function bigEndianTransform (data) { 398 | let dataArray = [] 399 | for (let i = 0; i < data.length; i += 2) { 400 | dataArray.push(data.substring(i, i + 2)) 401 | } 402 | return dataArray 403 | } 404 | 405 | function toBinary (arr) { 406 | let binaryData = arr.map((item) => { 407 | let data = parseInt(item, 16) 408 | .toString(2) 409 | let dataLength = data.length 410 | if (data.length !== 8) { 411 | for (let i = 0; i < 8 - dataLength; i++) { 412 | data = `0` + data 413 | } 414 | } 415 | return data 416 | }) 417 | return binaryData.toString().replace(/,/g, '') 418 | } 419 | 420 | function getSOSMode (str) { 421 | return loraWANV2DataFormat(str) 422 | } 423 | 424 | function getMacAndRssiObj (pair) { 425 | let pairs = [] 426 | if (pair.length % 14 === 0) { 427 | for (let i = 0; i < pair.length; i += 14) { 428 | let mac = getMacAddress(pair.substring(i, i + 12)) 429 | if (mac) { 430 | let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) 431 | pairs.push({mac: mac, rssi: rssi}) 432 | } else { 433 | continue 434 | } 435 | } 436 | } 437 | return pairs 438 | } 439 | 440 | function getMacAddress (str) { 441 | if (str.toLowerCase() === 'ffffffffffff') { 442 | return null 443 | } 444 | let macArr = [] 445 | for (let i = 1; i < str.length; i++) { 446 | if (i % 2 === 1) { 447 | macArr.push(str.substring(i - 1, i + 1)) 448 | } 449 | } 450 | let mac = '' 451 | for (let i = 0; i < macArr.length; i++) { 452 | mac = mac + macArr[i] 453 | if (i < macArr.length - 1) { 454 | mac = mac + ':' 455 | } 456 | } 457 | return mac 458 | } 459 | 460 | function getInt8RSSI (str) { 461 | return this.loraWANV2DataFormat(str) 462 | } 463 | 464 | function getInt (str) { 465 | return parseInt(str) 466 | } 467 | 468 | /** 469 | * 1.MOVING_STARTING 470 | * 2.MOVING_END 471 | * 3.DEVICE_STATIC 472 | * 4.SHOCK_EVENT 473 | * 5.TEMP_EVENT 474 | * 6.LIGHTING_EVENT 475 | * 7.SOS_EVENT 476 | * 8.CUSTOMER_EVENT 477 | * */ 478 | function getEventStatus (str) { 479 | let bitStr = this.getByteArray(str) 480 | let event = [] 481 | for (let i = bitStr.length; i >= 0; i--) { 482 | if (i === 0) { 483 | event[i] = bitStr.substring(0) 484 | } else { 485 | event[i] = bitStr.substring(i - 1, i) 486 | } 487 | } 488 | return event.reverse() 489 | } 490 | 491 | function getByteArray (str) { 492 | let bytes = [] 493 | for (let i = 0; i < str.length; i += 2) { 494 | bytes.push(str.substring(i, i + 2)) 495 | } 496 | return toBinary(bytes) 497 | } 498 | 499 | function getWorkingMode (workingMode) { 500 | return getInt(workingMode) 501 | } 502 | 503 | function getUTCTimestamp(str){ 504 | return parseInt(this.loraWANV2PositiveDataFormat(str)) * 1000 505 | } 506 | 507 | function loraWANV2PositiveDataFormat (str, divisor = 1) { 508 | let strReverse = this.bigEndianTransform(str) 509 | let str2 = this.toBinary(strReverse) 510 | return parseInt(str2, 2) / divisor 511 | } -------------------------------------------------------------------------------- /SenseCAP_S2120_Weather_Station_Decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | function decodeUplink (input, port) { 5 | // data split 6 | 7 | var bytes = input['bytes'] 8 | // init 9 | bytes = bytes2HexString(bytes) 10 | .toLocaleUpperCase() 11 | 12 | let result = { 13 | 'err': 0, 'payload': bytes, 'valid': true, messages: [] 14 | } 15 | let splitArray = dataSplit(bytes) 16 | // data decoder 17 | let decoderArray = [] 18 | for (let i = 0; i < splitArray.length; i++) { 19 | let item = splitArray[i] 20 | let dataId = item.dataId 21 | let dataValue = item.dataValue 22 | let messages = dataIdAndDataValueJudge(dataId, dataValue) 23 | decoderArray.push(messages) 24 | } 25 | result.messages = decoderArray 26 | return { data: result } 27 | } 28 | 29 | /** 30 | * data splits 31 | * @param bytes 32 | * @returns {*[]} 33 | */ 34 | function dataSplit (bytes) { 35 | let frameArray = [] 36 | 37 | for (let i = 0; i < bytes.length; i++) { 38 | let remainingValue = bytes 39 | let dataId = remainingValue.substring(0, 2) 40 | let dataValue 41 | let dataObj = {} 42 | switch (dataId) { 43 | case '01' : 44 | case '20' : 45 | case '21' : 46 | case '30' : 47 | case '31' : 48 | case '33' : 49 | case '40' : 50 | case '41' : 51 | case '42' : 52 | case '43' : 53 | case '44' : 54 | case '45' : 55 | dataValue = remainingValue.substring(2, 22) 56 | bytes = remainingValue.substring(22) 57 | dataObj = { 58 | 'dataId': dataId, 'dataValue': dataValue 59 | } 60 | break 61 | case '02': 62 | dataValue = remainingValue.substring(2, 18) 63 | bytes = remainingValue.substring(18) 64 | dataObj = { 65 | 'dataId': '02', 'dataValue': dataValue 66 | } 67 | break 68 | case '03' : 69 | case '06': 70 | dataValue = remainingValue.substring(2, 4) 71 | bytes = remainingValue.substring(4) 72 | dataObj = { 73 | 'dataId': dataId, 'dataValue': dataValue 74 | } 75 | break 76 | case '05' : 77 | case '34': 78 | dataValue = bytes.substring(2, 10) 79 | bytes = remainingValue.substring(10) 80 | dataObj = { 81 | 'dataId': dataId, 'dataValue': dataValue 82 | } 83 | break 84 | case '04': 85 | case '10': 86 | case '32': 87 | case '35': 88 | case '36': 89 | case '37': 90 | case '38': 91 | case '39': 92 | dataValue = bytes.substring(2, 20) 93 | bytes = remainingValue.substring(20) 94 | dataObj = { 95 | 'dataId': dataId, 'dataValue': dataValue 96 | } 97 | break 98 | default: 99 | dataValue = '9' 100 | break 101 | } 102 | if (dataValue.length < 2) { 103 | break 104 | } 105 | frameArray.push(dataObj) 106 | } 107 | return frameArray 108 | } 109 | 110 | function dataIdAndDataValueJudge (dataId, dataValue) { 111 | let messages = [] 112 | switch (dataId) { 113 | case '01': 114 | let temperature = dataValue.substring(0, 4) 115 | let humidity = dataValue.substring(4, 6) 116 | let illumination = dataValue.substring(6, 14) 117 | let uv = dataValue.substring(14, 16) 118 | let windSpeed = dataValue.substring(16, 20) 119 | messages = [{ 120 | measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' 121 | }, { 122 | measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' 123 | }, { 124 | measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' 125 | }, { 126 | measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' 127 | }, { 128 | measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' 129 | }] 130 | break 131 | case '02': 132 | let windDirection = dataValue.substring(0, 4) 133 | let rainfall = dataValue.substring(4, 12) 134 | let airPressure = dataValue.substring(12, 16) 135 | messages = [{ 136 | measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Direction Sensor' 137 | }, { 138 | measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'Rain Gauge' 139 | }, { 140 | 141 | measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' 142 | }] 143 | break 144 | case '03': 145 | let Electricity = dataValue 146 | messages = [{ 147 | 'Battery(%)': loraWANV2DataFormat(Electricity) 148 | }] 149 | break 150 | case '04': 151 | let electricityWhether = dataValue.substring(0, 2) 152 | let hwv = dataValue.substring(2, 6) 153 | let bdv = dataValue.substring(6, 10) 154 | let sensorAcquisitionInterval = dataValue.substring(10, 14) 155 | let gpsAcquisitionInterval = dataValue.substring(14, 18) 156 | messages = [{ 157 | 'Battery(%)': loraWANV2DataFormat(electricityWhether), 158 | 'Hardware Version': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, 159 | 'Firmware Version': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, 160 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, 161 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 162 | }] 163 | break 164 | case '05': 165 | let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) 166 | let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) 167 | messages = [{ 168 | 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, 169 | 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 170 | }] 171 | break 172 | case '06': 173 | let errorCode = dataValue 174 | let descZh 175 | switch (errorCode) { 176 | case '00': 177 | descZh = 'CCL_SENSOR_ERROR_NONE' 178 | break 179 | case '01': 180 | descZh = 'CCL_SENSOR_NOT_FOUND' 181 | break 182 | case '02': 183 | descZh = 'CCL_SENSOR_WAKEUP_ERROR' 184 | break 185 | case '03': 186 | descZh = 'CCL_SENSOR_NOT_RESPONSE' 187 | break 188 | case '04': 189 | descZh = 'CCL_SENSOR_DATA_EMPTY' 190 | break 191 | case '05': 192 | descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' 193 | break 194 | case '06': 195 | descZh = 'CCL_SENSOR_DATA_CRC_ERROR' 196 | break 197 | case '07': 198 | descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' 199 | break 200 | case '08': 201 | descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' 202 | break 203 | case '09': 204 | descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' 205 | break 206 | case '0A': 207 | descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' 208 | break 209 | case '0B': 210 | descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' 211 | break 212 | case '0C': 213 | descZh = 'CCL_SENSOR_DATA_VALUE_HI' 214 | break 215 | case '0D': 216 | descZh = 'CCL_SENSOR_DATA_VALUE_LOW' 217 | break 218 | case '0E': 219 | descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' 220 | break 221 | case '0F': 222 | descZh = 'CCL_SENSOR_ARG_INVAILD' 223 | break 224 | case '10': 225 | descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' 226 | break 227 | case '11': 228 | descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' 229 | break 230 | case '12': 231 | descZh = 'CCL_SENSOR_RS485_REG_MISSED' 232 | break 233 | case '13': 234 | descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' 235 | break 236 | case '14': 237 | descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' 238 | break 239 | case '15': 240 | descZh = 'CCL_SENSOR_CONFIG_ERROR' 241 | break 242 | case 'FF': 243 | descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' 244 | break 245 | default: 246 | descZh = 'CC_OTHER_FAILED' 247 | break 248 | } 249 | messages = [{ 250 | measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh 251 | }] 252 | break 253 | case '10': 254 | let statusValue = dataValue.substring(0, 2) 255 | let { status, type } = loraWANV2BitDataFormat(statusValue) 256 | let sensecapId = dataValue.substring(2) 257 | messages = [{ 258 | status: status, channelType: type, sensorEui: sensecapId 259 | }] 260 | break 261 | default: 262 | break 263 | } 264 | return messages 265 | } 266 | 267 | /** 268 | * 269 | * data formatting 270 | * @param str 271 | * @param divisor 272 | * @returns {string|number} 273 | */ 274 | function loraWANV2DataFormat (str, divisor = 1) { 275 | let strReverse = bigEndianTransform(str) 276 | let str2 = toBinary(strReverse) 277 | if (str2.substring(0, 1) === '1') { 278 | let arr = str2.split('') 279 | let reverseArr = arr.map((item) => { 280 | if (parseInt(item) === 1) { 281 | return 0 282 | } else { 283 | return 1 284 | } 285 | }) 286 | str2 = parseInt(reverseArr.join(''), 2) + 1 287 | return '-' + str2 / divisor 288 | } 289 | return parseInt(str2, 2) / divisor 290 | } 291 | 292 | /** 293 | * Handling big-endian data formats 294 | * @param data 295 | * @returns {*[]} 296 | */ 297 | function bigEndianTransform (data) { 298 | let dataArray = [] 299 | for (let i = 0; i < data.length; i += 2) { 300 | dataArray.push(data.substring(i, i + 2)) 301 | } 302 | // array of hex 303 | return dataArray 304 | } 305 | 306 | /** 307 | * Convert to an 8-digit binary number with 0s in front of the number 308 | * @param arr 309 | * @returns {string} 310 | */ 311 | function toBinary (arr) { 312 | let binaryData = arr.map((item) => { 313 | let data = parseInt(item, 16) 314 | .toString(2) 315 | let dataLength = data.length 316 | if (data.length !== 8) { 317 | for (let i = 0; i < 8 - dataLength; i++) { 318 | data = `0` + data 319 | } 320 | } 321 | return data 322 | }) 323 | let ret = binaryData.toString() 324 | .replace(/,/g, '') 325 | return ret 326 | } 327 | 328 | /** 329 | * sensor 330 | * @param str 331 | * @returns {{channel: number, type: number, status: number}} 332 | */ 333 | function loraWANV2BitDataFormat (str) { 334 | let strReverse = bigEndianTransform(str) 335 | let str2 = toBinary(strReverse) 336 | let channel = parseInt(str2.substring(0, 4), 2) 337 | let status = parseInt(str2.substring(4, 5), 2) 338 | let type = parseInt(str2.substring(5), 2) 339 | return { channel, status, type } 340 | } 341 | 342 | /** 343 | * channel info 344 | * @param str 345 | * @returns {{channelTwo: number, channelOne: number}} 346 | */ 347 | function loraWANV2ChannelBitFormat (str) { 348 | let strReverse = bigEndianTransform(str) 349 | let str2 = toBinary(strReverse) 350 | let one = parseInt(str2.substring(0, 4), 2) 351 | let two = parseInt(str2.substring(4, 8), 2) 352 | let resultInfo = { 353 | one: one, two: two 354 | } 355 | return resultInfo 356 | } 357 | 358 | /** 359 | * data log status bit 360 | * @param str 361 | * @returns {{total: number, level: number, isTH: number}} 362 | */ 363 | function loraWANV2DataLogBitFormat (str) { 364 | let strReverse = bigEndianTransform(str) 365 | let str2 = toBinary(strReverse) 366 | let isTH = parseInt(str2.substring(0, 1), 2) 367 | let total = parseInt(str2.substring(1, 5), 2) 368 | let left = parseInt(str2.substring(5), 2) 369 | let resultInfo = { 370 | isTH: isTH, total: total, left: left 371 | } 372 | return resultInfo 373 | } 374 | 375 | function bytes2HexString (arrBytes) { 376 | var str = '' 377 | for (var i = 0; i < arrBytes.length; i++) { 378 | var tmp 379 | var num = arrBytes[i] 380 | if (num < 0) { 381 | tmp = (255 + num + 1).toString(16) 382 | } else { 383 | tmp = num.toString(16) 384 | } 385 | if (tmp.length === 1) { 386 | tmp = '0' + tmp 387 | } 388 | str += tmp 389 | } 390 | return str 391 | } 392 | -------------------------------------------------------------------------------- /datacake/decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry, decoder.js 3 | */ 4 | function Decoder (bytes, port) { 5 | // init 6 | var bytesString = bytes2HexString(bytes) 7 | .toLocaleUpperCase() 8 | // var bytesString = bytes 9 | var decoded = { 10 | // valid 11 | valid: true, err: 0, // bytes 12 | payload: bytesString, // messages array 13 | messages: [] 14 | } 15 | 16 | // CRC check 17 | if (!crc16Check(bytesString)) { 18 | decoded['valid'] = false 19 | decoded['err'] = -1 // "crc check fail." 20 | return decoded 21 | } 22 | 23 | // Length Check 24 | if ((((bytesString.length / 2) - 2) % 7) !== 0) { 25 | decoded['valid'] = false 26 | decoded['err'] = -2 // "length check fail." 27 | return decoded 28 | } 29 | 30 | // Cache sensor id 31 | var sensorEuiLowBytes 32 | var sensorEuiHighBytes 33 | 34 | // Handle each frame 35 | var frameArray = divideBy7Bytes(bytesString) 36 | for (var forFrame = 0; forFrame < frameArray.length; forFrame++) { 37 | var frame = frameArray[forFrame] 38 | // Extract key parameters 39 | var channel = strTo10SysNub(frame.substring(0, 2)) 40 | var dataID = strTo10SysNub(frame.substring(2, 6)) 41 | var dataValue = frame.substring(6, 14) 42 | var realDataValue = isSpecialDataId(dataID) ? ttnDataSpecialFormat(dataID, dataValue) : ttnDataFormat(dataValue) 43 | 44 | if (checkDataIdIsMeasureUpload(dataID)) { 45 | // if telemetry. 46 | decoded.messages.push({ 47 | type: 'report_telemetry', measurementId: dataID, measurementValue: realDataValue 48 | }) 49 | } else if (isSpecialDataId(dataID) || (dataID === 5) || (dataID === 6) || (dataID === 9)) { 50 | // if special order, except "report_sensor_id". 51 | switch (dataID) { 52 | case 0x00: 53 | // node version 54 | var versionData = sensorAttrForVersion(realDataValue) 55 | decoded.messages.push({ 56 | type: 'upload_version', hardwareVersion: versionData.ver_hardware, softwareVersion: versionData.ver_software 57 | }) 58 | break 59 | case 1: 60 | // sensor version 61 | break 62 | case 2: 63 | // sensor eui, low bytes 64 | sensorEuiLowBytes = realDataValue 65 | break 66 | case 3: 67 | // sensor eui, high bytes 68 | sensorEuiHighBytes = realDataValue 69 | break 70 | case 7: 71 | // battery power && interval 72 | decoded.messages.push({ 73 | type: 'upload_battery', battery: realDataValue.power 74 | }, { 75 | type: 'upload_interval', interval: parseInt(realDataValue.interval) * 60 76 | }) 77 | break 78 | case 9: 79 | decoded.messages.push({ 80 | type: 'model_info', 81 | detectionType: realDataValue.detectionType, 82 | modelId: realDataValue.modelId, 83 | modelVer: realDataValue.modelVer 84 | }) 85 | break 86 | case 0x120: 87 | // remove sensor 88 | decoded.messages.push({ 89 | type: 'report_remove_sensor', channel: 1 90 | }) 91 | break 92 | default: 93 | break 94 | } 95 | } else { 96 | decoded.messages.push({ 97 | type: 'unknown_message', dataID: dataID, dataValue: dataValue 98 | }) 99 | } 100 | 101 | } 102 | 103 | // if the complete id received, as "upload_sensor_id" 104 | if (sensorEuiHighBytes && sensorEuiLowBytes) { 105 | decoded.messages.unshift({ 106 | type: 'upload_sensor_id', channel: 1, sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase() 107 | }) 108 | } 109 | 110 | // return 111 | //return decoded; 112 | var datacakeFields = [] 113 | var messages = decoded.messages 114 | for (var i = 0; i < messages.length; i++) { 115 | var message = messages[i] 116 | var id = message.measurementId 117 | var type = message.type 118 | var value = message.measurementValue 119 | switch (type) { 120 | case 'upload_battery': 121 | datacakeFields.push({ 122 | 'field': 'BATTERY', 'value': message.battery 123 | }) 124 | break 125 | case 'upload_interval': 126 | datacakeFields.push({ 127 | 'field': 'UPLOAD_INTERVAL', 'value': message.interval 128 | }) 129 | break 130 | case 'upload_version': 131 | datacakeFields.push({ 132 | 'field': 'HARDWARE_VERSION', 'value': message.hardwareVersion 133 | }, { 134 | 'field': 'SOFTWARE_VERSION', 'value': message.softwareVersion 135 | }) 136 | break 137 | case 'model_info': 138 | datacakeFields.push({ 139 | 'field': 'MODEL_INFO', 140 | 'value': { detectionType: message.detectionType, modelId: message.modelId, modelVer: message.modelVer } 141 | }) 142 | break 143 | } 144 | switch (id) { 145 | case 4097: 146 | datacakeFields.push({ 147 | 'field': 'TEMPERATURE', 'value': value 148 | }) 149 | break 150 | case 4098: 151 | datacakeFields.push({ 152 | 'field': 'HUMIDITY', 'value': value 153 | }) 154 | break 155 | case 4099: 156 | datacakeFields.push({ 157 | 'field': 'LIGHT_INTENSITY', 'value': value 158 | }) 159 | break 160 | case 4100: 161 | datacakeFields.push({ 162 | 'field': 'CO2', 'value': value 163 | }) 164 | break 165 | case 4101: 166 | datacakeFields.push({ 167 | 'field': 'BAROMETRIC_PRESSURE', 'value': value 168 | }) 169 | break 170 | case 4102: 171 | datacakeFields.push({ 172 | 'field': 'SOIL_TEMPERATURE', 'value': value 173 | }) 174 | break 175 | case 4103: 176 | datacakeFields.push({ 177 | 'field': 'SOIL_MOISTURE', 'value': value 178 | }) 179 | break 180 | case 4104: 181 | datacakeFields.push({ 182 | 'field': 'WIND_DIRECTION', 'value': value 183 | }) 184 | break 185 | case 4105: 186 | datacakeFields.push({ 187 | 'field': 'WIND_SPEED', 'value': value 188 | }) 189 | break 190 | case 4106: 191 | datacakeFields.push({ 192 | 'field': 'PH', 'value': value 193 | }) 194 | break 195 | case 4107: 196 | datacakeFields.push({ 197 | 'field': 'LIGHT_QUANTUM', 'value': value 198 | }) 199 | break 200 | case 4108: 201 | datacakeFields.push({ 202 | 'field': 'ELECTRICAL_CONDUCTIVITY', 'value': value 203 | }) 204 | break 205 | case 4109: 206 | datacakeFields.push({ 207 | 'field': 'DISSOLVED_OXYGEN', 'value': value 208 | }) 209 | break 210 | case 4110: 211 | datacakeFields.push({ 212 | 'field': 'SOIL_VOLUMETRIC_WATER_CONTENT', 'value': value 213 | }) 214 | break 215 | case 4111: 216 | datacakeFields.push({ 217 | 'field': 'SOIL_ELECTRICAL_CONDUCTIVITY', 'value': value 218 | }) 219 | break 220 | case 4112: 221 | datacakeFields.push({ 222 | 'field': 'SOIL_TEMPERATURE(SOIL_TEMPERATURE, VWC & EC Sensor)', 'value': value 223 | }) 224 | break 225 | case 4113: 226 | datacakeFields.push({ 227 | 'field': 'RAINFALL_HOURLY', 'value': value 228 | }) 229 | break 230 | case 4115: 231 | datacakeFields.push({ 232 | 'field': 'DISTANCE', 'value': value 233 | }) 234 | break 235 | case 4116: 236 | datacakeFields.push({ 237 | 'field': 'WATER_LEAK', 'value': value 238 | }) 239 | break 240 | case 4117: 241 | datacakeFields.push({ 242 | 'field': 'LIGUID_LEVEL', 'value': value 243 | }) 244 | break 245 | case 4118: 246 | datacakeFields.push({ 247 | 'field': 'NH3', 'value': value 248 | }) 249 | break 250 | case 4119: 251 | datacakeFields.push({ 252 | 'field': 'H2S', 'value': value 253 | }) 254 | break 255 | case 4120: 256 | datacakeFields.push({ 257 | 'field': 'FLOW_RATE', 'value': value 258 | }) 259 | break 260 | case 4121: 261 | datacakeFields.push({ 262 | 'field': 'TOTAL_FLOW', 'value': value 263 | }) 264 | break 265 | case 4122: 266 | datacakeFields.push({ 267 | 'field': 'OXYGEN_CONCENTRATION', 'value': value 268 | }) 269 | break 270 | case 4123: 271 | datacakeFields.push({ 272 | 'field': 'WATER_ELETRICAL_CONDUCTIVITY', 'value': value 273 | }) 274 | break 275 | case 4124: 276 | datacakeFields.push({ 277 | 'field': 'WATER_TEMPERATURE', 'value': value 278 | }) 279 | break 280 | case 4125: 281 | datacakeFields.push({ 282 | 'field': 'SOIL_HEAT_FLUX', 'value': value 283 | }) 284 | break 285 | case 4126: 286 | datacakeFields.push({ 287 | 'field': 'SUNSHINE_DURATION', 'value': value 288 | }) 289 | break 290 | case 4127: 291 | datacakeFields.push({ 292 | 'field': 'TOTAL_SOLAR_RADIATION', 'value': value 293 | }) 294 | break 295 | case 4128: 296 | datacakeFields.push({ 297 | 'field': 'WATER_SURFACE_EVAPORATION', 'value': value 298 | }) 299 | break 300 | case 4129: 301 | datacakeFields.push({ 302 | 'field': 'PHOTOSYNTHETICALLY_ACTIVE_RADIATION_PAR', 'value': value 303 | }) 304 | break 305 | case 4130: 306 | datacakeFields.push({ 307 | 'field': 'ACCELEROMETER', 'value': value 308 | }) 309 | break 310 | case 4131: 311 | datacakeFields.push({ 312 | 'field': 'VOLUME', 'value': value 313 | }) 314 | break 315 | case 4133: 316 | datacakeFields.push({ 317 | 'field': 'SOIL_TENSION', 'value': value 318 | }) 319 | break 320 | case 4134: 321 | datacakeFields.push({ 322 | 'field': 'SALINITY', 'value': value 323 | }) 324 | break 325 | case 4135: 326 | datacakeFields.push({ 327 | 'field': 'TDS', 'value': value 328 | }) 329 | break 330 | case 4136: 331 | datacakeFields.push({ 332 | 'field': 'LEAF_TEMPERATURE', 'value': value 333 | }) 334 | break 335 | case 4137: 336 | datacakeFields.push({ 337 | 'field': 'LEAF_WETNESS', 'value': value 338 | }) 339 | break 340 | case 4138: 341 | datacakeFields.push({ 342 | 'field': 'SOIL_MOISTURE_10CM', 'value': value 343 | }) 344 | break 345 | case 4139: 346 | datacakeFields.push({ 347 | 'field': 'SOIL_MOISTURE_20CM', 'value': value 348 | }) 349 | break 350 | case 4140: 351 | datacakeFields.push({ 352 | 'field': 'SOIL_MOISTURE_30CM', 'value': value 353 | }) 354 | break 355 | case 4141: 356 | datacakeFields.push({ 357 | 'field': 'SOIL_MOISTURE_40CM', 'value': value 358 | }) 359 | break 360 | case 4142: 361 | datacakeFields.push({ 362 | 'field': 'SOIL_TEMPERATURE_10CM', 'value': value 363 | }) 364 | break 365 | case 4143: 366 | datacakeFields.push({ 367 | 'field': 'SOIL_TEMPERATURE_20CM', 'value': value 368 | }) 369 | break 370 | case 4144: 371 | datacakeFields.push({ 372 | 'field': 'SOIL_TEMPERATURE_30CM', 'value': value 373 | }) 374 | break 375 | case 4145: 376 | datacakeFields.push({ 377 | 'field': 'SOIL_TEMPERATURE_40CM', 'value': value 378 | }) 379 | break 380 | case 4146: 381 | datacakeFields.push({ 382 | 'field': 'PM2_5', 'value': value 383 | }) 384 | break 385 | case 4147: 386 | datacakeFields.push({ 387 | 'field': 'PM10', 'value': value 388 | }) 389 | break 390 | case 4148: 391 | datacakeFields.push({ 392 | 'field': 'NOISE', 'value': value 393 | }) 394 | break 395 | case 4150: 396 | datacakeFields.push({ 397 | 'field': 'ACCELEROMETERX', 'value': value 398 | }) 399 | break 400 | case 4151: 401 | datacakeFields.push({ 402 | 'field': 'ACCELEROMETERY', 'value': value 403 | }) 404 | break 405 | case 4152: 406 | datacakeFields.push({ 407 | 'field': 'ACCELEROMETERZ', 'value': value 408 | }) 409 | break 410 | case 4175: 411 | datacakeFields.push({ 412 | 'field': 'AI_DETECTION_NO_01', 'value': value 413 | }) 414 | break 415 | case 4176: 416 | datacakeFields.push({ 417 | 'field': 'AI_DETECTION_NO_02', 'value': value 418 | }) 419 | break 420 | case 4177: 421 | datacakeFields.push({ 422 | 'field': 'AI_DETECTION_NO_03', 'value': value 423 | }) 424 | break 425 | case 4178: 426 | datacakeFields.push({ 427 | 'field': 'AI_DETECTION_NO_04', 'value': value 428 | }) 429 | break 430 | case 4179: 431 | datacakeFields.push({ 432 | 'field': 'AI_DETECTION_NO_05', 'value': value 433 | }) 434 | break 435 | case 4180: 436 | datacakeFields.push({ 437 | 'field': 'AI_DETECTION_NO_06', 'value': value 438 | }) 439 | break 440 | case 4181: 441 | datacakeFields.push({ 442 | 'field': 'AI_DETECTION_NO_07', 'value': value 443 | }) 444 | break 445 | case 4182: 446 | datacakeFields.push({ 447 | 'field': 'AI_DETECTION_NO_08', 'value': value 448 | }) 449 | break 450 | case 4183: 451 | datacakeFields.push({ 452 | 'field': 'AI_DETECTION_NO_09', 'value': value 453 | }) 454 | break 455 | case 5100: 456 | datacakeFields.push({ 457 | 'field': 'SWITCH', 'value': value 458 | }) 459 | break 460 | case 9990100: 461 | datacakeFields.push({ 462 | 'field': 'BATTERY', 'value': message.battery 463 | }) 464 | break 465 | case 9990200: 466 | datacakeFields.push({ 467 | 'field': 'INTERVAL', 'value': message.interval 468 | }) 469 | break 470 | case 9990300: 471 | datacakeFields.push({ 472 | 'field': 'REMOVED', 'value': message.interval 473 | }) 474 | break 475 | } 476 | } 477 | return datacakeFields 478 | } 479 | 480 | function crc16Check (data) { 481 | var crc16tab = [0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78] 482 | var result = false 483 | var crc = 0 484 | var dataArray = [] 485 | for (var i = 0; i < data.length; i += 2) { 486 | dataArray.push(data.substring(i, i + 2)) 487 | } 488 | 489 | for (var j = 0; j < dataArray.length; j++) { 490 | var item = dataArray[j] 491 | crc = (crc >> 8) ^ crc16tab[(crc ^ parseInt(item, 16)) & 0xFF] 492 | } 493 | if (crc === 0) { 494 | result = true 495 | } 496 | return result 497 | } 498 | 499 | // util 500 | function bytes2HexString (arrBytes) { 501 | var str = '' 502 | for (var i = 0; i < arrBytes.length; i++) { 503 | var tmp 504 | var num = arrBytes[i] 505 | if (num < 0) { 506 | tmp = (255 + num + 1).toString(16) 507 | } else { 508 | tmp = num.toString(16) 509 | } 510 | if (tmp.length === 1) { 511 | tmp = '0' + tmp 512 | } 513 | str += tmp 514 | } 515 | return str 516 | } 517 | 518 | // util 519 | function divideBy7Bytes (str) { 520 | var frameArray = [] 521 | for (var i = 0; i < str.length - 4; i += 14) { 522 | var data = str.substring(i, i + 14) 523 | frameArray.push(data) 524 | } 525 | return frameArray 526 | } 527 | 528 | // util 529 | function littleEndianTransform (data) { 530 | var dataArray = [] 531 | for (var i = 0; i < data.length; i += 2) { 532 | dataArray.push(data.substring(i, i + 2)) 533 | } 534 | dataArray.reverse() 535 | return dataArray 536 | } 537 | 538 | // util 539 | function strTo10SysNub (str) { 540 | var arr = littleEndianTransform(str) 541 | return parseInt(arr.toString() 542 | .replace(/,/g, ''), 16) 543 | } 544 | 545 | // util 546 | function checkDataIdIsMeasureUpload (dataId) { 547 | return parseInt(dataId) > 4096 548 | } 549 | 550 | // configurable. 551 | function isSpecialDataId (dataID) { 552 | switch (dataID) { 553 | case 0: 554 | case 1: 555 | case 2: 556 | case 3: 557 | case 4: 558 | case 7: 559 | case 9: 560 | case 0x120: 561 | return true 562 | default: 563 | return false 564 | } 565 | } 566 | 567 | // configurable 568 | function ttnDataSpecialFormat (dataId, str) { 569 | var strReverse = littleEndianTransform(str) 570 | if (dataId === 2 || dataId === 3) { 571 | return strReverse.join('') 572 | } 573 | 574 | // handle unsigned number 575 | var str2 = toBinary(strReverse) 576 | 577 | var dataArray = [] 578 | switch (dataId) { 579 | case 0: // DATA_BOARD_VERSION 580 | case 1: // DATA_SENSOR_VERSION 581 | // Using point segmentation 582 | for (var k = 0; k < str2.length; k += 16) { 583 | var tmp146 = str2.substring(k, k + 16) 584 | tmp146 = (parseInt(tmp146.substring(0, 8), 2) || 0) + '.' + (parseInt(tmp146.substring(8, 16), 2) || 0) 585 | dataArray.push(tmp146) 586 | } 587 | return dataArray.join(',') 588 | case 4: 589 | for (var i = 0; i < str2.length; i += 8) { 590 | var item = parseInt(str2.substring(i, i + 8), 2) 591 | if (item < 10) { 592 | item = '0' + item.toString() 593 | } else { 594 | item = item.toString() 595 | } 596 | dataArray.push(item) 597 | } 598 | return dataArray.join('') 599 | case 7: 600 | // battery && interval 601 | return { 602 | interval: parseInt(str2.substr(0, 16), 2), power: parseInt(str2.substr(-16, 16), 2) 603 | } 604 | case 9: 605 | let dataValue = { 606 | detectionType: parseInt(str2.substring(0, 8), 2), 607 | modelId: parseInt(str2.substring(8, 16), 2), 608 | modelVer: parseInt(str2.substring(16, 24), 2) 609 | } 610 | // 01010000 611 | return dataValue 612 | 613 | } 614 | } 615 | 616 | // util 617 | function ttnDataFormat (str) { 618 | var strReverse = littleEndianTransform(str) 619 | var str2 = toBinary(strReverse) 620 | if (str2.substring(0, 1) === '1') { 621 | var arr = str2.split('') 622 | var reverseArr = [] 623 | for (var forArr = 0; forArr < arr.length; forArr++) { 624 | var item = arr[forArr] 625 | if (parseInt(item) === 1) { 626 | reverseArr.push(0) 627 | } else { 628 | reverseArr.push(1) 629 | } 630 | } 631 | str2 = parseInt(reverseArr.join(''), 2) + 1 632 | return parseFloat('-' + str2 / 1000) 633 | } 634 | return parseInt(str2, 2) / 1000 635 | } 636 | 637 | // util 638 | function sensorAttrForVersion (dataValue) { 639 | var dataValueSplitArray = dataValue.split(',') 640 | return { 641 | ver_hardware: dataValueSplitArray[0], ver_software: dataValueSplitArray[1] 642 | } 643 | } 644 | 645 | // util 646 | function toBinary (arr) { 647 | var binaryData = [] 648 | for (var forArr = 0; forArr < arr.length; forArr++) { 649 | var item = arr[forArr] 650 | var data = parseInt(item, 16) 651 | .toString(2) 652 | var dataLength = data.length 653 | if (data.length !== 8) { 654 | for (var i = 0; i < 8 - dataLength; i++) { 655 | data = '0' + data 656 | } 657 | } 658 | binaryData.push(data) 659 | } 660 | return binaryData.toString() 661 | .replace(/,/g, '') 662 | } 663 | -------------------------------------------------------------------------------- /decoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SenseCAP & TTN Converter 3 | * 4 | * @since 1.0 5 | * @return Object 6 | * @param Boolean valid Indicates whether the payload is a valid payload. 7 | * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. 8 | * @param String payload Hexadecimal string, to show the payload. 9 | * @param Array messages One or more messages are parsed according to payload. 10 | * type // Enum: 11 | * // - "report_telemetry" 12 | * // - "upload_battery" 13 | * // - "upload_interval" 14 | * // - "upload_version" 15 | * // - "upload_sensor_id" 16 | * // - "report_remove_sensor" 17 | * // - "unknown_message" 18 | * 19 | * 20 | * 21 | * 22 | * @sample-1 23 | * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); 24 | * { 25 | * valid: true, 26 | * err: 0, 27 | * payload: '0000000101000100070064003C00012001000000002890', 28 | * messages: [ 29 | * { type: 'upload_version', 30 | * hardwareVersion: '1.0', 31 | * softwareVersion: '1.1' }, 32 | * { type: 'upload_battery', battery: 100 }, 33 | * { type: 'upload_interval', interval: 3600 }, 34 | * { type: 'report_remove_sensor', channel: 1 } 35 | * ] 36 | * } 37 | * @sample-2 38 | * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); 39 | * { 40 | * valid: true, 41 | * err: 0, 42 | * payload: '01011098530000010210A87A0000AF51', 43 | * messages: [ 44 | * { type: 'report_telemetry', 45 | * measurementId: 4097, 46 | * measurementValue: 21.4 }, 47 | * { type: 'report_telemetry', 48 | * measurementId: 4098, 49 | * measurementValue: 31.4 } 50 | * ] 51 | * } 52 | * @sample-3 53 | * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); 54 | * { 55 | * valid: true, 56 | * err: 0, 57 | * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', 58 | * messages: [ 59 | * { type: 'upload_sensor_id', sensorId: '30F1F72C6A010015', channel: 1 } 60 | * ] 61 | * } 62 | */ 63 | 64 | 65 | /** 66 | * Entry, decoder.js 67 | */ 68 | function Decoder (bytes, port) { 69 | // init 70 | var bytesString = bytes2HexString(bytes) 71 | .toLocaleUpperCase(); 72 | var decoded = { 73 | // valid 74 | valid: true, 75 | err: 0, 76 | // bytes 77 | payload: bytesString, 78 | // messages array 79 | messages: [] 80 | }; 81 | 82 | // CRC check 83 | if (!crc16Check(bytesString)) { 84 | decoded["valid"] = false; 85 | decoded["err"] = -1; // "crc check fail." 86 | return decoded; 87 | } 88 | 89 | // Length Check 90 | if ((((bytesString.length / 2) - 2) % 7) !== 0) { 91 | decoded["valid"] = false; 92 | decoded["err"] = -2; // "length check fail." 93 | return decoded; 94 | } 95 | 96 | // Cache sensor id 97 | var sensorEuiLowBytes; 98 | var sensorEuiHighBytes; 99 | 100 | // Handle each frame 101 | var frameArray = divideBy7Bytes(bytesString); 102 | for (var forFrame = 0; forFrame < frameArray.length; forFrame++) { 103 | var frame = frameArray[forFrame]; 104 | // Extract key parameters 105 | var channel = strTo10SysNub(frame.substring(0, 2)); 106 | var dataID = strTo10SysNub(frame.substring(2, 6)); 107 | var dataValue = frame.substring(6, 14); 108 | var realDataValue = isSpecialDataId(dataID) ? ttnDataSpecialFormat(dataID, dataValue) : ttnDataFormat(dataValue); 109 | 110 | if (checkDataIdIsMeasureUpload(dataID)) { 111 | // if telemetry. 112 | decoded.messages.push({ 113 | type: "report_telemetry", 114 | measurementId: dataID, 115 | measurementValue: realDataValue 116 | }); 117 | } else if (isSpecialDataId(dataID) || (dataID === 5) || (dataID === 6)) { 118 | // if special order, except "report_sensor_id". 119 | switch (dataID) { 120 | case 0x00: 121 | // node version 122 | var versionData = sensorAttrForVersion(realDataValue); 123 | decoded.messages.push({ 124 | type: "upload_version", 125 | hardwareVersion: versionData.ver_hardware, 126 | softwareVersion: versionData.ver_software 127 | }); 128 | break; 129 | case 1: 130 | // sensor version 131 | break; 132 | case 2: 133 | // sensor eui, low bytes 134 | sensorEuiLowBytes = realDataValue; 135 | break; 136 | case 3: 137 | // sensor eui, high bytes 138 | sensorEuiHighBytes = realDataValue; 139 | break; 140 | case 7: 141 | // battery power && interval 142 | decoded.messages.push({ 143 | type: "upload_battery", 144 | battery: realDataValue.power 145 | }, { 146 | type: "upload_interval", 147 | interval: parseInt(realDataValue.interval) * 60 148 | }); 149 | break; 150 | case 0x120: 151 | // remove sensor 152 | decoded.messages.push({ 153 | type: "report_remove_sensor", 154 | channel: 1 155 | }); 156 | break; 157 | default: 158 | break; 159 | } 160 | } else { 161 | decoded.messages.push({ 162 | type: "unknown_message", 163 | dataID: dataID, 164 | dataValue: dataValue 165 | }); 166 | } 167 | 168 | } 169 | 170 | // if the complete id received, as "upload_sensor_id" 171 | if (sensorEuiHighBytes && sensorEuiLowBytes) { 172 | decoded.messages.unshift({ 173 | type: "upload_sensor_id", 174 | channel: 1, 175 | sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase() 176 | }); 177 | } 178 | 179 | // return 180 | return decoded; 181 | } 182 | 183 | function crc16Check (data) { 184 | var crc16tab = [ 185 | 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 186 | 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 187 | 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 188 | 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 189 | 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 190 | 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 191 | 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 192 | 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 193 | 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 194 | 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 195 | 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 196 | 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 197 | 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 198 | 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 199 | 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 200 | 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 201 | 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 202 | 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 203 | 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 204 | 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 205 | 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 206 | 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 207 | 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 208 | 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 209 | 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 210 | 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 211 | 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 212 | 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 213 | 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 214 | 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 215 | 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 216 | 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 217 | ]; 218 | var result = false; 219 | var crc = 0; 220 | var dataArray = []; 221 | for (var i = 0; i < data.length; i += 2) { 222 | dataArray.push(data.substring(i, i + 2)); 223 | } 224 | 225 | for (var j = 0; j < dataArray.length; j++) { 226 | var item = dataArray[j]; 227 | crc = (crc >> 8) ^ crc16tab[(crc ^ parseInt(item, 16)) & 0xFF]; 228 | } 229 | if (crc === 0) { 230 | result = true; 231 | } 232 | return result; 233 | } 234 | 235 | // util 236 | function bytes2HexString (arrBytes) { 237 | var str = ""; 238 | for (var i = 0; i < arrBytes.length; i++) { 239 | var tmp; 240 | var num = arrBytes[i]; 241 | if (num < 0) { 242 | tmp = (255 + num + 1).toString(16); 243 | } else { 244 | tmp = num.toString(16); 245 | } 246 | if (tmp.length === 1) { 247 | tmp = "0" + tmp; 248 | } 249 | str += tmp; 250 | } 251 | return str; 252 | } 253 | 254 | // util 255 | function divideBy7Bytes (str) { 256 | var frameArray = []; 257 | for (var i = 0; i < str.length - 4; i += 14) { 258 | var data = str.substring(i, i + 14); 259 | frameArray.push(data); 260 | } 261 | return frameArray; 262 | } 263 | 264 | // util 265 | function littleEndianTransform (data) { 266 | var dataArray = []; 267 | for (var i = 0; i < data.length; i += 2) { 268 | dataArray.push(data.substring(i, i + 2)); 269 | } 270 | dataArray.reverse(); 271 | return dataArray; 272 | } 273 | 274 | // util 275 | function strTo10SysNub (str) { 276 | var arr = littleEndianTransform(str); 277 | return parseInt(arr.toString() 278 | .replace(/,/g, ""), 16); 279 | } 280 | 281 | // util 282 | function checkDataIdIsMeasureUpload (dataId) { 283 | return parseInt(dataId) > 4096; 284 | } 285 | 286 | // configurable. 287 | function isSpecialDataId (dataID) { 288 | switch (dataID) { 289 | case 0: 290 | case 1: 291 | case 2: 292 | case 3: 293 | case 4: 294 | case 7: 295 | case 0x120: 296 | return true; 297 | default: 298 | return false; 299 | } 300 | } 301 | 302 | // configurable 303 | function ttnDataSpecialFormat (dataId, str) { 304 | var strReverse = littleEndianTransform(str); 305 | if (dataId === 2 || dataId === 3) { 306 | return strReverse.join(""); 307 | } 308 | 309 | // handle unsigned number 310 | var str2 = toBinary(strReverse); 311 | 312 | var dataArray = []; 313 | switch (dataId) { 314 | case 0: // DATA_BOARD_VERSION 315 | case 1: // DATA_SENSOR_VERSION 316 | // Using point segmentation 317 | for (var k = 0; k < str2.length; k += 16) { 318 | var tmp146 = str2.substring(k, k + 16); 319 | tmp146 = (parseInt(tmp146.substring(0, 8), 2) || 0) + "." + (parseInt(tmp146.substring(8, 16), 2) || 0); 320 | dataArray.push(tmp146); 321 | } 322 | return dataArray.join(","); 323 | case 4: 324 | for (var i = 0; i < str2.length; i += 8) { 325 | var item = parseInt(str2.substring(i, i + 8), 2); 326 | if (item < 10) { 327 | item = "0" + item.toString(); 328 | } else { 329 | item = item.toString(); 330 | } 331 | dataArray.push(item); 332 | } 333 | return dataArray.join(""); 334 | case 7: 335 | // battery && interval 336 | return { 337 | interval: parseInt(str2.substr(0, 16), 2), 338 | power: parseInt(str2.substr(-16, 16), 2) 339 | }; 340 | } 341 | } 342 | 343 | // util 344 | function ttnDataFormat (str) { 345 | var strReverse = littleEndianTransform(str); 346 | var str2 = toBinary(strReverse); 347 | if (str2.substring(0, 1) === "1") { 348 | var arr = str2.split(""); 349 | var reverseArr = []; 350 | for (var forArr = 0; forArr < arr.length; forArr++) { 351 | var item = arr[forArr]; 352 | if (parseInt(item) === 1) { 353 | reverseArr.push(0); 354 | } else { 355 | reverseArr.push(1); 356 | } 357 | } 358 | str2 = parseInt(reverseArr.join(""), 2) + 1; 359 | return parseFloat("-" + str2 / 1000); 360 | } 361 | return parseInt(str2, 2) / 1000; 362 | } 363 | 364 | // util 365 | function sensorAttrForVersion (dataValue) { 366 | var dataValueSplitArray = dataValue.split(","); 367 | return { 368 | ver_hardware: dataValueSplitArray[0], 369 | ver_software: dataValueSplitArray[1] 370 | }; 371 | } 372 | 373 | // util 374 | function toBinary (arr) { 375 | var binaryData = []; 376 | for (var forArr = 0; forArr < arr.length; forArr++) { 377 | var item = arr[forArr]; 378 | var data = parseInt(item, 16) 379 | .toString(2); 380 | var dataLength = data.length; 381 | if (data.length !== 8) { 382 | for (var i = 0; i < 8 - dataLength; i++) { 383 | data = "0" + data; 384 | } 385 | } 386 | binaryData.push(data); 387 | } 388 | return binaryData.toString() 389 | .replace(/,/g, ""); 390 | } 391 | 392 | // Samples 393 | // var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); 394 | // var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); 395 | // var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); 396 | // console.log(sample); 397 | -------------------------------------------------------------------------------- /decoder_for_aws_iot_core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SenseCAP & TTN (new v3) Converter 3 | * 4 | * @since 3.0 5 | * @return Object 6 | * @param Boolean valid Indicates whether the payload is a valid payload. 7 | * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. 8 | * @param String payload Hexadecimal string, to show the payload. 9 | * @param Array messages One or more messages are parsed according to payload. 10 | * type // Enum: 11 | * // - "report_telemetry" 12 | * // - "upload_battery" 13 | * // - "upload_interval" 14 | * // - "upload_version" 15 | * // - "upload_sensor_id" 16 | * // - "report_remove_sensor" 17 | * // - "unknown_message" 18 | * 19 | * 20 | * 21 | * 22 | * @sample-1 23 | * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); 24 | * { 25 | * valid: true, 26 | * err: 0, 27 | * payload: '0000000101000100070064003C00012001000000002890', 28 | * messages: [ 29 | * { type: 'upload_version', 30 | * hardwareVersion: '1.0', 31 | * softwareVersion: '1.1' }, 32 | * { type: 'upload_battery', battery: 100 }, 33 | * { type: 'upload_interval', interval: 3600 }, 34 | * { type: 'report_remove_sensor', channel: 1 } 35 | * ] 36 | * } 37 | * @sample-2 38 | * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); 39 | * { 40 | * valid: true, 41 | * err: 0, 42 | * payload: '01011098530000010210A87A0000AF51', 43 | * messages: [ 44 | * { type: 'report_telemetry', 45 | * measurementId: 4097, 46 | * measurementValue: 21.4 }, 47 | * { type: 'report_telemetry', 48 | * measurementId: 4098, 49 | * measurementValue: 31.4 } 50 | * ] 51 | * } 52 | * @sample-3 53 | * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); 54 | * { 55 | * valid: true, 56 | * err: 0, 57 | * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', 58 | * messages: [ 59 | * { type: 'upload_sensor_id', sensorId: '30F1F72C6A010015', channel: 1 } 60 | * ] 61 | * } 62 | */ 63 | 64 | /** 65 | * Entry, decoder.js 66 | */ 67 | function decodeUplink (PayloadData) { 68 | let bytes = new Buffer(PayloadData, 'base64').toString('hex') 69 | console.log(bytes) 70 | // init 71 | var bytesString = bytes 72 | var decoded = { 73 | // valid 74 | valid: true, err: 0, // bytes 75 | payload: bytesString, // messages array 76 | messages: [] 77 | } 78 | 79 | // CRC check 80 | if (!crc16Check(bytesString)) { 81 | decoded['valid'] = false 82 | decoded['err'] = -1 // "crc check fail." 83 | return { data: decoded } 84 | } 85 | 86 | // Length Check 87 | if ((((bytesString.length / 2) - 2) % 7) !== 0) { 88 | decoded['valid'] = false 89 | decoded['err'] = -2 // "length check fail." 90 | return { data: decoded } 91 | } 92 | 93 | // Cache sensor id 94 | var sensorEuiLowBytes 95 | var sensorEuiHighBytes 96 | 97 | // Handle each frame 98 | var frameArray = divideBy7Bytes(bytesString) 99 | for (var forFrame = 0; forFrame < frameArray.length; forFrame++) { 100 | var frame = frameArray[forFrame] 101 | // Extract key parameters 102 | var channel = strTo10SysNub(frame.substring(0, 2)) 103 | var dataID = strTo10SysNub(frame.substring(2, 6)) 104 | var dataValue = frame.substring(6, 14) 105 | var realDataValue = isSpecialDataId(dataID) ? ttnDataSpecialFormat(dataID, dataValue) : ttnDataFormat(dataValue) 106 | 107 | if (checkDataIdIsMeasureUpload(dataID)) { 108 | // if telemetry. 109 | decoded.messages.push({ 110 | type: 'report_telemetry', measurementId: dataID, measurementValue: realDataValue 111 | }) 112 | } else if (isSpecialDataId(dataID) || (dataID === 5) || (dataID === 6)) { 113 | // if special order, except "report_sensor_id". 114 | switch (dataID) { 115 | case 0x00: 116 | // node version 117 | var versionData = sensorAttrForVersion(realDataValue) 118 | decoded.messages.push({ 119 | type: 'upload_version', hardwareVersion: versionData.ver_hardware, softwareVersion: versionData.ver_software 120 | }) 121 | break 122 | case 1: 123 | // sensor version 124 | break 125 | case 2: 126 | // sensor eui, low bytes 127 | sensorEuiLowBytes = realDataValue 128 | break 129 | case 3: 130 | // sensor eui, high bytes 131 | sensorEuiHighBytes = realDataValue 132 | break 133 | case 7: 134 | // battery power && interval 135 | decoded.messages.push({ 136 | type: 'upload_battery', battery: realDataValue.power 137 | }, { 138 | type: 'upload_interval', interval: parseInt(realDataValue.interval) * 60 139 | }) 140 | break 141 | case 0x120: 142 | // remove sensor 143 | decoded.messages.push({ 144 | type: 'report_remove_sensor', channel: 1 145 | }) 146 | break 147 | default: 148 | break 149 | } 150 | } else { 151 | decoded.messages.push({ 152 | type: 'unknown_message', dataID: dataID, dataValue: dataValue 153 | }) 154 | } 155 | 156 | } 157 | 158 | // if the complete id received, as "upload_sensor_id" 159 | if (sensorEuiHighBytes && sensorEuiLowBytes) { 160 | decoded.messages.unshift({ 161 | type: 'upload_sensor_id', channel: 1, sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase() 162 | }) 163 | } 164 | 165 | // return 166 | return { data: decoded } 167 | } 168 | 169 | function crc16Check (data) { 170 | return true 171 | } 172 | 173 | // util 174 | function bytes2HexString (arrBytes) { 175 | var str = '' 176 | for (var i = 0; i < arrBytes.length; i++) { 177 | var tmp 178 | var num = arrBytes[i] 179 | if (num < 0) { 180 | tmp = (255 + num + 1).toString(16) 181 | } else { 182 | tmp = num.toString(16) 183 | } 184 | if (tmp.length === 1) { 185 | tmp = '0' + tmp 186 | } 187 | str += tmp 188 | } 189 | return str 190 | } 191 | 192 | // util 193 | function divideBy7Bytes (str) { 194 | var frameArray = [] 195 | for (var i = 0; i < str.length - 4; i += 14) { 196 | var data = str.substring(i, i + 14) 197 | frameArray.push(data) 198 | } 199 | return frameArray 200 | } 201 | 202 | // util 203 | function littleEndianTransform (data) { 204 | var dataArray = [] 205 | for (var i = 0; i < data.length; i += 2) { 206 | dataArray.push(data.substring(i, i + 2)) 207 | } 208 | dataArray.reverse() 209 | return dataArray 210 | } 211 | 212 | // util 213 | function strTo10SysNub (str) { 214 | var arr = littleEndianTransform(str) 215 | return parseInt(arr.toString() 216 | .replace(/,/g, ''), 16) 217 | } 218 | 219 | // util 220 | function checkDataIdIsMeasureUpload (dataId) { 221 | return parseInt(dataId) > 4096 222 | } 223 | 224 | // configurable. 225 | function isSpecialDataId (dataID) { 226 | switch (dataID) { 227 | case 0: 228 | case 1: 229 | case 2: 230 | case 3: 231 | case 4: 232 | case 7: 233 | case 0x120: 234 | return true 235 | default: 236 | return false 237 | } 238 | } 239 | 240 | // configurable 241 | function ttnDataSpecialFormat (dataId, str) { 242 | var strReverse = littleEndianTransform(str) 243 | if (dataId === 2 || dataId === 3) { 244 | return strReverse.join('') 245 | } 246 | 247 | // handle unsigned number 248 | var str2 = toBinary(strReverse) 249 | 250 | var dataArray = [] 251 | switch (dataId) { 252 | case 0: // DATA_BOARD_VERSION 253 | case 1: // DATA_SENSOR_VERSION 254 | // Using point segmentation 255 | for (var k = 0; k < str2.length; k += 16) { 256 | var tmp146 = str2.substring(k, k + 16) 257 | tmp146 = (parseInt(tmp146.substring(0, 8), 2) || 0) + '.' + (parseInt(tmp146.substring(8, 16), 2) || 0) 258 | dataArray.push(tmp146) 259 | } 260 | return dataArray.join(',') 261 | case 4: 262 | for (var i = 0; i < str2.length; i += 8) { 263 | var item = parseInt(str2.substring(i, i + 8), 2) 264 | if (item < 10) { 265 | item = '0' + item.toString() 266 | } else { 267 | item = item.toString() 268 | } 269 | dataArray.push(item) 270 | } 271 | return dataArray.join('') 272 | case 7: 273 | // battery && interval 274 | return { 275 | interval: parseInt(str2.substr(0, 16), 2), power: parseInt(str2.substr(-16, 16), 2) 276 | } 277 | } 278 | } 279 | 280 | // util 281 | function ttnDataFormat (str) { 282 | var strReverse = littleEndianTransform(str) 283 | var str2 = toBinary(strReverse) 284 | if (str2.substring(0, 1) === '1') { 285 | var arr = str2.split('') 286 | var reverseArr = [] 287 | for (var forArr = 0; forArr < arr.length; forArr++) { 288 | var item = arr[forArr] 289 | if (parseInt(item) === 1) { 290 | reverseArr.push(0) 291 | } else { 292 | reverseArr.push(1) 293 | } 294 | } 295 | str2 = parseInt(reverseArr.join(''), 2) + 1 296 | return parseFloat('-' + str2 / 1000) 297 | } 298 | return parseInt(str2, 2) / 1000 299 | } 300 | 301 | // util 302 | function sensorAttrForVersion (dataValue) { 303 | var dataValueSplitArray = dataValue.split(',') 304 | return { 305 | ver_hardware: dataValueSplitArray[0], ver_software: dataValueSplitArray[1] 306 | } 307 | } 308 | 309 | // util 310 | function toBinary (arr) { 311 | var binaryData = [] 312 | for (var forArr = 0; forArr < arr.length; forArr++) { 313 | var item = arr[forArr] 314 | var data = parseInt(item, 16) 315 | .toString(2) 316 | var dataLength = data.length 317 | if (data.length !== 8) { 318 | for (var i = 0; i < 8 - dataLength; i++) { 319 | data = '0' + data 320 | } 321 | } 322 | binaryData.push(data) 323 | } 324 | return binaryData.toString() 325 | .replace(/,/g, '') 326 | } 327 | 328 | // Samples 329 | // var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); 330 | // var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); 331 | // var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); 332 | // console.log(sample); 333 | -------------------------------------------------------------------------------- /decoder_new-v3-uglifyjs.js: -------------------------------------------------------------------------------- 1 | function decodeUplink(e){var r,t,e=l(e.bytes).toLocaleUpperCase(),s={valid:!0,err:0,payload:e,messages:[]};if(!g(e))return s.valid=!1,s.err=-1,{data:s};if((e.length/2-2)%7!=0)return s.valid=!1,s.err=-2,{data:s};for(var a=c(e),n=0;n 4096 230 | } 231 | 232 | // configurable. 233 | function isSpecialDataId (dataID) { 234 | switch (dataID) { 235 | case 0: 236 | case 1: 237 | case 2: 238 | case 3: 239 | case 4: 240 | case 7: 241 | case 9: 242 | case 0x120: 243 | return true 244 | default: 245 | return false 246 | } 247 | } 248 | 249 | // configurable 250 | function ttnDataSpecialFormat (dataId, str) { 251 | var strReverse = littleEndianTransform(str) 252 | if (dataId === 2 || dataId === 3) { 253 | return strReverse.join('') 254 | } 255 | 256 | // handle unsigned number 257 | var str2 = toBinary(strReverse) 258 | var dataArray = [] 259 | switch (dataId) { 260 | case 0: // DATA_BOARD_VERSION 261 | case 1: // DATA_SENSOR_VERSION 262 | // Using point segmentation 263 | for (var k = 0; k < str2.length; k += 16) { 264 | var tmp146 = str2.substring(k, k + 16) 265 | tmp146 = (parseInt(tmp146.substring(0, 8), 2) || 0) + '.' + (parseInt(tmp146.substring(8, 16), 2) || 0) 266 | dataArray.push(tmp146) 267 | } 268 | return dataArray.join(',') 269 | case 4: 270 | for (var i = 0; i < str2.length; i += 8) { 271 | var item = parseInt(str2.substring(i, i + 8), 2) 272 | if (item < 10) { 273 | item = '0' + item.toString() 274 | } else { 275 | item = item.toString() 276 | } 277 | dataArray.push(item) 278 | } 279 | return dataArray.join('') 280 | case 7: 281 | // battery && interval 282 | return { 283 | interval: parseInt(str2.substr(0, 16), 2), power: parseInt(str2.substr(-16, 16), 2) 284 | } 285 | case 9: 286 | let dataValue = { 287 | detectionType: parseInt(str2.substring(0, 8), 2), 288 | modelId: parseInt(str2.substring(8, 16), 2), 289 | modelVer: parseInt(str2.substring(16, 24), 2) 290 | } 291 | // 01010000 292 | return dataValue 293 | } 294 | } 295 | 296 | // util 297 | function ttnDataFormat (str) { 298 | var strReverse = littleEndianTransform(str) 299 | var str2 = toBinary(strReverse) 300 | if (str2.substring(0, 1) === '1') { 301 | var arr = str2.split('') 302 | var reverseArr = [] 303 | for (var forArr = 0; forArr < arr.length; forArr++) { 304 | var item = arr[forArr] 305 | if (parseInt(item) === 1) { 306 | reverseArr.push(0) 307 | } else { 308 | reverseArr.push(1) 309 | } 310 | } 311 | str2 = parseInt(reverseArr.join(''), 2) + 1 312 | return parseFloat('-' + str2 / 1000) 313 | } 314 | return parseInt(str2, 2) / 1000 315 | } 316 | 317 | // util 318 | function sensorAttrForVersion (dataValue) { 319 | var dataValueSplitArray = dataValue.split(',') 320 | return { 321 | ver_hardware: dataValueSplitArray[0], ver_software: dataValueSplitArray[1] 322 | } 323 | } 324 | 325 | // util 326 | function toBinary (arr) { 327 | var binaryData = [] 328 | for (var forArr = 0; forArr < arr.length; forArr++) { 329 | var item = arr[forArr] 330 | var data = parseInt(item, 16) 331 | .toString(2) 332 | var dataLength = data.length 333 | if (data.length !== 8) { 334 | for (var i = 0; i < 8 - dataLength; i++) { 335 | data = '0' + data 336 | } 337 | } 338 | binaryData.push(data) 339 | } 340 | return binaryData.toString() 341 | .replace(/,/g, '') 342 | } 343 | 344 | // Samples 345 | // var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); 346 | // var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); 347 | // var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); 348 | // console.log(sample); 349 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ttn-payload-decoder", 3 | "version": "1.0.0", 4 | "description": "TTN payload decoding script for SenseCAP LoRaWAN messages This script is used for decoding the LoRaWAN messages sent from the SenseCAP nodes. After decoding, users' applications will get more friendly and readable messages from TTN data API.", 5 | "main": "uglifyjs_decoder_new-v3.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Seeed-Solution/TTN-Payload-Decoder.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Seeed-Solution/TTN-Payload-Decoder/issues" 17 | }, 18 | "homepage": "https://github.com/Seeed-Solution/TTN-Payload-Decoder#readme", 19 | "dependencies": { 20 | "uglify-js": "^3.13.9" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /uglifyjs_decoder_new-v3.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let UglifyJS = require('uglify-js') 3 | 4 | var code = fs.readFileSync("decoder_new-v3.js", "utf8"); 5 | 6 | 7 | let options = { 8 | compress: { 9 | properties: {} 10 | }, 11 | mangle: { 12 | toplevel: true, 13 | reserved: ['firstLongName', 'decodeUplink'], 14 | // properties: { 15 | // debug: "", 16 | // reserved: ['bytes', 'hardwareVersion'] 17 | // } 18 | } 19 | } 20 | 21 | code = UglifyJS.minify(code, options).code; 22 | 23 | console.log('==== Length: ' + code.length) 24 | console.log(code); 25 | --------------------------------------------------------------------------------