Skip to content

Sensors

@symbiote-native/sensors wraps expo-sensors — Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated, and Pedometer — so every SymbioteNative adapter can use them. Unlike the slider (a native view) or splash screen (one imperative TurboModule), expo-sensors is built on expo-modules-core: every sensor is a pure EventEmitter + async-function surface, with no Fabric view or ViewConfig involved at all.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Terminal window
npm install @symbiote-native/sensors

expo-sensors and expo-modules-core come along as regular dependencies, pinned to exact versions — never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

A DeviceSensor (Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated)

Section titled “A DeviceSensor (Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated)”
import { Text } from '@symbiote-native/react';
import { useAccelerometer } from '@symbiote-native/sensors/react';
export default function AccelerometerReading() {
const accelerometer = useAccelerometer();
return (
<Text>
{accelerometer && `x ${accelerometer.x} · y ${accelerometer.y} · z ${accelerometer.z}`}
</Text>
);
}

Every other DeviceSensor (Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated) follows the exact same shape — swap Accelerometer/accelerometer for the sensor’s own name.

Pedometer — free functions, no shared instance

Section titled “Pedometer — free functions, no shared instance”

Unlike every other sensor, upstream Pedometer has no shared instance to hang addListener/ setUpdateInterval off — it ships as plain functions instead:

import { Text } from '@symbiote-native/react';
import { usePedometer } from '@symbiote-native/sensors/react';
export default function StepCount() {
const pedometer = usePedometer(); // { steps: number } | null, live-subscribed
return <Text>{pedometer && `${pedometer.steps} steps`}</Text>;
}

The one-shot functions (getStepCountAsync, isAvailableAsync, the permission functions) are already framework-agnostic — import them straight from the package root, on any adapter:

import { getStepCountAsync, isAvailableAsync } from '@symbiote-native/sensors';
const available = await isAvailableAsync();
const { steps } = await getStepCountAsync(startDate, endDate); // iOS only in practice

Sensor object (Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated)

Section titled “Sensor object (Accelerometer, Barometer, DeviceMotion, Gyroscope, LightSensor, Magnetometer, MagnetometerUncalibrated)”

Each is a shared singleton, importable from the package root, that every hook/composable/service subscribes to underneath.

Method Signature Description
addListener (listener: (measurement) => void) => EventSubscription Subscribes to live readings; call .remove() on the returned subscription to unsubscribe
setUpdateInterval (intervalMs: number) => void Requests a new native sampling interval; warns and no-ops where the platform/sensor doesn’t support it
isAvailableAsync () => Promise<boolean> Whether this sensor’s hardware is present and enabled on the current device — always false on a simulator with no real IMU
getPermissionsAsync / requestPermissionsAsync () => Promise<PermissionResponse> Reads/asks for the platform permission this sensor needs; a sensor needing none resolves already-granted
hasListeners / getListenerCount / removeAllListeners Inspect or clear this sensor’s own subscriptions
React (/react) Vue (/vue) Angular (/angular) Svelte (/svelte) Signature Returns
useAccelerometer useAccelerometer AccelerometerService.connect() useAccelerometer (updateIntervalMs?: number) Live IAccelerometerMeasurement | null
useBarometer useBarometer BarometerService.connect() useBarometer (updateIntervalMs?: number) Live IBarometerMeasurement | null
useDeviceMotion useDeviceMotion DeviceMotionService.connect() useDeviceMotion (updateIntervalMs?: number) Live IDeviceMotionMeasurement | null
useGyroscope useGyroscope GyroscopeService.connect() useGyroscope (updateIntervalMs?: number) Live IGyroscopeMeasurement | null
useLightSensor useLightSensor LightSensorService.connect() useLightSensor (updateIntervalMs?: number) Live ILightSensorMeasurement | null
useMagnetometer useMagnetometer MagnetometerService.connect() useMagnetometer (updateIntervalMs?: number) Live IMagnetometerMeasurement | null
useMagnetometerUncalibrated useMagnetometerUncalibrated MagnetometerUncalibratedService.connect() useMagnetometerUncalibrated (updateIntervalMs?: number) Live IMagnetometerUncalibratedMeasurement | null
usePedometer usePedometer PedometerService.connect() usePedometer () — no interval, Pedometer has none Live IPedometerResult | null

React/Vue return the measurement directly; Angular’s connect() returns a Signal — read it as accelerometer() in code or accelerometer() in a template. Svelte’s rune returns a boxed getter, { readonly current: IAccelerometerMeasurement | null } — read it as .current, same reason Vue’s Ref needs .value: Svelte 5 reactivity is lexically scoped to the declaring module and does not survive being returned as a raw value from a plain function. Passing updateIntervalMs re-subscribes with the new native sampling rate whenever it changes across renders on React; Vue and Svelte both apply it once at subscribe time and never react to a later change, since neither takes it as a getter.

Sensor Fields Units
Accelerometer x, y, z, timestamp g-force (1g = 9.81 m/s²)
Gyroscope x, y, z, timestamp rad/s
Magnetometer / MagnetometerUncalibrated x, y, z, timestamp µT
Barometer pressure, relativeAltitude?, timestamp hPa; relativeAltitude (meters) is iOS-only
LightSensor illuminance, timestamp lux — Android-only, see Notes
DeviceMotion acceleration, accelerationIncludingGravity, rotation, rotationRate, interval, orientation see below
Pedometer steps count

DeviceMotion’s nested fields: acceleration/accelerationIncludingGravity/rotationRate are { x, y, z, timestamp } (m/s² for acceleration, deg/s for rotationRate); rotation is { alpha, beta, gamma, timestamp } in degrees; interval is milliseconds; orientation is a DeviceMotionOrientation enum (Portrait/RightLandscape/UpsideDown/LeftLandscape).

Signature Description
watchStepCount(callback: (result) => void): EventSubscription Live step-count subscription — the same primitive usePedometer wraps
getStepCountAsync(start: Date, end: Date): Promise<{ steps: number }> One-shot historical step count between two dates — iOS only in practice, Android has no native equivalent
isAvailableAsync(): Promise<boolean> Whether step counting is available on this device
getPermissionsAsync / requestPermissionsAsync () => Promise<PermissionResponse>

LightSensor is Android-only — expo-sensors ships no iOS light-sensor implementation, so isAvailableAsync() always resolves false on iOS.

@symbiote-native/sensors ships zero React/Vue/Angular logic in expo-sensors itself — that package’s own JS hard-imports the expo meta-package (which this project never installs), so every sensor’s DeviceSensor base class and per-sensor subclass is hand-ported, verbatim, into this package’s own core/, changing only the one import line that now pulls PermissionResponse/PermissionStatus from expo-modules-core instead of expo:

packages/sensors/src/
├── core/ DeviceSensor base class + one class per sensor; native/ resolves each
│ sensor's native module by name via expo-modules-core's
│ requireNativeModule. Pedometer is free functions instead — upstream
│ has no shared instance for it.
├── react/hooks/ @symbiote-native/sensors/react — useAccelerometer, useBarometer, ...
├── vue/composables/ @symbiote-native/sensors/vue — useAccelerometer, useBarometer, ... (same names)
├── svelte/runes/ @symbiote-native/sensors/svelte — useAccelerometer, useBarometer, ... (same names)
└── angular/services/ @symbiote-native/sensors/angular — AccelerometerService, BarometerService, ...

Each adapter’s hook/composable/rune/service is a thin lifecycle wrapper — subscribe on mount, unsubscribe on unmount — over the same core singleton; the subscription, permission, and update-interval logic is written once and shared by all four, the same logic/lifecycle split as every other SymbioteNative component (see how it works). The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).