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/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", 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(); + }); + }); });