Ever jolted awake by a blaring alarm feeling groggy and disoriented? This feeling, known as sleep inertia, is often the result of being woken up during a deep sleep cycle. What if your alarm could intelligently wait for the optimal moment to wake you? That's the idea behind the "Smart Alarm" – a mobile app that uses your phone's accelerometer to estimate your sleep cycles and wake you during your lightest sleep phase.
In this project showcase, we'll walk through the creation of a mobile app that does just that. We'll explore the science behind sleep tracking with motion sensors, the challenges of interpreting accelerometer data, and the step-by-step implementation using React Native. This project is a fantastic blend of mobile development, data science, and health tech, offering a practical look at how we can use the power in our pockets to improve our well-being.
Prerequisites:
- Familiarity with React Native and JavaScript (ES6+).
- A physical device for testing (simulators don't have accelerometers).
- Node.js and a React Native development environment set up.
Understanding the Problem
Traditional alarms are simple: they go off at a fixed time, regardless of your sleep state. However, our sleep is cyclical, moving between light, deep, and REM stages. Waking up from a light sleep stage feels much more natural and leaves you feeling more refreshed.
The challenge is to determine these sleep stages without the advanced equipment of a sleep lab. This is where the accelerometer comes in. By placing your phone on your bed, it can detect your movements throughout the night. The core idea is that we move more during lighter sleep phases and are relatively still during deep sleep.
While not as precise as clinical methods like polysomnography, accelerometer-based tracking is a reliable way to estimate sleep patterns for consumer applications. Our approach will be to capture this motion data, process it to identify periods of low and high activity, and use this information to trigger the alarm at the right time.
Prerequisites
Before we start coding, let's get our environment ready.
-
React Native Project: If you don't have a project, create one using Expo:
codenpx create-expo-app SmartAlarm cd SmartAlarmCode collapsed -
Required Libraries: We'll need a library to access the accelerometer.
expo-sensorsis an excellent choice for this.codenpx expo install expo-sensorsCode collapsed -
Version Compatibility: This tutorial uses Expo SDK 50 and
expo-sensorsv13.0.0.
Step 1: Accessing Accelerometer Data
What we're doing
First, we need to get a continuous stream of data from the phone's accelerometer. The accelerometer measures acceleration forces along the x, y, and z axes. We'll set up a listener that captures this data and stores it in our component's state.
Implementation
Create a new file src/SleepTracker.js. This component will handle all the logic for tracking sleep.
// src/components/SleepTracker.js
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Accelerometer } from 'expo-sensors';
const SleepTracker = () => {
const [data, setData] = useState({ x: 0, y: 0, z: 0 });
const [subscription, setSubscription] = useState(null);
const _subscribe = () => {
setSubscription(
Accelerometer.addListener(accelerometerData => {
setData(accelerometerData);
})
);
// Set the update interval for the accelerometer
Accelerometer.setUpdateInterval(1000); // 1 second
};
const _unsubscribe = () => {
subscription && subscription.remove();
setSubscription(null);
};
useEffect(() => {
_subscribe();
return () => _unsubscribe();
}, []);
const { x, y, z } = data;
return (
<View style={styles.container}>
<Text style={styles.text}>Accelerometer:</Text>
<Text style={styles.text}>x: {Math.round(x)} y: {Math.round(y)} z: {Math.round(z)}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 18,
},
});
export default SleepTracker;
How it works
- We import
Accelerometerfromexpo-sensors. - The
useEffecthook is used to subscribe to the accelerometer when the component mounts and unsubscribe when it unmounts. This is crucial to prevent memory leaks. Accelerometer.addListenerregisters a callback function that receives the accelerometer data.- We store the latest sensor reading in the component's state using
useState.
Step 2: Processing Movement Data
What we're doing
Raw x, y, and z values are hard to work with directly. We need a single value that represents the magnitude of movement. We can calculate this by finding the vector magnitude of the acceleration. This will give us a simple number that indicates how much the phone is moving.
Implementation
Let's modify our SleepTracker component to calculate and store the movement magnitude.
// src/components/SleepTracker.js (Updated)
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import { Accelerometer } from 'expo-sensors';
const SleepTracker = () => {
const [movementData, setMovementData] = useState([]);
const [isTracking, setIsTracking] = useState(false);
const [subscription, setSubscription] = useState(null);
const startTracking = () => {
setIsTracking(true);
setSubscription(
Accelerometer.addListener(({ x, y, z }) => {
// Calculate magnitude of movement
const magnitude = Math.sqrt(x ** 2 + y ** 2 + z ** 2);
// We are interested in the change from the force of gravity (1)
const movement = Math.abs(magnitude - 1);
setMovementData(prevData => [...prevData, { movement, timestamp: new Date() }]);
})
);
Accelerometer.setUpdateInterval(2000); // 2 seconds
};
const stopTracking = () => {
setIsTracking(false);
subscription && subscription.remove();
setSubscription(null);
// Here you would typically process the movementData
console.log('Final Movement Data:', movementData);
};
useEffect(() => {
return () => {
subscription && subscription.remove();
};
}, [subscription]);
return (
<View style={styles.container}>
<Text style={styles.text}>
{isTracking ? 'Tracking Sleep...' : 'Ready to Track'}
</Text>
<Button title={isTracking ? "Stop Tracking" : "Start Tracking"} onPress={isTracking ? stopTracking : startTracking} />
</View>
);
};
// Styles remain the same
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
fontSize: 18,
marginBottom: 20,
},
});
export default SleepTracker;
How it works
- We've added
startTrackingandstopTrackingfunctions to control when we're collecting data. - Inside the accelerometer listener, we calculate the magnitude of the acceleration vector.
- Since gravity exerts a force of approximately 1g, we subtract 1 from the magnitude to isolate movement. We take the absolute value as we're interested in any deviation.
- Each movement value is stored with a timestamp in an array.
Step 3: The Smart Alarm Algorithm
What we're doing
This is the core logic of our smart alarm. The alarm will have a "wake-up window" (e.g., 30 minutes before the set alarm time). During this window, the app will monitor the user's movement. If the movement exceeds a certain threshold, indicating a light sleep phase, the alarm will sound.
Implementation
Let's create a new component for the alarm logic. For this example, we'll simulate the alarm window with a button press.
// src/components/SmartAlarm.js
import React, { useState, useEffect } from 'react';
import { View, Text, Button, Alert } from 'react-native';
import { Accelerometer } from 'expo-sensors';
const WAKE_UP_WINDOW_MINUTES = 30;
const MOVEMENT_THRESHOLD = 0.1; // This will need tuning
const SmartAlarm = () => {
const [targetAlarmTime, setTargetAlarmTime] = useState(null);
const [inWakeUpWindow, setInWakeUpWindow] = useState(false);
const [subscription, setSubscription] = useState(null);
useEffect(() => {
// Check every second if we are in the wake up window
const interval = setInterval(() => {
if (targetAlarmTime) {
const now = new Date();
const windowStart = new Date(targetAlarmTime.getTime() - WAKE_UP_WINDOW_MINUTES * 60000);
if (now >= windowStart && now <= targetAlarmTime) {
if (!inWakeUpWindow) {
console.log("Entering wake-up window. Starting to monitor for movement.");
setInWakeUpWindow(true);
startMonitoring();
}
} else if (now > targetAlarmTime) {
console.log("Target alarm time passed. Firing alarm.");
fireAlarm();
}
}
}, 1000);
return () => clearInterval(interval);
}, [targetAlarmTime, inWakeUpWindow]);
const setAlarm = () => {
const targetTime = new Date();
targetTime.setMinutes(targetTime.getMinutes() + 1); // For demo, set alarm 1 minute from now
setTargetAlarmTime(targetTime);
Alert.alert("Alarm Set", `Will look for light sleep between ${new Date(targetTime.getTime() - WAKE_UP_WINDOW_MINUTES * 60000).toLocaleTimeString()} and ${targetTime.toLocaleTimeString()}`);
};
const startMonitoring = () => {
setSubscription(
Accelerometer.addListener(({ x, y, z }) => {
const magnitude = Math.sqrt(x ** 2 + y ** 2 + z ** 2);
const movement = Math.abs(magnitude - 1);
if (movement > MOVEMENT_THRESHOLD) {
console.log(`Movement detected (${movement.toFixed(2)}). Firing smart alarm!`);
fireAlarm();
}
})
);
Accelerometer.setUpdateInterval(2000);
};
const fireAlarm = () => {
stopMonitoring();
Alert.alert("Good Morning!", "Woke you up during a light sleep phase.");
setTargetAlarmTime(null);
setInWakeUpWindow(false);
};
const stopMonitoring = () => {
subscription && subscription.remove();
setSubscription(null);
};
return (
<View>
<Button title="Set Smart Alarm for 1 Minute from Now" onPress={setAlarm} />
{targetAlarmTime && <Text>Alarm set for: {targetAlarmTime.toLocaleTimeString()}</Text>}
</View>
);
};
export default SmartAlarm;
How it works
- Setting the Alarm: The user sets a target time. We calculate a "wake-up window" that starts 30 minutes before this time.
- Entering the Window: A
setIntervalchecks if the current time is within this window. - Monitoring Movement: Once inside the window, we start listening to the accelerometer.
- Threshold Check: We calculate the movement magnitude. If it surpasses our
MOVEMENT_THRESHOLD, it suggests the user is in a lighter sleep stage. - Firing the Alarm: The alarm is triggered, and the monitoring stops.
- Failsafe: If no significant movement is detected by the target alarm time, the alarm goes off anyway.
Challenges and Considerations
- Interpreting Sensor Data: The biggest challenge is the inherent noise in accelerometer data. A cough, a pet jumping on the bed, or the phone shifting can all create false positives. More sophisticated algorithms might use a moving average of movement to smooth out the data.
- Threshold Tuning: The
MOVEMENT_THRESHOLDis a critical value. It will vary depending on the phone's sensitivity, its placement on the bed, and the individual's sleep habits. A production-ready app would need a calibration phase. - Battery Life: Continuously polling the accelerometer can drain the battery. In a real app, you would use background processing and optimize the polling frequency.
- Device Placement: The app's effectiveness depends on the phone being placed on the bed in a way that it can detect the user's movements.
Conclusion
We've successfully built the core of a "Smart Alarm" app in React Native. We learned how to access and process accelerometer data to estimate sleep phases and created an algorithm to wake a user during their lightest sleep. While this is a simplified implementation, it serves as a strong foundation for a more advanced sleep-tracking application.
The next steps could involve adding a user interface for setting alarms, persisting alarm data, and refining the sleep detection algorithm. This project demonstrates the exciting potential of using mobile sensors to create personalized health and wellness experiences.