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.

354 lines (283 loc) 9.84 kB
/** * Copyright reelyActive 2016-2025 * Adapted from node-bluetooth-hci-socket example by Sandeep Mistry (c) 2015 * and @stoprocent/noble/hci-socket by Marek Serafin (c) 2025 * 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; const DEFAULT_KICK_INTERVAL_MILLISECONDS = 1000; /** * 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.deviceId = options.deviceId; this.driver = options.driver; 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.kickIntervalMilliseconds = options.kickIntervalMilliseconds || DEFAULT_KICK_INTERVAL_MILLISECONDS; this.origin = 'socket'; if(this.deviceId) { this.origin = 'hci' + this.deviceId; } openHciSocket(self, (err, socket) => { if(err) { let device = self.deviceId ? 'hci' + self.deviceId : 'hciX'; return console.log('barnowl-hci (' + device + '): ' + err.message); } else { self.socket = socket; handleSocketEvents(self); setInterval(kick, self.kickIntervalMilliseconds, self.socket); } }); } /** * Exit gracefully, disabling scanning and stopping the socket. * @param {function} callback The function to call on completion. */ close(callback) { setScanEnable(this.socket, false, true); this.socket.stop(); return callback(); } } /** * 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(); let type = instance.decoder.handleHciData(data, instance.origin, time); if(type === HciPacket.TYPE_RESET_CMD) { init(instance.socket); } }); instance.socket.on('error', (err) => { console.log('barnowl-hci:', err.message); }); } /** * Open the HCI socket based on the given deviceId and driver. * @param {SocketListener} instance The SocketListener instance. * @param {function} callback The function to call on completion. */ function openHciSocket(instance, callback) { let bluetoothHciSocket; switch(instance.driver) { case 'uart': case 'usb': case 'native': const SpecificBluetoothHciSocket = BluetoothHciSocket.loadDriver(instance.driver); bluetoothHciSocket = new SpecificBluetoothHciSocket(); break; default: bluetoothHciSocket = new BluetoothHciSocket(); } try { if(Number.isInteger(instance.deviceId)) { bluetoothHciSocket.bindRaw(instance.deviceId); } else { bluetoothHciSocket.bindRaw(); } } catch(error) { return callback(error, bluetoothHciSocket); } reset(bluetoothHciSocket); setFilter(bluetoothHciSocket); bluetoothHciSocket.start(); setScanEnable(bluetoothHciSocket, false, true); setScanParameters(bluetoothHciSocket, instance.enableActiveScanning, instance.scanIntervalMilliseconds, instance.scanWindowMilliseconds); setScanEnable(bluetoothHciSocket, true, true); callback(null, bluetoothHciSocket); } /** * Initialise the HCI socket (typically after a reset). * @param {Object} socket The Bluetooth HCI Socket. */ function init(socket) { setEventMask(socket); setLeEventMask(socket); readBdAddr(socket); } /** * Kick the HCI socket (to keep it up, running and scanning). * @param {Object} socket The Bluetooth HCI Socket. */ function kick(socket) { if(socket.isDevUp()) { setScanEnable(socket, true, true); } else { /* TODO: handle down/up cycle? */ } } /** * Reset the Bluetooth HCI Socket. * @param {Object} socket The Bluetooth HCI Socket. */ function reset(socket) { const cmd = Buffer.alloc(4); // header cmd.writeUInt8(HciPacket.HCI_COMMAND_PKT, 0); cmd.writeUInt16LE(HciPacket.RESET_CMD, 1); // length cmd.writeUInt8(0x00, 3); socket.write(cmd); }; /** * 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? // TODO: Extended advertising: '1f00000000000000' // 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_COMMAND_PKT) | (1 << HciPacket.HCI_EVENT_PKT) | (1 << HciPacket.HCI_ACLDATA_PKT); let eventMask1 = (1 << HciPacket.EVT_DISCONN_COMPLETE) | (1 << HciPacket.EVT_ENCRYPT_CHANGE) | (1 << HciPacket.EVT_CMD_COMPLETE) | (1 << HciPacket.EVT_CMD_STATUS) | (1 << HciPacket.EVT_NUMBER_OF_COMPLETED_PACKETS); 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) { // TODO: handle extended advertising 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) { // TODO: handle extended advertising 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;