react-native-nitro-compass 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NitroCompass.podspec +31 -0
- package/README.md +206 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +148 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/nitrocompass/HybridNitroCompass.kt +481 -0
- package/android/src/main/java/com/margelo/nitro/nitrocompass/NitroCompassPackage.kt +18 -0
- package/app.plugin.js +16 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridNitroCompass.swift +473 -0
- package/lib/commonjs/hook.js +69 -0
- package/lib/commonjs/hook.js.map +1 -0
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/multiplex.js +109 -0
- package/lib/commonjs/multiplex.js.map +1 -0
- package/lib/commonjs/native.js +9 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NitroCompass.nitro.js +6 -0
- package/lib/commonjs/specs/NitroCompass.nitro.js.map +1 -0
- package/lib/module/hook.js +65 -0
- package/lib/module/hook.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/multiplex.js +103 -0
- package/lib/module/multiplex.js.map +1 -0
- package/lib/module/native.js +5 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/specs/NitroCompass.nitro.js +4 -0
- package/lib/module/specs/NitroCompass.nitro.js.map +1 -0
- package/lib/typescript/src/hook.d.ts +49 -0
- package/lib/typescript/src/hook.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/multiplex.d.ts +38 -0
- package/lib/typescript/src/multiplex.d.ts.map +1 -0
- package/lib/typescript/src/native.d.ts +3 -0
- package/lib/typescript/src/native.d.ts.map +1 -0
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts +176 -0
- package/lib/typescript/src/specs/NitroCompass.nitro.d.ts.map +1 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroCompass+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroCompass+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroCompassOnLoad.cpp +60 -0
- package/nitrogen/generated/android/NitroCompassOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JAccuracyQuality.hpp +64 -0
- package/nitrogen/generated/android/c++/JCompassSample.hpp +61 -0
- package/nitrogen/generated/android/c++/JFunc_void_AccuracyQuality.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_CompassSample.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.cpp +143 -0
- package/nitrogen/generated/android/c++/JHybridNitroCompassSpec.hpp +75 -0
- package/nitrogen/generated/android/c++/JPermissionStatus.hpp +61 -0
- package/nitrogen/generated/android/c++/JSensorDiagnostics.hpp +58 -0
- package/nitrogen/generated/android/c++/JSensorKind.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/AccuracyQuality.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/CompassSample.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_AccuracyQuality.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_CompassSample.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/HybridNitroCompassSpec.kt +118 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/NitroCompassOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/PermissionStatus.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorDiagnostics.kt +51 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrocompass/SensorKind.kt +24 -0
- package/nitrogen/generated/ios/NitroCompass+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.cpp +73 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Bridge.hpp +267 -0
- package/nitrogen/generated/ios/NitroCompass-Swift-Cxx-Umbrella.hpp +61 -0
- package/nitrogen/generated/ios/NitroCompassAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroCompassAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroCompassSpecSwift.hpp +180 -0
- package/nitrogen/generated/ios/swift/AccuracyQuality.swift +48 -0
- package/nitrogen/generated/ios/swift/CompassSample.swift +34 -0
- package/nitrogen/generated/ios/swift/Func_void_AccuracyQuality.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_CompassSample.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_PermissionStatus.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec.swift +67 -0
- package/nitrogen/generated/ios/swift/HybridNitroCompassSpec_cxx.swift +309 -0
- package/nitrogen/generated/ios/swift/PermissionStatus.swift +44 -0
- package/nitrogen/generated/ios/swift/SensorDiagnostics.swift +29 -0
- package/nitrogen/generated/ios/swift/SensorKind.swift +44 -0
- package/nitrogen/generated/shared/c++/AccuracyQuality.hpp +84 -0
- package/nitrogen/generated/shared/c++/CompassSample.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.cpp +33 -0
- package/nitrogen/generated/shared/c++/HybridNitroCompassSpec.hpp +87 -0
- package/nitrogen/generated/shared/c++/PermissionStatus.hpp +80 -0
- package/nitrogen/generated/shared/c++/SensorDiagnostics.hpp +84 -0
- package/nitrogen/generated/shared/c++/SensorKind.hpp +80 -0
- package/package.json +136 -0
- package/react-native.config.js +11 -0
- package/src/hook.ts +118 -0
- package/src/index.ts +28 -0
- package/src/multiplex.ts +117 -0
- package/src/native.ts +5 -0
- package/src/specs/NitroCompass.nitro.ts +193 -0
package/package.json
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-nitro-compass",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=18"
|
|
6
|
+
},
|
|
7
|
+
"description": "Fast, accurate compass heading for React Native, powered by Nitro Modules. Uses Android TYPE_ROTATION_VECTOR sensor fusion and iOS CLHeading.",
|
|
8
|
+
"main": "./lib/commonjs/index.js",
|
|
9
|
+
"module": "./lib/module/index.js",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"react-native": "src/index",
|
|
12
|
+
"source": "src/index",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"clean": "git clean -dfX",
|
|
17
|
+
"release": "semantic-release",
|
|
18
|
+
"build": "npm run typecheck && bob build",
|
|
19
|
+
"prepare": "bob build",
|
|
20
|
+
"codegen": "nitrogen --logLevel=\"debug\" && npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react-native",
|
|
24
|
+
"nitro",
|
|
25
|
+
"nitro-modules",
|
|
26
|
+
"compass",
|
|
27
|
+
"heading",
|
|
28
|
+
"magnetometer",
|
|
29
|
+
"qibla",
|
|
30
|
+
"rotation-vector",
|
|
31
|
+
"ios",
|
|
32
|
+
"android"
|
|
33
|
+
],
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"!src/__tests__",
|
|
37
|
+
"react-native.config.js",
|
|
38
|
+
"lib",
|
|
39
|
+
"!lib/typescript/src/__tests__",
|
|
40
|
+
"nitrogen",
|
|
41
|
+
"cpp",
|
|
42
|
+
"nitro.json",
|
|
43
|
+
"android/build.gradle",
|
|
44
|
+
"android/fix-prefab.gradle",
|
|
45
|
+
"android/gradle.properties",
|
|
46
|
+
"android/CMakeLists.txt",
|
|
47
|
+
"android/src",
|
|
48
|
+
"ios/**/*.h",
|
|
49
|
+
"ios/**/*.m",
|
|
50
|
+
"ios/**/*.mm",
|
|
51
|
+
"ios/**/*.cpp",
|
|
52
|
+
"ios/**/*.swift",
|
|
53
|
+
"app.plugin.js",
|
|
54
|
+
"*.podspec",
|
|
55
|
+
"README.md"
|
|
56
|
+
],
|
|
57
|
+
"repository": "https://github.com/omarsdev/react-native-nitro-compass.git",
|
|
58
|
+
"author": "Omar Sukarieh",
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"bugs": "https://github.com/omarsdev/react-native-nitro-compass/issues",
|
|
61
|
+
"homepage": "https://github.com/omarsdev/react-native-nitro-compass#readme",
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public",
|
|
64
|
+
"registry": "https://registry.npmjs.org/"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@jamesacarr/eslint-formatter-github-actions": "^0.2.0",
|
|
68
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
69
|
+
"@semantic-release/git": "^10.0.1",
|
|
70
|
+
"@types/jest": "^29.5.12",
|
|
71
|
+
"@types/react": "19.2.0",
|
|
72
|
+
"babel-jest": "^30.3.0",
|
|
73
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
74
|
+
"jest": "^30.3.0",
|
|
75
|
+
"nitrogen": "0.35.6",
|
|
76
|
+
"react": "19.2.3",
|
|
77
|
+
"react-native": "0.84.1",
|
|
78
|
+
"react-native-builder-bob": "^0.40.18",
|
|
79
|
+
"react-native-nitro-modules": "0.35.6",
|
|
80
|
+
"semantic-release": "^25.0.3",
|
|
81
|
+
"typescript": "^5.8.3"
|
|
82
|
+
},
|
|
83
|
+
"peerDependencies": {
|
|
84
|
+
"react": "*",
|
|
85
|
+
"react-native": "*",
|
|
86
|
+
"react-native-nitro-modules": "*"
|
|
87
|
+
},
|
|
88
|
+
"eslintConfig": {
|
|
89
|
+
"root": true,
|
|
90
|
+
"extends": [
|
|
91
|
+
"@react-native",
|
|
92
|
+
"prettier"
|
|
93
|
+
],
|
|
94
|
+
"plugins": [
|
|
95
|
+
"prettier"
|
|
96
|
+
],
|
|
97
|
+
"rules": {
|
|
98
|
+
"prettier/prettier": [
|
|
99
|
+
"warn",
|
|
100
|
+
{
|
|
101
|
+
"quoteProps": "consistent",
|
|
102
|
+
"singleQuote": true,
|
|
103
|
+
"tabWidth": 2,
|
|
104
|
+
"trailingComma": "es5",
|
|
105
|
+
"useTabs": false
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"eslintIgnore": [
|
|
111
|
+
"node_modules/",
|
|
112
|
+
"lib/"
|
|
113
|
+
],
|
|
114
|
+
"prettier": {
|
|
115
|
+
"quoteProps": "consistent",
|
|
116
|
+
"singleQuote": true,
|
|
117
|
+
"tabWidth": 2,
|
|
118
|
+
"trailingComma": "es5",
|
|
119
|
+
"useTabs": false,
|
|
120
|
+
"semi": false
|
|
121
|
+
},
|
|
122
|
+
"react-native-builder-bob": {
|
|
123
|
+
"source": "src",
|
|
124
|
+
"output": "lib",
|
|
125
|
+
"targets": [
|
|
126
|
+
"commonjs",
|
|
127
|
+
"module",
|
|
128
|
+
[
|
|
129
|
+
"typescript",
|
|
130
|
+
{
|
|
131
|
+
"project": "tsconfig.json"
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/hook.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
AccuracyQuality,
|
|
4
|
+
CompassSample,
|
|
5
|
+
SensorDiagnostics,
|
|
6
|
+
} from './specs/NitroCompass.nitro'
|
|
7
|
+
import { NitroCompass } from './native'
|
|
8
|
+
import {
|
|
9
|
+
addCalibrationListener,
|
|
10
|
+
addHeadingListener,
|
|
11
|
+
addInterferenceListener,
|
|
12
|
+
} from './multiplex'
|
|
13
|
+
|
|
14
|
+
export interface UseCompassOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Minimum change between samples in degrees. Pass `0` for "every
|
|
17
|
+
* event". Default `1`. Updated live via `NitroCompass.setFilter()`.
|
|
18
|
+
* Note: this is global state shared with any other consumer of
|
|
19
|
+
* the library — last-write-wins.
|
|
20
|
+
*/
|
|
21
|
+
filterDegrees?: number
|
|
22
|
+
/**
|
|
23
|
+
* Magnetic-to-true offset in signed degrees. Default `0` (magnetic).
|
|
24
|
+
* Pull from a model like `geomagnetism` keyed on the user's lat/lon.
|
|
25
|
+
* Like `filterDegrees`, this is shared global state.
|
|
26
|
+
*/
|
|
27
|
+
declination?: number
|
|
28
|
+
/**
|
|
29
|
+
* Pause the underlying sensor when the app is backgrounded. Default
|
|
30
|
+
* `true`. Shared global state.
|
|
31
|
+
*/
|
|
32
|
+
pauseOnBackground?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Toggle the heading subscription without unmounting the hook.
|
|
35
|
+
* Default `true`. When `false`, the hook still observes calibration
|
|
36
|
+
* and interference (so you can show warnings) but won't tear down
|
|
37
|
+
* its own state — `reading` simply stops updating.
|
|
38
|
+
*/
|
|
39
|
+
enabled?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UseCompassResult {
|
|
43
|
+
/** Latest emitted sample, or `null` until the first arrives. */
|
|
44
|
+
reading: CompassSample | null
|
|
45
|
+
/** Coarse accuracy bucket, or `null` until the first transition. */
|
|
46
|
+
quality: AccuracyQuality | null
|
|
47
|
+
/** Whether external magnetic interference is currently detected. */
|
|
48
|
+
interfering: boolean
|
|
49
|
+
/** Hardware availability — read once on first render. */
|
|
50
|
+
hasCompass: boolean
|
|
51
|
+
/** Which sensor backs the readings on this device. */
|
|
52
|
+
diagnostics: SensorDiagnostics | undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ergonomic React wrapper for the NitroCompass surface. Handles
|
|
57
|
+
* subscription lifecycle, callback registration, and the live-tuneable
|
|
58
|
+
* knobs. Multiple instances mounted at once safely share the same
|
|
59
|
+
* underlying native subscription via the multi-listener primitives in
|
|
60
|
+
* `./multiplex`.
|
|
61
|
+
*/
|
|
62
|
+
export function useCompass(
|
|
63
|
+
options: UseCompassOptions = {}
|
|
64
|
+
): UseCompassResult {
|
|
65
|
+
const {
|
|
66
|
+
filterDegrees = 1,
|
|
67
|
+
declination = 0,
|
|
68
|
+
pauseOnBackground = true,
|
|
69
|
+
enabled = true,
|
|
70
|
+
} = options
|
|
71
|
+
|
|
72
|
+
const [reading, setReading] = useState<CompassSample | null>(null)
|
|
73
|
+
const [quality, setQuality] = useState<AccuracyQuality | null>(null)
|
|
74
|
+
const [interfering, setInterfering] = useState(false)
|
|
75
|
+
|
|
76
|
+
const [hasCompass] = useState(() => NitroCompass.hasCompass())
|
|
77
|
+
const [diagnostics] = useState(() => NitroCompass.getDiagnostics())
|
|
78
|
+
|
|
79
|
+
// Tracked via ref so the heading-subscription effect can re-apply
|
|
80
|
+
// the user's filter after a stop/start cycle without restarting on
|
|
81
|
+
// every filterDegrees change.
|
|
82
|
+
const filterRef = useRef(filterDegrees)
|
|
83
|
+
filterRef.current = filterDegrees
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
NitroCompass.setFilter(filterDegrees)
|
|
87
|
+
}, [filterDegrees])
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
NitroCompass.setDeclination(declination)
|
|
91
|
+
}, [declination])
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
NitroCompass.setPauseOnBackground(pauseOnBackground)
|
|
95
|
+
}, [pauseOnBackground])
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!hasCompass) return
|
|
99
|
+
return addCalibrationListener(setQuality)
|
|
100
|
+
}, [hasCompass])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!hasCompass) return
|
|
104
|
+
return addInterferenceListener(setInterfering)
|
|
105
|
+
}, [hasCompass])
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!hasCompass || !enabled) return
|
|
109
|
+
const off = addHeadingListener(setReading)
|
|
110
|
+
// Multiplex starts the sensor with a default filter; re-apply the
|
|
111
|
+
// current option after subscribing.
|
|
112
|
+
NitroCompass.setFilter(filterRef.current)
|
|
113
|
+
return off
|
|
114
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
115
|
+
}, [hasCompass, enabled])
|
|
116
|
+
|
|
117
|
+
return { reading, quality, interfering, hasCompass, diagnostics }
|
|
118
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccuracyQuality,
|
|
3
|
+
CompassSample,
|
|
4
|
+
NitroCompass as NitroCompassSpec,
|
|
5
|
+
PermissionStatus,
|
|
6
|
+
SensorDiagnostics,
|
|
7
|
+
SensorKind,
|
|
8
|
+
} from './specs/NitroCompass.nitro'
|
|
9
|
+
|
|
10
|
+
export { NitroCompass } from './native'
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
AccuracyQuality,
|
|
14
|
+
CompassSample,
|
|
15
|
+
PermissionStatus,
|
|
16
|
+
SensorDiagnostics,
|
|
17
|
+
SensorKind,
|
|
18
|
+
}
|
|
19
|
+
export type { NitroCompassSpec as NitroCompassHybridObject }
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
addCalibrationListener,
|
|
23
|
+
addHeadingListener,
|
|
24
|
+
addInterferenceListener,
|
|
25
|
+
} from './multiplex'
|
|
26
|
+
|
|
27
|
+
export { useCompass } from './hook'
|
|
28
|
+
export type { UseCompassOptions, UseCompassResult } from './hook'
|
package/src/multiplex.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JS-side fan-out so multiple consumers can subscribe to the same
|
|
3
|
+
* compass stream without clobbering each other. The native API is
|
|
4
|
+
* single-callback by design (start, setOnCalibrationNeeded,
|
|
5
|
+
* setOnInterferenceDetected each own one slot); these helpers wrap
|
|
6
|
+
* that into multi-listener primitives with reference-counted
|
|
7
|
+
* lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* Mixing direct `NitroCompass.start()` / `setOnCalibrationNeeded()` /
|
|
10
|
+
* `setOnInterferenceDetected()` calls with these helpers will
|
|
11
|
+
* clobber the multiplex's internal callback slot — pick one path.
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
AccuracyQuality,
|
|
15
|
+
CompassSample,
|
|
16
|
+
} from './specs/NitroCompass.nitro'
|
|
17
|
+
import { NitroCompass } from './native'
|
|
18
|
+
|
|
19
|
+
type HeadingListener = (sample: CompassSample) => void
|
|
20
|
+
type CalibrationListener = (quality: AccuracyQuality) => void
|
|
21
|
+
type InterferenceListener = (interferenceDetected: boolean) => void
|
|
22
|
+
|
|
23
|
+
const headingListeners = new Set<HeadingListener>()
|
|
24
|
+
const calibrationListeners = new Set<CalibrationListener>()
|
|
25
|
+
const interferenceListeners = new Set<InterferenceListener>()
|
|
26
|
+
|
|
27
|
+
let calibrationRegistered = false
|
|
28
|
+
let interferenceRegistered = false
|
|
29
|
+
|
|
30
|
+
const DEFAULT_FILTER_DEG = 1
|
|
31
|
+
|
|
32
|
+
function dispatchHeading(sample: CompassSample) {
|
|
33
|
+
for (const cb of Array.from(headingListeners)) {
|
|
34
|
+
try {
|
|
35
|
+
cb(sample)
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('[NitroCompass] heading listener threw:', e)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function dispatchCalibration(quality: AccuracyQuality) {
|
|
43
|
+
for (const cb of Array.from(calibrationListeners)) {
|
|
44
|
+
try {
|
|
45
|
+
cb(quality)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('[NitroCompass] calibration listener threw:', e)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dispatchInterference(detected: boolean) {
|
|
53
|
+
for (const cb of Array.from(interferenceListeners)) {
|
|
54
|
+
try {
|
|
55
|
+
cb(detected)
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error('[NitroCompass] interference listener threw:', e)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to heading samples. The first listener implicitly calls
|
|
64
|
+
* `NitroCompass.start()`; the last `unsubscribe()` calls
|
|
65
|
+
* `NitroCompass.stop()`. Returns the unsubscribe function.
|
|
66
|
+
*
|
|
67
|
+
* Filter, declination, and pauseOnBackground remain global state on
|
|
68
|
+
* `NitroCompass` and are shared across all listeners — call
|
|
69
|
+
* `NitroCompass.setFilter()` etc. directly to tune them.
|
|
70
|
+
*/
|
|
71
|
+
export function addHeadingListener(cb: HeadingListener): () => void {
|
|
72
|
+
const wasEmpty = headingListeners.size === 0
|
|
73
|
+
headingListeners.add(cb)
|
|
74
|
+
if (wasEmpty) {
|
|
75
|
+
NitroCompass.start(DEFAULT_FILTER_DEG, dispatchHeading)
|
|
76
|
+
}
|
|
77
|
+
return () => {
|
|
78
|
+
if (!headingListeners.delete(cb)) return
|
|
79
|
+
if (headingListeners.size === 0) {
|
|
80
|
+
NitroCompass.stop()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Subscribe to calibration-bucket transitions. Only fires while a
|
|
87
|
+
* heading subscription is active. Returns the unsubscribe function.
|
|
88
|
+
*/
|
|
89
|
+
export function addCalibrationListener(
|
|
90
|
+
cb: CalibrationListener
|
|
91
|
+
): () => void {
|
|
92
|
+
if (!calibrationRegistered) {
|
|
93
|
+
NitroCompass.setOnCalibrationNeeded(dispatchCalibration)
|
|
94
|
+
calibrationRegistered = true
|
|
95
|
+
}
|
|
96
|
+
calibrationListeners.add(cb)
|
|
97
|
+
return () => {
|
|
98
|
+
calibrationListeners.delete(cb)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Subscribe to magnetic-interference transitions. Only fires while a
|
|
104
|
+
* heading subscription is active. Returns the unsubscribe function.
|
|
105
|
+
*/
|
|
106
|
+
export function addInterferenceListener(
|
|
107
|
+
cb: InterferenceListener
|
|
108
|
+
): () => void {
|
|
109
|
+
if (!interferenceRegistered) {
|
|
110
|
+
NitroCompass.setOnInterferenceDetected(dispatchInterference)
|
|
111
|
+
interferenceRegistered = true
|
|
112
|
+
}
|
|
113
|
+
interferenceListeners.add(cb)
|
|
114
|
+
return () => {
|
|
115
|
+
interferenceListeners.delete(cb)
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One compass heading sample, delivered to the JS callback registered with
|
|
5
|
+
* `start()`. Both fields are in degrees.
|
|
6
|
+
*
|
|
7
|
+
* - `heading`: heading clockwise from north, in `[0, 360)`. Magnetic by
|
|
8
|
+
* default; if you call `setDeclination(deg)` the offset is applied
|
|
9
|
+
* natively before this sample is delivered (and reflected in
|
|
10
|
+
* `getCurrentHeading()`).
|
|
11
|
+
* - `accuracy`: estimated heading uncertainty in degrees, or `-1` when the
|
|
12
|
+
* platform has not yet reported a usable accuracy. Smaller is better.
|
|
13
|
+
* On Android this is read from `event.values[4]` of the rotation-vector
|
|
14
|
+
* sensor when available, otherwise mapped from `SensorManager.SENSOR_STATUS_*`.
|
|
15
|
+
* On iOS this is `CLHeading.headingAccuracy`.
|
|
16
|
+
*/
|
|
17
|
+
export interface CompassSample {
|
|
18
|
+
heading: number
|
|
19
|
+
accuracy: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Coarse calibration bucket reported via `setOnCalibrationNeeded`. Buckets
|
|
24
|
+
* are derived from numeric heading accuracy on both platforms (same
|
|
25
|
+
* thresholds), so values agree across iOS and Android:
|
|
26
|
+
*
|
|
27
|
+
* `<5°` → `high`, `<15°` → `medium`, `<30°` → `low`, otherwise `unreliable`.
|
|
28
|
+
*
|
|
29
|
+
* On iOS `unreliable` is also reported when the system asks to display
|
|
30
|
+
* its built-in calibration UI (we suppress it).
|
|
31
|
+
*/
|
|
32
|
+
export type AccuracyQuality = 'high' | 'medium' | 'low' | 'unreliable'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Identifies which underlying sensor / framework is producing headings.
|
|
36
|
+
*
|
|
37
|
+
* - `rotationVector` — Android `Sensor.TYPE_ROTATION_VECTOR` (gyro + accel
|
|
38
|
+
* + magnetometer fused). Best quality.
|
|
39
|
+
* - `geomagneticRotationVector` — Android
|
|
40
|
+
* `Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR` (accel + magnetometer only).
|
|
41
|
+
* Used as fallback on gyroless / budget devices; lower update rate and
|
|
42
|
+
* more susceptible to magnetic interference.
|
|
43
|
+
* - `coreLocation` — iOS `CLLocationManager` heading. Apple's stack
|
|
44
|
+
* handles fusion natively.
|
|
45
|
+
*/
|
|
46
|
+
export type SensorKind =
|
|
47
|
+
| 'rotationVector'
|
|
48
|
+
| 'geomagneticRotationVector'
|
|
49
|
+
| 'coreLocation'
|
|
50
|
+
|
|
51
|
+
export interface SensorDiagnostics {
|
|
52
|
+
sensor: SensorKind
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Platform permission state required to deliver headings.
|
|
57
|
+
*
|
|
58
|
+
* - `granted` — headings will deliver. iOS: `authorizedAlways` /
|
|
59
|
+
* `authorizedWhenInUse`. Android: always (sensors require no permission).
|
|
60
|
+
* - `denied` — user has refused or the OS has restricted access (e.g.
|
|
61
|
+
* parental controls). On iOS, `start()` will throw with this status.
|
|
62
|
+
* - `unknown` — iOS `notDetermined`: nothing has been asked yet. Calling
|
|
63
|
+
* `requestPermission()` is the way to resolve from `unknown` to
|
|
64
|
+
* `granted`/`denied`.
|
|
65
|
+
*/
|
|
66
|
+
export type PermissionStatus = 'granted' | 'denied' | 'unknown'
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Native compass module. Pull this from `NitroModules.createHybridObject`
|
|
70
|
+
* via the bundled `NitroCompass` export, e.g.:
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { NitroCompass } from 'react-native-nitro-compass'
|
|
74
|
+
*
|
|
75
|
+
* NitroCompass.start(1, ({ heading, accuracy }) => {
|
|
76
|
+
* console.log(heading, accuracy)
|
|
77
|
+
* })
|
|
78
|
+
* NitroCompass.stop()
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export interface NitroCompass extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
|
|
82
|
+
/**
|
|
83
|
+
* Begin emitting heading samples to `onHeading`. If `start()` has already
|
|
84
|
+
* been called without a matching `stop()`, the previous subscription is
|
|
85
|
+
* silently torn down and replaced — the old `onHeading` is detached.
|
|
86
|
+
*
|
|
87
|
+
* @param filterDegrees Minimum change (in degrees) between samples before
|
|
88
|
+
* the next one is emitted. Pass `0` for "every event"; typical UI values
|
|
89
|
+
* are 1–3. Use `setFilter()` to update without restarting.
|
|
90
|
+
* @param onHeading JS callback invoked on each accepted sample. Called on
|
|
91
|
+
* the JS thread; calling `stop()` from inside this callback is safe.
|
|
92
|
+
*/
|
|
93
|
+
start(filterDegrees: number, onHeading: (sample: CompassSample) => void): void
|
|
94
|
+
|
|
95
|
+
/** Stop the underlying sensor / location-manager subscription. Idempotent — safe to call when not started. */
|
|
96
|
+
stop(): void
|
|
97
|
+
|
|
98
|
+
/** Whether `start()` has been called without a matching `stop()`. */
|
|
99
|
+
isStarted(): boolean
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Update the deadband filter live without tearing down the subscription.
|
|
103
|
+
* Same semantics as the `filterDegrees` argument to `start()`. Has no
|
|
104
|
+
* effect until `start()` is called.
|
|
105
|
+
*/
|
|
106
|
+
setFilter(degrees: number): void
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Describe which underlying sensor / framework would produce headings on
|
|
110
|
+
* this device. Returns `undefined` if the device has no compass hardware
|
|
111
|
+
* (equivalent to `hasCompass() === false`). Safe to call before `start()`.
|
|
112
|
+
*/
|
|
113
|
+
getDiagnostics(): SensorDiagnostics | undefined
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Whether the device has the hardware required for a compass reading.
|
|
117
|
+
* Android: a rotation-vector sensor (fused or geomagnetic) is present.
|
|
118
|
+
* iOS: `CLLocationManager.headingAvailable()`.
|
|
119
|
+
*/
|
|
120
|
+
hasCompass(): boolean
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Last sample emitted by the active subscription, with declination
|
|
124
|
+
* already applied. Returns `undefined` when not started, or when started
|
|
125
|
+
* but no sample has arrived yet.
|
|
126
|
+
*/
|
|
127
|
+
getCurrentHeading(): CompassSample | undefined
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Magnetic-to-true offset (degrees, signed) added to every heading
|
|
131
|
+
* before it leaves the native side. Pass `0` to revert to magnetic.
|
|
132
|
+
* Survives across `start`/`stop`. Apply your own declination from a
|
|
133
|
+
* model like `geomagnetism` keyed on the user's lat/lon.
|
|
134
|
+
*/
|
|
135
|
+
setDeclination(degrees: number): void
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Register a callback fired when the calibration bucket transitions.
|
|
139
|
+
* Replaces any previously registered callback. Pass a no-op to mute.
|
|
140
|
+
* The callback is invoked on the JS thread.
|
|
141
|
+
*/
|
|
142
|
+
setOnCalibrationNeeded(onChange: (quality: AccuracyQuality) => void): void
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Register a callback fired when external magnetic interference is
|
|
146
|
+
* detected — typical sources are laptops, monitors, car engines, and
|
|
147
|
+
* large steel structures, all of which can skew heading by tens of
|
|
148
|
+
* degrees while the calibration bucket still reads `'medium'` or
|
|
149
|
+
* better. Fires `true` when the raw magnetic field magnitude leaves
|
|
150
|
+
* the normal Earth band (~20–70 µT) and `false` when it returns.
|
|
151
|
+
* Only transitions are reported; the callback is debounce-free, so
|
|
152
|
+
* brief excursions still fire.
|
|
153
|
+
*
|
|
154
|
+
* Replaces any previously registered callback. Pass a no-op to mute.
|
|
155
|
+
* The callback is invoked on the JS thread. Only meaningful while
|
|
156
|
+
* `start()` is active.
|
|
157
|
+
*
|
|
158
|
+
* iOS uses `CMDeviceMotion.magneticField` — the calibrated field
|
|
159
|
+
* with the device's own hard-iron bias subtracted in real time.
|
|
160
|
+
* No transitions are reported until CoreMotion's bias estimate
|
|
161
|
+
* converges (a second or two of normal device movement); that's
|
|
162
|
+
* required, otherwise raw readings dominated by internal bias
|
|
163
|
+
* would fire false alarms continuously.
|
|
164
|
+
*/
|
|
165
|
+
setOnInterferenceDetected(onChange: (interferenceDetected: boolean) => void): void
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Toggle automatic pause/resume on app background/foreground. Default
|
|
169
|
+
* `true`. When enabled, the underlying sensor / location-manager
|
|
170
|
+
* subscription is silently paused while the app is backgrounded and
|
|
171
|
+
* resumed when it returns to the foreground; the JS callback and
|
|
172
|
+
* declination are preserved across the pause. Call before or after
|
|
173
|
+
* `start()`; takes effect immediately.
|
|
174
|
+
*/
|
|
175
|
+
setPauseOnBackground(enabled: boolean): void
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read the current platform permission state synchronously.
|
|
179
|
+
* On Android this is always `'granted'` (sensors require no permission);
|
|
180
|
+
* on iOS it maps `CLLocationManager.authorizationStatus`.
|
|
181
|
+
*/
|
|
182
|
+
getPermissionStatus(): PermissionStatus
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Request the platform permission required to deliver headings. On iOS
|
|
186
|
+
* this prompts the system "Allow location" dialog if the status is
|
|
187
|
+
* `'unknown'` and resolves once the user makes a choice. If already
|
|
188
|
+
* `'granted'` or `'denied'` it resolves immediately with that value
|
|
189
|
+
* (iOS does not re-prompt). On Android it resolves immediately with
|
|
190
|
+
* `'granted'`.
|
|
191
|
+
*/
|
|
192
|
+
requestPermission(): Promise<PermissionStatus>
|
|
193
|
+
}
|