react-native-nitro-compass 0.1.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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/NitroCompass.podspec +31 -0
  3. package/README.md +206 -0
  4. package/android/CMakeLists.txt +32 -0
  5. package/android/build.gradle +148 -0
  6. package/android/fix-prefab.gradle +51 -0
  7. package/android/gradle.properties +5 -0
  8. package/android/src/main/AndroidManifest.xml +2 -0
  9. package/android/src/main/cpp/cpp-adapter.cpp +9 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +481 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrocompass/NitroCompassPackage.kt +18 -0
  12. package/app.plugin.js +16 -0
  13. package/ios/Bridge.h +8 -0
  14. package/ios/HybridNitroCompass.swift +473 -0
  15. package/lib/commonjs/hook.js +69 -0
  16. package/lib/commonjs/hook.js.map +1 -0
  17. package/lib/commonjs/index.js +39 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/multiplex.js +109 -0
  20. package/lib/commonjs/multiplex.js.map +1 -0
  21. package/lib/commonjs/native.js +9 -0
  22. package/lib/commonjs/native.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/specs/NitroCompass.nitro.js +6 -0
  25. package/lib/commonjs/specs/NitroCompass.nitro.js.map +1 -0
  26. package/lib/module/hook.js +65 -0
  27. package/lib/module/hook.js.map +1 -0
  28. package/lib/module/index.js +6 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/multiplex.js +103 -0
  31. package/lib/module/multiplex.js.map +1 -0
  32. package/lib/module/native.js +5 -0
  33. package/lib/module/native.js.map +1 -0
  34. package/lib/module/specs/NitroCompass.nitro.js +4 -0
  35. package/lib/module/specs/NitroCompass.nitro.js.map +1 -0
  36. package/lib/typescript/src/hook.d.ts +49 -0
  37. package/lib/typescript/src/hook.d.ts.map +1 -0
  38. package/lib/typescript/src/index.d.ts +8 -0
  39. package/lib/typescript/src/index.d.ts.map +1 -0
  40. package/lib/typescript/src/multiplex.d.ts +38 -0
  41. package/lib/typescript/src/multiplex.d.ts.map +1 -0
  42. package/lib/typescript/src/native.d.ts +3 -0
  43. package/lib/typescript/src/native.d.ts.map +1 -0
  44. package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +176 -0
  45. package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -0
  46. package/nitro.json +30 -0
  47. package/nitrogen/generated/.gitattributes +1 -0
  48. package/nitrogen/generated/android/NitroCompass+autolinking.cmake +81 -0
  49. package/nitrogen/generated/android/NitroCompass+autolinking.gradle +27 -0
  50. package/nitrogen/generated/android/NitroCompassOnLoad.cpp +60 -0
  51. package/nitrogen/generated/android/NitroCompassOnLoad.hpp +34 -0
  52. package/nitrogen/generated/android/c++/JAccuracyQuality.hpp +64 -0
  53. package/nitrogen/generated/android/c++/JCompassSample.hpp +61 -0
  54. package/nitrogen/generated/android/c++/JFunc_void_AccuracyQuality.hpp +77 -0
  55. package/nitrogen/generated/android/c++/JFunc_void_CompassSample.hpp +77 -0
  56. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  57. package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +143 -0
  58. package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +75 -0
  59. package/nitrogen/generated/android/c++/JPermissionStatus.hpp +61 -0
  60. package/nitrogen/generated/android/c++/JSensorDiagnostics.hpp +58 -0
  61. package/nitrogen/generated/android/c++/JSensorKind.hpp +61 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/AccuracyQuality.kt +25 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/CompassSample.kt +56 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_AccuracyQuality.kt +80 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_CompassSample.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_bool.kt +80 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +118 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/NitroCompassOnLoad.kt +35 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/PermissionStatus.kt +24 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorDiagnostics.kt +51 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorKind.kt +24 -0
  72. package/nitrogen/generated/ios/NitroCompass+autolinking.rb +62 -0
  73. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.cpp +73 -0
  74. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.hpp +267 -0
  75. package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Umbrella.hpp +61 -0
  76. package/nitrogen/generated/ios/NitroCompassAutolinking.mm +33 -0
  77. package/nitrogen/generated/ios/NitroCompassAutolinking.swift +26 -0
  78. package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.cpp +11 -0
  79. package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +180 -0
  80. package/nitrogen/generated/ios/swift/AccuracyQuality.swift +48 -0
  81. package/nitrogen/generated/ios/swift/CompassSample.swift +34 -0
  82. package/nitrogen/generated/ios/swift/Func_void_AccuracyQuality.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_CompassSample.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +46 -0
  85. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  86. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  87. package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +67 -0
  88. package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +309 -0
  89. package/nitrogen/generated/ios/swift/PermissionStatus.swift +44 -0
  90. package/nitrogen/generated/ios/swift/SensorDiagnostics.swift +29 -0
  91. package/nitrogen/generated/ios/swift/SensorKind.swift +44 -0
  92. package/nitrogen/generated/shared/c++/AccuracyQuality.hpp +84 -0
  93. package/nitrogen/generated/shared/c++/CompassSample.hpp +87 -0
  94. package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +33 -0
  95. package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +87 -0
  96. package/nitrogen/generated/shared/c++/PermissionStatus.hpp +80 -0
  97. package/nitrogen/generated/shared/c++/SensorDiagnostics.hpp +84 -0
  98. package/nitrogen/generated/shared/c++/SensorKind.hpp +80 -0
  99. package/package.json +136 -0
  100. package/react-native.config.js +11 -0
  101. package/src/hook.ts +118 -0
  102. package/src/index.ts +28 -0
  103. package/src/multiplex.ts +117 -0
  104. package/src/native.ts +5 -0
  105. package/src/specs/NitroCompass.nitro.ts +193 -0
@@ -0,0 +1,473 @@
1
+ //
2
+ // HybridNitroCompass.swift
3
+ // NitroCompass
4
+ //
5
+ // iOS implementation of the NitroCompass HybridObject. Uses
6
+ // CLLocationManager for the heading source — Apple's stack already does
7
+ // proper sensor fusion natively, so the Swift side stays simple.
8
+ //
9
+
10
+ import Foundation
11
+ import CoreLocation
12
+ import CoreMotion
13
+ import NitroModules
14
+ import UIKit
15
+
16
+ // Earth's magnetic field magnitude is typically 25–65 µT. Anything
17
+ // outside this band (with a small grace margin) is treated as
18
+ // external interference — laptops, monitors, car engines, and
19
+ // structural steel routinely push readings well above 100 µT.
20
+ private let earthFieldMinUT: Double = 20
21
+ private let earthFieldMaxUT: Double = 70
22
+
23
+ class HybridNitroCompass: HybridNitroCompassSpec {
24
+ // CLLocationManager must be created on a thread with an active run loop
25
+ // (typically main). Nitro can instantiate HybridObjects off-main, so we
26
+ // hop to main for construction and configuration.
27
+ private var manager: CLLocationManager!
28
+ private var delegateProxy: HeadingDelegate?
29
+ private var onSample: ((CompassSample) -> Void)?
30
+ private var orientationObserver: NSObjectProtocol?
31
+ private var backgroundObserver: NSObjectProtocol?
32
+ private var foregroundObserver: NSObjectProtocol?
33
+ private var declinationDeg: Double = 0
34
+ private var lastSample: CompassSample?
35
+ private var lastQuality: AccuracyQuality?
36
+ private var calibrationCb: ((AccuracyQuality) -> Void)?
37
+ private var interferenceCb: ((Bool) -> Void)?
38
+ private let motionManager = CMMotionManager()
39
+ private let motionQueue: OperationQueue = {
40
+ let q = OperationQueue()
41
+ q.name = "NitroCompass.motion"
42
+ q.maxConcurrentOperationCount = 1
43
+ return q
44
+ }()
45
+ private var lastInterference: Bool?
46
+ private var pauseOnBackground: Bool = true
47
+ private var started: Bool = false
48
+ private var isSubscribed: Bool = false
49
+ private var activeFilterDegrees: Double = 1
50
+ // Mirrors UIApplication.applicationState. Updated from background/foreground
51
+ // notification observers (delivered on main) so JS-thread callers don't
52
+ // have to hop to main to read it.
53
+ private var appIsBackgrounded: Bool = false
54
+ // Holds an in-flight permission request. CLLocationManager.delegate is
55
+ // weak, so we own the resolver here for the duration of the request.
56
+ private var authResolver: AuthRequestResolver?
57
+
58
+ override init() {
59
+ super.init()
60
+ let setup = {
61
+ let m = CLLocationManager()
62
+ m.headingFilter = 1
63
+ m.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
64
+ self.manager = m
65
+ }
66
+ if Thread.isMainThread {
67
+ setup()
68
+ } else {
69
+ DispatchQueue.main.sync(execute: setup)
70
+ }
71
+ }
72
+
73
+ deinit {
74
+ stopInternal()
75
+ }
76
+
77
+ func start(filterDegrees: Double, onHeading: @escaping (_ sample: CompassSample) -> Void) throws {
78
+ guard CLLocationManager.headingAvailable() else {
79
+ throw NSError(
80
+ domain: "NitroCompass",
81
+ code: 1,
82
+ userInfo: [NSLocalizedDescriptionKey: "Heading unavailable on this device"]
83
+ )
84
+ }
85
+
86
+ // CLLocationManager.startUpdatingHeading silently delivers nothing
87
+ // when location authorization is denied. Surface that explicitly so
88
+ // callers don't wait on a callback that will never fire.
89
+ // .notDetermined still proceeds — the host may request later.
90
+ let authStatus = manager.authorizationStatus
91
+ if authStatus == .denied || authStatus == .restricted {
92
+ throw NSError(
93
+ domain: "NitroCompass",
94
+ code: 2,
95
+ userInfo: [NSLocalizedDescriptionKey: "Location authorization denied — request authorization before calling start()"]
96
+ )
97
+ }
98
+
99
+ stopInternal()
100
+
101
+ onSample = onHeading
102
+ activeFilterDegrees = filterDegrees
103
+ started = true
104
+
105
+ let proxy = HeadingDelegate(
106
+ onSample: { [weak self] heading, accuracy in
107
+ self?.deliver(heading: heading, accuracy: accuracy)
108
+ },
109
+ onCalibrationOverride: { [weak self] in
110
+ self?.fireCalibration(.unreliable)
111
+ }
112
+ )
113
+ delegateProxy = proxy
114
+ manager.delegate = proxy
115
+
116
+ orientationObserver = NotificationCenter.default.addObserver(
117
+ forName: UIDevice.orientationDidChangeNotification,
118
+ object: nil,
119
+ queue: .main
120
+ ) { [weak self] _ in
121
+ self?.applyHeadingOrientation()
122
+ }
123
+
124
+ backgroundObserver = NotificationCenter.default.addObserver(
125
+ forName: UIApplication.didEnterBackgroundNotification,
126
+ object: nil,
127
+ queue: .main
128
+ ) { [weak self] _ in
129
+ self?.handleBackground()
130
+ }
131
+
132
+ foregroundObserver = NotificationCenter.default.addObserver(
133
+ forName: UIApplication.willEnterForegroundNotification,
134
+ object: nil,
135
+ queue: .main
136
+ ) { [weak self] _ in
137
+ self?.handleForeground()
138
+ }
139
+
140
+ // Read background state synchronously so a start() call from
141
+ // background correctly skips the initial subscribe under
142
+ // pauseOnBackground=true. Without this the lifecycle observers
143
+ // wouldn't fire (we're already in BG) and a useless subscription
144
+ // would sit there until the next FG↔BG cycle.
145
+ let backgroundedAtStart: Bool
146
+ if Thread.isMainThread {
147
+ backgroundedAtStart = UIApplication.shared.applicationState == .background
148
+ } else {
149
+ var bg = false
150
+ DispatchQueue.main.sync {
151
+ bg = UIApplication.shared.applicationState == .background
152
+ }
153
+ backgroundedAtStart = bg
154
+ }
155
+ appIsBackgrounded = backgroundedAtStart
156
+
157
+ DispatchQueue.main.async { [weak self] in
158
+ UIDevice.current.beginGeneratingDeviceOrientationNotifications()
159
+ self?.applyHeadingOrientation()
160
+ }
161
+
162
+ if !(pauseOnBackground && backgroundedAtStart) {
163
+ subscribe()
164
+ }
165
+ }
166
+
167
+ func stop() throws {
168
+ stopInternal()
169
+ }
170
+
171
+ func hasCompass() throws -> Bool {
172
+ return CLLocationManager.headingAvailable()
173
+ }
174
+
175
+ func isStarted() throws -> Bool {
176
+ return started
177
+ }
178
+
179
+ func setFilter(degrees: Double) throws {
180
+ activeFilterDegrees = degrees
181
+ if isSubscribed {
182
+ manager.headingFilter = degrees == 0 ? kCLHeadingFilterNone : degrees
183
+ }
184
+ }
185
+
186
+ func getDiagnostics() throws -> SensorDiagnostics? {
187
+ guard CLLocationManager.headingAvailable() else { return nil }
188
+ return SensorDiagnostics(sensor: .corelocation)
189
+ }
190
+
191
+ func getCurrentHeading() throws -> CompassSample? {
192
+ return lastSample
193
+ }
194
+
195
+ func setDeclination(degrees: Double) throws {
196
+ declinationDeg = degrees
197
+ }
198
+
199
+ func setOnCalibrationNeeded(onChange: @escaping (_ quality: AccuracyQuality) -> Void) throws {
200
+ calibrationCb = onChange
201
+ }
202
+
203
+ func setOnInterferenceDetected(onChange: @escaping (_ interferenceDetected: Bool) -> Void) throws {
204
+ interferenceCb = onChange
205
+ // Replay current state so a late-registering consumer sees the truth
206
+ // instead of waiting for the next transition (which may never come
207
+ // if the field stays stable).
208
+ if let last = lastInterference { onChange(last) }
209
+ }
210
+
211
+ func setPauseOnBackground(enabled: Bool) throws {
212
+ pauseOnBackground = enabled
213
+ if enabled, started, isSubscribed, appIsBackgrounded {
214
+ unsubscribe()
215
+ } else if !enabled, started, !isSubscribed {
216
+ subscribe()
217
+ }
218
+ }
219
+
220
+ func getPermissionStatus() throws -> PermissionStatus {
221
+ return Self.mapAuthStatus(manager.authorizationStatus)
222
+ }
223
+
224
+ func requestPermission() throws -> Promise<PermissionStatus> {
225
+ let current = manager.authorizationStatus
226
+ if current != .notDetermined {
227
+ return Promise.resolved(withResult: Self.mapAuthStatus(current))
228
+ }
229
+
230
+ let promise = Promise<PermissionStatus>()
231
+
232
+ DispatchQueue.main.async { [weak self] in
233
+ guard let self = self else {
234
+ promise.reject(withError: NSError(
235
+ domain: "NitroCompass",
236
+ code: 4,
237
+ userInfo: [NSLocalizedDescriptionKey: "Compass instance was deallocated"]
238
+ ))
239
+ return
240
+ }
241
+
242
+ // Cancel any in-flight request — only one outstanding system
243
+ // prompt makes sense.
244
+ self.authResolver?.cancel()
245
+
246
+ let resolver = AuthRequestResolver(promise: promise)
247
+ // Save the existing delegate so heading delivery can resume after
248
+ // the auth callback fires. CLLocationManager.delegate is weak, so
249
+ // we have to keep our resolver alive on `self` for the duration.
250
+ resolver.savedDelegate = self.manager.delegate
251
+ resolver.onResolved = { [weak self] in self?.authResolver = nil }
252
+ self.authResolver = resolver
253
+ self.manager.delegate = resolver
254
+ self.manager.requestWhenInUseAuthorization()
255
+ }
256
+
257
+ return promise
258
+ }
259
+
260
+ private static func mapAuthStatus(_ status: CLAuthorizationStatus) -> PermissionStatus {
261
+ switch status {
262
+ case .authorizedAlways, .authorizedWhenInUse:
263
+ return .granted
264
+ case .denied, .restricted:
265
+ return .denied
266
+ case .notDetermined:
267
+ return .unknown
268
+ @unknown default:
269
+ return .unknown
270
+ }
271
+ }
272
+
273
+ // MARK: - Helpers
274
+
275
+ private func subscribe() {
276
+ guard !isSubscribed else { return }
277
+ manager.headingFilter = activeFilterDegrees == 0 ? kCLHeadingFilterNone : activeFilterDegrees
278
+ manager.startUpdatingHeading()
279
+ startMagnetometerIfAvailable()
280
+ isSubscribed = true
281
+ }
282
+
283
+ private func unsubscribe() {
284
+ guard isSubscribed else { return }
285
+ manager.stopUpdatingHeading()
286
+ stopMagnetometerIfRunning()
287
+ isSubscribed = false
288
+ }
289
+
290
+ private func startMagnetometerIfAvailable() {
291
+ guard motionManager.isDeviceMotionAvailable,
292
+ !motionManager.isDeviceMotionActive else { return }
293
+ motionManager.deviceMotionUpdateInterval = 0.2 // 5Hz
294
+ motionManager.startDeviceMotionUpdates(to: motionQueue) { [weak self] motion, _ in
295
+ guard let self = self, let m = motion else { return }
296
+ let cal = m.magneticField
297
+ if cal.accuracy == .uncalibrated { return }
298
+ let f = cal.field
299
+ let magnitude = sqrt(f.x * f.x + f.y * f.y + f.z * f.z)
300
+ self.evaluateInterference(magnitude: magnitude)
301
+ }
302
+ }
303
+
304
+ private func stopMagnetometerIfRunning() {
305
+ if motionManager.isDeviceMotionActive {
306
+ motionManager.stopDeviceMotionUpdates()
307
+ }
308
+ }
309
+
310
+ private func evaluateInterference(magnitude: Double) {
311
+ let isInterference = magnitude < earthFieldMinUT || magnitude > earthFieldMaxUT
312
+ if lastInterference == isInterference { return }
313
+ lastInterference = isInterference
314
+ interferenceCb?(isInterference)
315
+ }
316
+
317
+ private func handleBackground() {
318
+ appIsBackgrounded = true
319
+ if pauseOnBackground, started, isSubscribed {
320
+ unsubscribe()
321
+ }
322
+ }
323
+
324
+ private func handleForeground() {
325
+ appIsBackgrounded = false
326
+ if pauseOnBackground, started, !isSubscribed {
327
+ subscribe()
328
+ }
329
+ }
330
+
331
+ private func stopInternal() {
332
+ started = false
333
+ unsubscribe()
334
+ if delegateProxy != nil {
335
+ manager.delegate = nil
336
+ delegateProxy = nil
337
+ }
338
+ if let observer = orientationObserver {
339
+ NotificationCenter.default.removeObserver(observer)
340
+ orientationObserver = nil
341
+ DispatchQueue.main.async {
342
+ UIDevice.current.endGeneratingDeviceOrientationNotifications()
343
+ }
344
+ }
345
+ if let observer = backgroundObserver {
346
+ NotificationCenter.default.removeObserver(observer)
347
+ backgroundObserver = nil
348
+ }
349
+ if let observer = foregroundObserver {
350
+ NotificationCenter.default.removeObserver(observer)
351
+ foregroundObserver = nil
352
+ }
353
+ onSample = nil
354
+ lastSample = nil
355
+ lastQuality = nil
356
+ lastInterference = nil
357
+ }
358
+
359
+ private func deliver(heading magnetic: Double, accuracy: Double) {
360
+ var heading = magnetic + declinationDeg
361
+ heading = heading.truncatingRemainder(dividingBy: 360)
362
+ if heading < 0 { heading += 360 }
363
+ let sample = CompassSample(heading: heading, accuracy: accuracy)
364
+ lastSample = sample
365
+
366
+ let quality: AccuracyQuality
367
+ if accuracy < 0 {
368
+ quality = .unreliable
369
+ } else if accuracy < 5 {
370
+ quality = .high
371
+ } else if accuracy < 15 {
372
+ quality = .medium
373
+ } else if accuracy < 30 {
374
+ quality = .low
375
+ } else {
376
+ quality = .unreliable
377
+ }
378
+ fireCalibration(quality)
379
+
380
+ onSample?(sample)
381
+ }
382
+
383
+ private func fireCalibration(_ quality: AccuracyQuality) {
384
+ guard quality != lastQuality else { return }
385
+ lastQuality = quality
386
+ calibrationCb?(quality)
387
+ }
388
+
389
+ private func applyHeadingOrientation() {
390
+ let scene = UIApplication.shared.connectedScenes
391
+ .compactMap { $0 as? UIWindowScene }
392
+ .first
393
+ let clOrientation: CLDeviceOrientation
394
+ switch scene?.interfaceOrientation {
395
+ case .landscapeLeft:
396
+ clOrientation = .landscapeRight
397
+ case .landscapeRight:
398
+ clOrientation = .landscapeLeft
399
+ case .portraitUpsideDown:
400
+ clOrientation = .portraitUpsideDown
401
+ default:
402
+ clOrientation = .portrait
403
+ }
404
+ manager.headingOrientation = clOrientation
405
+ }
406
+ }
407
+
408
+ /// One-shot delegate that drives a `requestPermission()` call. It owns
409
+ /// the `Promise` until the system delivers the user's choice via
410
+ /// `locationManagerDidChangeAuthorization`, then restores the prior
411
+ /// delegate so heading delivery resumes if a subscription was active.
412
+ private class AuthRequestResolver: NSObject, CLLocationManagerDelegate {
413
+ private let promise: Promise<PermissionStatus>
414
+ weak var savedDelegate: CLLocationManagerDelegate?
415
+ var onResolved: (() -> Void)?
416
+ private var resolved = false
417
+
418
+ init(promise: Promise<PermissionStatus>) {
419
+ self.promise = promise
420
+ }
421
+
422
+ func cancel() {
423
+ guard !resolved else { return }
424
+ resolved = true
425
+ promise.reject(withError: NSError(
426
+ domain: "NitroCompass",
427
+ code: 3,
428
+ userInfo: [NSLocalizedDescriptionKey: "Permission request superseded"]
429
+ ))
430
+ onResolved?()
431
+ }
432
+
433
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
434
+ let status = manager.authorizationStatus
435
+ if status == .notDetermined { return }
436
+ if resolved { return }
437
+ resolved = true
438
+ manager.delegate = savedDelegate
439
+ let mapped: PermissionStatus
440
+ switch status {
441
+ case .authorizedAlways, .authorizedWhenInUse: mapped = .granted
442
+ case .denied, .restricted: mapped = .denied
443
+ default: mapped = .unknown
444
+ }
445
+ promise.resolve(withResult: mapped)
446
+ onResolved?()
447
+ }
448
+ }
449
+
450
+ /// CLLocationManager requires an NSObject delegate. Wrapping it lets the
451
+ /// HybridObject stay a pure Swift class.
452
+ private class HeadingDelegate: NSObject, CLLocationManagerDelegate {
453
+ private let onSample: (Double, Double) -> Void
454
+ private let onCalibrationOverride: () -> Void
455
+
456
+ init(
457
+ onSample: @escaping (Double, Double) -> Void,
458
+ onCalibrationOverride: @escaping () -> Void
459
+ ) {
460
+ self.onSample = onSample
461
+ self.onCalibrationOverride = onCalibrationOverride
462
+ }
463
+
464
+ func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
465
+ guard newHeading.headingAccuracy >= 0 else { return }
466
+ onSample(newHeading.magneticHeading, newHeading.headingAccuracy)
467
+ }
468
+
469
+ func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
470
+ onCalibrationOverride()
471
+ return false
472
+ }
473
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useCompass = useCompass;
7
+ var _react = require("react");
8
+ var _native = require("./native");
9
+ var _multiplex = require("./multiplex");
10
+ /**
11
+ * Ergonomic React wrapper for the NitroCompass surface. Handles
12
+ * subscription lifecycle, callback registration, and the live-tuneable
13
+ * knobs. Multiple instances mounted at once safely share the same
14
+ * underlying native subscription via the multi-listener primitives in
15
+ * `./multiplex`.
16
+ */
17
+ function useCompass(options = {}) {
18
+ const {
19
+ filterDegrees = 1,
20
+ declination = 0,
21
+ pauseOnBackground = true,
22
+ enabled = true
23
+ } = options;
24
+ const [reading, setReading] = (0, _react.useState)(null);
25
+ const [quality, setQuality] = (0, _react.useState)(null);
26
+ const [interfering, setInterfering] = (0, _react.useState)(false);
27
+ const [hasCompass] = (0, _react.useState)(() => _native.NitroCompass.hasCompass());
28
+ const [diagnostics] = (0, _react.useState)(() => _native.NitroCompass.getDiagnostics());
29
+
30
+ // Tracked via ref so the heading-subscription effect can re-apply
31
+ // the user's filter after a stop/start cycle without restarting on
32
+ // every filterDegrees change.
33
+ const filterRef = (0, _react.useRef)(filterDegrees);
34
+ filterRef.current = filterDegrees;
35
+ (0, _react.useEffect)(() => {
36
+ _native.NitroCompass.setFilter(filterDegrees);
37
+ }, [filterDegrees]);
38
+ (0, _react.useEffect)(() => {
39
+ _native.NitroCompass.setDeclination(declination);
40
+ }, [declination]);
41
+ (0, _react.useEffect)(() => {
42
+ _native.NitroCompass.setPauseOnBackground(pauseOnBackground);
43
+ }, [pauseOnBackground]);
44
+ (0, _react.useEffect)(() => {
45
+ if (!hasCompass) return;
46
+ return (0, _multiplex.addCalibrationListener)(setQuality);
47
+ }, [hasCompass]);
48
+ (0, _react.useEffect)(() => {
49
+ if (!hasCompass) return;
50
+ return (0, _multiplex.addInterferenceListener)(setInterfering);
51
+ }, [hasCompass]);
52
+ (0, _react.useEffect)(() => {
53
+ if (!hasCompass || !enabled) return;
54
+ const off = (0, _multiplex.addHeadingListener)(setReading);
55
+ // Multiplex starts the sensor with a default filter; re-apply the
56
+ // current option after subscribing.
57
+ _native.NitroCompass.setFilter(filterRef.current);
58
+ return off;
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [hasCompass, enabled]);
61
+ return {
62
+ reading,
63
+ quality,
64
+ interfering,
65
+ hasCompass,
66
+ diagnostics
67
+ };
68
+ }
69
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_native","_multiplex","useCompass","options","filterDegrees","declination","pauseOnBackground","enabled","reading","setReading","useState","quality","setQuality","interfering","setInterfering","hasCompass","NitroCompass","diagnostics","getDiagnostics","filterRef","useRef","current","useEffect","setFilter","setDeclination","setPauseOnBackground","addCalibrationListener","addInterferenceListener","off","addHeadingListener"],"sourceRoot":"../../src","sources":["hook.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAMA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,UAAA,GAAAF,OAAA;AA+CA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASG,UAAUA,CACxBC,OAA0B,GAAG,CAAC,CAAC,EACb;EAClB,MAAM;IACJC,aAAa,GAAG,CAAC;IACjBC,WAAW,GAAG,CAAC;IACfC,iBAAiB,GAAG,IAAI;IACxBC,OAAO,GAAG;EACZ,CAAC,GAAGJ,OAAO;EAEX,MAAM,CAACK,OAAO,EAAEC,UAAU,CAAC,GAAG,IAAAC,eAAQ,EAAuB,IAAI,CAAC;EAClE,MAAM,CAACC,OAAO,EAAEC,UAAU,CAAC,GAAG,IAAAF,eAAQ,EAAyB,IAAI,CAAC;EACpE,MAAM,CAACG,WAAW,EAAEC,cAAc,CAAC,GAAG,IAAAJ,eAAQ,EAAC,KAAK,CAAC;EAErD,MAAM,CAACK,UAAU,CAAC,GAAG,IAAAL,eAAQ,EAAC,MAAMM,oBAAY,CAACD,UAAU,CAAC,CAAC,CAAC;EAC9D,MAAM,CAACE,WAAW,CAAC,GAAG,IAAAP,eAAQ,EAAC,MAAMM,oBAAY,CAACE,cAAc,CAAC,CAAC,CAAC;;EAEnE;EACA;EACA;EACA,MAAMC,SAAS,GAAG,IAAAC,aAAM,EAAChB,aAAa,CAAC;EACvCe,SAAS,CAACE,OAAO,GAAGjB,aAAa;EAEjC,IAAAkB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACO,SAAS,CAACnB,aAAa,CAAC;EACvC,CAAC,EAAE,CAACA,aAAa,CAAC,CAAC;EAEnB,IAAAkB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACQ,cAAc,CAACnB,WAAW,CAAC;EAC1C,CAAC,EAAE,CAACA,WAAW,CAAC,CAAC;EAEjB,IAAAiB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACS,oBAAoB,CAACnB,iBAAiB,CAAC;EACtD,CAAC,EAAE,CAACA,iBAAiB,CAAC,CAAC;EAEvB,IAAAgB,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,EAAE;IACjB,OAAO,IAAAW,iCAAsB,EAACd,UAAU,CAAC;EAC3C,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;EAEhB,IAAAO,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,EAAE;IACjB,OAAO,IAAAY,kCAAuB,EAACb,cAAc,CAAC;EAChD,CAAC,EAAE,CAACC,UAAU,CAAC,CAAC;EAEhB,IAAAO,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,IAAI,CAACR,OAAO,EAAE;IAC7B,MAAMqB,GAAG,GAAG,IAAAC,6BAAkB,EAACpB,UAAU,CAAC;IAC1C;IACA;IACAO,oBAAY,CAACO,SAAS,CAACJ,SAAS,CAACE,OAAO,CAAC;IACzC,OAAOO,GAAG;IACV;EACF,CAAC,EAAE,CAACb,UAAU,EAAER,OAAO,CAAC,CAAC;EAEzB,OAAO;IAAEC,OAAO;IAAEG,OAAO;IAAEE,WAAW;IAAEE,UAAU;IAAEE;EAAY,CAAC;AACnE","ignoreList":[]}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "NitroCompass", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _native.NitroCompass;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "addCalibrationListener", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _multiplex.addCalibrationListener;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "addHeadingListener", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _multiplex.addHeadingListener;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "addInterferenceListener", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _multiplex.addInterferenceListener;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "useCompass", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _hook.useCompass;
34
+ }
35
+ });
36
+ var _native = require("./native");
37
+ var _multiplex = require("./multiplex");
38
+ var _hook = require("./hook");
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_native","require","_multiplex","_hook"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AASA,IAAAA,OAAA,GAAAC,OAAA;AAWA,IAAAC,UAAA,GAAAD,OAAA;AAMA,IAAAE,KAAA,GAAAF,OAAA","ignoreList":[]}
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.addCalibrationListener = addCalibrationListener;
7
+ exports.addHeadingListener = addHeadingListener;
8
+ exports.addInterferenceListener = addInterferenceListener;
9
+ var _native = require("./native");
10
+ /**
11
+ * JS-side fan-out so multiple consumers can subscribe to the same
12
+ * compass stream without clobbering each other. The native API is
13
+ * single-callback by design (start, setOnCalibrationNeeded,
14
+ * setOnInterferenceDetected each own one slot); these helpers wrap
15
+ * that into multi-listener primitives with reference-counted
16
+ * lifecycle.
17
+ *
18
+ * Mixing direct `NitroCompass.start()` / `setOnCalibrationNeeded()` /
19
+ * `setOnInterferenceDetected()` calls with these helpers will
20
+ * clobber the multiplex's internal callback slot — pick one path.
21
+ */
22
+
23
+ const headingListeners = new Set();
24
+ const calibrationListeners = new Set();
25
+ const interferenceListeners = new Set();
26
+ let calibrationRegistered = false;
27
+ let interferenceRegistered = false;
28
+ const DEFAULT_FILTER_DEG = 1;
29
+ function dispatchHeading(sample) {
30
+ for (const cb of Array.from(headingListeners)) {
31
+ try {
32
+ cb(sample);
33
+ } catch (e) {
34
+ console.error('[NitroCompass] heading listener threw:', e);
35
+ }
36
+ }
37
+ }
38
+ function dispatchCalibration(quality) {
39
+ for (const cb of Array.from(calibrationListeners)) {
40
+ try {
41
+ cb(quality);
42
+ } catch (e) {
43
+ console.error('[NitroCompass] calibration listener threw:', e);
44
+ }
45
+ }
46
+ }
47
+ function dispatchInterference(detected) {
48
+ for (const cb of Array.from(interferenceListeners)) {
49
+ try {
50
+ cb(detected);
51
+ } catch (e) {
52
+ console.error('[NitroCompass] interference listener threw:', e);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Subscribe to heading samples. The first listener implicitly calls
59
+ * `NitroCompass.start()`; the last `unsubscribe()` calls
60
+ * `NitroCompass.stop()`. Returns the unsubscribe function.
61
+ *
62
+ * Filter, declination, and pauseOnBackground remain global state on
63
+ * `NitroCompass` and are shared across all listeners — call
64
+ * `NitroCompass.setFilter()` etc. directly to tune them.
65
+ */
66
+ function addHeadingListener(cb) {
67
+ const wasEmpty = headingListeners.size === 0;
68
+ headingListeners.add(cb);
69
+ if (wasEmpty) {
70
+ _native.NitroCompass.start(DEFAULT_FILTER_DEG, dispatchHeading);
71
+ }
72
+ return () => {
73
+ if (!headingListeners.delete(cb)) return;
74
+ if (headingListeners.size === 0) {
75
+ _native.NitroCompass.stop();
76
+ }
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Subscribe to calibration-bucket transitions. Only fires while a
82
+ * heading subscription is active. Returns the unsubscribe function.
83
+ */
84
+ function addCalibrationListener(cb) {
85
+ if (!calibrationRegistered) {
86
+ _native.NitroCompass.setOnCalibrationNeeded(dispatchCalibration);
87
+ calibrationRegistered = true;
88
+ }
89
+ calibrationListeners.add(cb);
90
+ return () => {
91
+ calibrationListeners.delete(cb);
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Subscribe to magnetic-interference transitions. Only fires while a
97
+ * heading subscription is active. Returns the unsubscribe function.
98
+ */
99
+ function addInterferenceListener(cb) {
100
+ if (!interferenceRegistered) {
101
+ _native.NitroCompass.setOnInterferenceDetected(dispatchInterference);
102
+ interferenceRegistered = true;
103
+ }
104
+ interferenceListeners.add(cb);
105
+ return () => {
106
+ interferenceListeners.delete(cb);
107
+ };
108
+ }
109
+ //# sourceMappingURL=multiplex.js.map