expo-beacon 0.4.0 → 0.4.1
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.
package/README.md
CHANGED
|
@@ -48,9 +48,9 @@ In Xcode under **Signing & Capabilities**, enable:
|
|
|
48
48
|
|
|
49
49
|
> iOS limits apps to **20 simultaneously monitored regions** (iBeacon only — Eddystone beacons are monitored via BLE and do not count toward this limit).
|
|
50
50
|
>
|
|
51
|
-
> **
|
|
51
|
+
> **iBeacon scanning on iOS**: Apple strips iBeacon manufacturer data from CoreBluetooth BLE advertisements, so wildcard iBeacon discovery is **not possible** on iOS. You must provide at least one proximity UUID, or pair beacons first (the module will use paired beacon UUIDs automatically). UUID-targeted scans use CoreLocation ranging and work in both foreground and background.
|
|
52
52
|
>
|
|
53
|
-
> **Eddystone scanning**: Eddystone beacons use standard BLE service data (UUID `0xFEAA`), which iOS does not filter. `scanForEddystonesAsync()` and continuous scanning
|
|
53
|
+
> **Eddystone scanning**: Eddystone beacons use standard BLE service data (UUID `0xFEAA`), which iOS does not filter. `scanForEddystonesAsync()` and continuous scanning discover Eddystones on both platforms without restrictions.
|
|
54
54
|
|
|
55
55
|
### Android
|
|
56
56
|
|
|
@@ -203,30 +203,30 @@ Starts a **one-shot BLE scan**, waits for `scanDurationMs` milliseconds, then re
|
|
|
203
203
|
|
|
204
204
|
| Parameter | Type | Default | Description |
|
|
205
205
|
| ---------------- | ---------- | ------- | ---------------------------------------- |
|
|
206
|
-
| `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by.
|
|
206
|
+
| `uuids` | `string[]` | `[]` | Proximity UUIDs to filter by. **iOS**: at least one UUID is required (or have paired beacons). **Android**: pass `[]` for a wildcard scan. |
|
|
207
207
|
| `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds (1–60 000 recommended) |
|
|
208
208
|
|
|
209
209
|
Returns an array of [`BeaconScanResult`](#beaconscanresult) objects. Rejects with `SCAN_IN_PROGRESS` if another scan is already running.
|
|
210
210
|
|
|
211
|
-
**
|
|
211
|
+
**Platform differences**
|
|
212
212
|
|
|
213
|
-
| |
|
|
213
|
+
| | Empty UUIDs (`[]`) | Targeted (`['UUID-1', …]`) |
|
|
214
214
|
|---|---|---|
|
|
215
215
|
| **Android** | Discovers all iBeacons via AltBeacon | Filters results to matching UUIDs |
|
|
216
|
-
| **iOS** |
|
|
216
|
+
| **iOS** | Uses paired beacon UUIDs automatically. Rejects if no UUIDs and no paired beacons. | CoreLocation ranging (works in foreground & background) |
|
|
217
217
|
|
|
218
|
-
> **iOS limitation**: Wildcard scanning
|
|
218
|
+
> **iOS limitation**: Apple strips iBeacon manufacturer data from CoreBluetooth BLE advertisements. Wildcard iBeacon scanning (no UUID filter) is not possible on iOS. When you pass an empty `uuids` array, the module automatically uses UUIDs from your paired beacons. If no beacons are paired, the call rejects with `WILDCARD_NOT_SUPPORTED`. For Eddystone scanning, use `scanForEddystonesAsync()` instead — it works without restrictions on both platforms.
|
|
219
219
|
|
|
220
220
|
```ts
|
|
221
|
-
//
|
|
222
|
-
const all = await ExpoBeacon.scanForBeaconsAsync([], 5000);
|
|
223
|
-
|
|
224
|
-
// Targeted scan — only beacons matching these UUIDs
|
|
221
|
+
// Scan by UUID — works on both platforms
|
|
225
222
|
const filtered = await ExpoBeacon.scanForBeaconsAsync(
|
|
226
223
|
['E2C56DB5-DFFB-48D2-B060-D0F5A71096E0'],
|
|
227
224
|
8000,
|
|
228
225
|
);
|
|
229
226
|
|
|
227
|
+
// Wildcard scan — Android only. On iOS, uses paired beacon UUIDs.
|
|
228
|
+
const all = await ExpoBeacon.scanForBeaconsAsync([], 5000);
|
|
229
|
+
|
|
230
230
|
filtered.forEach((b) =>
|
|
231
231
|
console.log(`${b.uuid} major=${b.major} minor=${b.minor} dist=${b.distance.toFixed(1)}m rssi=${b.rssi}dBm`)
|
|
232
232
|
);
|
|
@@ -5,10 +5,12 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
5
5
|
* Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.
|
|
6
6
|
*
|
|
7
7
|
* Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* On iOS, at least one UUID is required — Apple strips iBeacon data from BLE
|
|
9
|
+
* advertisements, making wildcard discovery impossible. When you pass an empty
|
|
10
|
+
* array, the module automatically uses UUIDs from paired beacons.
|
|
11
|
+
* On Android, pass an empty array to discover all nearby iBeacons.
|
|
10
12
|
*
|
|
11
|
-
* @param uuids Proximity UUIDs to filter by. Empty/omitted = wildcard
|
|
13
|
+
* @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).
|
|
12
14
|
* @param scanDuration Duration in ms (default 5000)
|
|
13
15
|
*/
|
|
14
16
|
scanForBeaconsAsync(uuids?: string[], scanDuration?: number): Promise<BeaconScanResult[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;CAC5C;;AAED,wBAAmE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAiHzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
|
|
@@ -52,11 +52,9 @@ public class ExpoBeaconModule: Module {
|
|
|
52
52
|
// Constraints started exclusively for continuous scan (not shared with distance ranging)
|
|
53
53
|
private var continuousScanOnlyConstraints: [CLBeaconIdentityConstraint] = []
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// CoreBluetooth scan state (Eddystone + monitoring)
|
|
56
56
|
private lazy var bluetoothDelegate = BluetoothDelegate(module: self)
|
|
57
57
|
private var centralManager: CBCentralManager?
|
|
58
|
-
private var wildcardScannedBeacons: [[String: Any]] = []
|
|
59
|
-
private var wildcardScanTimer: DispatchWorkItem?
|
|
60
58
|
|
|
61
59
|
// Eddystone (CoreBluetooth) scan state
|
|
62
60
|
fileprivate var eddystoneScanPromise: Promise?
|
|
@@ -450,89 +448,6 @@ public class ExpoBeaconModule: Module {
|
|
|
450
448
|
scannedBeacons = []
|
|
451
449
|
}
|
|
452
450
|
|
|
453
|
-
// MARK: - Wildcard (CoreBluetooth) scan
|
|
454
|
-
|
|
455
|
-
private func startWildcardScan(durationMs: Int) {
|
|
456
|
-
wildcardScannedBeacons = []
|
|
457
|
-
|
|
458
|
-
if centralManager == nil {
|
|
459
|
-
// Creating CBCentralManager triggers a Bluetooth power-on check.
|
|
460
|
-
// Once .poweredOn, the delegate calls beginWildcardScanning().
|
|
461
|
-
centralManager = CBCentralManager(delegate: bluetoothDelegate, queue: .main)
|
|
462
|
-
} else if centralManager?.state == .poweredOn {
|
|
463
|
-
beginWildcardScanning()
|
|
464
|
-
}
|
|
465
|
-
// If state is not yet .poweredOn the delegate will call beginWildcardScanning()
|
|
466
|
-
// when centralManagerDidUpdateState fires.
|
|
467
|
-
|
|
468
|
-
let timer = DispatchWorkItem { [weak self] in
|
|
469
|
-
self?.stopWildcardScanAndResolve()
|
|
470
|
-
}
|
|
471
|
-
wildcardScanTimer = timer
|
|
472
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(durationMs), execute: timer)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
fileprivate func beginWildcardScanning() {
|
|
476
|
-
guard scanPromise != nil, wildcardScanTimer != nil else { return }
|
|
477
|
-
centralManager?.scanForPeripherals(
|
|
478
|
-
withServices: nil,
|
|
479
|
-
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
480
|
-
)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
fileprivate func handleWildcardDiscovery(advertisementData: [String: Any], rssi: NSNumber) {
|
|
484
|
-
guard let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data,
|
|
485
|
-
mfgData.count >= 25 else { return }
|
|
486
|
-
|
|
487
|
-
// iBeacon format: Apple company ID (0x4C 0x00) + type 0x02 + length 0x15
|
|
488
|
-
guard mfgData[0] == 0x4C, mfgData[1] == 0x00,
|
|
489
|
-
mfgData[2] == 0x02, mfgData[3] == 0x15 else { return }
|
|
490
|
-
|
|
491
|
-
// Parse UUID (bytes 4–19)
|
|
492
|
-
let uuidBytes = [UInt8](mfgData[4..<20])
|
|
493
|
-
let uuid = NSUUID(uuidBytes: uuidBytes) as UUID
|
|
494
|
-
|
|
495
|
-
// Parse major (bytes 20–21, big-endian) and minor (bytes 22–23, big-endian)
|
|
496
|
-
let major = Int(UInt16(mfgData[20]) << 8 | UInt16(mfgData[21]))
|
|
497
|
-
let minor = Int(UInt16(mfgData[22]) << 8 | UInt16(mfgData[23]))
|
|
498
|
-
|
|
499
|
-
// TX power (byte 24, signed)
|
|
500
|
-
let txPower = Int(Int8(bitPattern: mfgData[24]))
|
|
501
|
-
|
|
502
|
-
let rssiValue = rssi.intValue
|
|
503
|
-
let distance = Self.calculateDistance(rssi: rssiValue, txPower: txPower)
|
|
504
|
-
|
|
505
|
-
let result: [String: Any] = [
|
|
506
|
-
"uuid": uuid.uuidString.uppercased(),
|
|
507
|
-
"major": major,
|
|
508
|
-
"minor": minor,
|
|
509
|
-
"rssi": rssiValue,
|
|
510
|
-
"distance": distance,
|
|
511
|
-
"txPower": txPower
|
|
512
|
-
]
|
|
513
|
-
wildcardScannedBeacons.append(result)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
private func stopWildcardScanAndResolve() {
|
|
517
|
-
wildcardScanTimer?.cancel()
|
|
518
|
-
wildcardScanTimer = nil
|
|
519
|
-
stopBleScanIfUnneeded()
|
|
520
|
-
|
|
521
|
-
// Deduplicate by uuid:major:minor, keeping the last (freshest) reading
|
|
522
|
-
var seen = Set<String>()
|
|
523
|
-
var deduped: [[String: Any]] = []
|
|
524
|
-
for beacon in wildcardScannedBeacons.reversed() {
|
|
525
|
-
let key = "\(beacon["uuid"] ?? ""):\(beacon["major"] ?? ""):\(beacon["minor"] ?? "")"
|
|
526
|
-
guard !seen.contains(key) else { continue }
|
|
527
|
-
seen.insert(key)
|
|
528
|
-
deduped.append(beacon)
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
scanPromise?.resolve(deduped)
|
|
532
|
-
scanPromise = nil
|
|
533
|
-
wildcardScannedBeacons = []
|
|
534
|
-
}
|
|
535
|
-
|
|
536
451
|
// MARK: - Eddystone Scan
|
|
537
452
|
|
|
538
453
|
private func startEddystoneScan(durationMs: Int) {
|
|
@@ -727,7 +642,7 @@ public class ExpoBeaconModule: Module {
|
|
|
727
642
|
}
|
|
728
643
|
|
|
729
644
|
private func stopBleScanIfUnneeded() {
|
|
730
|
-
guard
|
|
645
|
+
guard eddystoneScanTimer == nil && !continuousScanActive && !eddystoneMonitoringActive else { return }
|
|
731
646
|
centralManager?.stopScan()
|
|
732
647
|
}
|
|
733
648
|
|
|
@@ -1105,7 +1020,7 @@ private class LocationDelegate: NSObject, CLLocationManagerDelegate {
|
|
|
1105
1020
|
}
|
|
1106
1021
|
}
|
|
1107
1022
|
|
|
1108
|
-
// MARK: - CBCentralManagerDelegate (
|
|
1023
|
+
// MARK: - CBCentralManagerDelegate (Eddystone BLE scanning)
|
|
1109
1024
|
|
|
1110
1025
|
private class BluetoothDelegate: NSObject, CBCentralManagerDelegate {
|
|
1111
1026
|
private weak var module: ExpoBeaconModule?
|
|
@@ -1137,7 +1052,6 @@ private class BluetoothDelegate: NSObject, CBCentralManagerDelegate {
|
|
|
1137
1052
|
didDiscover peripheral: CBPeripheral,
|
|
1138
1053
|
advertisementData: [String: Any],
|
|
1139
1054
|
rssi RSSI: NSNumber) {
|
|
1140
|
-
module?.handleWildcardDiscovery(advertisementData: advertisementData, rssi: RSSI)
|
|
1141
1055
|
module?.handleEddystoneDiscovery(advertisementData: advertisementData, rssi: RSSI)
|
|
1142
1056
|
}
|
|
1143
1057
|
}
|
package/package.json
CHANGED
package/src/ExpoBeaconModule.ts
CHANGED
|
@@ -15,10 +15,12 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
15
15
|
* Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.
|
|
16
16
|
*
|
|
17
17
|
* Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* On iOS, at least one UUID is required — Apple strips iBeacon data from BLE
|
|
19
|
+
* advertisements, making wildcard discovery impossible. When you pass an empty
|
|
20
|
+
* array, the module automatically uses UUIDs from paired beacons.
|
|
21
|
+
* On Android, pass an empty array to discover all nearby iBeacons.
|
|
20
22
|
*
|
|
21
|
-
* @param uuids Proximity UUIDs to filter by. Empty/omitted = wildcard
|
|
23
|
+
* @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).
|
|
22
24
|
* @param scanDuration Duration in ms (default 5000)
|
|
23
25
|
*/
|
|
24
26
|
scanForBeaconsAsync(
|