From 564017a82a6aa8ac9abe421ed7603e0e6ac3496f Mon Sep 17 00:00:00 2001 From: Marek Serafin Date: Thu, 7 May 2026 09:34:46 +0200 Subject: [PATCH 1/2] chore: declare dbus-next as optional peer dependency Replaces undocumented prose-only requirement with a machine-readable version range so Renovate/Dependabot/`npm ls` can track supported versions. Marked optional so npm doesn't auto-install on Mac/Win. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/package.json b/package.json index 3b4c3290..7ee529ac 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,14 @@ "optionalDependencies": { "@stoprocent/bluetooth-hci-socket": "^2.2.6" }, + "peerDependencies": { + "dbus-next": "^0.10.0" + }, + "peerDependenciesMeta": { + "dbus-next": { + "optional": true + } + }, "devDependencies": { "@babel/eslint-parser": "^7.27.0", "@commitlint/cli": "^19.3.0", From 90396cbe16690d213c966cf8dcfbd4ec12898b42 Mon Sep 17 00:00:00 2001 From: Marek Serafin Date: Thu, 7 May 2026 09:42:50 +0200 Subject: [PATCH 2/2] fix(dbus): normalize peripheral id at every public entry point BlueZ emits MAC addresses in uppercase colon form (AA:BB:CC:DD:EE:FF), while noble's canonical peripheral id is the colon-stripped lowercase form (aabbccddeeff). External callers may pass any of: canonical id, uppercase id, mixed-case id, or uppercase/lowercase/mixed colon-MAC form. Add a small `normalizeId` helper and apply it at the top of every public method that accepts a peripheralUuid (connect, disconnect, cancelConnect, updateRssi, discoverServices, discoverIncludedServices, discoverCharacteristics, read, write, broadcast, notify, discoverDescriptors, readValue, writeValue, readHandle, writeHandle). Internal lookups now match regardless of the casing/separator the caller used, and emitted events always carry the canonical id. Null/undefined ids are passed through unchanged so existing "unknown peripheral" code paths still kick in instead of throwing. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/dbus/bindings.js | 24 +++++++++++- test/lib/dbus/bindings.test.js | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/lib/dbus/bindings.js b/lib/dbus/bindings.js index 79cb3125..8f77ec20 100644 --- a/lib/dbus/bindings.js +++ b/lib/dbus/bindings.js @@ -49,6 +49,13 @@ function loadDbus () { } } +function normalizeId (id) { + // BlueZ emits MACs uppercase; noble's id form is colon-stripped lowercase. + // Accept either, plus mixed case, so external callers don't have to care. + if (id == null) return id; + return String(id).replace(/:/g, '').toLowerCase(); +} + function unwrapVariant (variant) { if (variant && typeof variant === 'object' && 'value' in variant && 'signature' in variant) { return variant.value; @@ -485,6 +492,7 @@ class DbusBindings extends EventEmitter { // ---- Connect / disconnect ---- connect (peripheralUuid, _parameters) { + peripheralUuid = normalizeId(peripheralUuid); this._connect(peripheralUuid).catch(err => { this.emit('connect', peripheralUuid, err); }); @@ -518,10 +526,11 @@ class DbusBindings extends EventEmitter { } cancelConnect (peripheralUuid, _parameters) { - this.disconnect(peripheralUuid); + this.disconnect(normalizeId(peripheralUuid)); } disconnect (peripheralUuid) { + peripheralUuid = normalizeId(peripheralUuid); this._disconnect(peripheralUuid).catch(err => { this.emit('warning', `disconnect failed: ${err.message}`); }); @@ -540,6 +549,7 @@ class DbusBindings extends EventEmitter { } updateRssi (peripheralUuid) { + peripheralUuid = normalizeId(peripheralUuid); const device = this._devices.get(peripheralUuid); if (!device || !device.path) { this.emit('rssiUpdate', peripheralUuid, 0, new Error('unknown peripheral')); @@ -596,6 +606,7 @@ class DbusBindings extends EventEmitter { } discoverServices (peripheralUuid, uuids) { + peripheralUuid = normalizeId(peripheralUuid); const wanted = (uuids || []).map(normalizeUuid); const found = this._findServicesForDevice(peripheralUuid); const filtered = wanted.length === 0 ? found : found.filter(s => wanted.includes(s.uuid)); @@ -605,11 +616,13 @@ class DbusBindings extends EventEmitter { } discoverIncludedServices (peripheralUuid, serviceUuid, _serviceUuids) { + peripheralUuid = normalizeId(peripheralUuid); // BlueZ does not expose included services directly via D-Bus. this.emit('includedServicesDiscover', peripheralUuid, serviceUuid, []); } discoverCharacteristics (peripheralUuid, serviceUuid, characteristicUuids) { + peripheralUuid = normalizeId(peripheralUuid); const services = this._findServicesForDevice(peripheralUuid); const service = services.find(s => s.uuid === normalizeUuid(serviceUuid)); if (!service) { @@ -642,6 +655,7 @@ class DbusBindings extends EventEmitter { } read (peripheralUuid, serviceUuid, characteristicUuid) { + peripheralUuid = normalizeId(peripheralUuid); this._readChar(peripheralUuid, serviceUuid, characteristicUuid).catch(err => { this.emit('read', peripheralUuid, serviceUuid, characteristicUuid, null, false, err); }); @@ -658,6 +672,7 @@ class DbusBindings extends EventEmitter { } write (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) { + peripheralUuid = normalizeId(peripheralUuid); this._writeChar(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse).catch(err => { this.emit('write', peripheralUuid, serviceUuid, characteristicUuid, err); }); @@ -676,11 +691,13 @@ class DbusBindings extends EventEmitter { } broadcast (peripheralUuid, serviceUuid, characteristicUuid, _broadcast) { + peripheralUuid = normalizeId(peripheralUuid); this.emit('warning', 'broadcast is not supported on the dbus backend'); this.emit('broadcast', peripheralUuid, serviceUuid, characteristicUuid, false); } notify (peripheralUuid, serviceUuid, characteristicUuid, notify) { + peripheralUuid = normalizeId(peripheralUuid); this._setNotify(peripheralUuid, serviceUuid, characteristicUuid, notify).catch(err => { this.emit('notify', peripheralUuid, serviceUuid, characteristicUuid, false, err); }); @@ -724,6 +741,7 @@ class DbusBindings extends EventEmitter { } discoverDescriptors (peripheralUuid, serviceUuid, characteristicUuid) { + peripheralUuid = normalizeId(peripheralUuid); const charPath = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid); if (!charPath) { this.emit('descriptorsDiscover', peripheralUuid, serviceUuid, characteristicUuid, [], new Error('characteristic not found')); @@ -734,6 +752,7 @@ class DbusBindings extends EventEmitter { } readValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { + peripheralUuid = normalizeId(peripheralUuid); this._readDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid).catch(err => { this.emit('valueRead', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, null, err); }); @@ -750,6 +769,7 @@ class DbusBindings extends EventEmitter { } writeValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { + peripheralUuid = normalizeId(peripheralUuid); this._writeDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data).catch(err => { this.emit('valueWrite', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, err); }); @@ -766,11 +786,13 @@ class DbusBindings extends EventEmitter { } readHandle (peripheralUuid, handle) { + peripheralUuid = normalizeId(peripheralUuid); const err = new Error('readHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)'); this.emit('handleRead', peripheralUuid, handle, null, err); } writeHandle (peripheralUuid, handle, _data, _withoutResponse) { + peripheralUuid = normalizeId(peripheralUuid); const err = new Error('writeHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)'); this.emit('handleWrite', peripheralUuid, handle, err); } diff --git a/test/lib/dbus/bindings.test.js b/test/lib/dbus/bindings.test.js index cbabe658..6475523f 100644 --- a/test/lib/dbus/bindings.test.js +++ b/test/lib/dbus/bindings.test.js @@ -409,4 +409,75 @@ describe('dbus/bindings', () => { const bindings = new DbusBindings(); expect(bindings.addressToId('AA:BB:CC:DD:EE:FF')).toBe('aabbccddeeff'); }); + + describe('peripheral id normalization on public methods', () => { + const tree = () => adapterTree({ + '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF': { + 'org.bluez.Device1': { Address: 'AA:BB:CC:DD:EE:FF', AddressType: 'public', Connected: false } + }, + '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0001': { + 'org.bluez.GattService1': { UUID: '0000180d-0000-1000-8000-00805f9b34fb', Primary: true } + }, + '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0001/char0002': { + 'org.bluez.GattCharacteristic1': { + UUID: '00002a37-0000-1000-8000-00805f9b34fb', + Flags: ['read', 'notify'] + } + } + }); + + const variants = [ + ['canonical id', 'aabbccddeeff'], + ['uppercase id', 'AABBCCDDEEFF'], + ['mixed-case id', 'AaBbCcDdEeFf'], + ['colon MAC uppercase', 'AA:BB:CC:DD:EE:FF'], + ['colon MAC lowercase', 'aa:bb:cc:dd:ee:ff'], + ['colon MAC mixed', 'Aa:Bb:Cc:Dd:Ee:Ff'] + ]; + + test.each(variants)('discoverServices accepts %s and emits canonical id', async (_label, input) => { + resetState(tree()); + const bindings = new DbusBindings(); + bindings.start(); + await flush(); + + const services = []; + bindings.on('servicesDiscover', (...a) => services.push(a)); + + bindings.discoverServices(input, []); + + expect(services[0]).toEqual(['aabbccddeeff', ['180d']]); + }); + + test.each(variants)('read accepts %s and emits canonical id', async (_label, input) => { + resetState(tree()); + const bindings = new DbusBindings(); + bindings.start(); + await flush(); + + const reads = []; + bindings.on('read', (...a) => reads.push(a)); + + bindings.read(input, '180d', '2a37'); + await flush(); + + expect(reads.length).toBe(1); + expect(reads[0][0]).toBe('aabbccddeeff'); + }); + + test.each(variants)('readHandle (unsupported) emits canonical id for %s', (_label, input) => { + resetState(tree()); + const bindings = new DbusBindings(); + const events = []; + bindings.on('handleRead', (...a) => events.push(a)); + bindings.readHandle(input, 0x42); + expect(events[0][0]).toBe('aabbccddeeff'); + }); + + test('null/undefined peripheralUuid does not throw', () => { + const bindings = new DbusBindings(); + expect(() => bindings.discoverServices(undefined, [])).not.toThrow(); + expect(() => bindings.discoverServices(null, [])).not.toThrow(); + }); + }); });