react-native-nitro-compass 1.0.9 → 1.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.
package/README.md CHANGED
@@ -52,6 +52,7 @@ NitroCompass.isStarted(): boolean
52
52
  NitroCompass.hasCompass(): boolean
53
53
 
54
54
  NitroCompass.setFilter(degrees: number): void
55
+ NitroCompass.setSmoothing(alpha: number): void
55
56
  NitroCompass.getCurrentHeading(): CompassSample | undefined
56
57
  NitroCompass.getDiagnostics(): SensorDiagnostics | undefined
57
58
  NitroCompass.setDeclination(degrees: number): void
@@ -70,6 +71,7 @@ interface SensorDiagnostics { sensor: SensorKind }
70
71
  ```
71
72
 
72
73
  - `filterDegrees` — minimum change between successive samples before the next one is delivered. Pass `0` for "every event"; typical UI values are `1`–`3`. Use `setFilter()` to change live without tearing down the subscription.
74
+ - `setSmoothing(alpha)` — low-pass smoothing factor (EMA α) applied to heading samples on Android. Range `(0, 1]`, default `0.2` (~100 ms time constant at 50 Hz). `1.0` disables smoothing; smaller values smooth more (kills jitter, adds a touch of latency). **No-op on iOS** — `CLLocationManager` filters internally with Apple's own algorithm, so layering an EMA on top would only add latency. See [Smoothing](#smoothing) below.
73
75
  - `start()` is idempotent in the destructive sense — calling it while already started silently replaces the previous subscription with the new callback. `stop()` is idempotent and safe from inside the `onHeading` callback.
74
76
  - `getDiagnostics()` reports which sensor would produce headings on this device — useful for explaining quality differences (e.g. a phone falling back to `geomagneticRotationVector` will be more susceptible to magnetic interference than one using `rotationVector`). Safe to call before `start()`.
75
77
  - `accuracy` is a numeric uncertainty (degrees). On iOS it comes from `CLHeading.headingAccuracy` directly. On Android it comes from `event.values[4]` of the rotation-vector sensor; if the sensor stack does not publish that (rare), the module falls back to a coarse degree estimate from `SensorManager.SENSOR_STATUS_*` (`HIGH→5°`, `MEDIUM→15°`, `LOW→30°`).
@@ -77,7 +79,12 @@ interface SensorDiagnostics { sensor: SensorKind }
77
79
 
78
80
  ### Calibration
79
81
 
80
- `setOnCalibrationNeeded(cb)` registers a callback fired whenever the calibration bucket transitions. Buckets are derived from numeric accuracy on both platforms using the same thresholds, so values agree across iOS and Android: `<5°` → `'high'`, `<15°` → `'medium'`, `<30°` → `'low'`, otherwise `'unreliable'`. On iOS, the system's "wave the device in a figure-8" prompt is suppressed and reported to your callback as `'unreliable'` — show your own UI when you receive that bucket.
82
+ `setOnCalibrationNeeded(cb)` registers a callback fired whenever the calibration bucket transitions. Each platform's bucket is derived from its **native** accuracy semantics, since the underlying values are not directly comparable:
83
+
84
+ - **iOS** uses `CLHeading.headingAccuracy` (degrees). Apple is conservative — even well-calibrated iPhones typically report `10–15°` and rarely below `5°` (per [Apple staff on the developer forums](https://developer.apple.com/forums/thread/79687)). Buckets: `<20°` → `'high'`, `<35°` → `'medium'`, `<55°` → `'low'`, otherwise `'unreliable'`. The system's "wave the device in a figure-8" prompt is suppressed and reported to your callback as `'unreliable'` — show your own UI when you receive that bucket.
85
+ - **Android** uses `SensorManager.SENSOR_STATUS_*` from `onAccuracyChanged` directly (`HIGH` / `MEDIUM` / `LOW` / `UNRELIABLE`); when `event.values[4]` of the rotation vector publishes a per-sample degree estimate, that's used with thresholds `<5°` / `<15°` / `<30°`. **When magnetic interference is currently detected, the surfaced bucket is downgraded by one notch** (`HIGH→MEDIUM`, `MEDIUM→LOW`, `LOW→UNRELIABLE`) — Android's gyro+accel sensor fusion can keep the OS rotation-vector bucket high even while the magnetometer is being skewed, and surfacing `quality='high'` alongside `interfering=true` is contradictory UX.
86
+
87
+ Both platforms can plausibly emit `'high'` on a clean device — the threshold split just reflects each OS's reporting style.
81
88
 
82
89
  ```ts
83
90
  NitroCompass.setOnCalibrationNeeded((q) => {
@@ -87,7 +94,9 @@ NitroCompass.setOnCalibrationNeeded((q) => {
87
94
 
88
95
  ### Magnetic interference
89
96
 
90
- `setOnInterferenceDetected(cb)` fires `true` when the raw magnetic field magnitude leaves the normal Earth band (~20–70 µT) and `false` when it returns. Typical sources are laptops, monitors, car engines, and large steel structures — these can skew heading by tens of degrees while the calibration bucket still reads `'medium'` or better, so this is complementary to the calibration callback rather than a replacement.
97
+ `setOnInterferenceDetected(cb)` fires `true` when the raw magnetic field magnitude leaves the normal Earth band (~20–70 µT) and `false` when it returns. Typical sources are laptops, monitors, car engines, and large steel structures — these can skew heading by tens of degrees.
98
+
99
+ Interference is surfaced two ways: (1) directly via this callback, and (2) on Android, the calibration bucket emitted by `setOnCalibrationNeeded` is downgraded by one notch while interference is detected (see the Calibration section above). On iOS, only the direct callback fires — `CLLocationManager`'s own accuracy reporting already responds to magnetometer disturbance, so a separate downgrade would double-count.
91
100
 
92
101
  ```ts
93
102
  NitroCompass.setOnInterferenceDetected((interfering) => {
@@ -116,6 +125,20 @@ NitroCompass.setDeclination(declination)
116
125
 
117
126
  Pass `0` to revert to magnetic. Declination survives `stop()`/`start()` cycles.
118
127
 
128
+ ### Smoothing
129
+
130
+ Android's raw `TYPE_ROTATION_VECTOR` output jitters by `±1–3°` even at rest, which produces a visibly twitchy compass dial. iOS's `CLLocationManager` filters internally; Android does not. The library applies a circular EMA low-pass filter on `(sin θ, cos θ)` (handles `359°→0°` wraparound cleanly) before delivering samples, with `α = 0.2` by default — the same value used in [phishman3579/android-compass](https://github.com/phishman3579/android-compass/blob/master/src/com/jwetherell/compass/common/LowPassFilter.java) and within the range used by [Trail Sense](https://github.com/kylecorry31/Trail-Sense)'s production compass code.
131
+
132
+ Tune live:
133
+
134
+ ```ts
135
+ NitroCompass.setSmoothing(0.2) // default — kills jitter, ~100 ms latency
136
+ NitroCompass.setSmoothing(0.4) // snappier, more visible jitter
137
+ NitroCompass.setSmoothing(1.0) // disabled — every sample passes through
138
+ ```
139
+
140
+ `setSmoothing` is a no-op on iOS — Apple's stack already filters heading internally, so layering an EMA on top would only add latency without removing noise.
141
+
119
142
  ### Background pause
120
143
 
121
144
  By default the underlying sensor / location-manager subscription is silently paused while the app is backgrounded and resumed when it returns to the foreground; the JS callback and any declination set via `setDeclination` are preserved across the pause. To opt out (e.g. for a fitness tracker that needs heading while screen-off):
@@ -134,6 +157,7 @@ import { useCompass } from 'react-native-nitro-compass'
134
157
  function CompassView() {
135
158
  const { reading, quality, interfering, hasCompass } = useCompass({
136
159
  filterDegrees: 1,
160
+ smoothingAlpha: 0.2,
137
161
  declination: 0,
138
162
  pauseOnBackground: true,
139
163
  enabled: true,
@@ -161,11 +185,12 @@ function useCompass(options?: UseCompassOptions): UseCompassResult
161
185
  | Option | Type | Default | Description |
162
186
  | --- | --- | --- | --- |
163
187
  | `filterDegrees` | `number` | `1` | Minimum change between successive samples in degrees. Pass `0` for "every event". Updated live via `NitroCompass.setFilter()` whenever the prop changes. |
188
+ | `smoothingAlpha` | `number` | `0.2` | Low-pass smoothing factor (EMA α) on Android. `1.0` disables smoothing; smaller values smooth more. No-op on iOS. See [Smoothing](#smoothing). |
164
189
  | `declination` | `number` | `0` | Magnetic-to-true offset in signed degrees. Pull from a model like [`geomagnetism`](https://github.com/kahirokunn/geomagnetism) keyed on the user's lat/lon. When non-zero, every emitted sample is true-north. |
165
190
  | `pauseOnBackground` | `boolean` | `true` | Pause the underlying sensor / location-manager subscription while the app is backgrounded and resume on foreground. |
166
191
  | `enabled` | `boolean` | `true` | Toggle the heading subscription without unmounting. When `false`, `reading` stops updating but calibration and interference observation continue (so you can still show warnings). |
167
192
 
168
- `filterDegrees`, `declination`, and `pauseOnBackground` map to global state on `NitroCompass` — if multiple hooks set them, last-write-wins.
193
+ `filterDegrees`, `smoothingAlpha`, `declination`, and `pauseOnBackground` map to global state on `NitroCompass` — if multiple hooks set them, last-write-wins.
169
194
 
170
195
  #### Result
171
196
 
@@ -56,6 +56,16 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
56
56
  // structural steel routinely push readings well above 100 µT.
57
57
  private const val EARTH_FIELD_MIN_UT = 20.0
58
58
  private const val EARTH_FIELD_MAX_UT = 70.0
59
+
60
+ // Default low-pass smoothing for the rotation-vector output. iOS's
61
+ // CLLocationManager already filters heading internally; the raw
62
+ // Android rotation vector does not, so the dial visibly jitters
63
+ // by 1–3° at rest. Smoothing (sin θ, cos θ) instead of θ avoids
64
+ // 359°→0° wraparound artifacts. α=0.2 gives a ~5-sample time
65
+ // constant — at SENSOR_DELAY_GAME (~20ms) that's ~100ms of lag,
66
+ // imperceptible compared to the noise it removes. Tunable live
67
+ // via setSmoothing().
68
+ private const val DEFAULT_SMOOTHING_ALPHA = 0.2
59
69
  }
60
70
 
61
71
  @Volatile private var filterDeg: Double = 1.0
@@ -70,6 +80,17 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
70
80
  @Volatile private var lastEventNs: Long = 0L
71
81
  @Volatile private var lastInterference: Boolean? = null
72
82
  @Volatile private var currentActivityRef: WeakReference<Activity>? = null
83
+ @Volatile private var smoothedSin: Double = Double.NaN
84
+ @Volatile private var smoothedCos: Double = Double.NaN
85
+ @Volatile private var smoothingAlpha: Double = DEFAULT_SMOOTHING_ALPHA
86
+ // Tracks whether the rotation-vector sensor publishes a per-sample
87
+ // accuracy in `event.values[4]`. Many devices don't, in which case the
88
+ // synthetic degree floor derived from SENSOR_STATUS_* is the only
89
+ // accuracy signal available — and we have to keep it updated.
90
+ @Volatile private var hasPerSampleAccuracy: Boolean = false
91
+ // Last raw quality from the OS, before the interference downgrade is
92
+ // applied. Used to re-derive `lastQuality` when interference toggles.
93
+ @Volatile private var lastRawQuality: AccuracyQuality? = null
73
94
 
74
95
  private val rotationMatrix = FloatArray(16)
75
96
  private val remappedMatrix = FloatArray(16)
@@ -131,6 +152,8 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
131
152
  lastSample = null
132
153
  lastQuality = null
133
154
 
155
+ hasPerSampleAccuracy = false
156
+ lastRawQuality = null
134
157
  registerLifecycleCallbacks()
135
158
  subscribeLocked()
136
159
  }
@@ -155,6 +178,10 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
155
178
  filterDeg = degrees.coerceAtLeast(0.0)
156
179
  }
157
180
 
181
+ override fun setSmoothing(alpha: Double) {
182
+ smoothingAlpha = alpha.coerceIn(0.0, 1.0)
183
+ }
184
+
158
185
  override fun getDiagnostics(): SensorDiagnostics? {
159
186
  val sm = NitroModules.applicationContext?.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
160
187
  ?: return null
@@ -254,6 +281,8 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
254
281
  activeListener = listener
255
282
  isSubscribed = true
256
283
 
284
+ smoothedSin = Double.NaN
285
+ smoothedCos = Double.NaN
257
286
  lastEventNs = 0L
258
287
  watchdogHandler.removeCallbacks(watchdogRunnable)
259
288
  watchdogHandler.postDelayed(watchdogRunnable, WATCHDOG_PERIOD_MS)
@@ -380,9 +409,11 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
380
409
 
381
410
  var heading = Math.toDegrees(orientation[0].toDouble())
382
411
  if (heading < 0.0) heading += 360.0
412
+ heading = smoothHeading(heading)
383
413
 
384
414
  if (event.values.size > 4 && event.values[4] >= 0f) {
385
415
  val acc = Math.toDegrees(event.values[4].toDouble())
416
+ hasPerSampleAccuracy = true
386
417
  lastAccuracyDeg = acc
387
418
  fireCalibration(qualityFor(acc))
388
419
  }
@@ -409,6 +440,14 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
409
440
  if (lastInterference == isInterference) return
410
441
  lastInterference = isInterference
411
442
  interferenceCb?.invoke(isInterference)
443
+ // External interference makes the heading less trustworthy even when
444
+ // the OS rotation-vector accuracy hasn't downgraded (gyro+accel can
445
+ // keep its bucket high while the magnetometer is being skewed). Pump
446
+ // the last raw quality back through fireCalibration so the
447
+ // interference-aware downgrade is applied, and refresh the synthetic
448
+ // degree value to match.
449
+ lastRawQuality?.let { fireCalibration(it) }
450
+ if (!hasPerSampleAccuracy) refreshSyntheticAccuracy()
412
451
  }
413
452
 
414
453
  private fun handleAccuracyChanged(accuracy: Int) {
@@ -418,15 +457,34 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
418
457
  SensorManager.SENSOR_STATUS_ACCURACY_LOW -> AccuracyQuality.LOW
419
458
  else -> AccuracyQuality.UNRELIABLE
420
459
  }
421
- if (lastAccuracyDeg < 0.0) {
422
- lastAccuracyDeg = when (quality) {
423
- AccuracyQuality.HIGH -> 5.0
424
- AccuracyQuality.MEDIUM -> 15.0
425
- AccuracyQuality.LOW -> 30.0
426
- AccuracyQuality.UNRELIABLE -> -1.0
427
- }
428
- }
429
460
  fireCalibration(quality)
461
+ if (!hasPerSampleAccuracy) refreshSyntheticAccuracy()
462
+ }
463
+
464
+ private fun smoothHeading(degrees: Double): Double {
465
+ val rad = Math.toRadians(degrees)
466
+ val s = Math.sin(rad)
467
+ val c = Math.cos(rad)
468
+ val ss = smoothedSin
469
+ val cs = smoothedCos
470
+ if (ss.isNaN() || cs.isNaN()) {
471
+ smoothedSin = s
472
+ smoothedCos = c
473
+ return degrees
474
+ }
475
+ val a = smoothingAlpha
476
+ if (a >= 1.0) {
477
+ smoothedSin = s
478
+ smoothedCos = c
479
+ return degrees
480
+ }
481
+ val newSin = a * s + (1.0 - a) * ss
482
+ val newCos = a * c + (1.0 - a) * cs
483
+ smoothedSin = newSin
484
+ smoothedCos = newCos
485
+ var deg = Math.toDegrees(Math.atan2(newSin, newCos))
486
+ if (deg < 0.0) deg += 360.0
487
+ return deg
430
488
  }
431
489
 
432
490
  private fun qualityFor(accuracyDeg: Double): AccuracyQuality {
@@ -439,10 +497,44 @@ class HybridNitroCompass : HybridNitroCompassSpec() {
439
497
  }
440
498
  }
441
499
 
500
+ private fun degreesFor(quality: AccuracyQuality): Double = when (quality) {
501
+ AccuracyQuality.HIGH -> 5.0
502
+ AccuracyQuality.MEDIUM -> 15.0
503
+ AccuracyQuality.LOW -> 30.0
504
+ AccuracyQuality.UNRELIABLE -> -1.0
505
+ }
506
+
507
+ // Magnetic interference is a separate signal from rotation-vector
508
+ // accuracy on Android — gyro+accel can keep the OS bucket "HIGH" even
509
+ // while the magnetometer is being skewed by a laptop / car / steel.
510
+ // Reporting `quality=high` while `interfering=true` is contradictory
511
+ // UX, so we downgrade the surfaced bucket by one notch when
512
+ // interference is currently detected.
513
+ private fun effectiveQuality(raw: AccuracyQuality): AccuracyQuality {
514
+ if (lastInterference != true) return raw
515
+ return when (raw) {
516
+ AccuracyQuality.HIGH -> AccuracyQuality.MEDIUM
517
+ AccuracyQuality.MEDIUM -> AccuracyQuality.LOW
518
+ AccuracyQuality.LOW -> AccuracyQuality.UNRELIABLE
519
+ AccuracyQuality.UNRELIABLE -> AccuracyQuality.UNRELIABLE
520
+ }
521
+ }
522
+
523
+ // For devices that don't publish a per-sample accuracy in
524
+ // event.values[4], the only accuracy signal is the OS bucket. Map the
525
+ // current effective bucket back to a representative degree value so
526
+ // CompassSample.accuracy reflects interference too.
527
+ private fun refreshSyntheticAccuracy() {
528
+ val raw = lastRawQuality ?: return
529
+ lastAccuracyDeg = degreesFor(effectiveQuality(raw))
530
+ }
531
+
442
532
  private fun fireCalibration(quality: AccuracyQuality) {
443
- if (quality == lastQuality) return
444
- lastQuality = quality
445
- calibrationCb?.invoke(quality)
533
+ lastRawQuality = quality
534
+ val effective = effectiveQuality(quality)
535
+ if (effective == lastQuality) return
536
+ lastQuality = effective
537
+ calibrationCb?.invoke(effective)
446
538
  }
447
539
 
448
540
  private fun currentSurfaceRotation(): Int {
@@ -183,6 +183,11 @@ class HybridNitroCompass: HybridNitroCompassSpec {
183
183
  }
184
184
  }
185
185
 
186
+ // CLLocationManager filters heading internally with Apple's own
187
+ // algorithm; layering an EMA on top would only add latency. Kept as a
188
+ // no-op so cross-platform JS callers can call it unconditionally.
189
+ func setSmoothing(alpha: Double) throws {}
190
+
186
191
  func getDiagnostics() throws -> SensorDiagnostics? {
187
192
  guard CLLocationManager.headingAvailable() else { return nil }
188
193
  return SensorDiagnostics(sensor: .corelocation)
@@ -363,14 +368,19 @@ class HybridNitroCompass: HybridNitroCompassSpec {
363
368
  let sample = CompassSample(heading: heading, accuracy: accuracy)
364
369
  lastSample = sample
365
370
 
371
+ // CLLocationManager.headingAccuracy is conservative — even a
372
+ // well-calibrated compass usually reports 5–15°, and values under
373
+ // 5° are basically never produced. Bucket against the realistic
374
+ // distribution so `.high` is reachable, mirroring how Android maps
375
+ // SENSOR_STATUS_ACCURACY_HIGH.
366
376
  let quality: AccuracyQuality
367
377
  if accuracy < 0 {
368
378
  quality = .unreliable
369
- } else if accuracy < 5 {
379
+ } else if accuracy < 20 {
370
380
  quality = .high
371
- } else if accuracy < 15 {
381
+ } else if accuracy < 35 {
372
382
  quality = .medium
373
- } else if accuracy < 30 {
383
+ } else if accuracy < 55 {
374
384
  quality = .low
375
385
  } else {
376
386
  quality = .unreliable
@@ -17,6 +17,7 @@ var _multiplex = require("./multiplex");
17
17
  function useCompass(options = {}) {
18
18
  const {
19
19
  filterDegrees = 1,
20
+ smoothingAlpha = 0.2,
20
21
  declination = 0,
21
22
  pauseOnBackground = true,
22
23
  enabled = true
@@ -35,6 +36,9 @@ function useCompass(options = {}) {
35
36
  (0, _react.useEffect)(() => {
36
37
  _native.NitroCompass.setFilter(filterDegrees);
37
38
  }, [filterDegrees]);
39
+ (0, _react.useEffect)(() => {
40
+ _native.NitroCompass.setSmoothing(smoothingAlpha);
41
+ }, [smoothingAlpha]);
38
42
  (0, _react.useEffect)(() => {
39
43
  _native.NitroCompass.setDeclination(declination);
40
44
  }, [declination]);
@@ -1 +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":[]}
1
+ {"version":3,"names":["_react","require","_native","_multiplex","useCompass","options","filterDegrees","smoothingAlpha","declination","pauseOnBackground","enabled","reading","setReading","useState","quality","setQuality","interfering","setInterfering","hasCompass","NitroCompass","diagnostics","getDiagnostics","filterRef","useRef","current","useEffect","setFilter","setSmoothing","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;AAyDA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASG,UAAUA,CACxBC,OAA0B,GAAG,CAAC,CAAC,EACb;EAClB,MAAM;IACJC,aAAa,GAAG,CAAC;IACjBC,cAAc,GAAG,GAAG;IACpBC,WAAW,GAAG,CAAC;IACfC,iBAAiB,GAAG,IAAI;IACxBC,OAAO,GAAG;EACZ,CAAC,GAAGL,OAAO;EAEX,MAAM,CAACM,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,EAACjB,aAAa,CAAC;EACvCgB,SAAS,CAACE,OAAO,GAAGlB,aAAa;EAEjC,IAAAmB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACO,SAAS,CAACpB,aAAa,CAAC;EACvC,CAAC,EAAE,CAACA,aAAa,CAAC,CAAC;EAEnB,IAAAmB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACQ,YAAY,CAACpB,cAAc,CAAC;EAC3C,CAAC,EAAE,CAACA,cAAc,CAAC,CAAC;EAEpB,IAAAkB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACS,cAAc,CAACpB,WAAW,CAAC;EAC1C,CAAC,EAAE,CAACA,WAAW,CAAC,CAAC;EAEjB,IAAAiB,gBAAS,EAAC,MAAM;IACdN,oBAAY,CAACU,oBAAoB,CAACpB,iBAAiB,CAAC;EACtD,CAAC,EAAE,CAACA,iBAAiB,CAAC,CAAC;EAEvB,IAAAgB,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,EAAE;IACjB,OAAO,IAAAY,iCAAsB,EAACf,UAAU,CAAC;EAC3C,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;EAEhB,IAAAO,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,EAAE;IACjB,OAAO,IAAAa,kCAAuB,EAACd,cAAc,CAAC;EAChD,CAAC,EAAE,CAACC,UAAU,CAAC,CAAC;EAEhB,IAAAO,gBAAS,EAAC,MAAM;IACd,IAAI,CAACP,UAAU,IAAI,CAACR,OAAO,EAAE;IAC7B,MAAMsB,GAAG,GAAG,IAAAC,6BAAkB,EAACrB,UAAU,CAAC;IAC1C;IACA;IACAO,oBAAY,CAACO,SAAS,CAACJ,SAAS,CAACE,OAAO,CAAC;IACzC,OAAOQ,GAAG;IACV;EACF,CAAC,EAAE,CAACd,UAAU,EAAER,OAAO,CAAC,CAAC;EAEzB,OAAO;IAAEC,OAAO;IAAEG,OAAO;IAAEE,WAAW;IAAEE,UAAU;IAAEE;EAAY,CAAC;AACnE","ignoreList":[]}
@@ -13,6 +13,7 @@ import { addCalibrationListener, addHeadingListener, addInterferenceListener } f
13
13
  export function useCompass(options = {}) {
14
14
  const {
15
15
  filterDegrees = 1,
16
+ smoothingAlpha = 0.2,
16
17
  declination = 0,
17
18
  pauseOnBackground = true,
18
19
  enabled = true
@@ -31,6 +32,9 @@ export function useCompass(options = {}) {
31
32
  useEffect(() => {
32
33
  NitroCompass.setFilter(filterDegrees);
33
34
  }, [filterDegrees]);
35
+ useEffect(() => {
36
+ NitroCompass.setSmoothing(smoothingAlpha);
37
+ }, [smoothingAlpha]);
34
38
  useEffect(() => {
35
39
  NitroCompass.setDeclination(declination);
36
40
  }, [declination]);
@@ -1 +1 @@
1
- {"version":3,"names":["useEffect","useRef","useState","NitroCompass","addCalibrationListener","addHeadingListener","addInterferenceListener","useCompass","options","filterDegrees","declination","pauseOnBackground","enabled","reading","setReading","quality","setQuality","interfering","setInterfering","hasCompass","diagnostics","getDiagnostics","filterRef","current","setFilter","setDeclination","setPauseOnBackground","off"],"sourceRoot":"../../src","sources":["hook.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAMnD,SAASC,YAAY,QAAQ,UAAU;AACvC,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,uBAAuB,QAClB,aAAa;AA2CpB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,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,GAAGZ,QAAQ,CAAuB,IAAI,CAAC;EAClE,MAAM,CAACa,OAAO,EAAEC,UAAU,CAAC,GAAGd,QAAQ,CAAyB,IAAI,CAAC;EACpE,MAAM,CAACe,WAAW,EAAEC,cAAc,CAAC,GAAGhB,QAAQ,CAAC,KAAK,CAAC;EAErD,MAAM,CAACiB,UAAU,CAAC,GAAGjB,QAAQ,CAAC,MAAMC,YAAY,CAACgB,UAAU,CAAC,CAAC,CAAC;EAC9D,MAAM,CAACC,WAAW,CAAC,GAAGlB,QAAQ,CAAC,MAAMC,YAAY,CAACkB,cAAc,CAAC,CAAC,CAAC;;EAEnE;EACA;EACA;EACA,MAAMC,SAAS,GAAGrB,MAAM,CAACQ,aAAa,CAAC;EACvCa,SAAS,CAACC,OAAO,GAAGd,aAAa;EAEjCT,SAAS,CAAC,MAAM;IACdG,YAAY,CAACqB,SAAS,CAACf,aAAa,CAAC;EACvC,CAAC,EAAE,CAACA,aAAa,CAAC,CAAC;EAEnBT,SAAS,CAAC,MAAM;IACdG,YAAY,CAACsB,cAAc,CAACf,WAAW,CAAC;EAC1C,CAAC,EAAE,CAACA,WAAW,CAAC,CAAC;EAEjBV,SAAS,CAAC,MAAM;IACdG,YAAY,CAACuB,oBAAoB,CAACf,iBAAiB,CAAC;EACtD,CAAC,EAAE,CAACA,iBAAiB,CAAC,CAAC;EAEvBX,SAAS,CAAC,MAAM;IACd,IAAI,CAACmB,UAAU,EAAE;IACjB,OAAOf,sBAAsB,CAACY,UAAU,CAAC;EAC3C,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;EAEhBnB,SAAS,CAAC,MAAM;IACd,IAAI,CAACmB,UAAU,EAAE;IACjB,OAAOb,uBAAuB,CAACY,cAAc,CAAC;EAChD,CAAC,EAAE,CAACC,UAAU,CAAC,CAAC;EAEhBnB,SAAS,CAAC,MAAM;IACd,IAAI,CAACmB,UAAU,IAAI,CAACP,OAAO,EAAE;IAC7B,MAAMe,GAAG,GAAGtB,kBAAkB,CAACS,UAAU,CAAC;IAC1C;IACA;IACAX,YAAY,CAACqB,SAAS,CAACF,SAAS,CAACC,OAAO,CAAC;IACzC,OAAOI,GAAG;IACV;EACF,CAAC,EAAE,CAACR,UAAU,EAAEP,OAAO,CAAC,CAAC;EAEzB,OAAO;IAAEC,OAAO;IAAEE,OAAO;IAAEE,WAAW;IAAEE,UAAU;IAAEC;EAAY,CAAC;AACnE","ignoreList":[]}
1
+ {"version":3,"names":["useEffect","useRef","useState","NitroCompass","addCalibrationListener","addHeadingListener","addInterferenceListener","useCompass","options","filterDegrees","smoothingAlpha","declination","pauseOnBackground","enabled","reading","setReading","quality","setQuality","interfering","setInterfering","hasCompass","diagnostics","getDiagnostics","filterRef","current","setFilter","setSmoothing","setDeclination","setPauseOnBackground","off"],"sourceRoot":"../../src","sources":["hook.ts"],"mappings":";;AAAA,SAASA,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,OAAO;AAMnD,SAASC,YAAY,QAAQ,UAAU;AACvC,SACEC,sBAAsB,EACtBC,kBAAkB,EAClBC,uBAAuB,QAClB,aAAa;AAqDpB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CACxBC,OAA0B,GAAG,CAAC,CAAC,EACb;EAClB,MAAM;IACJC,aAAa,GAAG,CAAC;IACjBC,cAAc,GAAG,GAAG;IACpBC,WAAW,GAAG,CAAC;IACfC,iBAAiB,GAAG,IAAI;IACxBC,OAAO,GAAG;EACZ,CAAC,GAAGL,OAAO;EAEX,MAAM,CAACM,OAAO,EAAEC,UAAU,CAAC,GAAGb,QAAQ,CAAuB,IAAI,CAAC;EAClE,MAAM,CAACc,OAAO,EAAEC,UAAU,CAAC,GAAGf,QAAQ,CAAyB,IAAI,CAAC;EACpE,MAAM,CAACgB,WAAW,EAAEC,cAAc,CAAC,GAAGjB,QAAQ,CAAC,KAAK,CAAC;EAErD,MAAM,CAACkB,UAAU,CAAC,GAAGlB,QAAQ,CAAC,MAAMC,YAAY,CAACiB,UAAU,CAAC,CAAC,CAAC;EAC9D,MAAM,CAACC,WAAW,CAAC,GAAGnB,QAAQ,CAAC,MAAMC,YAAY,CAACmB,cAAc,CAAC,CAAC,CAAC;;EAEnE;EACA;EACA;EACA,MAAMC,SAAS,GAAGtB,MAAM,CAACQ,aAAa,CAAC;EACvCc,SAAS,CAACC,OAAO,GAAGf,aAAa;EAEjCT,SAAS,CAAC,MAAM;IACdG,YAAY,CAACsB,SAAS,CAAChB,aAAa,CAAC;EACvC,CAAC,EAAE,CAACA,aAAa,CAAC,CAAC;EAEnBT,SAAS,CAAC,MAAM;IACdG,YAAY,CAACuB,YAAY,CAAChB,cAAc,CAAC;EAC3C,CAAC,EAAE,CAACA,cAAc,CAAC,CAAC;EAEpBV,SAAS,CAAC,MAAM;IACdG,YAAY,CAACwB,cAAc,CAAChB,WAAW,CAAC;EAC1C,CAAC,EAAE,CAACA,WAAW,CAAC,CAAC;EAEjBX,SAAS,CAAC,MAAM;IACdG,YAAY,CAACyB,oBAAoB,CAAChB,iBAAiB,CAAC;EACtD,CAAC,EAAE,CAACA,iBAAiB,CAAC,CAAC;EAEvBZ,SAAS,CAAC,MAAM;IACd,IAAI,CAACoB,UAAU,EAAE;IACjB,OAAOhB,sBAAsB,CAACa,UAAU,CAAC;EAC3C,CAAC,EAAE,CAACG,UAAU,CAAC,CAAC;EAEhBpB,SAAS,CAAC,MAAM;IACd,IAAI,CAACoB,UAAU,EAAE;IACjB,OAAOd,uBAAuB,CAACa,cAAc,CAAC;EAChD,CAAC,EAAE,CAACC,UAAU,CAAC,CAAC;EAEhBpB,SAAS,CAAC,MAAM;IACd,IAAI,CAACoB,UAAU,IAAI,CAACP,OAAO,EAAE;IAC7B,MAAMgB,GAAG,GAAGxB,kBAAkB,CAACU,UAAU,CAAC;IAC1C;IACA;IACAZ,YAAY,CAACsB,SAAS,CAACF,SAAS,CAACC,OAAO,CAAC;IACzC,OAAOK,GAAG;IACV;EACF,CAAC,EAAE,CAACT,UAAU,EAAEP,OAAO,CAAC,CAAC;EAEzB,OAAO;IAAEC,OAAO;IAAEE,OAAO;IAAEE,WAAW;IAAEE,UAAU;IAAEC;EAAY,CAAC;AACnE","ignoreList":[]}
@@ -7,6 +7,16 @@ export interface UseCompassOptions {
7
7
  * the library — last-write-wins.
8
8
  */
9
9
  filterDegrees?: number;
10
+ /**
11
+ * Low-pass smoothing factor (EMA α) applied to heading samples.
12
+ * Range `(0, 1]`. Default `0.2` ≈ 100ms time constant at typical
13
+ * Android sample rates. `1.0` disables smoothing. Smaller values
14
+ * smooth more (kills jitter, adds a touch of latency).
15
+ *
16
+ * No-op on iOS — CLLocationManager filters internally.
17
+ * Shared global state — last-write-wins.
18
+ */
19
+ smoothingAlpha?: number;
10
20
  /**
11
21
  * Magnetic-to-true offset in signed degrees. Default `0` (magnetic).
12
22
  * Pull from a model like `geomagnetism` keyed on the user's lat/lon.
@@ -1 +1 @@
1
- {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../src/hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAQnC,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,oEAAoE;IACpE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;IAC/B,oEAAoE;IACpE,WAAW,EAAE,OAAO,CAAA;IACpB,yDAAyD;IACzD,UAAU,EAAE,OAAO,CAAA;IACnB,sDAAsD;IACtD,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAA;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CAsDlB"}
1
+ {"version":3,"file":"hook.d.ts","sourceRoot":"","sources":["../../../src/hook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,iBAAiB,EAClB,MAAM,4BAA4B,CAAA;AAQnC,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,oEAAoE;IACpE,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;IAC/B,oEAAoE;IACpE,WAAW,EAAE,OAAO,CAAA;IACpB,yDAAyD;IACzD,UAAU,EAAE,OAAO,CAAA;IACnB,sDAAsD;IACtD,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAA;CAC3C;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,OAAO,GAAE,iBAAsB,GAC9B,gBAAgB,CA2DlB"}
@@ -95,6 +95,22 @@ export interface NitroCompass extends HybridObject<{
95
95
  * effect until `start()` is called.
96
96
  */
97
97
  setFilter(degrees: number): void;
98
+ /**
99
+ * Set the low-pass smoothing factor (EMA α) applied to heading samples
100
+ * before delivery. Range `(0, 1]`. Default `0.2` ≈ 100ms time constant
101
+ * at Android's typical 50 Hz sample rate.
102
+ *
103
+ * - `1.0` disables smoothing (every sample passes through unfiltered).
104
+ * - Smaller values smooth more — eliminates rotation-vector jitter at
105
+ * the cost of a small amount of latency.
106
+ *
107
+ * Implemented as a circular EMA on `(sin θ, cos θ)` so the 359°→0°
108
+ * wraparound doesn't bias the average. Survives `start`/`stop`.
109
+ *
110
+ * **No-op on iOS.** `CLLocationManager` filters heading internally with
111
+ * Apple's own algorithm; layering EMA on top would only add latency.
112
+ */
113
+ setSmoothing(alpha: number): void;
98
114
  /**
99
115
  * Describe which underlying sensor / framework would produce headings on
100
116
  * this device. Returns `undefined` if the device has no compass hardware
@@ -1 +1 @@
1
- {"version":3,"file":"NitroCompass.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/NitroCompass.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,YAAY,CAAA;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAClB,gBAAgB,GAChB,2BAA2B,GAC3B,cAAc,CAAA;AAElB,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,UAAU,CAAA;CACnB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE/D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAa,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACrF;;;;;;;;;;OAUG;IACH,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAA;IAE9E,8GAA8G;IAC9G,IAAI,IAAI,IAAI,CAAA;IAEZ,qEAAqE;IACrE,SAAS,IAAI,OAAO,CAAA;IAEpB;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAEhC;;;;OAIG;IACH,cAAc,IAAI,iBAAiB,GAAG,SAAS,CAAA;IAE/C;;;;OAIG;IACH,UAAU,IAAI,OAAO,CAAA;IAErB;;;;OAIG;IACH,iBAAiB,IAAI,aAAa,GAAG,SAAS,CAAA;IAE9C;;;;;OAKG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI,CAAA;IAE1E;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,yBAAyB,CAAC,QAAQ,EAAE,CAAC,oBAAoB,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAA;IAElF;;;;;;;OAOG;IACH,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAE5C;;;;OAIG;IACH,mBAAmB,IAAI,gBAAgB,CAAA;IAEvC;;;;;;;OAOG;IACH,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC/C"}
1
+ {"version":3,"file":"NitroCompass.nitro.d.ts","sourceRoot":"","sources":["../../../../src/specs/NitroCompass.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,YAAY,CAAA;AAEtE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,UAAU,GAClB,gBAAgB,GAChB,2BAA2B,GAC3B,cAAc,CAAA;AAElB,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,UAAU,CAAA;CACnB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAA;AAE/D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAa,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACrF;;;;;;;;;;OAUG;IACH,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAA;IAE9E,8GAA8G;IAC9G,IAAI,IAAI,IAAI,CAAA;IAEZ,qEAAqE;IACrE,SAAS,IAAI,OAAO,CAAA;IAEpB;;;;OAIG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAEhC;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAEjC;;;;OAIG;IACH,cAAc,IAAI,iBAAiB,GAAG,SAAS,CAAA;IAE/C;;;;OAIG;IACH,UAAU,IAAI,OAAO,CAAA;IAErB;;;;OAIG;IACH,iBAAiB,IAAI,aAAa,GAAG,SAAS,CAAA;IAE9C;;;;;OAKG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAErC;;;;OAIG;IACH,sBAAsB,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI,CAAA;IAE1E;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,yBAAyB,CAAC,QAAQ,EAAE,CAAC,oBAAoB,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAA;IAElF;;;;;;;OAOG;IACH,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IAE5C;;;;OAIG;IACH,mBAAmB,IAAI,gBAAgB,CAAA;IAEvC;;;;;;;OAOG;IACH,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAA;CAC/C"}
@@ -87,6 +87,10 @@ namespace margelo::nitro::nitrocompass {
87
87
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* degrees */)>("setFilter");
88
88
  method(_javaPart, degrees);
89
89
  }
90
+ void JHybridNitroCompassSpec::setSmoothing(double alpha) {
91
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* alpha */)>("setSmoothing");
92
+ method(_javaPart, alpha);
93
+ }
90
94
  std::optional<SensorDiagnostics> JHybridNitroCompassSpec::getDiagnostics() {
91
95
  static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSensorDiagnostics>()>("getDiagnostics");
92
96
  auto __result = method(_javaPart);
@@ -58,6 +58,7 @@ namespace margelo::nitro::nitrocompass {
58
58
  void stop() override;
59
59
  bool isStarted() override;
60
60
  void setFilter(double degrees) override;
61
+ void setSmoothing(double alpha) override;
61
62
  std::optional<SensorDiagnostics> getDiagnostics() override;
62
63
  bool hasCompass() override;
63
64
  std::optional<CompassSample> getCurrentHeading() override;
@@ -50,6 +50,10 @@ abstract class HybridNitroCompassSpec: HybridObject() {
50
50
  @Keep
51
51
  abstract fun setFilter(degrees: Double): Unit
52
52
 
53
+ @DoNotStrip
54
+ @Keep
55
+ abstract fun setSmoothing(alpha: Double): Unit
56
+
53
57
  @DoNotStrip
54
58
  @Keep
55
59
  abstract fun getDiagnostics(): SensorDiagnostics?
@@ -108,6 +108,12 @@ namespace margelo::nitro::nitrocompass {
108
108
  std::rethrow_exception(__result.error());
109
109
  }
110
110
  }
111
+ inline void setSmoothing(double alpha) override {
112
+ auto __result = _swiftPart.setSmoothing(std::forward<decltype(alpha)>(alpha));
113
+ if (__result.hasError()) [[unlikely]] {
114
+ std::rethrow_exception(__result.error());
115
+ }
116
+ }
111
117
  inline std::optional<SensorDiagnostics> getDiagnostics() override {
112
118
  auto __result = _swiftPart.getDiagnostics();
113
119
  if (__result.hasError()) [[unlikely]] {
@@ -17,6 +17,7 @@ public protocol HybridNitroCompassSpec_protocol: HybridObject {
17
17
  func stop() throws -> Void
18
18
  func isStarted() throws -> Bool
19
19
  func setFilter(degrees: Double) throws -> Void
20
+ func setSmoothing(alpha: Double) throws -> Void
20
21
  func getDiagnostics() throws -> SensorDiagnostics?
21
22
  func hasCompass() throws -> Bool
22
23
  func getCurrentHeading() throws -> CompassSample?
@@ -174,6 +174,17 @@ open class HybridNitroCompassSpec_cxx {
174
174
  }
175
175
  }
176
176
 
177
+ @inline(__always)
178
+ public final func setSmoothing(alpha: Double) -> bridge.Result_void_ {
179
+ do {
180
+ try self.__implementation.setSmoothing(alpha: alpha)
181
+ return bridge.create_Result_void_()
182
+ } catch (let __error) {
183
+ let __exceptionPtr = __error.toCpp()
184
+ return bridge.create_Result_void_(__exceptionPtr)
185
+ }
186
+ }
187
+
177
188
  @inline(__always)
178
189
  public final func getDiagnostics() -> bridge.Result_std__optional_SensorDiagnostics__ {
179
190
  do {
@@ -18,6 +18,7 @@ namespace margelo::nitro::nitrocompass {
18
18
  prototype.registerHybridMethod("stop", &HybridNitroCompassSpec::stop);
19
19
  prototype.registerHybridMethod("isStarted", &HybridNitroCompassSpec::isStarted);
20
20
  prototype.registerHybridMethod("setFilter", &HybridNitroCompassSpec::setFilter);
21
+ prototype.registerHybridMethod("setSmoothing", &HybridNitroCompassSpec::setSmoothing);
21
22
  prototype.registerHybridMethod("getDiagnostics", &HybridNitroCompassSpec::getDiagnostics);
22
23
  prototype.registerHybridMethod("hasCompass", &HybridNitroCompassSpec::hasCompass);
23
24
  prototype.registerHybridMethod("getCurrentHeading", &HybridNitroCompassSpec::getCurrentHeading);
@@ -65,6 +65,7 @@ namespace margelo::nitro::nitrocompass {
65
65
  virtual void stop() = 0;
66
66
  virtual bool isStarted() = 0;
67
67
  virtual void setFilter(double degrees) = 0;
68
+ virtual void setSmoothing(double alpha) = 0;
68
69
  virtual std::optional<SensorDiagnostics> getDiagnostics() = 0;
69
70
  virtual bool hasCompass() = 0;
70
71
  virtual std::optional<CompassSample> getCurrentHeading() = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-compass",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },
package/src/hook.ts CHANGED
@@ -19,6 +19,16 @@ export interface UseCompassOptions {
19
19
  * the library — last-write-wins.
20
20
  */
21
21
  filterDegrees?: number
22
+ /**
23
+ * Low-pass smoothing factor (EMA α) applied to heading samples.
24
+ * Range `(0, 1]`. Default `0.2` ≈ 100ms time constant at typical
25
+ * Android sample rates. `1.0` disables smoothing. Smaller values
26
+ * smooth more (kills jitter, adds a touch of latency).
27
+ *
28
+ * No-op on iOS — CLLocationManager filters internally.
29
+ * Shared global state — last-write-wins.
30
+ */
31
+ smoothingAlpha?: number
22
32
  /**
23
33
  * Magnetic-to-true offset in signed degrees. Default `0` (magnetic).
24
34
  * Pull from a model like `geomagnetism` keyed on the user's lat/lon.
@@ -64,6 +74,7 @@ export function useCompass(
64
74
  ): UseCompassResult {
65
75
  const {
66
76
  filterDegrees = 1,
77
+ smoothingAlpha = 0.2,
67
78
  declination = 0,
68
79
  pauseOnBackground = true,
69
80
  enabled = true,
@@ -86,6 +97,10 @@ export function useCompass(
86
97
  NitroCompass.setFilter(filterDegrees)
87
98
  }, [filterDegrees])
88
99
 
100
+ useEffect(() => {
101
+ NitroCompass.setSmoothing(smoothingAlpha)
102
+ }, [smoothingAlpha])
103
+
89
104
  useEffect(() => {
90
105
  NitroCompass.setDeclination(declination)
91
106
  }, [declination])
@@ -105,6 +105,23 @@ export interface NitroCompass extends HybridObject<{ ios: 'swift'; android: 'kot
105
105
  */
106
106
  setFilter(degrees: number): void
107
107
 
108
+ /**
109
+ * Set the low-pass smoothing factor (EMA α) applied to heading samples
110
+ * before delivery. Range `(0, 1]`. Default `0.2` ≈ 100ms time constant
111
+ * at Android's typical 50 Hz sample rate.
112
+ *
113
+ * - `1.0` disables smoothing (every sample passes through unfiltered).
114
+ * - Smaller values smooth more — eliminates rotation-vector jitter at
115
+ * the cost of a small amount of latency.
116
+ *
117
+ * Implemented as a circular EMA on `(sin θ, cos θ)` so the 359°→0°
118
+ * wraparound doesn't bias the average. Survives `start`/`stop`.
119
+ *
120
+ * **No-op on iOS.** `CLLocationManager` filters heading internally with
121
+ * Apple's own algorithm; layering EMA on top would only add latency.
122
+ */
123
+ setSmoothing(alpha: number): void
124
+
108
125
  /**
109
126
  * Describe which underlying sensor / framework would produce headings on
110
127
  * this device. Returns `undefined` if the device has no compass hardware