Building a Smart Alarm with React Native and Accelerometer Data
Waking up during a deep sleep phase leaves you groggy and disoriented, a phenomenon known as sleep inertia. A smart alarm aims to solve this by monitoring movement during sleep and waking you during a lighter phase within a configurable window before your target time. This tutorial builds a complete smart alarm system using React Native and device accelerometer data.
How Accelerometer-Based Sleep Tracking Works
During sleep, the body's movement patterns change predictably across sleep stages. Deep sleep (N3) is characterized by minimal movement. Light sleep (N1/N2) involves occasional positional shifts. REM sleep produces brief, isolated muscle twitches. Wakefulness shows sustained, varied movement.
By placing the phone on the mattress and sampling accelerometer data at regular intervals, we can estimate when the sleeper is in lighter stages and trigger the alarm during an optimal window.
This approach is less accurate than EEG-based polysomnography, but research published in the Journal of Clinical Sleep Medicine has shown that actigraphy (movement-based sleep monitoring) achieves roughly 80% agreement with PSG for sleep/wake classification. For a consumer alarm application, this accuracy is sufficient to meaningfully improve the wake-up experience.
Architecture Overview
+-----------------+ +--------------------+ +-------------------+
| Accelerometer |---->| Movement Analyzer |---->| Sleep Stage |
| (Device Sensor) | | (Signal Processing)| | Estimator |
+-----------------+ +--------------------+ +-------------------+
|
v
+--------------------+ +-------------------+
| Alarm Scheduler |<----| Phase Detector |
| (Background Task) | | (Light Sleep |
+--------------------+ | Identification) |
| +-------------------+
v
+--------------------+
| Alarm UI |
| (Sound + Vibration)|
+--------------------+
Project Setup
npx react-native init SmartAlarm --template react-native-template-typescript
cd SmartAlarm
# Sensor access
npm install react-native-sensors
# Background execution
npm install react-native-background-actions
npm install react-native-background-timer
# Date and time
npm install date-fns
# Storage for sleep data
npm install @react-native-async-storage/async-storage
# Notifications for alarm
npm install @notifee/react-native
npm install react-native-push-notification
Sensor Data Acquisition
The accelerometer provides x, y, z acceleration values sampled at a configurable frequency. For sleep tracking, 10-25 Hz is sufficient. Higher frequencies consume more battery without meaningful accuracy gains.
Create src/services/AccelerometerService.ts:
import { Accelerometer, AccelerometerData } from 'react-native-sensors';
import { Subscription } from 'rxjs';
export interface ProcessedAcceleration {
timestamp: number;
magnitude: number;
}
class AccelerometerService {
private subscription: Subscription | null = null;
private sampleBuffer: ProcessedAcceleration[] = [];
private onEpochComplete: ((data: ProcessedAcceleration[]) => void) | null = null;
private epochDurationMs: number = 30000; // 30-second epochs
private epochStartTime: number = 0;
/**
* Start collecting accelerometer data at the specified frequency.
*/
start(
frequencyHz: number = 20,
onEpoch: (data: ProcessedAcceleration[]) => void
): void {
this.onEpochComplete = onEpoch;
this.sampleBuffer = [];
this.epochStartTime = Date.now();
this.subscription = new Accelerometer({
updateInterval: Math.round(1000 / frequencyHz),
}).subscribe((data: AccelerometerData) => {
const magnitude = this.calculateMagnitude(data);
const now = Date.now();
this.sampleBuffer.push({
timestamp: now,
magnitude,
});
// Check if epoch is complete
if (now - this.epochStartTime >= this.epochDurationMs) {
if (this.onEpochComplete) {
this.onEpochComplete([...this.sampleBuffer]);
}
this.sampleBuffer = [];
this.epochStartTime = now;
}
});
}
stop(): void {
if (this.subscription) {
this.subscription.unsubscribe();
this.subscription = null;
}
// Flush remaining buffer
if (this.sampleBuffer.length > 0 && this.onEpochComplete) {
this.onEpochComplete(this.sampleBuffer);
}
this.sampleBuffer = [];
}
/**
* Calculate the magnitude of the acceleration vector.
* With gravity removed via high-pass filtering approximation.
*/
private calculateMagnitude(data: AccelerometerData): number {
// Total acceleration magnitude
return Math.sqrt(
data.x * data.x + data.y * data.y + data.z * data.z
);
}
setEpochDuration(durationMs: number): void {
this.epochDurationMs = durationMs;
}
}
export const accelerometerService = new AccelerometerService();
Movement Analysis and Sleep Stage Estimation
The movement analyzer processes raw accelerometer epochs into activity counts, then estimates sleep stages based on activity thresholds calibrated against clinical data.
Create src/services/MovementAnalyzer.ts:
import { ProcessedAcceleration } from './AccelerometerService';
export type SleepStage = 'deep' | 'light' | 'rem' | 'awake';
export interface EpochAnalysis {
timestamp: number;
activityCount: number;
estimatedStage: SleepStage;
confidence: number; // 0-1
}
export interface NightSummary {
startTime: number;
endTime: number;
totalDuration: number;
stageBreakdown: Record<SleepStage, number>;
awakenings: number;
averageActivity: number;
}
// Gravity constant for calibration
const GRAVITY = 9.81;
// Activity threshold sensitivity (tuned for mattress placement)
const SENSITIVITY = 0.05;
class MovementAnalyzer {
private epochHistory: EpochAnalysis[] = [];
private previousStage: SleepStage = 'awake';
/**
* Analyze a 30-second epoch of accelerometer data.
*/
analyzeEpoch(samples: ProcessedAcceleration[]): EpochAnalysis {
if (samples.length === 0) {
return {
timestamp: Date.now(),
activityCount: 0,
estimatedStage: 'light',
confidence: 0,
};
}
const activityCount = this.calculateActivityCount(samples);
const estimatedStage = this.estimateStage(activityCount);
const confidence = this.calculateConfidence(activityCount, estimatedStage);
const analysis: EpochAnalysis = {
timestamp: samples[0].timestamp,
activityCount,
estimatedStage,
confidence,
};
this.epochHistory.push(analysis);
// Keep only last 120 epochs (1 hour at 30s epochs)
if (this.epochHistory.length > 120) {
this.epochHistory = this.epochHistory.slice(-120);
}
this.previousStage = estimatedStage;
return analysis;
}
/**
* Calculate activity count for the epoch.
* Activity count = sum of movements exceeding the sensitivity threshold.
*/
private calculateActivityCount(samples: ProcessedAcceleration[]): number {
let count = 0;
// Calculate baseline (approximate gravity component)
const avgMagnitude = samples.reduce((sum, s) => sum + s.magnitude, 0) / samples.length;
for (const sample of samples) {
const deviation = Math.abs(sample.magnitude - avgMagnitude);
if (deviation > SENSITIVITY) {
count += deviation;
}
}
// Normalize by epoch duration
return Math.round(count * 100) / 100;
}
/**
* Estimate sleep stage from activity count using threshold-based rules.
* These thresholds are derived from actigraphy validation studies.
*/
private estimateStage(activityCount: number): SleepStage {
// Consider recent history for contextual accuracy
const recentActivity = this.epochHistory.slice(-6).map((e) => e.activityCount);
const recentAvg = recentActivity.length > 0
? recentActivity.reduce((a, b) => a + b, 0) / recentActivity.length
: activityCount;
// Use weighted combination of current and recent activity
const weightedActivity = activityCount * 0.6 + recentAvg * 0.4;
if (weightedActivity > 2.0) return 'awake';
if (weightedActivity > 1.0) return 'rem';
if (weightedActivity > 0.3) return 'light';
return 'deep';
}
/**
* Calculate confidence in the stage estimate.
* Clear thresholds = high confidence. Borderline values = lower confidence.
*/
private calculateConfidence(activityCount: number, stage: SleepStage): number {
const thresholds: Record<SleepStage, [number, number]> = {
awake: [1.0, 3.0],
rem: [0.5, 1.5],
light: [0.15, 0.5],
deep: [0.0, 0.3],
};
const [lower, upper] = thresholds[stage];
const midpoint = (lower + upper) / 2;
const range = (upper - lower) / 2;
const distanceFromBoundary = Math.min(
Math.abs(activityCount - lower),
Math.abs(activityCount - upper)
);
return Math.min(1, distanceFromBoundary / range);
}
/**
* Determine if the current state is light sleep (good time to wake up).
*/
isLightSleep(): boolean {
const recent = this.epochHistory.slice(-4);
if (recent.length < 2) return true; // Default to true if insufficient data
const lightOrRem = recent.filter(
(e) => e.estimatedStage === 'light' || e.estimatedStage === 'rem' || e.estimatedStage === 'awake'
);
return lightOrRem.length >= recent.length * 0.5;
}
/**
* Generate a summary of the night's sleep.
*/
generateNightSummary(startTime: number, endTime: number): NightSummary {
const totalDuration = endTime - startTime;
const stageBreakdown: Record<SleepStage, number> = {
deep: 0, light: 0, rem: 0, awake: 0,
};
let awakenings = 0;
let lastStage: SleepStage = 'awake';
this.epochHistory.forEach((epoch) => {
stageBreakdown[epoch.estimatedStage]++;
if (epoch.estimatedStage === 'awake' && lastStage !== 'awake') {
awakenings++;
}
lastStage = epoch.estimatedStage;
});
const totalEpochs = this.epochHistory.length;
const averageActivity = this.epochHistory.reduce((s, e) => s + e.activityCount, 0) / totalEpochs;
return {
startTime,
endTime,
totalDuration,
stageBreakdown,
awakenings,
averageActivity,
};
}
/**
* Reset state for a new night.
*/
reset(): void {
this.epochHistory = [];
this.previousStage = 'awake';
}
}
export const movementAnalyzer = new MovementAnalyzer();
The Smart Alarm Scheduler
The alarm scheduler manages the wake window, monitors sleep stages, and triggers the alarm at the optimal moment.
Create src/services/SmartAlarmScheduler.ts:
import { accelerometerService } from './AccelerometerService';
import { movementAnalyzer, SleepStage } from './MovementAnalyzer';
import BackgroundTimer from 'react-native-background-timer';
import { Platform } from 'react-native';
export interface AlarmConfig {
targetTime: number; // Target wake time (timestamp)
windowMinutes: number; // How many minutes before target to start looking
smartMode: boolean; // Use accelerometer-based detection vs fixed time
}
export interface AlarmResult {
wokenAt: number;
targetTime: number;
stageAtWake: SleepStage;
earlyByMinutes: number;
nightSummary: any;
}
type AlarmCallback = (result: AlarmResult) => void;
class SmartAlarmScheduler {
private isRunning = false;
private alarmConfig: AlarmConfig | null = null;
private onAlarm: AlarmCallback | null = null;
private startTimestamp: number = 0;
private fallbackTimer: ReturnType<typeof setTimeout> | null = null;
private checkInterval: ReturnType<typeof setInterval> | null = null;
/**
* Schedule a smart alarm.
*/
schedule(config: AlarmConfig, callback: AlarmCallback): void {
this.alarmConfig = config;
this.onAlarm = callback;
this.startTimestamp = Date.now();
this.isRunning = true;
movementAnalyzer.reset();
// Calculate when to start monitoring (window start)
const windowStart = config.targetTime - config.windowMinutes * 60 * 1000;
if (config.smartMode) {
// Start accelerometer monitoring
accelerometerService.start(20, (epochData) => {
movementAnalyzer.analyzeEpoch(epochData);
});
// Start periodic check during the wake window
const checkIntervalMs = 30000; // Check every 30 seconds
this.checkInterval = setInterval(() => {
const now = Date.now();
if (now >= windowStart && now <= config.targetTime) {
if (movementAnalyzer.isLightSleep()) {
this.triggerAlarm();
}
}
// Safety: trigger at target time regardless
if (now >= config.targetTime) {
this.triggerAlarm();
}
}, checkIntervalMs);
}
// Fallback: always set a timer for the exact target time
const timeUntilTarget = config.targetTime - Date.now();
this.fallbackTimer = setTimeout(() => {
if (this.isRunning) {
this.triggerAlarm();
}
}, Math.max(timeUntilTarget, 0));
}
/**
* Trigger the alarm and clean up resources.
*/
private triggerAlarm(): void {
if (!this.isRunning || !this.alarmConfig || !this.onAlarm) return;
this.isRunning = false;
accelerometerService.stop();
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.fallbackTimer) {
clearTimeout(this.fallbackTimer);
this.fallbackTimer = null;
}
const now = Date.now();
const nightSummary = movementAnalyzer.generateNightSummary(
this.startTimestamp,
now
);
const result: AlarmResult = {
wokenAt: now,
targetTime: this.alarmConfig.targetTime,
stageAtWake: movementAnalyzer.isLightSleep() ? 'light' : 'deep',
earlyByMinutes: Math.round(
(this.alarmConfig.targetTime - now) / (60 * 1000)
),
nightSummary,
};
this.onAlarm(result);
}
/**
* Cancel the alarm.
*/
cancel(): void {
this.isRunning = false;
accelerometerService.stop();
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
if (this.fallbackTimer) {
clearTimeout(this.fallbackTimer);
this.fallbackTimer = null;
}
movementAnalyzer.reset();
}
getIsRunning(): boolean {
return this.isRunning;
}
}
export const smartAlarmScheduler = new SmartAlarmScheduler();
The Alarm Setup Screen
Create src/screens/AlarmSetupScreen.tsx:
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Alert,
Switch,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';
import { smartAlarmScheduler, AlarmConfig } from '../services/SmartAlarmScheduler';
export default function AlarmSetupScreen() {
const [alarmTime, setAlarmTime] = useState(() => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(7, 0, 0, 0);
return tomorrow;
});
const [windowMinutes, setWindowMinutes] = useState(30);
const [smartMode, setSmartMode] = useState(true);
const [isAlarmSet, setIsAlarmSet] = useState(false);
useEffect(() => {
return () => {
smartAlarmScheduler.cancel();
};
}, []);
const handleSetAlarm = () => {
const config: AlarmConfig = {
targetTime: alarmTime.getTime(),
windowMinutes,
smartMode,
};
smartAlarmScheduler.schedule(config, (result) => {
Alert.alert(
'Good Morning!',
`Woke you up ${result.earlyByMinutes > 0 ? `${result.earlyByMinutes} minutes early` : 'on time'}. ` +
`Sleep stage at wake: ${result.stageAtWake}.`
);
setIsAlarmSet(false);
});
setIsAlarmSet(true);
Alert.alert(
'Alarm Set',
`Alarm set for ${alarmTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` +
(smartMode ? ` with a ${windowMinutes}-minute smart window.` : '.')
);
};
const handleCancel = () => {
smartAlarmScheduler.cancel();
setIsAlarmSet(false);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Smart Alarm</Text>
<View style={styles.timeSection}>
<Text style={styles.label}>Wake-up Time</Text>
<DateTimePicker
value={alarmTime}
mode="time"
display="spinner"
onChange={(_, date) => date && setAlarmTime(date)}
minuteInterval={5}
/>
</View>
<View style={styles.settingsSection}>
<View style={styles.settingRow}>
<View>
<Text style={styles.settingLabel}>Smart Wake</Text>
<Text style={styles.settingDescription}>
Uses movement to find the best wake moment
</Text>
</View>
<Switch
value={smartMode}
onValueChange={setSmartMode}
trackColor={{ false: '#334155', true: '#6366f1' }}
thumbColor="#fff"
accessibilityLabel="Enable smart wake mode"
/>
</View>
{smartMode && (
<View style={styles.windowSection}>
<Text style={styles.settingLabel}>Wake Window</Text>
<Text style={styles.settingDescription}>
Alarm can wake you up to {windowMinutes} min before target
</Text>
<View style={styles.windowButtons}>
{[15, 30, 45, 60].map((mins) => (
<TouchableOpacity
key={mins}
style={[
styles.windowButton,
windowMinutes === mins && styles.windowButtonActive,
]}
onPress={() => setWindowMinutes(mins)}
>
<Text
style={[
styles.windowButtonText,
windowMinutes === mins && styles.windowButtonTextActive,
]}
>
{mins} min
</Text>
</TouchableOpacity>
))}
</View>
</View>
)}
</View>
{isAlarmSet ? (
<TouchableOpacity style={styles.cancelButton} onPress={handleCancel}>
<Text style={styles.cancelButtonText}>Cancel Alarm</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={styles.setButton} onPress={handleSetAlarm}>
<Text style={styles.setButtonText}>Set Alarm</Text>
</TouchableOpacity>
)}
<View style={styles.tipsSection}>
<Text style={styles.tipsTitle}>Tips for Best Results</Text>
<Text style={styles.tip}>
Place your phone on the mattress near your pillow for optimal movement detection.
</Text>
<Text style={styles.tip}>
Keep the phone plugged in to prevent battery drain during the night.
</Text>
<Text style={styles.tip}>
A 30-minute wake window provides the best balance between accuracy and flexibility.
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0a0a1a', padding: 24 },
title: { color: '#e2e8f0', fontSize: 28, fontWeight: '700', marginBottom: 32 },
timeSection: { backgroundColor: '#1a1a2e', borderRadius: 16, padding: 16, marginBottom: 24 },
label: { color: '#94a3b8', fontSize: 14, marginBottom: 8 },
settingsSection: { backgroundColor: '#1a1a2e', borderRadius: 16, padding: 16, marginBottom: 24 },
settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
settingLabel: { color: '#e2e8f0', fontSize: 16, fontWeight: '600' },
settingDescription: { color: '#64748b', fontSize: 12, marginTop: 2 },
windowSection: { marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#252240' },
windowButtons: { flexDirection: 'row', gap: 8, marginTop: 12 },
windowButton: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, backgroundColor: '#252240' },
windowButtonActive: { backgroundColor: '#6366f1' },
windowButtonText: { color: '#94a3b8', fontSize: 14 },
windowButtonTextActive: { color: '#fff' },
setButton: { backgroundColor: '#6366f1', borderRadius: 16, paddingVertical: 18, alignItems: 'center', marginTop: 16 },
setButtonText: { color: '#fff', fontSize: 18, fontWeight: '600' },
cancelButton: { backgroundColor: '#dc2626', borderRadius: 16, paddingVertical: 18, alignItems: 'center', marginTop: 16 },
cancelButtonText: { color: '#fff', fontSize: 18, fontWeight: '600' },
tipsSection: { marginTop: 32 },
tipsTitle: { color: '#94a3b8', fontSize: 14, fontWeight: '600', marginBottom: 12 },
tip: { color: '#64748b', fontSize: 13, lineHeight: 20, marginBottom: 8 },
});
Background Execution
React Native apps are normally suspended when the backgrounded. For a sleep alarm to work, we need background execution.
Create src/services/BackgroundService.ts:
import BackgroundService from 'react-native-background-actions';
const sleepAlarmTask = async (taskData: any) => {
const { delay } = taskData;
await new Promise<void>((resolve) => {
const interval = setInterval(() => {
// Keep the background task alive
// The SmartAlarmScheduler handles the actual logic
if (!BackgroundService.isRunning()) {
clearInterval(interval);
resolve();
}
}, delay);
});
};
export async function startBackgroundService() {
const taskOptions = {
taskName: 'SmartAlarm',
taskTitle: 'Smart Alarm Active',
taskDesc: 'Monitoring sleep for optimal wake time',
taskIcon: { name: 'ic_launcher', type: 'mipmap' },
color: '#6366f1',
linkingURI: 'smartalarm://alarm',
parameters: { delay: 60000 }, // Check every minute
};
await BackgroundService.start(sleepAlarmTask, taskOptions);
}
export async function stopBackgroundService() {
await BackgroundService.stop();
}
Data Persistence for Sleep History
Create src/services/SleepHistoryService.ts:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NightSummary } from './MovementAnalyzer';
const STORAGE_KEY = '@smartalarm_sleep_history';
export interface SleepRecord {
id: string;
date: string;
nightSummary: NightSummary;
alarmTime: number;
actualWakeTime: number;
wokeEarlyMinutes: number;
}
class SleepHistoryService {
async saveRecord(record: SleepRecord): Promise<void> {
const history = await this.getAllRecords();
history.unshift(record);
// Keep only last 365 records
const trimmed = history.slice(0, 365);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
}
async getAllRecords(): Promise<SleepRecord[]> {
const data = await AsyncStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data) : [];
}
async getRecentRecords(days: number): Promise<SleepRecord[]> {
const history = await this.getAllRecords();
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
return history.filter((r) => r.actualWakeTime >= cutoff);
}
async getAverageSleepDuration(days: number): Promise<number | null> {
const records = await this.getRecentRecords(days);
if (records.length === 0) return null;
const totalMinutes = records.reduce(
(sum, r) => sum + r.nightSummary.totalDuration / (60 * 1000), 0
);
return Math.round(totalMinutes / records.length);
}
async clearHistory(): Promise<void> {
await AsyncStorage.removeItem(STORAGE_KEY);
}
}
export const sleepHistoryService = new SleepHistoryService();
Testing the Signal Processing
Unit tests verify the movement analysis logic:
import { movementAnalyzer } from '../services/MovementAnalyzer';
import { ProcessedAcceleration } from '../services/AccelerometerService';
describe('MovementAnalyzer', () => {
beforeEach(() => {
movementAnalyzer.reset();
});
function generateEpoch(
baseMagnitude: number,
noise: number,
count: number = 100
): ProcessedAcceleration[] {
return Array.from({ length: count }, (_, i) => ({
timestamp: Date.now() - (count - i) * 300,
magnitude: baseMagnitude + (Math.random() - 0.5) * noise * 2,
}));
}
it('classifies low movement as deep sleep', () => {
const lowMovement = generateEpoch(9.81, 0.01); // Near gravity, minimal noise
const result = movementAnalyzer.analyzeEpoch(lowMovement);
expect(['deep', 'light']).toContain(result.estimatedStage);
});
it('classifies high movement as awake', () => {
const highMovement = generateEpoch(9.81, 3.0); // Large movements
const result = movementAnalyzer.analyzeEpoch(highMovement);
expect(result.estimatedStage).toBe('awake');
});
it('detects light sleep for moderate movement', () => {
const moderateMovement = generateEpoch(9.81, 0.5);
const result = movementAnalyzer.analyzeEpoch(moderateMovement);
expect(['light', 'rem']).toContain(result.estimatedStage);
});
});
Battery Optimization
Running the accelerometer all night consumes significant battery. These strategies minimize the impact:
Adaptive sampling rate: Start at 20 Hz during the first sleep cycles, then reduce to 10 Hz during the likely deep sleep period (first 3-4 hours). Increase back to 20 Hz when the wake window begins.
Batch processing: Accumulate samples in memory and process them in batches rather than running analysis on every single data point.
Phone placement guidance: Advise users to plug in their phone. Show a battery warning if the charge is below 30% at alarm set time.
Limitations and Accuracy Considerations
Actigraphy-based sleep tracking has inherent limitations:
- Cannot distinguish REM from light sleep reliably based on movement alone. Our algorithm combines them as "optimal wake windows."
- Shared beds: Partner movement can produce false activity readings. Placing the phone on the user's side of the bed helps.
- Mattress type: Memory foam absorbs more movement than spring mattresses, affecting sensitivity. The app should include a calibration step.
- Restless leg syndrome and similar conditions: Users with involuntary movement during sleep may see reduced accuracy.
For users who need clinical-grade sleep monitoring, recommend a referral to a sleep medicine specialist for polysomnography.
Conclusion
A smart alarm using accelerometer data demonstrates that meaningful sleep technology can be built with consumer-grade sensors and thoughtful signal processing. The key engineering decisions: using epoch-based analysis rather than sample-by-sample classification, maintaining a fallback timer for reliability, and adapting the sampling rate to conserve battery.
The movement analysis thresholds in this tutorial are starting points. For a production application, conduct your own calibration study by having users run the app alongside a validated actigraphy device, then adjust the thresholds based on your collected data.