UNPKG

barnowl-aruba

Version:

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

389 lines (327 loc) 13.2 kB
/** * Copyright reelyActive 2020-2024 * We believe in an open Internet of Things */ const Raddec = require('raddec'); const ID_TYPES = [ Raddec.identifiers.TYPE_EUI48, Raddec.identifiers.TYPE_RND48, Raddec.identifiers.TYPE_RND48, Raddec.identifiers.TYPE_RND48 ]; const DEFAULT_HISTORY_MILLISECONDS = 2000; const MIN_ESP3_PACKET_LENGTH_BYTES = 8; const RANDOM_STATIC_CHARS = [ 'c', 'd', 'e', 'f' ]; /** * Decode all the reported transmitters from the telemetry report. * @param {Object} report The telemetry report. * @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(report, origin, time, aps, options) { options = options || {}; if(!Number.isInteger(options.historyMilliseconds)) { options.historyMilliseconds = DEFAULT_HISTORY_MILLISECONDS; } let isValidBleData = report && report.hasOwnProperty('reporter') && Array.isArray(report.bleData) && (report.bleData.length > 0); let isValidSerialData = report && report.hasOwnProperty('reporter') && Array.isArray(report.nbSData) && (report.nbSData.length > 0); let isValidReported = report && report.hasOwnProperty('reporter') && Array.isArray(report.reported) && (report.reported.length > 0); let isValidWiFiData = report && report.hasOwnProperty('reporter') && Array.isArray(report.wifiData) && (report.wifiData.length > 0); let isValidApHealth = report && report.hasOwnProperty('reporter') && report.hasOwnProperty('apHealth'); let isValidCharacteristics = report && report.hasOwnProperty('reporter') && Array.isArray(report.characteristics) && (report.characteristics.length > 0); if(isValidBleData) { return decodeBleData(report, origin, time, aps, options); } else if(isValidSerialData) { return decodeSerialData(report, origin, time, aps, options); } else if(isValidReported && options.acceptTelemetryReports) { return decodeReported(report, origin, time, aps, options); } else if(isValidWiFiData && options.acceptTelemetryReports) { return decodeWiFiData(report, origin, time, aps, options); } else if(isValidApHealth) { return decodeApHealth(report, origin, time, aps, options); } else if(isValidCharacteristics) { return decodeCharacteristics(report, origin, time, aps, options); } else { return { raddecs: [], infrastructureMessages: [] }; } } /** * Decode all the bleData from the telemetry report. * @param {Object} report The telemetry report. * @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 decodeBleData(report, origin, time, aps, options) { let raddecs = []; let apMac = report.reporter.mac; let receiverIdType = Raddec.identifiers.TYPE_EUI48; let apbMac; report.bleData.forEach((transmitter) => { let reporterTime = report.reporter.time.low * 1000; let headerAddress = reconstructHeaderAddress(transmitter.mac, transmitter.addrType, transmitter.frameType, transmitter.data); let packet = headerAddress + transmitter.data; apbMac = transmitter.apbMac; let raddec = new Raddec({ transmitterId: transmitter.mac, transmitterIdType: ID_TYPES[transmitter.addrType], packets: [ packet ], timestamp: reporterTime }); raddec.addDecoding({ receiverId: apbMac || apMac, receiverIdType: receiverIdType, rssi: transmitter.rssi }); if(options.gattManager) { options.gattManager.handlePacket(packet, transmitter.mac, apMac, raddec, options.responseHandler); } raddecs.push(raddec); }); if(!aps.has(apMac)) { aps.set(apMac, { apbMac: apbMac }); } return { raddecs: raddecs, infrastructureMessages: [] }; } /** * Decode all the nbSData from the telemetry report. * @param {Object} report The telemetry report. * @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(report, origin, time, aps, options) { let raddecs = []; let receiverId = report.reporter.mac; let receiverIdType = Raddec.identifiers.TYPE_EUI48; report.nbSData.forEach((transmitter) => { let reporterTime = report.reporter.time.low * 1000; if((typeof transmitter.nbDeviceId === 'string') && transmitter.nbDeviceId.startsWith('ENOCEAN_USB')) { let raddec = decodeESPPacket(transmitter.nbSerialPayload, receiverId, receiverIdType, reporterTime); if(raddec) { raddecs.push(raddec); } } }); return { raddecs: raddecs, 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; } /** * Decode all the reported BLE transmitters from the telemetry report. * @param {Object} report The telemetry report. * @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 decodeReported(report, origin, time, aps, options) { let raddecs = []; let apMac = report.reporter.mac; let receiverIdType = Raddec.identifiers.TYPE_EUI48; let apbMac; if(aps.has(apMac)) { apbMac = aps.get(apMac).apbMac; } report.reported.forEach((transmitter) => { let lastSeenTimestamp = parseInt(transmitter.lastSeen) * 1000; let transmitterIdType = Raddec.identifiers.TYPE_EUI48; // TODO: extract transmitterIdType from report, if made available if(RANDOM_STATIC_CHARS.includes(transmitter.mac.substring(0,1))) { transmitterIdType = Raddec.identifiers.TYPE_RND48; } let raddec = new Raddec({ transmitterId: transmitter.mac, transmitterIdType: transmitterIdType, timestamp: lastSeenTimestamp }); if(transmitter.hasOwnProperty('rssi')) { let rssi = transmitter.rssi.last; // Use individual RSSI samples from history, when available if(Array.isArray(transmitter.rssi.history) && (transmitter.rssi.history.length > 0)) { transmitter.rssi.history.forEach((entry) => { let isStale = ((time - (entry.time.low * 1000)) > options.historyMilliseconds); if(!isStale && Number.isInteger(entry.rssi)) { raddec.addDecoding({ receiverId: apbMac || apMac, receiverIdType: receiverIdType, rssi: entry.rssi }); } }); if(raddec.rssiSignature.length > 0) { raddecs.push(raddec); } } // Use last/smooth/avg/max RSSI, otherwise else { let rssi = transmitter.rssi.last || transmitter.rssi.smooth || transmitter.rssi.avg || transmitter.rssi.max; if(rssi) { raddec.addDecoding({ receiverId: apbMac || apMac, receiverIdType: receiverIdType, rssi: rssi }); raddecs.push(raddec); } } } }); return { raddecs: raddecs, infrastructureMessages: [] }; } /** * Decode all the reported WiFi transmitters from the telemetry report. * @param {Object} report The telemetry report. * @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 decodeWiFiData(report, origin, time, aps, options) { let raddecs = []; let receiverId = report.reporter.mac; let receiverIdType = Raddec.identifiers.TYPE_EUI48; report.wifiData.forEach((transmitter) => { let raddec = new Raddec({ transmitterId: transmitter.mac, transmitterIdType: Raddec.identifiers.TYPE_EUI48, timestamp: Date.now() }); raddec.addDecoding({ receiverId: receiverId, receiverIdType: receiverIdType, rssi: transmitter.rssi }); if(transmitter.hasOwnProperty('rtls_payload')) { raddec.addPacket(transmitter.rtls_payload); } raddecs.push(raddec); }); return { raddecs: raddecs, infrastructureMessages: [] }; } /** * Decode the AP Health report. * @param {Object} report The telemetry report. * @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 decodeApHealth(report, origin, time, aps, options) { let infrastructureMessages = []; let apMac = report.reporter.mac; if(Array.isArray(report.apHealth.radio) && (report.apHealth.radio.length === 1)) { let deviceId = report.apHealth.radio[0].mac; aps.set(apMac, { apbMac: deviceId }); infrastructureMessages.push({ deviceId: deviceId, deviceIdType: Raddec.identifiers.TYPE_EUI48, isHealthy: true, // TODO: check! timestamp: report.reporter.time * 1000 }); } return { raddecs: [], infrastructureMessages: infrastructureMessages }; } /** * Decode all the BLE characteristics from the telemetry report. * @param {Object} report The telemetry report. * @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 decodeCharacteristics(report, origin, time, aps, options) { if(!Array.isArray(report.results) || (report.results.length === 0)) { return { raddecs: [], infrastructureMessages: [] }; } let type = report.results[0].type; report.characteristics.forEach((characteristic) => { if(options.gattManager) { options.gattManager.handleCharacteristic(characteristic, type); } }); return { raddecs: [], infrastructureMessages: [] }; } /** * 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;