UNPKG

advlib-ble-manufacturers

Version:

Wireless advertising packet decoding library for Bluetooth Low Energy manufacturer-specific data. We believe in an open Internet of Things.

432 lines (367 loc) 13.7 kB
/** * Copyright reelyActive 2021-2024 * We believe in an open Internet of Things */ const utils = require('./utils'); const MIN_DATA_LENGTH_BYTES = 1; const S3_STATIC_FRAME_LENGTH = 20; const S3_TEMPERATURE_HUMIDITY_FRAME_LENGTH = 16; const S4_STATIC_FRAME_LENGTH = 18; const S4_DOOR_ALARM_FRAME_LENGTH = 15; const FRAME_VERSION_OFFSET = 1; const FIRMWARE_VERSION_OFFSET = 2; const HARDWARE_VERSION_OFFSET = 5; const BATTERY_PERCENTAGE_OFFSET = 2; const DOOR_MAGNET_ALARM_OFFSET = 3; const ANTI_DISASSEMBLY_ALARM_OFFSET = 4; const ALARM_TRIGGER_OFFSET = 5; const S3_TEMPERATURE_OFFSET = 3; const S3_HUMIDITY_OFFSET = 5; const S3_STATIC_MAC_OFFSET = 12; const S3_TEMPERATURE_HUMIDITY_MAC_OFFSET = 7; const S4_STATIC_MAC_OFFSET = 10; const S4_DOOR_ALARM_MAC_OFFSET = 7; const V3_00_FRAME_LENGTH = 24; const V3_05_FRAME_LENGTH = 24; const V3_18_FRAME_LENGTH = 24; const V3_1B_FRAME_LENGTH = 24; const V3_SUBFRAME_VERSION_OFFSET = 2; const V3_00_MAC_OFFSET = 2; const V3_00_BATTERY_PERCENTAGE_OFFSET = 8; const V3_00_FIRMWARE_VERSION_OFFSET = 9; const V3_03_MIN_FRAME_LENGTH = 6; const V3_03_POST_BLOCK_LENGTH = 4; const V3_05_DEVICE_INFO_OFFSET = 3; const V3_05_TEMPERATURE_OFFSET = 5; const V3_05_HUMIDITY_OFFSET = 7; const V3_05_NAME_OFFSET = 9; const V3_05_MARK_OFFSET = 17; const V3_05_NAME_LENGTH = 8; const V3_05_DEVICE_INFO_TEMPERATURE_ONLY_MASK = 0x02; const V3_18_SERIAL_NUMBER_OFFSET = 3; const V3_18_NUMBER_OF_ENTRIES_OFFSET = 4; const V3_18_NUMBER_OF_EXITS_OFFSET = 6; const V3_18_NUMBER_OF_PEOPLE_OFFSET = 4; const V3_1B_SUBVERSION_OFFSET = 2; const V3_1B_PRODUCT_ID_OFFSET = 3; const V3_1B_STATIC_MAC_OFFSET = 6; const V3_1B_BATTERY_VOLTAGE_OFFSET = 12; const V3_1B_TEMPERATURE_OFFSET = 14; const V3_1B_TX_POWER_OFFSET = 17; const V3_1B_ENCRYPTION_MASK = 0x80; const IDENTIFIER_TYPE_EUI_48 = 2; const MINEW_URI = "https://sniffypedia.org/Organization/Shenzhen_Minew_Technologies_Co_Ltd/"; const V3_PRODUCT_URIS = new Map([ [ 0x007, "https://sniffypedia.org/Product/Minew_MTB02/" ] ]); const V3_BLOCKS = new Map([ [ 0x03, { length: 7, process: processV3Block03 } ], [ 0x21, { length: 2, process: processV3Block21 } ], [ 0x22, { length: 2, process: processV3Block22 } ] ]); /** * Process Minew manufacturer-specific data. * @param {Object} data The manufacturer data as a hexadecimal-string or Buffer. * @return {Object} The processed Minew data as JSON. */ function process(data) { let buf = utils.convertToBuffer(data); if((buf === null) || (buf.length < MIN_DATA_LENGTH_BYTES)) { return null; } let frameType = buf.readUInt8(); switch(frameType) { case 0xa3: return processS3(buf); case 0xa4: return processS4(buf); case 0xca: return processConnectV3(buf); } return null; } /** * Process Minew S3 data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processS3(data) { let frameVersion = data.readUInt8(FRAME_VERSION_OFFSET); if((frameVersion === 1) && // Static information frame (data.length === S3_STATIC_FRAME_LENGTH)) { let firmwareVersion = data.readUInt8(FIRMWARE_VERSION_OFFSET) + '.' + data.readUInt8(FIRMWARE_VERSION_OFFSET + 1) + '.' + data.readUInt8(FIRMWARE_VERSION_OFFSET + 2); let hardwareVersion = data.readUInt16BE(HARDWARE_VERSION_OFFSET); let macSignature = toMacSignature(data, S3_STATIC_MAC_OFFSET); return { deviceIds: [ macSignature ], uri: MINEW_URI, version: firmwareVersion }; } else if((frameVersion === 3) && // Temperature humidity frame (data.length === S3_TEMPERATURE_HUMIDITY_FRAME_LENGTH)) { let batteryPercentage = data.readUInt8(BATTERY_PERCENTAGE_OFFSET); let temperature = utils.parseSigned88(data.subarray(S3_TEMPERATURE_OFFSET)); let relativeHumidity = utils.parseSigned88(data.subarray(S3_HUMIDITY_OFFSET)); let macSignature = toMacSignature(data, S3_TEMPERATURE_HUMIDITY_MAC_OFFSET); return { batteryPercentage: batteryPercentage, deviceIds: [ macSignature ], relativeHumidity: relativeHumidity, temperature: temperature, uri: MINEW_URI }; } return null; } /** * Process Minew S4 data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processS4(data) { let frameVersion = data.readUInt8(FRAME_VERSION_OFFSET); if((frameVersion === 0) && // Static information frame (data.length === S4_STATIC_FRAME_LENGTH)) { let firmwareVersion = data.readUInt8(FIRMWARE_VERSION_OFFSET) + '.' + data.readUInt8(FIRMWARE_VERSION_OFFSET + 1) + '.' + data.readUInt8(FIRMWARE_VERSION_OFFSET + 2); let hardwareVersion = data.readUInt16BE(HARDWARE_VERSION_OFFSET); let macSignature = toMacSignature(data, S4_STATIC_MAC_OFFSET); return { deviceIds: [ macSignature ], uri: MINEW_URI, version: firmwareVersion }; } else if((frameVersion === 1) && // Door alarm frame (data.length === S4_DOOR_ALARM_FRAME_LENGTH)) { let batteryPercentage = data.readUInt8(BATTERY_PERCENTAGE_OFFSET); let isDoorMagnetAlarm = (data.readUInt8(DOOR_MAGNET_ALARM_OFFSET) > 0); let isAntiDisassemblyAlarm = (data.readUInt8( ANTI_DISASSEMBLY_ALARM_OFFSET) > 0); let isCurrentAlarmTrigger = (data.readUInt8(ALARM_TRIGGER_OFFSET) > 0); let macSignature = toMacSignature(data, S4_DOOR_ALARM_MAC_OFFSET); return { batteryPercentage: batteryPercentage, deviceIds: [ macSignature ], isContactDetected: [ !isDoorMagnetAlarm ], uri: MINEW_URI }; } return null; } /** * Process Minew Connect V3 data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3(data) { let frameVersion = data.readUInt8(FRAME_VERSION_OFFSET); switch(frameVersion) { case 0x00: return processConnectV3Type00(data); case 0x03: return processConnectV3Type03(data); case 0x05: return processConnectV3Type05(data); case 0x18: return processConnectV3Type18(data); case 0x1b: return processConnectV3Type1b(data); } return null; } /** * Process Minew Connect V3 type 0x00 device information frame data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3Type00(data) { if(data.length !== V3_00_FRAME_LENGTH) { return null; } let macSignature = toMacSignature(data, V3_00_MAC_OFFSET); let batteryPercentage = data.readUInt8(V3_00_BATTERY_PERCENTAGE_OFFSET); let firmwareVersionBinary = data.readUInt16BE(V3_00_FIRMWARE_VERSION_OFFSET); let firmwareVersion = ((firmwareVersionBinary >> 13) & 0x0003) + '.' + ((firmwareVersionBinary >> 7) & 0x003f) + '.' + (firmwareVersionBinary & 0x007f); return { batteryPercentage: batteryPercentage, deviceIds: [ macSignature ], uri: MINEW_URI, version: firmwareVersion }; } /** * Process Minew Connect V3 type 0x03 combination frame data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3Type03(data) { if(data.length < V3_03_MIN_FRAME_LENGTH) { return null; } let sensorData = { uri: MINEW_URI }; let currentBlockOffset = V3_SUBFRAME_VERSION_OFFSET; while(currentBlockOffset < (data.length - V3_03_POST_BLOCK_LENGTH)) { let currentBlockId = data.readUInt8(currentBlockOffset); if(V3_BLOCKS.has(currentBlockId)) { let currentBlockParameters = V3_BLOCKS.get(currentBlockId); let currentBlockLength = currentBlockParameters.length; let currentBlock = data.subarray(currentBlockOffset + 1, currentBlockOffset + currentBlockLength); let blockData = currentBlockParameters.process(currentBlock); Object.assign(sensorData, blockData); currentBlockOffset += currentBlockLength; } else { return sensorData; } } return sensorData; } /** * Process Minew Connect V3 type 0x05 temperature/humidity frame data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3Type05(data) { if(data.length !== V3_05_FRAME_LENGTH) { return null; } let subframeVersion = data.readUInt8(V3_SUBFRAME_VERSION_OFFSET); let deviceInfo = data.readUInt8(V3_05_DEVICE_INFO_OFFSET); let temperature = utils.parseSigned88(data.subarray(V3_05_TEMPERATURE_OFFSET)); let relativeHumidity = utils.parseSigned88(data.subarray(V3_05_HUMIDITY_OFFSET)); let name = data.toString('utf8', V3_05_NAME_OFFSET, V3_05_NAME_OFFSET + V3_05_NAME_LENGTH).replace(/\0/g, ''); let sensorData = { name: name, temperature: temperature, uri: MINEW_URI }; if(!(deviceInfo & V3_05_DEVICE_INFO_TEMPERATURE_ONLY_MASK)) { sensorData.relativeHumidity = relativeHumidity; } if(subframeVersion > 0) { let isMarked = (data.readUInt8(V3_05_MARK_OFFSET) === 0x01); sensorData.isMarked = [ isMarked ]; } return sensorData; } /** * Process Minew Connect V3 type 18 radar monitor frame data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3Type18(data) { if(data.length !== V3_18_FRAME_LENGTH) { return null; } let subframeVersion = data.readUInt8(V3_SUBFRAME_VERSION_OFFSET); let sensorData = { uri: MINEW_URI }; if(subframeVersion === 0x00) { // Human traffic monitoring let serialNumber = data.readUInt8(V3_18_SERIAL_NUMBER_OFFSET); let numberOfEntries = data.readUInt16LE(V3_18_NUMBER_OF_ENTRIES_OFFSET); let numberOfExits = data.readUInt16LE(V3_18_NUMBER_OF_EXITS_OFFSET); sensorData.passageCounts = [ numberOfEntries, numberOfExits ]; sensorData.passageCountsCycle = serialNumber; } else if(subframeVersion === 0x01) { // Person coordinate info let serialNumber = data.readUInt8(V3_18_SERIAL_NUMBER_OFFSET); let statusByte = data.readUInt8(V3_18_NUMBER_OF_PEOPLE_OFFSET); let packetIndex = statusByte & 0x30; sensorData.numberOfOccupants = statusByte & 0x0f; sensorData.numberOfOccupantsCycle = serialNumber; // TODO: nearest? } else { return null; } return sensorData; } /** * Process Minew Connect V3 type 0x1b temperature frame data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processConnectV3Type1b(data) { if(data.length !== V3_1B_FRAME_LENGTH) { return null; } let isEncrypted = (data.readUInt8(V3_1B_SUBVERSION_OFFSET) & V3_1B_ENCRYPTION_MASK) === V3_1B_ENCRYPTION_MASK; if(isEncrypted) { return {}; // TODO: extract data for decryption } let productId = data.readUInt16BE(V3_1B_PRODUCT_ID_OFFSET); let macSignature = toMacSignature(data, V3_1B_STATIC_MAC_OFFSET); let batteryVoltage = data.readUInt16BE(V3_1B_BATTERY_VOLTAGE_OFFSET) / 1000; let temperature = utils.parseSigned88(data.subarray(V3_1B_TEMPERATURE_OFFSET)); let txPower = data.readInt8(V3_1B_TX_POWER_OFFSET); let uri = V3_PRODUCT_URIS.has(productId) ? V3_PRODUCT_URIS.get(productId) : MINEW_URI; let sensorData = { batteryVoltage: batteryVoltage, temperature: temperature, txPower: txPower, deviceIds: [ macSignature ], uri: uri }; return sensorData; } /** * Process Minew Connect V3 block type 0x03 acceleration data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processV3Block03(data) { return { acceleration: [ utils.parseSigned88(data.subarray(0, 2)), utils.parseSigned88(data.subarray(2, 4)), utils.parseSigned88(data.subarray(4, 6)) ] }; } /** * Process Minew Connect V3 block type 0x21 battery data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processV3Block21(data) { return { batteryPercentage: data.readUInt8() }; } /** * Process Minew Connect V3 block type 0x22 tamper data. * @param {Object} data The raw manufacturer data as a Buffer. * @return {Object} The processed beacon data as JSON. */ function processV3Block22(data) { let isTriggered = (data.readUInt8() > 0); return { isContactDetected: [ !isTriggered ] }; } /** * Convert the given 48-bit data into a MAC address signature. * @param {Buffer} data The buffer containing the little-endian MAC address. * @param {Integer} index The index to the start of the MAC address. * @return {String} The MAC address signature. */ function toMacSignature(data, index) { return data.toString('hex', index + 5, index + 6) + data.toString('hex', index + 4, index + 5) + data.toString('hex', index + 3, index + 4) + data.toString('hex', index + 2, index + 3) + data.toString('hex', index + 1, index + 2) + data.toString('hex', index + 0, index + 1) + '/' + IDENTIFIER_TYPE_EUI_48; } module.exports.process = process;