react-native-nitro-compass 1.1.0 → 1.2.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 +141 -20
- package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +654 -133
- package/ios/HybridNitroCompass.swift +106 -3
- package/lib/commonjs/hook.js +98 -11
- package/lib/commonjs/hook.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/multiplex.js +23 -2
- package/lib/commonjs/multiplex.js.map +1 -1
- package/lib/module/hook.js +99 -12
- package/lib/module/hook.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/multiplex.js +23 -2
- package/lib/module/multiplex.js.map +1 -1
- package/lib/typescript/src/hook.d.ts +39 -1
- package/lib/typescript/src/hook.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/multiplex.d.ts.map +1 -1
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +142 -18
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCompassSample.hpp +7 -3
- package/nitrogen/generated/android/c++/JDebugInfo.hpp +85 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +17 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +3 -0
- package/nitrogen/generated/android/c++/JSensorKind.hpp +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/CompassSample.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/DebugInfo.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +12 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorKind.kt +4 -3
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.hpp +12 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +23 -0
- package/nitrogen/generated/ios/swift/CompassSample.swift +7 -2
- package/nitrogen/generated/ios/swift/DebugInfo.swift +64 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +34 -0
- package/nitrogen/generated/ios/swift/SensorKind.swift +8 -4
- package/nitrogen/generated/shared/c++/CompassSample.hpp +6 -2
- package/nitrogen/generated/shared/c++/DebugInfo.hpp +111 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +3 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +6 -0
- package/nitrogen/generated/shared/c++/SensorKind.hpp +10 -6
- package/package.json +2 -2
- package/src/hook.ts +146 -12
- package/src/index.ts +2 -0
- package/src/multiplex.ts +23 -2
- package/src/specs/NitroCompass.nitro.ts +147 -18
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Fast, accurate compass heading for React Native, powered by [Nitro Modules](https://github.com/mrousavy/nitro).
|
|
4
4
|
|
|
5
|
-
- **Android**: `
|
|
6
|
-
- **iOS**: `CLLocationManager` heading via `CLHeading.magneticHeading
|
|
7
|
-
- **JS API**: type-safe Nitro callbacks — no `NativeEventEmitter`, no string event names.
|
|
5
|
+
- **Android**: raw `TYPE_MAGNETIC_FIELD_UNCALIBRATED` + `TYPE_ACCELEROMETER` fed through `SensorManager.getRotationMatrix()` + `getOrientation()`, with a `TYPE_GAME_ROTATION_VECTOR` complementary filter on top for steady-state smoothness. This path is **stateless** — when a magnet or laptop is removed, the very next sample produces the correct heading instead of waiting for OS-level fusion to re-converge. We also detect OS hard-iron-bias jumps as a separate interference signal, so weak magnet events that don't push field magnitude out-of-band still register. Sensor delivery on a dedicated `HandlerThread` — never blocks the UI thread.
|
|
6
|
+
- **iOS**: `CLLocationManager` heading via `CLHeading.magneticHeading` for direction; `CMDeviceMotion.magneticField` (calibrated) for field strength + interference detection. Apple's stack handles sensor fusion natively.
|
|
7
|
+
- **JS API**: type-safe Nitro callbacks — no `NativeEventEmitter`, no string event names. Optional `useCompass()` React hook bundles subscription lifecycle, calibration / interference observation, and live-tuneable knobs into one ergonomic call.
|
|
8
8
|
|
|
9
9
|
## Why
|
|
10
10
|
|
|
11
|
-
Most React Native compass libraries use the
|
|
11
|
+
Most React Native compass libraries use Android's `TYPE_ROTATION_VECTOR`, which feels great until you put a magnet, a phone, or a laptop next to the device — then the OS-level Kalman filter holds a poisoned bias estimate for many seconds after the source is removed. This library computes heading directly from raw `accelerometer + magnetometer` via `getRotationMatrix()` (the same approach used by popular consumer compass apps), so recovery from interference is instant. We trade a few degrees of steady-state jitter for stateless behaviour, then add back smoothness via two layers: an adaptive input-side low-pass on the accel and mag vectors, plus a `TYPE_GAME_ROTATION_VECTOR` (gyro+accel) complementary filter that integrates Δyaw between events and lets mag samples pull it back to absolute. The end result tracks fast turns without lag, ignores transient magnet events, and snaps back instantly when interference clears.
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
@@ -53,36 +53,65 @@ NitroCompass.hasCompass(): boolean
|
|
|
53
53
|
|
|
54
54
|
NitroCompass.setFilter(degrees: number): void
|
|
55
55
|
NitroCompass.setSmoothing(alpha: number): void
|
|
56
|
+
NitroCompass.setDeclination(degrees: number): void
|
|
57
|
+
NitroCompass.setLocation(latitude: number, longitude: number): void
|
|
58
|
+
NitroCompass.setPauseOnBackground(enabled: boolean): void
|
|
59
|
+
|
|
56
60
|
NitroCompass.getCurrentHeading(): CompassSample | undefined
|
|
57
61
|
NitroCompass.getDiagnostics(): SensorDiagnostics | undefined
|
|
58
|
-
NitroCompass.
|
|
62
|
+
NitroCompass.getDebugInfo(): DebugInfo
|
|
63
|
+
|
|
59
64
|
NitroCompass.setOnCalibrationNeeded(onChange: (quality: AccuracyQuality) => void): void
|
|
60
65
|
NitroCompass.setOnInterferenceDetected(onChange: (interferenceDetected: boolean) => void): void
|
|
61
|
-
|
|
66
|
+
|
|
67
|
+
NitroCompass.recalibrate(): void
|
|
68
|
+
NitroCompass.getPermissionStatus(): PermissionStatus
|
|
69
|
+
NitroCompass.requestPermission(): Promise<PermissionStatus>
|
|
62
70
|
|
|
63
71
|
interface CompassSample {
|
|
64
|
-
heading: number
|
|
65
|
-
accuracy: number
|
|
72
|
+
heading: number // degrees, [0, 360); magnetic by default, true-north if setDeclination was called
|
|
73
|
+
accuracy: number // degrees, smaller is better; -1 if unknown
|
|
74
|
+
fieldStrengthMicroTesla: number // µT magnitude of the local magnetic field; -1 until first reading
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
type AccuracyQuality = 'high' | 'medium' | 'low' | 'unreliable'
|
|
69
|
-
type
|
|
78
|
+
type PermissionStatus = 'granted' | 'denied' | 'unknown'
|
|
79
|
+
type SensorKind =
|
|
80
|
+
| 'magnetometer'
|
|
81
|
+
| 'coreLocation'
|
|
82
|
+
| 'rotationVector' // legacy, no longer returned
|
|
83
|
+
| 'geomagneticRotationVector' // legacy, no longer returned
|
|
70
84
|
interface SensorDiagnostics { sensor: SensorKind }
|
|
85
|
+
interface DebugInfo {
|
|
86
|
+
interferenceActive: boolean
|
|
87
|
+
msSinceLastBiasJump: number // -1 if never seen / iOS
|
|
88
|
+
expectedFieldMicroTesla: number // -1 if setLocation not called
|
|
89
|
+
lastFieldMicroTesla: number // -1 if no reading
|
|
90
|
+
fusedYawDeg: number // NaN before first sample / iOS
|
|
91
|
+
lastYawRateDegPerS: number // 0 if game-RV unavailable
|
|
92
|
+
hasGameRotationVector: boolean // false on iOS
|
|
93
|
+
usingUncalibratedMag: boolean // false on iOS
|
|
94
|
+
}
|
|
71
95
|
```
|
|
72
96
|
|
|
73
97
|
- `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
98
|
- `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.
|
|
75
99
|
- `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.
|
|
76
|
-
- `getDiagnostics()` reports which sensor would produce headings on this device
|
|
77
|
-
- `accuracy` is a numeric uncertainty (degrees). On iOS it comes from `CLHeading.headingAccuracy` directly. On Android it
|
|
100
|
+
- `getDiagnostics()` reports which sensor would produce headings on this device. On Android this is always `magnetometer` for current builds (older versions returned `rotationVector` / `geomagneticRotationVector`); on iOS it's `coreLocation`. Safe to call before `start()`.
|
|
101
|
+
- `accuracy` is a numeric uncertainty (degrees). On iOS it comes from `CLHeading.headingAccuracy` directly. On Android it's a coarse degree estimate derived from the magnetometer's `SensorManager.SENSOR_STATUS_*` accuracy bucket — Android's figure-8 calibration signal — mapped to `HIGH→5°`, `MEDIUM→15°`, `LOW→30°`.
|
|
102
|
+
- `fieldStrengthMicroTesla` is the magnitude of the local magnetic field in µT, or `-1` until the first reading lands. Earth's field is normally 25–65 µT — values well outside this band signal external interference (laptops, monitors, magnets, ferrous metal). Useful for rendering a field-strength meter à la consumer compass apps.
|
|
78
103
|
- `getCurrentHeading()` returns the most recently emitted sample (with declination already applied), or `undefined` if not started yet or no sample has arrived.
|
|
104
|
+
- `setLocation(lat, lon)` lets the library tighten the interference detection band on Android. With a valid location, the generic 20–70 µT "Earth field" band is replaced by `expectedField ± 15 µT`, where `expectedField` comes from the WMM2025 model bundled in `GeomagneticField`. This catches weak interference at high/low latitudes where Earth's natural field is near or above 60 µT. Pass `NaN` or out-of-range values to revert to the generic band. **No-op on iOS** — `CLLocationManager` already uses GPS-derived location internally.
|
|
105
|
+
- `recalibrate()` is a manual nudge for stuck calibration state. On Android it re-registers the sensor listeners (often nudges the magnetometer driver to re-evaluate soft/hard-iron calibration); on iOS it dismisses the system heading-calibration overlay and stops/restarts heading updates. Idempotent; safe to call before `start()`. The user still has to move the device through varying orientations — this just clears cached state so progress is reflected promptly. Useful behind a "Refresh" button in your calibration UI.
|
|
106
|
+
- `getDebugInfo()` returns a live snapshot of internal pipeline state. Intended for diagnosing user-reported issues — not needed for normal operation. The bundled `<DebugPanel />` in [example/components/DebugPanel.tsx](./example/components/DebugPanel.tsx) polls it at 4 Hz behind a collapsible footer; copy/adapt it into your debug build to make user bug reports self-diagnosing. Most fields are Android-only — see the inline JSDoc on the `DebugInfo` interface.
|
|
107
|
+
- `getPermissionStatus()` / `requestPermission()` map to platform location permission. Always `'granted'` on Android (sensors require no permission). On iOS, `getPermissionStatus()` reads `CLLocationManager.authorizationStatus`; `requestPermission()` prompts the system "Allow location" dialog if status is `'unknown'` and resolves once the user makes a choice. iOS does not re-prompt — subsequent calls resolve immediately with the cached status.
|
|
79
108
|
|
|
80
109
|
### Calibration
|
|
81
110
|
|
|
82
111
|
`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
112
|
|
|
84
113
|
- **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`)
|
|
114
|
+
- **Android** uses the magnetometer's `SensorManager.SENSOR_STATUS_*` bucket from `onAccuracyChanged` directly (`HIGH` / `MEDIUM` / `LOW` / `UNRELIABLE`) — Android's signal that the user should do (or has done) a figure-8 to recalibrate. **When magnetic interference is currently detected, the surfaced bucket is downgraded by one notch** (`HIGH→MEDIUM`, `MEDIUM→LOW`, `LOW→UNRELIABLE`) — calibration ("the magnetometer needs to be tuned") and interference ("the field is currently being skewed by something nearby") are independent signals, and surfacing `quality='high'` alongside `interfering=true` is contradictory UX.
|
|
86
115
|
|
|
87
116
|
Both platforms can plausibly emit `'high'` on a clean device — the threshold split just reflects each OS's reporting style.
|
|
88
117
|
|
|
@@ -96,7 +125,7 @@ NitroCompass.setOnCalibrationNeeded((q) => {
|
|
|
96
125
|
|
|
97
126
|
`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
127
|
|
|
99
|
-
Interference is surfaced
|
|
128
|
+
Interference is surfaced three ways: (1) directly via this callback, (2) on Android, the calibration bucket emitted by `setOnCalibrationNeeded` is downgraded by one notch while interference is detected (see the Calibration section above), and (3) every `CompassSample` carries `fieldStrengthMicroTesla` so you can render a live strength meter. On iOS, the calibration downgrade is skipped — `CLLocationManager`'s own accuracy reporting already responds to magnetometer disturbance, so a separate downgrade would double-count.
|
|
100
129
|
|
|
101
130
|
```ts
|
|
102
131
|
NitroCompass.setOnInterferenceDetected((interfering) => {
|
|
@@ -105,7 +134,94 @@ NitroCompass.setOnInterferenceDetected((interfering) => {
|
|
|
105
134
|
})
|
|
106
135
|
```
|
|
107
136
|
|
|
108
|
-
Android
|
|
137
|
+
Detection on **Android** combines two signals: (1) the raw magnetic field magnitude leaving the Earth band, and (2) recent OS hard-iron-bias jumps on `TYPE_MAGNETIC_FIELD_UNCALIBRATED`. The bias-jump signal catches *weak* interference events the magnitude check alone would miss — e.g. another phone placed on top of yours, where the corrected field magnitude stays near 50 µT but the OS still revises its bias estimate. Either signal flips `interfering` to `true`; both must clear (and a 1.5 s grace window expire) before `false` is reported. If you call `setLocation(lat, lon)`, the magnitude band tightens to `expectedField ± 15 µT` for a more sensitive gate at high or low latitudes.
|
|
138
|
+
|
|
139
|
+
On **iOS**, detection uses `CMDeviceMotion.magneticField` (calibrated, with the device's own hard-iron bias subtracted in real time). Transitions wait for CoreMotion's bias estimate to converge (5 consecutive non-`uncalibrated` samples — typically a second or two of normal device movement after subscribe) so the first second post-`start()` doesn't fire false positives.
|
|
140
|
+
|
|
141
|
+
Only triggered while `start()` is active; no debounce, so brief excursions still fire.
|
|
142
|
+
|
|
143
|
+
### Location-aware interference (recipe)
|
|
144
|
+
|
|
145
|
+
`setLocation(lat, lon)` tightens the Android interference gate from the generic 20–70 µT band to `expectedField ± 15 µT`, where `expectedField` comes from the WMM2025 model bundled in `GeomagneticField`. This catches weak interference at high or low latitudes where Earth's natural field is near or above 60 µT — exactly the cases where the generic band is too loose to detect, say, another phone placed nearby.
|
|
146
|
+
|
|
147
|
+
Pair it with any geolocation library — the example below uses [`react-native-geolocation-service`](https://github.com/Agontuk/react-native-geolocation-service):
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { useEffect } from 'react'
|
|
151
|
+
import { Platform, PermissionsAndroid } from 'react-native'
|
|
152
|
+
import Geolocation from 'react-native-geolocation-service'
|
|
153
|
+
import { useCompass } from 'react-native-nitro-compass'
|
|
154
|
+
|
|
155
|
+
async function ensureLocationPermission(): Promise<boolean> {
|
|
156
|
+
if (Platform.OS === 'android') {
|
|
157
|
+
const granted = await PermissionsAndroid.request(
|
|
158
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
|
|
159
|
+
)
|
|
160
|
+
return granted === PermissionsAndroid.RESULTS.GRANTED
|
|
161
|
+
}
|
|
162
|
+
// iOS — useCompass already prompts for location auth for the compass
|
|
163
|
+
// itself, so a granted permission lets us read position too.
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function CompassScreen() {
|
|
168
|
+
const compass = useCompass({ enabled: true })
|
|
169
|
+
const { setLocation } = compass
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
let cancelled = false
|
|
173
|
+
let watchId: number | undefined
|
|
174
|
+
|
|
175
|
+
void (async () => {
|
|
176
|
+
if (!(await ensureLocationPermission()) || cancelled) return
|
|
177
|
+
|
|
178
|
+
// One-shot fix at start. Coarse accuracy is fine — Earth's
|
|
179
|
+
// field varies < 0.5 % per km, so a city-block-resolution
|
|
180
|
+
// position is more than enough for the ±15 µT tolerance.
|
|
181
|
+
Geolocation.getCurrentPosition(
|
|
182
|
+
({ coords }) => {
|
|
183
|
+
if (!cancelled) setLocation(coords.latitude, coords.longitude)
|
|
184
|
+
},
|
|
185
|
+
() => {
|
|
186
|
+
/* swallow — falls back to the generic 20–70 µT band */
|
|
187
|
+
},
|
|
188
|
+
{ enableHighAccuracy: false, timeout: 15_000, maximumAge: 60_000 },
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Optional: keep `expectedField` in sync if the user moves
|
|
192
|
+
// long-distance. A 50 km move shifts the WMM-derived field by
|
|
193
|
+
// ~0.3 µT — well inside the tolerance — so a coarse 10 km /
|
|
194
|
+
// 10 minute filter is plenty.
|
|
195
|
+
watchId = Geolocation.watchPosition(
|
|
196
|
+
({ coords }) => {
|
|
197
|
+
if (!cancelled) setLocation(coords.latitude, coords.longitude)
|
|
198
|
+
},
|
|
199
|
+
() => {},
|
|
200
|
+
{
|
|
201
|
+
enableHighAccuracy: false,
|
|
202
|
+
distanceFilter: 10_000,
|
|
203
|
+
interval: 10 * 60 * 1000,
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
})()
|
|
207
|
+
|
|
208
|
+
return () => {
|
|
209
|
+
cancelled = true
|
|
210
|
+
if (watchId !== undefined) Geolocation.clearWatch(watchId)
|
|
211
|
+
}
|
|
212
|
+
}, [setLocation])
|
|
213
|
+
|
|
214
|
+
// …render compass.reading, compass.quality, etc.
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
A few notes:
|
|
219
|
+
|
|
220
|
+
- **`setLocation` from `useCompass()` has a stable identity**, so listing it in the effect dependency array is safe — it won't re-run on every compass tick.
|
|
221
|
+
- **No location permission required** for the compass itself on Android (sensors are unrestricted). The location permission requested above is purely so `react-native-geolocation-service` can give you a fix; if it's denied, the compass still works — it just falls back to the generic interference band.
|
|
222
|
+
- **iOS is a no-op** for `setLocation` — `CLLocationManager` already uses GPS-derived location internally for all field-related reasoning, so calling it changes nothing on iOS. The recipe still works cross-platform; it's just that on iOS the call is effectively wasted. You can guard with `if (Platform.OS === 'android')` if you'd rather skip the geolocation request entirely on iOS.
|
|
223
|
+
- **One-shot vs watch**: if your app is stationary (typical phone use), the one-shot `getCurrentPosition` is enough. The `watchPosition` only matters if your user is driving / flying long distances; the field strength changes slowly enough that a coarse low-frequency watch is fine.
|
|
224
|
+
- **Pass `NaN` to revert** to the generic band if the location becomes stale or the user revokes permission: `setLocation(NaN, NaN)`.
|
|
109
225
|
|
|
110
226
|
### Magnetic vs true north
|
|
111
227
|
|
|
@@ -127,7 +243,7 @@ Pass `0` to revert to magnetic. Declination survives `stop()`/`start()` cycles.
|
|
|
127
243
|
|
|
128
244
|
### Smoothing
|
|
129
245
|
|
|
130
|
-
Android's raw
|
|
246
|
+
Android's raw accelerometer + magnetometer heading jitters by `±1–3°` even at rest. 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
247
|
|
|
132
248
|
Tune live:
|
|
133
249
|
|
|
@@ -196,11 +312,16 @@ function useCompass(options?: UseCompassOptions): UseCompassResult
|
|
|
196
312
|
|
|
197
313
|
| Field | Type | Description |
|
|
198
314
|
| --- | --- | --- |
|
|
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. |
|
|
315
|
+
| `reading` | `CompassSample \| null` | Latest emitted sample (`{ heading, accuracy, fieldStrengthMicroTesla }`), or `null` until the first arrives. Heading is true-north when `declination` is set, magnetic otherwise. |
|
|
200
316
|
| `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
|
|
317
|
+
| `interfering` | `boolean` | `true` while external magnetic interference is detected (field-magnitude band check + Android bias-jump grace). See [Magnetic interference](#magnetic-interference). |
|
|
202
318
|
| `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 (`
|
|
319
|
+
| `diagnostics` | `SensorDiagnostics \| undefined` | Which sensor backs the readings on this device (`magnetometer` on Android, `coreLocation` on iOS). Useful for explaining quality differences. |
|
|
320
|
+
| `permission` | `PermissionStatus` | Latest platform permission state. Always `'granted'` on Android. On iOS may transition `'unknown'` → `'granted'`/`'denied'` after `requestPermission()` resolves. |
|
|
321
|
+
| `getCurrentHeading` | `() => CompassSample \| undefined` | Synchronous read of the most recent sample. Stable identity across renders. Useful inside event handlers without forcing a re-render. |
|
|
322
|
+
| `recalibrate` | `() => void` | Force a best-effort sensor recalibration (Refresh button behind a calibration banner). Stable identity. See [`NitroCompass.recalibrate()`](#api). |
|
|
323
|
+
| `setLocation` | `(lat: number, lon: number) => void` | Tighten the interference gate using `expectedField ± 15 µT` (Android only). No-op on iOS. Stable identity. |
|
|
324
|
+
| `requestPermission` | `() => Promise<PermissionStatus>` | Prompt the platform permission dialog (iOS) and update the hook's `permission` field. Resolves with the resulting status. Stable identity. |
|
|
204
325
|
|
|
205
326
|
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.
|
|
206
327
|
|
|
@@ -236,7 +357,7 @@ The same pattern is used in [example/components/Compass.tsx](./example/component
|
|
|
236
357
|
## Permissions
|
|
237
358
|
|
|
238
359
|
- **iOS**: requires `NSLocationWhenInUseUsageDescription` in `Info.plist`. `CLLocationManager` only emits headings when location permission is granted.
|
|
239
|
-
- **Android**: no permission required for the
|
|
360
|
+
- **Android**: no permission required for the magnetometer or accelerometer.
|
|
240
361
|
|
|
241
362
|
## Example app
|
|
242
363
|
|
|
@@ -275,7 +396,7 @@ The example imports `NitroCompass` directly from the workspace `src/` (via Metro
|
|
|
275
396
|
|
|
276
397
|
## Acknowledgments
|
|
277
398
|
|
|
278
|
-
The Android
|
|
399
|
+
The Android sensor pattern (raw mag + accel fusion via `getRotationMatrix`, surface-rotation remapping, `getOrientation` extraction, EMA on `(sin θ, cos θ)`) is adapted from the MIT-licensed [Andromeda](https://github.com/kylecorry31/andromeda) sensor library by Kyle Corry, which powers the [Trail Sense](https://github.com/kylecorry31/Trail-Sense) wilderness navigation app.
|
|
279
400
|
|
|
280
401
|
Bootstrapped with [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module).
|
|
281
402
|
|