expo-beacon 0.3.2 → 0.4.0

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
@@ -1,8 +1,8 @@
1
1
  # expo-beacon
2
2
 
3
- An Expo module for scanning, pairing, and monitoring iBeacons in React Native apps.
3
+ An Expo module for scanning, pairing, and monitoring iBeacons and Eddystone beacons in React Native apps.
4
4
 
5
- - **Scan** for nearby iBeacons via a one-shot or continuous BLE scan
5
+ - **Scan** for nearby iBeacons and Eddystone beacons via one-shot or continuous BLE scans
6
6
  - **Pair** specific beacons for persistent tracking across app restarts
7
7
  - **Monitor** paired beacons in the background with enter/exit callbacks
8
8
  - **Distance events** fired continuously while a monitored beacon is in range
@@ -11,7 +11,7 @@ An Expo module for scanning, pairing, and monitoring iBeacons in React Native ap
11
11
  | Platform | Native implementation |
12
12
  | -------- | --------------------------------------------------------------------------------------------------------- |
13
13
  | Android | [AltBeacon](https://altbeacon.github.io/android-beacon-library/) (`org.altbeacon:android-beacon-library`) |
14
- | iOS | CoreLocation (UUID-targeted scans & monitoring) + CoreBluetooth (wildcard scanning) |
14
+ | iOS | CoreLocation (UUID-targeted scans & monitoring) + CoreBluetooth (wildcard scanning & Eddystone) |
15
15
  | Web | Not supported (throws on all calls) |
16
16
 
17
17
  ---
@@ -46,9 +46,11 @@ In Xcode under **Signing & Capabilities**, enable:
46
46
  - **Background Modes → Location updates**
47
47
  - **Background Modes → Uses Bluetooth LE accessories**
48
48
 
49
- > iOS limits apps to **20 simultaneously monitored regions**.
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
  > **Wildcard scanning**: When you call `scanForBeaconsAsync()` with an empty or omitted `uuids` array, iOS uses CoreBluetooth raw BLE scanning to discover all nearby iBeacons. This works **in the foreground only** — it is an Apple platform limitation. UUID-targeted scans and background monitoring continue to use CoreLocation.
52
+ >
53
+ > **Eddystone scanning**: Eddystone beacons use standard BLE service data (UUID `0xFEAA`), which iOS does not filter. `scanForEddystonesAsync()` and continuous scanning work in the foreground on both platforms.
52
54
 
53
55
  ### Android
54
56
 
@@ -70,13 +72,16 @@ import type {
70
72
  BeaconScanResult,
71
73
  BeaconRegionEvent,
72
74
  BeaconDistanceEvent,
75
+ EddystoneScanResult,
76
+ EddystoneRegionEvent,
77
+ EddystoneDistanceEvent,
73
78
  } from "expo-beacon";
74
79
 
75
80
  export default function App() {
76
81
  const [beacons, setBeacons] = useState<BeaconScanResult[]>([]);
77
82
 
78
83
  useEffect(() => {
79
- // 1. Pair a known beacon for monitoring
84
+ // 1. Pair a known iBeacon for monitoring
80
85
  ExpoBeacon.pairBeacon(
81
86
  "lobby-entrance",
82
87
  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
@@ -84,7 +89,14 @@ export default function App() {
84
89
  100,
85
90
  );
86
91
 
87
- // 2. Subscribe to region events
92
+ // 1b. Pair a known Eddystone-UID beacon for monitoring
93
+ ExpoBeacon.pairEddystone(
94
+ "meeting-room",
95
+ "edd1ebeac04e5defa017", // 10-byte namespace
96
+ "0123456789ab", // 6-byte instance
97
+ );
98
+
99
+ // 2. Subscribe to iBeacon region events
88
100
  const enterSub = ExpoBeacon.addListener(
89
101
  "onBeaconEnter",
90
102
  (e: BeaconRegionEvent) =>
@@ -99,7 +111,24 @@ export default function App() {
99
111
  console.log(`${e.identifier}: ${e.distance.toFixed(2)} m`),
100
112
  );
101
113
 
102
- // 3. Start background monitoring (only fires for paired beacons)
114
+ // 3. Subscribe to Eddystone region events
115
+ const eddyEnterSub = ExpoBeacon.addListener(
116
+ "onEddystoneEnter",
117
+ (e: EddystoneRegionEvent) =>
118
+ console.log(`Eddystone entered ${e.identifier} (${e.namespace})`),
119
+ );
120
+ const eddyExitSub = ExpoBeacon.addListener(
121
+ "onEddystoneExit",
122
+ (e: EddystoneRegionEvent) =>
123
+ console.log(`Eddystone exited ${e.identifier}`),
124
+ );
125
+ const eddyDistSub = ExpoBeacon.addListener(
126
+ "onEddystoneDistance",
127
+ (e: EddystoneDistanceEvent) =>
128
+ console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`),
129
+ );
130
+
131
+ // 4. Start background monitoring (fires for both paired iBeacons and Eddystones)
103
132
  ExpoBeacon.requestPermissionsAsync().then((granted) => {
104
133
  if (granted) ExpoBeacon.startMonitoring(10); // enter events within 10 m
105
134
  });
@@ -108,6 +137,9 @@ export default function App() {
108
137
  enterSub.remove();
109
138
  exitSub.remove();
110
139
  distSub.remove();
140
+ eddyEnterSub.remove();
141
+ eddyExitSub.remove();
142
+ eddyDistSub.remove();
111
143
  ExpoBeacon.stopMonitoring();
112
144
  };
113
145
  }, []);
@@ -210,20 +242,24 @@ startContinuousScan(): void
210
242
 
211
243
  Begins a **continuous BLE scan** that fires an [`onBeaconFound`](#onbeaconfound) event every time a beacon advertisement is received. Call [`stopContinuousScan()`](#stopcontinuousscan) to end it.
212
244
 
213
- Unlike `scanForBeaconsAsync`, this never resolves — it streams results in real time.
245
+ Unlike `scanForBeaconsAsync`, this never resolves — it streams results in real time. Eddystone beacons are also reported via the [`onEddystoneFound`](#oneddystonefound) event.
214
246
 
215
- > **iOS note**: Due to CoreLocation API constraints, `startContinuousScan()` on iOS only ranges beacons that have been previously paired with `pairBeacon()`. On Android, all nearby BLE beacons are reported regardless of pairing status.
247
+ **iOS note**: Due to CoreLocation API constraints, `startContinuousScan()` on iOS only ranges iBeacons that have been previously paired with `pairBeacon()`. On Android, all nearby BLE beacons are reported regardless of pairing status. Eddystone beacons are discovered on both platforms via CoreBluetooth / AltBeacon regardless of pairing.
216
248
 
217
249
  ```ts
218
250
  const sub = ExpoBeacon.addListener("onBeaconFound", (beacon) => {
219
251
  console.log("Live:", beacon.uuid, beacon.distance);
220
252
  });
253
+ const eddySub = ExpoBeacon.addListener("onEddystoneFound", (beacon) => {
254
+ console.log("Eddystone:", beacon.frameType, beacon.namespace ?? beacon.url);
255
+ });
221
256
 
222
257
  ExpoBeacon.startContinuousScan();
223
258
 
224
259
  // later, when done:
225
260
  ExpoBeacon.stopContinuousScan();
226
261
  sub.remove();
262
+ eddySub.remove();
227
263
  ```
228
264
 
229
265
  ---
@@ -299,13 +335,99 @@ paired.forEach((b) =>
299
335
 
300
336
  ---
301
337
 
338
+ ### `scanForEddystonesAsync(scanDurationMs?)`
339
+
340
+ ```ts
341
+ scanForEddystonesAsync(scanDurationMs?: number): Promise<EddystoneScanResult[]>
342
+ ```
343
+
344
+ Starts a **one-shot BLE scan** for Eddystone beacons, waits for `scanDurationMs` milliseconds, then resolves with all Eddystone beacons discovered.
345
+
346
+ | Parameter | Type | Default | Description |
347
+ | ---------------- | -------- | ------- | ---------------------------------------------- |
348
+ | `scanDurationMs` | `number` | `5000` | How long to scan in milliseconds |
349
+
350
+ Returns an array of [`EddystoneScanResult`](#eddystonescanresult) objects. Discovers both Eddystone-UID and Eddystone-URL frames.
351
+
352
+ ```ts
353
+ const eddystones = await ExpoBeacon.scanForEddystonesAsync(5000);
354
+ eddystones.forEach((b) => {
355
+ if (b.frameType === "uid") {
356
+ console.log(`UID: ns=${b.namespace} inst=${b.instance} dist=${b.distance.toFixed(1)}m`);
357
+ } else {
358
+ console.log(`URL: ${b.url} dist=${b.distance.toFixed(1)}m`);
359
+ }
360
+ });
361
+ ```
362
+
363
+ ---
364
+
365
+ ### `pairEddystone(identifier, namespace, instance)`
366
+
367
+ ```ts
368
+ pairEddystone(identifier: string, namespace: string, instance: string): void
369
+ ```
370
+
371
+ Registers an Eddystone-UID beacon for persistent region monitoring. Paired Eddystones survive app restarts (stored in `SharedPreferences` on Android, `UserDefaults` on iOS). Calling `pairEddystone` with an existing `identifier` replaces that entry.
372
+
373
+ | Parameter | Type | Description |
374
+ | ------------ | -------- | ---------------------------------------------------------------- |
375
+ | `identifier` | `string` | Your unique label for this beacon (e.g. `"meeting-room"`) |
376
+ | `namespace` | `string` | 10-byte namespace ID as a hex string (20 characters) |
377
+ | `instance` | `string` | 6-byte instance ID as a hex string (12 characters) |
378
+
379
+ ```ts
380
+ ExpoBeacon.pairEddystone(
381
+ "meeting-room",
382
+ "edd1ebeac04e5defa017",
383
+ "0123456789ab",
384
+ );
385
+ ```
386
+
387
+ ---
388
+
389
+ ### `unpairEddystone(identifier)`
390
+
391
+ ```ts
392
+ unpairEddystone(identifier: string): void
393
+ ```
394
+
395
+ Removes a previously paired Eddystone beacon.
396
+
397
+ | Parameter | Type | Description |
398
+ | ------------ | -------- | ---------------------------------- |
399
+ | `identifier` | `string` | The label used when pairing |
400
+
401
+ ```ts
402
+ ExpoBeacon.unpairEddystone("meeting-room");
403
+ ```
404
+
405
+ ---
406
+
407
+ ### `getPairedEddystones()`
408
+
409
+ ```ts
410
+ getPairedEddystones(): PairedEddystone[]
411
+ ```
412
+
413
+ Returns the list of all currently paired Eddystone beacons from persistent storage.
414
+
415
+ ```ts
416
+ const paired = ExpoBeacon.getPairedEddystones();
417
+ paired.forEach((e) =>
418
+ console.log(e.identifier, e.namespace, e.instance)
419
+ );
420
+ ```
421
+
422
+ ---
423
+
302
424
  ### `startMonitoring(options?)`
303
425
 
304
426
  ```ts
305
427
  startMonitoring(options?: MonitoringOptions | number): Promise<void>
306
428
  ```
307
429
 
308
- Starts background region monitoring for all paired beacons.
430
+ Starts background region monitoring for all paired beacons (both iBeacons and Eddystones).
309
431
 
310
432
  Accepts either a `MonitoringOptions` object or a plain `number` (backward-compatible shorthand for `maxDistance`).
311
433
 
@@ -463,7 +585,7 @@ ExpoBeacon.addListener("onBeaconDistance", (e) => {
463
585
 
464
586
  ### `onBeaconFound`
465
587
 
466
- Fired during a **continuous scan** (started with `startContinuousScan()`) each time a beacon advertisement is received.
588
+ Fired during a **continuous scan** (started with `startContinuousScan()`) each time an iBeacon advertisement is received.
467
589
 
468
590
  **Payload**: [`BeaconScanResult`](#beaconscanresult)
469
591
 
@@ -475,6 +597,66 @@ ExpoBeacon.addListener("onBeaconFound", (b) => {
475
597
 
476
598
  ---
477
599
 
600
+ ### `onEddystoneFound`
601
+
602
+ Fired during a **continuous scan** (started with `startContinuousScan()`) each time an Eddystone advertisement is received.
603
+
604
+ **Payload**: [`EddystoneScanResult`](#eddystonescanresult)
605
+
606
+ ```ts
607
+ ExpoBeacon.addListener("onEddystoneFound", (b) => {
608
+ if (b.frameType === "uid") {
609
+ console.log(`Eddystone-UID: ${b.namespace}/${b.instance} at ${b.distance.toFixed(1)} m`);
610
+ } else {
611
+ console.log(`Eddystone-URL: ${b.url}`);
612
+ }
613
+ });
614
+ ```
615
+
616
+ ---
617
+
618
+ ### `onEddystoneEnter`
619
+
620
+ Fired when a paired Eddystone-UID beacon enters range during monitoring. If `maxDistance` was set in `startMonitoring`, this only fires when the measured distance is within that threshold.
621
+
622
+ **Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
623
+
624
+ ```ts
625
+ ExpoBeacon.addListener("onEddystoneEnter", (e) => {
626
+ console.log(`Eddystone entered "${e.identifier}" (ns: ${e.namespace}) at ~${e.distance.toFixed(1)} m`);
627
+ });
628
+ ```
629
+
630
+ ---
631
+
632
+ ### `onEddystoneExit`
633
+
634
+ Fired when a paired Eddystone-UID beacon leaves range during monitoring.
635
+
636
+ **Payload**: [`EddystoneRegionEvent`](#eddystoneregionevent)
637
+
638
+ ```ts
639
+ ExpoBeacon.addListener("onEddystoneExit", (e) => {
640
+ console.log(`Eddystone left "${e.identifier}"`);
641
+ });
642
+ ```
643
+
644
+ ---
645
+
646
+ ### `onEddystoneDistance`
647
+
648
+ Fired continuously during monitoring whenever a distance update is received for a paired Eddystone beacon (~1 update/sec).
649
+
650
+ **Payload**: [`EddystoneDistanceEvent`](#eddystonedistanceevent)
651
+
652
+ ```ts
653
+ ExpoBeacon.addListener("onEddystoneDistance", (e) => {
654
+ console.log(`Eddystone ${e.identifier}: ${e.distance.toFixed(2)} m`);
655
+ });
656
+ ```
657
+
658
+ ---
659
+
478
660
  ## TypeScript Types
479
661
 
480
662
  ### `BeaconScanResult`
@@ -549,6 +731,61 @@ type BeaconDistanceEvent = {
549
731
  };
550
732
  ```
551
733
 
734
+ ### `EddystoneScanResult`
735
+
736
+ Returned by `scanForEddystonesAsync` and used in `onEddystoneFound` events.
737
+
738
+ ```ts
739
+ type EddystoneScanResult = {
740
+ frameType: "uid" | "url";
741
+ namespace?: string; // 10-byte hex string (20 chars). Present for UID frames.
742
+ instance?: string; // 6-byte hex string (12 chars). Present for UID frames.
743
+ url?: string; // Decoded URL. Present for URL frames.
744
+ rssi: number; // Signal strength in dBm
745
+ distance: number; // Estimated distance in metres
746
+ txPower: number; // Calibrated TX power
747
+ };
748
+ ```
749
+
750
+ ### `PairedEddystone`
751
+
752
+ Returned by `getPairedEddystones`.
753
+
754
+ ```ts
755
+ type PairedEddystone = {
756
+ identifier: string; // Your label
757
+ namespace: string; // 10-byte hex string (20 chars)
758
+ instance: string; // 6-byte hex string (12 chars)
759
+ };
760
+ ```
761
+
762
+ ### `EddystoneRegionEvent`
763
+
764
+ Payload for `onEddystoneEnter` and `onEddystoneExit`.
765
+
766
+ ```ts
767
+ type EddystoneRegionEvent = {
768
+ identifier: string; // Matches PairedEddystone.identifier
769
+ namespace: string;
770
+ instance: string;
771
+ event: "enter" | "exit";
772
+ distance: number; // Measured distance in metres at event time; –1 if unavailable
773
+ };
774
+ ```
775
+
776
+ ### `EddystoneDistanceEvent`
777
+
778
+ Payload for `onEddystoneDistance`.
779
+
780
+ ```ts
781
+ type EddystoneDistanceEvent = {
782
+ identifier: string;
783
+ namespace: string;
784
+ instance: string;
785
+ distance: number; // Estimated distance in metres
786
+ };
787
+ ```
788
+
552
789
  ---
553
790
 
554
791
  ### `MonitoringOptions`
@@ -626,15 +863,17 @@ Default scan timing: 1.1 s scan window every 5 s.
626
863
 
627
864
  ### iOS
628
865
 
629
- `startMonitoring()` activates `CLLocationManager` **region monitoring**. iOS can wake or relaunch the app when the device crosses a region boundary, even if the app has been force-quit. `allowsBackgroundLocationUpdates` is `true` and `pausesLocationUpdatesAutomatically` is `false`.
866
+ `startMonitoring()` activates `CLLocationManager` **region monitoring** for paired iBeacons. iOS can wake or relaunch the app when the device crosses a region boundary, even if the app has been force-quit. `allowsBackgroundLocationUpdates` is `true` and `pausesLocationUpdatesAutomatically` is `false`.
867
+
868
+ For paired Eddystone beacons, iOS uses **CoreBluetooth BLE scanning** during monitoring. BLE scanning works reliably in the foreground and while the app is backgrounded with `Uses Bluetooth LE accessories` background mode enabled. However, BLE scanning may be throttled or stopped by iOS when the app is suspended.
630
869
 
631
- > iOS limits apps to **20 simultaneously monitored regions**.
870
+ > iOS limits apps to **20 simultaneously monitored regions** (iBeacon only — Eddystone monitoring does not count toward this limit).
632
871
 
633
872
  ---
634
873
 
635
874
  ## Notifications
636
875
 
637
- A local notification is posted for every `onBeaconEnter` and `onBeaconExit` event. All notification settings can be customised via [`setNotificationConfig()`](#setnotificationconfigconfig) or inline in [`startMonitoring(options)`](#startmonitoringoptions).
876
+ A local notification is posted for every beacon enter and exit event (both iBeacon and Eddystone). All notification settings can be customised via [`setNotificationConfig()`](#setnotificationconfigconfig) or inline in [`startMonitoring(options)`](#startmonitoringoptions).
638
877
 
639
878
  ### Defaults
640
879
 
@@ -16,26 +16,50 @@ class BeaconEventReceiver(
16
16
  if (intent.action != ACTION_BEACON_EVENT) return
17
17
 
18
18
  val identifier = intent.getStringExtra("identifier") ?: return
19
- val uuid = intent.getStringExtra("uuid") ?: ""
20
- val major = intent.getIntExtra("major", 0)
21
- val minor = intent.getIntExtra("minor", 0)
22
19
  val eventType = intent.getStringExtra("event") ?: return
20
+ val beaconType = intent.getStringExtra("beaconType") ?: "ibeacon"
21
+ val distance = intent.getDoubleExtra("distance", -1.0)
23
22
 
24
- val params = mapOf(
25
- "identifier" to identifier,
26
- "uuid" to uuid,
27
- "major" to major,
28
- "minor" to minor,
29
- "event" to eventType,
30
- "distance" to intent.getDoubleExtra("distance", -1.0)
31
- )
32
-
33
- val eventName = when (eventType) {
34
- "enter" -> "onBeaconEnter"
35
- "exit" -> "onBeaconExit"
36
- "distance" -> "onBeaconDistance"
37
- else -> return
23
+ if (beaconType == "eddystone") {
24
+ val namespace = intent.getStringExtra("namespace") ?: ""
25
+ val instance = intent.getStringExtra("instance") ?: ""
26
+
27
+ val params = mapOf(
28
+ "identifier" to identifier,
29
+ "namespace" to namespace,
30
+ "instance" to instance,
31
+ "event" to eventType,
32
+ "distance" to distance
33
+ )
34
+
35
+ val eventName = when (eventType) {
36
+ "enter" -> "onEddystoneEnter"
37
+ "exit" -> "onEddystoneExit"
38
+ "distance" -> "onEddystoneDistance"
39
+ else -> return
40
+ }
41
+ onEvent(eventName, params)
42
+ } else {
43
+ val uuid = intent.getStringExtra("uuid") ?: ""
44
+ val major = intent.getIntExtra("major", 0)
45
+ val minor = intent.getIntExtra("minor", 0)
46
+
47
+ val params = mapOf(
48
+ "identifier" to identifier,
49
+ "uuid" to uuid,
50
+ "major" to major,
51
+ "minor" to minor,
52
+ "event" to eventType,
53
+ "distance" to distance
54
+ )
55
+
56
+ val eventName = when (eventType) {
57
+ "enter" -> "onBeaconEnter"
58
+ "exit" -> "onBeaconExit"
59
+ "distance" -> "onBeaconDistance"
60
+ else -> return
61
+ }
62
+ onEvent(eventName, params)
38
63
  }
39
- onEvent(eventName, params)
40
64
  }
41
65
  }
@@ -16,6 +16,8 @@ import org.json.JSONArray
16
16
 
17
17
  private const val PREFS_NAME = "expo.beacon.paired"
18
18
  private const val PREFS_KEY = "paired_beacons"
19
+ private const val EDDYSTONE_PREFS_NAME = "expo.beacon.paired_eddystones"
20
+ private const val EDDYSTONE_PREFS_KEY = "paired_eddystones"
19
21
  private const val CHANNEL_ID = "expo_beacon_channel"
20
22
  private const val FOREGROUND_NOTIF_ID = 1001
21
23
  private const val ENTER_EXIT_NOTIF_BASE_ID = 2000
@@ -84,6 +86,18 @@ class BeaconForegroundService : Service(), BeaconConsumer {
84
86
  BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25")
85
87
  )
86
88
  }
89
+ // Register Eddystone-UID parser
90
+ if (manager.beaconParsers.none { it.layout?.contains("s:0-1=feaa,m:2-2=00") == true }) {
91
+ manager.beaconParsers.add(
92
+ BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=00,p:3-3:-41,i:4-13,i:14-19")
93
+ )
94
+ }
95
+ // Register Eddystone-URL parser
96
+ if (manager.beaconParsers.none { it.layout?.contains("s:0-1=feaa,m:2-2=10") == true }) {
97
+ manager.beaconParsers.add(
98
+ BeaconParser().setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v")
99
+ )
100
+ }
87
101
  // Use continuous scanning (not JobScheduler) for foreground service
88
102
  // Guard: throws if called after ranging/monitoring has already started
89
103
  try { manager.setEnableScheduledScanJobs(false) } catch (_: IllegalStateException) {}
@@ -126,6 +140,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
126
140
  val json = prefs.getString(PREFS_KEY, "[]") ?: "[]"
127
141
  val beacons = try { JSONArray(json) } catch (_: Exception) { JSONArray() }
128
142
 
143
+ // Load paired Eddystones
144
+ val eddystonePrefs: SharedPreferences = getSharedPreferences(EDDYSTONE_PREFS_NAME, Context.MODE_PRIVATE)
145
+ val eddystoneJson = eddystonePrefs.getString(EDDYSTONE_PREFS_KEY, "[]") ?: "[]"
146
+ val eddystones = try { JSONArray(eddystoneJson) } catch (_: Exception) { JSONArray() }
147
+
129
148
  // Stop previous regions and distance-log ranging
130
149
  distanceLogRegions.forEach {
131
150
  try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
@@ -136,6 +155,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
136
155
  }
137
156
  monitoredRegions.clear()
138
157
 
158
+ // iBeacon regions
139
159
  for (i in 0 until beacons.length()) {
140
160
  val b = beacons.getJSONObject(i)
141
161
  val region = Region(
@@ -160,6 +180,34 @@ class BeaconForegroundService : Service(), BeaconConsumer {
160
180
  }
161
181
  }
162
182
  }
183
+
184
+ // Eddystone-UID regions
185
+ for (i in 0 until eddystones.length()) {
186
+ val e = eddystones.getJSONObject(i)
187
+ val identifier = e.getString("identifier")
188
+ val namespace = e.getString("namespace")
189
+ val instance = e.getString("instance")
190
+ val region = Region(
191
+ identifier,
192
+ Identifier.parse("0x$namespace"),
193
+ Identifier.parse("0x$instance"),
194
+ null
195
+ )
196
+ monitoredRegions.add(region)
197
+ try {
198
+ beaconManager.startMonitoringBeaconsInRegion(region)
199
+ } catch (ex: RemoteException) {
200
+ ex.printStackTrace()
201
+ }
202
+ if (distanceLogRegions.add(region)) {
203
+ try {
204
+ beaconManager.startRangingBeaconsInRegion(region)
205
+ } catch (ex: RemoteException) {
206
+ distanceLogRegions.remove(region)
207
+ ex.printStackTrace()
208
+ }
209
+ }
210
+ }
163
211
  }
164
212
 
165
213
  private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
@@ -284,13 +332,25 @@ class BeaconForegroundService : Service(), BeaconConsumer {
284
332
  }
285
333
 
286
334
  private fun sendBeaconBroadcast(region: Region, eventType: String, distance: Double) {
335
+ // Determine if this is an Eddystone region based on identifier format
336
+ // Eddystone regions have id1 as a hex namespace (not a UUID)
337
+ val id1Str = region.id1?.toString() ?: ""
338
+ val isEddystone = id1Str.startsWith("0x")
339
+
287
340
  val intent = Intent(ACTION_BEACON_EVENT).apply {
288
341
  putExtra("identifier", region.uniqueId)
289
- putExtra("uuid", region.id1?.toString() ?: "")
290
- putExtra("major", region.id2?.toInt() ?: 0)
291
- putExtra("minor", region.id3?.toInt() ?: 0)
292
342
  putExtra("event", eventType)
293
343
  putExtra("distance", distance)
344
+ if (isEddystone) {
345
+ putExtra("beaconType", "eddystone")
346
+ putExtra("namespace", id1Str.removePrefix("0x"))
347
+ putExtra("instance", region.id2?.toString()?.removePrefix("0x") ?: "")
348
+ } else {
349
+ putExtra("beaconType", "ibeacon")
350
+ putExtra("uuid", id1Str)
351
+ putExtra("major", region.id2?.toInt() ?: 0)
352
+ putExtra("minor", region.id3?.toInt() ?: 0)
353
+ }
294
354
  setPackage(packageName)
295
355
  }
296
356
  sendBroadcast(intent)