@stoprocent/noble 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,6 +49,13 @@ function loadDbus () {
49
49
  }
50
50
  }
51
51
 
52
+ function normalizeId (id) {
53
+ // BlueZ emits MACs uppercase; noble's id form is colon-stripped lowercase.
54
+ // Accept either, plus mixed case, so external callers don't have to care.
55
+ if (id == null) return id;
56
+ return String(id).replace(/:/g, '').toLowerCase();
57
+ }
58
+
52
59
  function unwrapVariant (variant) {
53
60
  if (variant && typeof variant === 'object' && 'value' in variant && 'signature' in variant) {
54
61
  return variant.value;
@@ -102,7 +109,8 @@ function buildAdvertisement (deviceProps) {
102
109
  const entries = Object.entries(deviceProps.ManufacturerData);
103
110
  if (entries.length > 0) {
104
111
  const buffers = [];
105
- for (const [companyId, payload] of entries) {
112
+ for (const [companyId, rawPayload] of entries) {
113
+ const payload = unwrapVariant(rawPayload);
106
114
  const id = Number(companyId) & 0xffff;
107
115
  const header = Buffer.from([id & 0xff, (id >> 8) & 0xff]);
108
116
  const data = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
@@ -113,7 +121,8 @@ function buildAdvertisement (deviceProps) {
113
121
  }
114
122
 
115
123
  if (deviceProps.ServiceData && typeof deviceProps.ServiceData === 'object') {
116
- for (const [uuid, payload] of Object.entries(deviceProps.ServiceData)) {
124
+ for (const [uuid, rawPayload] of Object.entries(deviceProps.ServiceData)) {
125
+ const payload = unwrapVariant(rawPayload);
117
126
  advertisement.serviceData.push({
118
127
  uuid: normalizeUuid(uuid),
119
128
  data: Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
@@ -485,6 +494,7 @@ class DbusBindings extends EventEmitter {
485
494
  // ---- Connect / disconnect ----
486
495
 
487
496
  connect (peripheralUuid, _parameters) {
497
+ peripheralUuid = normalizeId(peripheralUuid);
488
498
  this._connect(peripheralUuid).catch(err => {
489
499
  this.emit('connect', peripheralUuid, err);
490
500
  });
@@ -518,10 +528,11 @@ class DbusBindings extends EventEmitter {
518
528
  }
519
529
 
520
530
  cancelConnect (peripheralUuid, _parameters) {
521
- this.disconnect(peripheralUuid);
531
+ this.disconnect(normalizeId(peripheralUuid));
522
532
  }
523
533
 
524
534
  disconnect (peripheralUuid) {
535
+ peripheralUuid = normalizeId(peripheralUuid);
525
536
  this._disconnect(peripheralUuid).catch(err => {
526
537
  this.emit('warning', `disconnect failed: ${err.message}`);
527
538
  });
@@ -540,6 +551,7 @@ class DbusBindings extends EventEmitter {
540
551
  }
541
552
 
542
553
  updateRssi (peripheralUuid) {
554
+ peripheralUuid = normalizeId(peripheralUuid);
543
555
  const device = this._devices.get(peripheralUuid);
544
556
  if (!device || !device.path) {
545
557
  this.emit('rssiUpdate', peripheralUuid, 0, new Error('unknown peripheral'));
@@ -596,6 +608,7 @@ class DbusBindings extends EventEmitter {
596
608
  }
597
609
 
598
610
  discoverServices (peripheralUuid, uuids) {
611
+ peripheralUuid = normalizeId(peripheralUuid);
599
612
  const wanted = (uuids || []).map(normalizeUuid);
600
613
  const found = this._findServicesForDevice(peripheralUuid);
601
614
  const filtered = wanted.length === 0 ? found : found.filter(s => wanted.includes(s.uuid));
@@ -605,11 +618,13 @@ class DbusBindings extends EventEmitter {
605
618
  }
606
619
 
607
620
  discoverIncludedServices (peripheralUuid, serviceUuid, _serviceUuids) {
621
+ peripheralUuid = normalizeId(peripheralUuid);
608
622
  // BlueZ does not expose included services directly via D-Bus.
609
623
  this.emit('includedServicesDiscover', peripheralUuid, serviceUuid, []);
610
624
  }
611
625
 
612
626
  discoverCharacteristics (peripheralUuid, serviceUuid, characteristicUuids) {
627
+ peripheralUuid = normalizeId(peripheralUuid);
613
628
  const services = this._findServicesForDevice(peripheralUuid);
614
629
  const service = services.find(s => s.uuid === normalizeUuid(serviceUuid));
615
630
  if (!service) {
@@ -642,6 +657,7 @@ class DbusBindings extends EventEmitter {
642
657
  }
643
658
 
644
659
  read (peripheralUuid, serviceUuid, characteristicUuid) {
660
+ peripheralUuid = normalizeId(peripheralUuid);
645
661
  this._readChar(peripheralUuid, serviceUuid, characteristicUuid).catch(err => {
646
662
  this.emit('read', peripheralUuid, serviceUuid, characteristicUuid, null, false, err);
647
663
  });
@@ -658,6 +674,7 @@ class DbusBindings extends EventEmitter {
658
674
  }
659
675
 
660
676
  write (peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) {
677
+ peripheralUuid = normalizeId(peripheralUuid);
661
678
  this._writeChar(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse).catch(err => {
662
679
  this.emit('write', peripheralUuid, serviceUuid, characteristicUuid, err);
663
680
  });
@@ -676,11 +693,13 @@ class DbusBindings extends EventEmitter {
676
693
  }
677
694
 
678
695
  broadcast (peripheralUuid, serviceUuid, characteristicUuid, _broadcast) {
696
+ peripheralUuid = normalizeId(peripheralUuid);
679
697
  this.emit('warning', 'broadcast is not supported on the dbus backend');
680
698
  this.emit('broadcast', peripheralUuid, serviceUuid, characteristicUuid, false);
681
699
  }
682
700
 
683
701
  notify (peripheralUuid, serviceUuid, characteristicUuid, notify) {
702
+ peripheralUuid = normalizeId(peripheralUuid);
684
703
  this._setNotify(peripheralUuid, serviceUuid, characteristicUuid, notify).catch(err => {
685
704
  this.emit('notify', peripheralUuid, serviceUuid, characteristicUuid, false, err);
686
705
  });
@@ -724,6 +743,7 @@ class DbusBindings extends EventEmitter {
724
743
  }
725
744
 
726
745
  discoverDescriptors (peripheralUuid, serviceUuid, characteristicUuid) {
746
+ peripheralUuid = normalizeId(peripheralUuid);
727
747
  const charPath = this._findCharacteristicPath(peripheralUuid, serviceUuid, characteristicUuid);
728
748
  if (!charPath) {
729
749
  this.emit('descriptorsDiscover', peripheralUuid, serviceUuid, characteristicUuid, [], new Error('characteristic not found'));
@@ -734,6 +754,7 @@ class DbusBindings extends EventEmitter {
734
754
  }
735
755
 
736
756
  readValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) {
757
+ peripheralUuid = normalizeId(peripheralUuid);
737
758
  this._readDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid).catch(err => {
738
759
  this.emit('valueRead', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, null, err);
739
760
  });
@@ -750,6 +771,7 @@ class DbusBindings extends EventEmitter {
750
771
  }
751
772
 
752
773
  writeValue (peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) {
774
+ peripheralUuid = normalizeId(peripheralUuid);
753
775
  this._writeDesc(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data).catch(err => {
754
776
  this.emit('valueWrite', peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, err);
755
777
  });
@@ -766,11 +788,13 @@ class DbusBindings extends EventEmitter {
766
788
  }
767
789
 
768
790
  readHandle (peripheralUuid, handle) {
791
+ peripheralUuid = normalizeId(peripheralUuid);
769
792
  const err = new Error('readHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)');
770
793
  this.emit('handleRead', peripheralUuid, handle, null, err);
771
794
  }
772
795
 
773
796
  writeHandle (peripheralUuid, handle, _data, _withoutResponse) {
797
+ peripheralUuid = normalizeId(peripheralUuid);
774
798
  const err = new Error('writeHandle is not supported on the dbus backend (BlueZ exposes UUIDs only)');
775
799
  this.emit('handleWrite', peripheralUuid, handle, err);
776
800
  }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "license": "MIT",
7
7
  "name": "@stoprocent/noble",
8
8
  "description": "A Node.js BLE (Bluetooth Low Energy) central library.",
9
- "version": "2.5.0",
9
+ "version": "2.5.2",
10
10
  "repository": {
11
11
  "type": "git",
12
12
  "url": "https://github.com/stoprocent/noble.git"
@@ -36,6 +36,14 @@
36
36
  "optionalDependencies": {
37
37
  "@stoprocent/bluetooth-hci-socket": "^2.2.6"
38
38
  },
39
+ "peerDependencies": {
40
+ "dbus-next": "^0.10.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "dbus-next": {
44
+ "optional": true
45
+ }
46
+ },
39
47
  "devDependencies": {
40
48
  "@babel/eslint-parser": "^7.27.0",
41
49
  "@commitlint/cli": "^19.3.0",
@@ -270,6 +270,44 @@ describe('dbus/bindings', () => {
270
270
  expect(advertisement.serviceUuids).toEqual(['180d']);
271
271
  });
272
272
 
273
+ test('InterfacesAdded unwraps Variant-wrapped ManufacturerData and ServiceData payloads', async () => {
274
+ const bindings = new DbusBindings();
275
+ const discoveries = [];
276
+ bindings.on('discover', (...args) => discoveries.push(args));
277
+
278
+ bindings.start();
279
+ await flush();
280
+
281
+ // BlueZ exposes ManufacturerData as a{qv} and ServiceData as a{sv}; dbus-next
282
+ // surfaces each inner value as a Variant, not the raw bytes.
283
+ const mfgPayload = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
284
+ const svcPayload = Buffer.from([0x01, 0x02, 0x03]);
285
+
286
+ const om = state.rootProxy.getInterface('org.freedesktop.DBus.ObjectManager');
287
+ om.emit('InterfacesAdded', '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF', {
288
+ 'org.bluez.Device1': wrapDict({
289
+ Address: 'AA:BB:CC:DD:EE:FF',
290
+ AddressType: 'public',
291
+ ManufacturerData: { 0x004c: v('ay', mfgPayload) },
292
+ ServiceData: { '0000180d-0000-1000-8000-00805f9b34fb': v('ay', svcPayload) }
293
+ })
294
+ });
295
+ await flush();
296
+
297
+ expect(discoveries.length).toBe(1);
298
+ const advertisement = discoveries[0][4];
299
+
300
+ // Manufacturer: 2-byte little-endian company id (0x004c => Apple) + payload
301
+ expect(Buffer.isBuffer(advertisement.manufacturerData)).toBe(true);
302
+ expect(advertisement.manufacturerData.equals(
303
+ Buffer.concat([Buffer.from([0x4c, 0x00]), mfgPayload])
304
+ )).toBe(true);
305
+
306
+ expect(advertisement.serviceData).toEqual([
307
+ { uuid: '180d', data: svcPayload }
308
+ ]);
309
+ });
310
+
273
311
  test('discoverServices/Characteristics/Descriptors walk the cached object tree', async () => {
274
312
  const tree = adapterTree({
275
313
  '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF': {
@@ -409,4 +447,75 @@ describe('dbus/bindings', () => {
409
447
  const bindings = new DbusBindings();
410
448
  expect(bindings.addressToId('AA:BB:CC:DD:EE:FF')).toBe('aabbccddeeff');
411
449
  });
450
+
451
+ describe('peripheral id normalization on public methods', () => {
452
+ const tree = () => adapterTree({
453
+ '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF': {
454
+ 'org.bluez.Device1': { Address: 'AA:BB:CC:DD:EE:FF', AddressType: 'public', Connected: false }
455
+ },
456
+ '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0001': {
457
+ 'org.bluez.GattService1': { UUID: '0000180d-0000-1000-8000-00805f9b34fb', Primary: true }
458
+ },
459
+ '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0001/char0002': {
460
+ 'org.bluez.GattCharacteristic1': {
461
+ UUID: '00002a37-0000-1000-8000-00805f9b34fb',
462
+ Flags: ['read', 'notify']
463
+ }
464
+ }
465
+ });
466
+
467
+ const variants = [
468
+ ['canonical id', 'aabbccddeeff'],
469
+ ['uppercase id', 'AABBCCDDEEFF'],
470
+ ['mixed-case id', 'AaBbCcDdEeFf'],
471
+ ['colon MAC uppercase', 'AA:BB:CC:DD:EE:FF'],
472
+ ['colon MAC lowercase', 'aa:bb:cc:dd:ee:ff'],
473
+ ['colon MAC mixed', 'Aa:Bb:Cc:Dd:Ee:Ff']
474
+ ];
475
+
476
+ test.each(variants)('discoverServices accepts %s and emits canonical id', async (_label, input) => {
477
+ resetState(tree());
478
+ const bindings = new DbusBindings();
479
+ bindings.start();
480
+ await flush();
481
+
482
+ const services = [];
483
+ bindings.on('servicesDiscover', (...a) => services.push(a));
484
+
485
+ bindings.discoverServices(input, []);
486
+
487
+ expect(services[0]).toEqual(['aabbccddeeff', ['180d']]);
488
+ });
489
+
490
+ test.each(variants)('read accepts %s and emits canonical id', async (_label, input) => {
491
+ resetState(tree());
492
+ const bindings = new DbusBindings();
493
+ bindings.start();
494
+ await flush();
495
+
496
+ const reads = [];
497
+ bindings.on('read', (...a) => reads.push(a));
498
+
499
+ bindings.read(input, '180d', '2a37');
500
+ await flush();
501
+
502
+ expect(reads.length).toBe(1);
503
+ expect(reads[0][0]).toBe('aabbccddeeff');
504
+ });
505
+
506
+ test.each(variants)('readHandle (unsupported) emits canonical id for %s', (_label, input) => {
507
+ resetState(tree());
508
+ const bindings = new DbusBindings();
509
+ const events = [];
510
+ bindings.on('handleRead', (...a) => events.push(a));
511
+ bindings.readHandle(input, 0x42);
512
+ expect(events[0][0]).toBe('aabbccddeeff');
513
+ });
514
+
515
+ test('null/undefined peripheralUuid does not throw', () => {
516
+ const bindings = new DbusBindings();
517
+ expect(() => bindings.discoverServices(undefined, [])).not.toThrow();
518
+ expect(() => bindings.discoverServices(null, [])).not.toThrow();
519
+ });
520
+ });
412
521
  });