UNPKG

barnowl-aruba

Version:

Collect ambient Bluetooth Low Energy, WiFi and EnOcean Alliance packets from HPE Aruba Networking access points for real-time location and sensing. We believe in an open Internet of Things.

232 lines (194 loc) 7.83 kB
/** * Copyright reelyActive 2024-2025 * We believe in an open Internet of Things */ const Raddec = require('raddec'); const BLE_ID_TYPES = [ Raddec.identifiers.TYPE_UNKNOWN, Raddec.identifiers.TYPE_EUI48, Raddec.identifiers.TYPE_RND48, Raddec.identifiers.TYPE_RND48, Raddec.identifiers.TYPE_RND48 ]; const BLE_PACKET_TYPES = [ 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]; const MIN_ESP3_PACKET_LENGTH_BYTES = 8; /** * Decode the given northbound IoT Operations data. * @param {Object} nbData The northbound IoT Operations data. * @param {String} origin Origin of the data stream. * @param {Number} time The time of the data capture. * @param {Map} aps The list of APs and their associated identifiers. * @param {Object} options The packet decoding options. */ function decode(nbData, origin, time, aps, options) { options = options || {}; let isValidMessage = nbData.hasOwnProperty('header') && Number.isInteger(nbData.header.topic) && nbData.hasOwnProperty('body'); if(!isValidMessage) { return []; } switch(nbData.header.topic) { case 1: // Topic: BLE Data return decodeBleRawData(nbData.body.bleRawData, origin, time, aps, options); case 2: // Topic: Telemetry return { raddecs: [], infrastructureMessages: [] }; // TODO case 3: // Topic: Serial USB Data return decodeSerialData(nbData.body.serialNbUsbData, origin, time, aps, options); case 4: // Topic: AP Inventory return decodeApInventory(nbData.body.inventory, origin, time, aps, options); case 5: // Topic: Keep-Alive return { raddecs: [], infrastructureMessages: [] }; case 6: // Topic: Device Location return { raddecs: [], infrastructureMessages: [] }; // TODO: position default: return { raddecs: [], infrastructureMessages: [] }; } } /** * Decode all the bleData from the telemetry report. * @param {Array} data The array of BLE data. * @param {String} origin Origin of the data stream. * @param {Number} time The time of the data capture. * @param {Map} aps The list of APs and their associated identifiers. * @param {Object} options The packet decoding options. */ function decodeBleRawData(data, origin, time, aps, options) { let raddecs = []; if(!Array.isArray(data)) { return { raddecs: [], infrastructureMessages: [] }; } data.forEach((transmitter) => { let transmitterId = transmitter.bleDeviceAddress.iotDeviceMac .toString('hex'); let bleMacType = transmitter.bleDeviceAddress.bleMacType; let packetType = BLE_PACKET_TYPES[transmitter.bleDeviceFrameType]; let payload = transmitter.bleDeviceRawData.toString('hex'); let headerAddress = reconstructHeaderAddress(transmitterId, bleMacType, packetType, payload); let packet = headerAddress + payload; let raddec = new Raddec({ transmitterId: transmitterId, transmitterIdType: BLE_ID_TYPES[bleMacType], packets: [ packet ], timestamp: time }); raddec.addDecoding({ receiverId: transmitter.apMac.toString('hex'), receiverIdType: Raddec.identifiers.TYPE_EUI48, rssi: transmitter.bleDeviceRssi }); raddecs.push(raddec); }); return { raddecs: raddecs, infrastructureMessages: [] }; } /** * Decode all the serialNbUsbData from the telemetry report. * @param {Array} data The array of serial northbound USB data. * @param {String} origin Origin of the data stream. * @param {Number} time The time of the data capture. * @param {Map} aps The list of APs and their associated identifiers. * @param {Object} options The packet decoding options. */ function decodeSerialData(data, origin, time, aps, options) { let raddecs = []; if(!Array.isArray(data)) { return { raddecs: [], infrastructureMessages: [] }; } data.forEach((transmitter) => { if(typeof transmitter.usbAddress?.hwId === 'string') { let receiverId = transmitter.usbAddress.hwId; let receiverIdType = Raddec.identifiers.TYPE_UUID128; let payloadBuffer = Buffer.from(transmitter.payload, 'base64'); let raddec = decodeESPPacket(payloadBuffer.toString('hex'), receiverId, receiverIdType, time); if(raddec) { raddecs.push(raddec); } } }); return { raddecs: raddecs, infrastructureMessages: [] }; } /** * Decode all the serialNbUsbData from the telemetry report. * @param {Object} data The inventory data. * @param {String} origin Origin of the data stream. * @param {Number} time The time of the data capture. * @param {Map} aps The list of APs and their associated identifiers. * @param {Object} options The packet decoding options. */ function decodeApInventory(data, origin, time, aps, options) { let infrastructureMessages = []; data.apInventories.forEach((inventory) => { let deviceId = inventory.mac.toString('hex'); infrastructureMessages.push({ deviceId: deviceId, deviceIdType: Raddec.identifiers.TYPE_EUI48, isHealthy: (inventory.status === 1), // TODO: confirm status codes timestamp: time // TODO: location? }); }); return { raddecs: [], infrastructureMessages: infrastructureMessages }; } /* Decoode the EnOcean Serial Protocol data into a raddec. * @param {String} payload The ESP data. * @param {String} receiverId The receiver id. * @param {Number} receiverIdType The receiver id type. * @param {Number} timestamp The time of the decoding. */ function decodeESPPacket(payload, receiverId, receiverIdType, timestamp) { if(payload.length < (MIN_ESP3_PACKET_LENGTH_BYTES * 2)) { return null; } let prefix = parseInt(payload.substring(0, 2), 16); let dataLength = parseInt(payload.substring(2, 6), 16); let optionalLength = parseInt(payload.substring(6, 8), 16); let packetType = parseInt(payload.substring(8, 10), 16); if((prefix !== 0x55) || (packetType !== 0x01)) { return null; } let indexOfData = 12; let indexOfOptionalData = 12 + (dataLength * 2); let data = payload.substring(indexOfData, indexOfOptionalData); let optionalData = payload.substring(indexOfOptionalData, indexOfOptionalData + (optionalLength * 2)); let rssi = 0 - parseInt(optionalData.substring(10, 12)); let transmitterId = data.substring(data.length - 10, data.length - 2); let raddec = new Raddec({ transmitterId: transmitterId, transmitterIdType: Raddec.identifiers.TYPE_EURID32 }); raddec.addDecoding({ receiverId: receiverId, receiverIdType: receiverIdType, rssi: rssi }); raddec.addPacket(payload); return raddec; } /** * Reconstruct the PDU header and address. * @param {String} address The transmitter address as a hexadecimal string. * @param {Number} addressType The address type. * @param {Number} packetType The packet type. * @param {String} payload The payload as a hexadecimal string. */ function reconstructHeaderAddress(address, addressType, packetType, payload) { let headerAddress = '0'; let length = 6 + (payload.length / 2); if(addressType > 0) { headerAddress = '4'; } headerAddress += packetType.toString(16); headerAddress += ('0' + length.toString(16)).substr(-2); headerAddress += address.substring(10,12); headerAddress += address.substring(8,10); headerAddress += address.substring(6,8); headerAddress += address.substring(4,6); headerAddress += address.substring(2,4); headerAddress += address.substring(0,2); return headerAddress; } module.exports.decode = decode;