accelerometer.ts
Purpose
This module abstracts accelerometer data collection across iOS and Android, maintaining a rolling in-memory buffer of raw samples suitable for downstream CPM inference. It handles platform-specific sensor APIs, timestamp reliability, unit conversion, and self-throttling diagnostics.
Its responsibilities include:
- Starting and stopping accelerometer streaming at a configurable sample rate
- Normalizing sensor timestamps to milliseconds with a fallback to callback receipt time
- Converting expo-sensors g-force readings to m/s²
- Maintaining a bounded rolling buffer pruned to a 75-second window
- Routing Android collection through a native service instead of the JS-side listener
- Exposing a windowed sample query with staleness detection
- Computing and throttle-logging gap statistics (mean, median, p95) and observed Hz
- Detecting and handling mid-session timestamp mode degradation
Invariants
Buffer Bound
The in-memory sample buffer is pruned to retain only the most recent 75,000 ms of samples.
This provides a 60-second CPM compute window plus a ~15-second margin for timer jitter and brief stalls. Pruning runs on every sample insertion.
Timestamp Mode Locking
The timestamp mode for a session ("native" or "fallback") is determined by the first sample received and locked for the lifetime of that session.
If the first sample has a valid sensor timestamp, the session is locked to "native". If native timestamps subsequently become invalid mid-session, the module switches to "fallback" and clears all buffered samples to prevent mixing timestamp bases.
Unit Conversion
expo-sensors reports acceleration in g-force units. All stored samples are converted to m/s² by multiplying each axis by G = 9.81 before buffering.
Staleness Gate on Sample Query
getRecentAccelerometerSamples returns an empty array if the most recent sample is older than the requested windowMs.
This prevents stale buffered data from being consumed by the inference pipeline after the accelerometer has been stopped or paused.
Android Native Routing
On Android, startAccelerometer and stopAccelerometer delegate entirely to startNativeService and stopNativeService. The JS-side listener and in-memory buffer are not used.
getRecentAccelerometerSamples and getAccelerometerDiagnostics read from getNativeSamples() on Android instead of the module-level samples array.
Diagnostics Log Throttling
Diagnostic log lines are throttled to at most one emission per 10 seconds and only when the diagnostics state string changes.
The state string encodes: timestampMode, active/empty, and full/building window — so a state change forces an immediate log regardless of the time interval.
Variants
Timestamp Mode — Native
When the sensor provides a valid timestamp field (a finite number in seconds), it is converted to milliseconds via sensorTimestampSeconds × 1000 and stored directly.
Native mode provides hardware-level timing accuracy independent of JS thread scheduling.
Timestamp Mode — Fallback
When the sensor timestamp is missing or non-finite, Date.now() at callback receipt time is used instead.
A one-time WARN log is emitted when the session is first locked to fallback mode. If native mode degrades mid-session, all buffered samples are cleared and a WARN is emitted before switching.
Android Permission Flow
On Android, startAccelerometer requests ACCESS_FINE_LOCATION via PermissionsAndroid before delegating to the native service.
The native service is only started if the permission is granted. No JS listener or buffer is initialized on Android regardless of the permission result.
Constants
| Constant | Value | Description |
|---|---|---|
DEFAULT_SAMPLE_RATE_HZ | 30 | Default accelerometer sample rate |
DEFAULT_SAMPLE_INTERVAL_MS | 34 | Derived from 1000 / 30, minimum 1 |
BUFFER_KEEP_MS | 75,000 | Rolling buffer retention window in ms |
DIAGNOSTICS_LOG_INTERVAL_MS | 10,000 | Minimum ms between identical diagnostics logs |
G | 9.81 | m/s² per g used for unit conversion |
Exports
AccelerometerSample
type AccelerometerSample = RawAccelerometerSample;
// { x: number; y: number; z: number; timestamp: number }
A single accelerometer reading in m/s² with a millisecond timestamp. Re-exported from inferenceActivity for consumer convenience.
AccelerometerDiagnostics
type AccelerometerDiagnostics = {
bufferSize: number;
lastSampleAgeMs: number | null;
meanGapMs: number | null;
medianGapMs: number | null;
observedHz: number | null;
p95GapMs: number | null;
sampleCount: number;
timestampMode: TimestampMode | "unknown";
windowMs: number;
windowSpanMs: number;
};
A snapshot of sensor health metrics for the requested diagnostic window.
startAccelerometer(...)
async function startAccelerometer(sampleRateHz?: number): Promise<void>
Starts accelerometer streaming at the given sample rate. No-ops silently if a subscription is already active (iOS). On Android, requests location permission and delegates to the native service.
Parameters
| Parameter | Description |
|---|---|
sampleRateHz | Target sample rate (default: 30) |
stopAccelerometer()
function stopAccelerometer(): void
Stops accelerometer streaming and resets all module-level state. On Android, delegates to stopNativeService.
getRecentAccelerometerSamples(...)
function getRecentAccelerometerSamples(windowMs: number): AccelerometerSample[]
Returns samples from the rolling buffer that fall within the most recent windowMs. Returns an empty array if the buffer is empty or the last sample is older than windowMs.
getAccelerometerDiagnostics(...)
function getAccelerometerDiagnostics(windowMs?: number): AccelerometerDiagnostics
Returns a diagnostics snapshot for the given window (default: 60,000 ms) and conditionally emits a throttled log line.