react-native-nitro-compass 1.0.8 → 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 +82 -4
- package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +103 -11
- package/ios/HybridNitroCompass.swift +13 -3
- package/lib/commonjs/hook.js +4 -0
- package/lib/commonjs/hook.js.map +1 -1
- package/lib/module/hook.js +4 -0
- package/lib/module/hook.js.map +1 -1
- package/lib/typescript/src/hook.d.ts +10 -0
- package/lib/typescript/src/hook.d.ts.map +1 -1
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +16 -0
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +1 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +4 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +6 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +11 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +1 -0
- package/package.json +1 -1
- package/src/hook.ts +15 -0
- package/src/specs/NitroCompass.nitro.ts +17 -0
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.
|
|
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
|
|
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,8 +157,10 @@ 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,
|
|
163
|
+
enabled: true,
|
|
139
164
|
})
|
|
140
165
|
|
|
141
166
|
if (!hasCompass) return <Text>No compass on this device.</Text>
|
|
@@ -151,10 +176,63 @@ function CompassView() {
|
|
|
151
176
|
}
|
|
152
177
|
```
|
|
153
178
|
|
|
154
|
-
|
|
179
|
+
```ts
|
|
180
|
+
function useCompass(options?: UseCompassOptions): UseCompassResult
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Options
|
|
184
|
+
|
|
185
|
+
| Option | Type | Default | Description |
|
|
186
|
+
| --- | --- | --- | --- |
|
|
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). |
|
|
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. |
|
|
190
|
+
| `pauseOnBackground` | `boolean` | `true` | Pause the underlying sensor / location-manager subscription while the app is backgrounded and resume on foreground. |
|
|
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). |
|
|
192
|
+
|
|
193
|
+
`filterDegrees`, `smoothingAlpha`, `declination`, and `pauseOnBackground` map to global state on `NitroCompass` — if multiple hooks set them, last-write-wins.
|
|
194
|
+
|
|
195
|
+
#### Result
|
|
196
|
+
|
|
197
|
+
| Field | Type | Description |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| `reading` | `CompassSample \| null` | Latest emitted sample (`{ heading, accuracy }`), or `null` until the first arrives. Heading is true-north when `declination` is set, magnetic otherwise. |
|
|
200
|
+
| `quality` | `AccuracyQuality \| null` | Coarse calibration bucket — `'high'`, `'medium'`, `'low'`, or `'unreliable'`. `null` until the first transition. Show your own calibration UI on `'unreliable'`. |
|
|
201
|
+
| `interfering` | `boolean` | `true` while the raw magnetic field magnitude is outside the normal Earth band (~20–70 µT) — laptops, monitors, car engines, steel structures. |
|
|
202
|
+
| `hasCompass` | `boolean` | Hardware availability — read once on first render. Render a fallback when `false`. |
|
|
203
|
+
| `diagnostics` | `SensorDiagnostics \| undefined` | Which sensor backs the readings on this device (`rotationVector`, `geomagneticRotationVector`, or `coreLocation`). Useful for explaining quality differences. |
|
|
155
204
|
|
|
156
205
|
For non-React state managers, lower-level `addHeadingListener(cb): () => void`, `addCalibrationListener(cb): () => void`, and `addInterferenceListener(cb): () => void` are also exported. They are reference-counted: the first heading listener calls `start()`, the last unsubscribe calls `stop()`. Mixing these helpers with direct `NitroCompass.start()` / `setOnCalibrationNeeded()` / `setOnInterferenceDetected()` will clobber the multiplex's internal callback slot — pick one path.
|
|
157
206
|
|
|
207
|
+
### Smooth dial animation (Reanimated)
|
|
208
|
+
|
|
209
|
+
`useCompass()` returns React state, so each sample re-renders the consumer — fine for a numeric readout, but a rotating dial driven that way will jitter on faster filter values. For 60 fps animations, subscribe with `addHeadingListener` and write directly into a Reanimated shared value on the UI thread:
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
|
|
213
|
+
import { addHeadingListener } from 'react-native-nitro-compass'
|
|
214
|
+
|
|
215
|
+
function Dial() {
|
|
216
|
+
const angle = useSharedValue(0)
|
|
217
|
+
const last = useRef(0)
|
|
218
|
+
|
|
219
|
+
useEffect(() => addHeadingListener(({ heading }) => {
|
|
220
|
+
// unwrap so 359° → 1° animates +2°, not -358°
|
|
221
|
+
const wrapped = ((last.current % 360) + 360) % 360
|
|
222
|
+
let delta = heading - wrapped
|
|
223
|
+
if (delta > 180) delta -= 360
|
|
224
|
+
else if (delta < -180) delta += 360
|
|
225
|
+
last.current += delta
|
|
226
|
+
angle.value = withTiming(last.current, { duration: 80, easing: Easing.out(Easing.quad) })
|
|
227
|
+
}), [angle])
|
|
228
|
+
|
|
229
|
+
const style = useAnimatedStyle(() => ({ transform: [{ rotate: `${-angle.value}deg` }] }))
|
|
230
|
+
return <Animated.View style={[styles.dial, style]}>{/* ticks */}</Animated.View>
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The same pattern is used in [example/components/Compass.tsx](./example/components/Compass.tsx).
|
|
235
|
+
|
|
158
236
|
## Permissions
|
|
159
237
|
|
|
160
238
|
- **iOS**: requires `NSLocationWhenInUseUsageDescription` in `Info.plist`. `CLLocationManager` only emits headings when location permission is granted.
|
|
@@ -162,7 +240,7 @@ For non-React state managers, lower-level `addHeadingListener(cb): () => void`,
|
|
|
162
240
|
|
|
163
241
|
## Example app
|
|
164
242
|
|
|
165
|
-
A bare React Native CLI app under [example/](./example) (RN 0.
|
|
243
|
+
A bare React Native CLI app under [example/](./example) (RN 0.85.3, New Arch enabled) consumes the library via a local symlink. It demos the full surface — `useCompass()` for the readout, calibration / interference banners, and a Reanimated-driven dial that subscribes via `addHeadingListener` so the rotation runs entirely on the UI thread. Use it to test changes on a real device — the iOS Simulator has no compass and the Android emulator's magnetometer is faked.
|
|
166
244
|
|
|
167
245
|
First-time setup:
|
|
168
246
|
|
|
@@ -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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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 <
|
|
379
|
+
} else if accuracy < 20 {
|
|
370
380
|
quality = .high
|
|
371
|
-
} else if accuracy <
|
|
381
|
+
} else if accuracy < 35 {
|
|
372
382
|
quality = .medium
|
|
373
|
-
} else if accuracy <
|
|
383
|
+
} else if accuracy < 55 {
|
|
374
384
|
quality = .low
|
|
375
385
|
} else {
|
|
376
386
|
quality = .unreliable
|
package/lib/commonjs/hook.js
CHANGED
|
@@ -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]);
|
package/lib/commonjs/hook.js.map
CHANGED
|
@@ -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;
|
|
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":[]}
|
package/lib/module/hook.js
CHANGED
|
@@ -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]);
|
package/lib/module/hook.js.map
CHANGED
|
@@ -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;
|
|
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,
|
|
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;
|
package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt
CHANGED
|
@@ -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
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
|