UNPKG

barnowl-hci

Version:

Collect ambient advertising packets from the Bluetooth Host Controller Interface (HCI) for real-time location and sensing. We believe in an open Internet of Things.

271 lines (210 loc) 7.5 kB
/** * Copyright reelyActive 2016-2024 * Adapted from node-bluetooth-hci-socket example by Sandeep Mistry (c) 2015 * We believe in an open Internet of Things */ const BluetoothHciSocket = require('@stoprocent/bluetooth-hci-socket'); const HciPacket = require('./hcipacket'); const DEFAULT_ENABLE_ACTIVE_SCANNING = false; const DEFAULT_SCAN_INTERVAL_MILLISECONDS = 10; const DEFAULT_SCAN_WINDOW_MILLISECONDS = 10; /** * SocketListener Class * Listens for data on a HCI socket. */ class SocketListener { /** * SocketListener constructor * @param {Object} options The options as a JSON object. * @constructor */ constructor(options) { let self = this; options = options || {}; this.decoder = options.decoder; this.path = options.path; this.enableActiveScanning = options.enableActiveScanning || DEFAULT_ENABLE_ACTIVE_SCANNING; this.scanIntervalMilliseconds = options.scanIntervalMilliseconds || DEFAULT_SCAN_INTERVAL_MILLISECONDS; this.scanWindowMilliseconds = options.scanWindowMilliseconds || DEFAULT_SCAN_WINDOW_MILLISECONDS; this.origin = 'socket'; if(this.path) { this.origin = this.path; } openHciSocket(self, (err, socket) => { if(err) { return console.log('barnowl-hci:', err); } else { self.socket = socket; handleSocketEvents(self); } }); } } /** * Handle events on the given socket. * @param {SocketListener} instance The SocketListener instance. */ function handleSocketEvents(instance) { instance.socket.on('data', (data) => { let time = new Date().getTime(); instance.decoder.handleHciData(data, instance.origin, time); }); instance.socket.on('error', (err) => { console.log('barnowl-hci:', err); }); } /** * Open the HCI socket based on the given path. * @param {SocketListener} instance The SocketListener instance. * @param {function} callback The function to call on completion. */ function openHciSocket(instance, callback) { let bluetoothHciSocket = new BluetoothHciSocket(); if(instance.path) { bluetoothHciSocket.bindRaw([ instance.path ]); } else { bluetoothHciSocket.bindRaw(); } setFilter(bluetoothHciSocket); bluetoothHciSocket.start(); //setEventMask(bluetoothHciSocket); // Maintain the default values //setLeEventMask(bluetoothHciSocket); // unless otherwise required setScanEnable(bluetoothHciSocket, false, true); setScanParameters(bluetoothHciSocket, instance.enableActiveScanning, instance.scanIntervalMilliseconds, instance.scanWindowMilliseconds); setScanEnable(bluetoothHciSocket, true, true); callback(null, bluetoothHciSocket); // TODO: include error readBdAddr(bluetoothHciSocket); } /** * Set the event mask of the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. */ function setEventMask(socket) { const cmd = Buffer.alloc(12); // See Bluetooth Core Specification 5.4,Vol. 4, Part E: 7.3.1 const eventMask = Buffer.from('fffffbff07f8bf3d', 'hex'); // TODO: tweak // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.SET_EVENT_MASK_CMD, 1); // length cmd.writeUInt8(eventMask.length, 3); eventMask.copy(cmd, 4); socket.write(cmd); } /** * Set the LE event mask of the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. */ function setLeEventMask(socket) { const cmd = Buffer.alloc(12); // See Bluetooth Core Specification 5.4,Vol. 4, Part E: 7.8.1 const leEventMask = Buffer.from('1fff000000000000', 'hex'); // TODO: tweak // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.LE_SET_EVENT_MASK_CMD, 1); // length cmd.writeUInt8(leEventMask.length, 3); leEventMask.copy(cmd, 4); socket.write(cmd); } /** * Set the filters of the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. */ function setFilter(socket) { let filter = Buffer.alloc(14); let typeMask = (1 << HciPacket.HCI_EVENT_PKT); let eventMask1 = (1 << HciPacket.EVT_CMD_COMPLETE) | (1 << HciPacket.EVT_CMD_STATUS); let eventMask2 = (1 << (HciPacket.EVT_LE_META_EVENT - 32)); let opcode = 0; filter.writeUInt32LE(typeMask, 0); filter.writeUInt32LE(eventMask1, 4); filter.writeUInt32LE(eventMask2, 8); filter.writeUInt16LE(opcode, 12); socket.setFilter(filter); } /** * Set the scan parameters of the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. * @param {boolean} enableActiveScanning Passive or active scan. * @param {Number} scanIntervalMilliseconds Duration of scan interval. * @param {Number} scanWindowMilliseconds Duration of scan window. */ function setScanParameters(socket, enableActiveScanning, scanIntervalMilliseconds, scanWindowMilliseconds) { let cmd = Buffer.alloc(11); let leScanInterval = convertScanDuration(scanIntervalMilliseconds); let leScanWindow = convertScanDuration(scanWindowMilliseconds); if(leScanWindow > leScanInterval) { leScanWindow = leScanInterval; console.log('barnowl-hci: scan window reduced to scan interval'); } // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.LE_SET_SCAN_PARAMETERS_CMD, 1); // length cmd.writeUInt8(0x07, 3); // See Bluetooth Core Specification 5.4,Vol. 4, Part E: 7.8.10 // data cmd.writeUInt8(enableActiveScanning ? 0x01 : 0x00, 4); cmd.writeUInt16LE(leScanInterval, 5); // 2.5ms to 10.24s in 0.625ms steps cmd.writeUInt16LE(leScanWindow, 7); // 2.5ms to 10.24s in 0.625ms steps cmd.writeUInt8(0x00, 9); // 0x00 = public device address cmd.writeUInt8(0x00, 10); // 0x00 = unfiltered scanning policy socket.write(cmd); } /** * Set the scan state of the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. */ function setScanEnable(socket, enabled, duplicates) { let cmd = Buffer.alloc(6); // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.LE_SET_SCAN_ENABLE_CMD, 1); // length cmd.writeUInt8(0x02, 3); // See Bluetooth Core Specification 5.4,Vol. 4, Part E: 7.8.11 // data cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enabled: 0 (no), 1 (yes) cmd.writeUInt8(duplicates ? 0x01 : 0x00, 5); // duplicates: 0 (no), 1 (yes) socket.write(cmd); } /** * Read the Bluetooth MAC address of the adapter. * @param {Object} socket The Bluetooth HCI Socket. */ function readBdAddr(socket) { let cmd = Buffer.alloc(4); // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.READ_BD_ADDR_CMD, 1); // length cmd.writeUInt8(0x0, 3); socket.write(cmd); }; /** * Convert the given scan duration to a value within the valid range. * @param {Number} durationMilliseconds The duration in milliseconds. */ function convertScanDuration(durationMilliseconds) { let value = Math.round(durationMilliseconds / 0.625); if(value > 0x4000) { console.log('barnowl-hci: scan parameter reduced to maximum permitted value of 10.24s'); return 0x4000; } else if(value < 0x0004) { console.log('barnowl-hci: scan parameter increased to minimum permitted value of 2.5ms'); return 0x0004; } return value; } module.exports = SocketListener;