diff --git a/lib/dbus/bindings.js b/lib/dbus/bindings.js index 648d75f2..6ba2377b 100644 --- a/lib/dbus/bindings.js +++ b/lib/dbus/bindings.js @@ -299,6 +299,7 @@ class DbusBindings extends EventEmitter { if (this._scanServiceUuids.length > 0) { filter.UUIDs = new Variant('as', this._scanServiceUuids.map(expandUuid)); } + debug('startScanning: filter UUIDs=%j duplicates=%s', this._scanServiceUuids, !!allowDuplicates); try { await this._adapterIface.SetDiscoveryFilter(filter); } catch (err) { @@ -307,7 +308,37 @@ class DbusBindings extends EventEmitter { if (!this._isScanning) { await this._adapterIface.StartDiscovery(); this._isScanning = true; + debug('startScanning: StartDiscovery issued'); + } else { + debug('startScanning: already scanning, skipping StartDiscovery'); } + + // Re-read BlueZ's object tree and surface cached devices. + // BlueZ won't emit InterfacesAdded for devices already in its cache, + // so without this refresh a new scan would miss them entirely. + try { + const managed = await this._objectManager.GetManagedObjects(); + let surfacedCount = 0; + for (const [path, ifaces] of Object.entries(managed)) { + const unwrapped = {}; + for (const [iface, props] of Object.entries(ifaces)) { + unwrapped[iface] = unwrapDict(props); + } + this._objects.set(path, Object.assign(this._objects.get(path) || {}, unwrapped)); + if (unwrapped[DEVICE_IFACE] && this._isUnderAdapter(path)) { + surfacedCount++; + const addr = unwrapped[DEVICE_IFACE].Address || 'unknown'; + const name = unwrapped[DEVICE_IFACE].Name || unwrapped[DEVICE_IFACE].Alias || ''; + const uuids = unwrapped[DEVICE_IFACE].UUIDs || []; + debug('startScanning: re-surfacing cached device %s addr=%s name=%s uuids=%j', path, addr, name, uuids); + this._handleDeviceProps(path, unwrapped[DEVICE_IFACE]); + } + } + debug('startScanning: re-surfaced %d cached device(s) from BlueZ', surfacedCount); + } catch (err) { + debug('startScanning: GetManagedObjects refresh failed: %s', err.message); + } + this.emit('scanStart', !!allowDuplicates); } @@ -341,6 +372,9 @@ class DbusBindings extends EventEmitter { this._objects.set(path, Object.assign(existing, unwrapped)); if (unwrapped[DEVICE_IFACE] && this._isUnderAdapter(path)) { + const addr = unwrapped[DEVICE_IFACE].Address || 'unknown'; + const name = unwrapped[DEVICE_IFACE].Name || unwrapped[DEVICE_IFACE].Alias || ''; + debug('InterfacesAdded: new device %s addr=%s name=%s', path, addr, name); this._handleDeviceProps(path, unwrapped[DEVICE_IFACE]); } // Trigger services-resolved processing if a device just gained ServicesResolved @@ -384,6 +418,10 @@ class DbusBindings extends EventEmitter { const address = props.Address || devicePathToAddress(path); if (!address) return; const id = addressToId(address); + const name = props.Name || props.Alias || ''; + const serviceData = props.ServiceData ? Object.keys(props.ServiceData) : []; + const uuids = props.UUIDs || []; + debug('handleDeviceProps: id=%s addr=%s name=%s uuids=%j serviceData=%j rssi=%s', id, address, name, uuids, serviceData, props.RSSI); let device = this._devices.get(id); if (!device) { device = { diff --git a/lib/hci-socket/gap.js b/lib/hci-socket/gap.js index 4659c09e..a3069faf 100644 --- a/lib/hci-socket/gap.js +++ b/lib/hci-socket/gap.js @@ -93,14 +93,19 @@ Gap.prototype.onHciLeScanEnableSet = function (status) { // Called when we see the actual command "LE Set Scan Enable" Gap.prototype.onLeScanEnableSetCmd = function (enable, filterDuplicates) { + debug('onLeScanEnableSetCmd: enable=%s filterDuplicates=%s scanState=%s ownFilterDuplicates=%s', + enable, filterDuplicates, this._scanState, this._scanFilterDuplicates); // Check to see if the new settings differ from what we expect. // If we are scanning, then a change happens if the new command stops // scanning or if duplicate filtering changes. // If we are not scanning, then a change happens if scanning was enabled. if (this._scanState === 'starting' || this._scanState === 'started') { if (!enable) { + debug('onLeScanEnableSetCmd: external process stopped our scan'); this.emit('scanStop'); } else if (this._scanFilterDuplicates !== filterDuplicates) { + debug('onLeScanEnableSetCmd: external process changed filterDuplicates from %s to %s', + this._scanFilterDuplicates, filterDuplicates); this._scanFilterDuplicates = filterDuplicates; this.emit('scanStart', this._scanFilterDuplicates); @@ -109,6 +114,7 @@ Gap.prototype.onLeScanEnableSetCmd = function (enable, filterDuplicates) { (this._scanState === 'stopping' || this._scanState === 'stopped') && enable ) { + debug('onLeScanEnableSetCmd: external process started scanning while we were %s', this._scanState); // Someone started scanning on us. this.emit('scanStart', this._scanFilterDuplicates); } @@ -165,12 +171,17 @@ Gap.prototype.onHciLeAdvertisingReport = function ( }; // only report after a scan response event or if non-connectable or more than one discovery without a scan response, so more data can be collected - if ( + const willEmit = type === LE_META_EVENT_TYPE_SCAN_RESPONSE || !connectable || (discoveryCount > 1 && !hasScanResponse) || - process.env.NOBLE_REPORT_ALL_HCI_EVENTS - ) { + process.env.NOBLE_REPORT_ALL_HCI_EVENTS; + + debug('adv: addr=%s type=0x%s name=%s rssi=%d count=%d connectable=%s hasScanResponse=%s → %s', + address, type.toString(16), advertisement.localName || '', rssi, discoveryCount, + connectable, hasScanResponse, willEmit ? 'EMIT' : 'SUPPRESS (waiting for scan response)'); + + if (willEmit) { this.emit( 'discover', status, @@ -238,12 +249,17 @@ Gap.prototype.onHciLeExtendedAdvertisingReport = function ( }; // only report after a scan response event or if non-connectable or more than one discovery without a scan response, so more data can be collected - if ( + const willEmit = type & LE_META_EXTENDED_EVENT_TYPE_SCAN_RESPONSE_MASK || (!connectable && !incomplete) || (discoveryCount > 1 && !hasScanResponse) || - process.env.NOBLE_REPORT_ALL_HCI_EVENTS - ) { + process.env.NOBLE_REPORT_ALL_HCI_EVENTS; + + debug('ext-adv: addr=%s type=0x%s name=%s rssi=%d count=%d connectable=%s incomplete=%s hasScanResponse=%s → %s', + address, type.toString(16), advertisement.localName || '', rssi, discoveryCount, + connectable, incomplete, hasScanResponse, willEmit ? 'EMIT' : 'SUPPRESS'); + + if (willEmit) { this.emit( 'discover', status,