A native Expo module for ambient light (lux) measurements. Uses hardware light sensor on Android (with camera fallback) and camera-based estimation on iOS.
- 📱 Cross-platform: Works on both iOS and Android
- 🔄 Real-time updates: Get continuous light level measurements
- ⚙️ Configurable: Adjustable update interval and calibration constant
- 🔐 Permission handling: Built-in permission request and status checking
- 📊 Event-based: Listen to light level changes via event listeners
- 💡 Smart sensor selection: Prefers hardware light sensor on Android, falls back to camera when unavailable
npm install expo-lux-sensorAfter installing the package, run:
npx pod-installNo additional setup required. The module will automatically be linked.
import {
addLuxListener,
startLuxUpdatesAsync,
stopLuxUpdatesAsync,
requestPermissionsAsync,
} from 'expo-lux-sensor';
import { useEffect, useState } from 'react';
export default function App() {
const [lux, setLux] = useState<number | null>(null);
useEffect(() => {
// Request permissions
requestPermissionsAsync();
// Listen to lux changes
const subscription = addLuxListener((sample) => {
setLux(sample.lux);
});
// Start sensor updates
startLuxUpdatesAsync();
// Cleanup
return () => {
subscription.remove();
stopLuxUpdatesAsync();
};
}, []);
return (
<View>
<Text>Light Level: {lux !== null ? `${lux.toFixed(0)} lux` : '--'}</Text>
</View>
);
}import {
addLuxListener,
startLuxUpdatesAsync,
stopLuxUpdatesAsync,
requestPermissionsAsync,
getPermissionsAsync,
} from 'expo-lux-sensor';
async function startLightSensor() {
// Check permissions first
const permission = await getPermissionsAsync();
if (!permission.granted) {
const result = await requestPermissionsAsync();
if (!result.granted) {
console.warn('Camera permission is required');
return;
}
}
// Start with custom options
await startLuxUpdatesAsync({
updateInterval: 0.5, // Update every 500ms
calibrationConstant: 50, // Default calibration (adjust 30-80 based on your needs)
});
// Listen to updates
const subscription = addLuxListener((sample) => {
console.log(`Lux: ${sample.lux}, Timestamp: ${sample.timestamp}`);
});
// Remember to clean up
// subscription.remove();
// stopLuxUpdatesAsync();
}Starts the light sensor updates. Requires camera permission.
Parameters:
options(optional): Configuration optionsupdateInterval?: number- Update interval in seconds (default: 0.4)calibrationConstant?: number- Calibration constant for lux calculation (default: 50)
Throws:
MissingPermissions- If camera permission is not granted
Stops the light sensor updates and releases camera resources.
Returns whether the sensor is currently running.
Gets the current permission status without requesting.
Returns:
{
granted: boolean;
status: 'undetermined' | 'granted' | 'denied';
}Requests camera permission from the user.
Returns:
{
granted: boolean;
status: 'undetermined' | 'granted' | 'denied';
}Adds a listener for lux measurement updates.
Parameters:
listener- Callback function that receivesLuxMeasurementobjects
Returns:
EventSubscription- Subscription object with aremove()method
Example:
const subscription = addLuxListener((sample) => {
console.log(`Lux: ${sample.lux}`);
console.log(`Timestamp: ${sample.timestamp}`);
});
// Later, remove the listener
subscription.remove();Removes all lux event listeners.
{
lux: number; // Light level in lux
timestamp: number; // Timestamp in milliseconds
}{
updateInterval?: number; // Update interval in seconds
calibrationConstant?: number; // Calibration constant
}{
granted: boolean;
status: 'undetermined' | 'granted' | 'denied';
}- Uses
AVCaptureDeviceto access camera metadata - Uses the back camera for light measurements (falls back to default camera if back camera is unavailable)
- Calculates lux from EXIF data (aperture, exposure time, ISO) using the formula:
lux = C × N² / (t × S) - Default calibration constant:
50(optimized for camera-based lux estimation) - Requires
NSCameraUsageDescriptioninInfo.plist(handled automatically by Expo)
- Prefers the device light sensor when available (no camera usage, returns hardware lux directly)
- Falls back to Camera2 API with exposure metadata when no light sensor is available
- Uses the back camera for light measurements (falls back to first available camera if back camera is unavailable)
- Calculates lux from exposure metadata (aperture, exposure time, ISO) using the formula:
lux = C × N² / (t × S) - Default calibration constant:
50(optimized for camera-based lux estimation) - Requires
CAMERApermission only when falling back to the camera (handled automatically by Expo)
The default calibration constant is 50, optimized for camera-based lux estimation using the ISO 2720 formula:
lux = (C × N²) / (t × S)
Where:
- C = Calibration constant (default: 50)
- N = Aperture (f-number)
- t = Exposure time (seconds)
- S = ISO sensitivity
Adjustment recommendations:
- 40-60: Indoor / typical scenes (default: 50)
- 60-80: Bright outdoor scenes
- 30-40: Darker environments or if readings seem too high
Note: The minimum measurable lux is not 0. Due to camera physics (minimum exposure time, minimum ISO), even in complete darkness you may see values around 1-10 lux. This is expected behavior.
Use this table to understand what different lux readings mean in real-world conditions:
| Condition | Lux |
|---|---|
| Full moon | ~0.1 |
| Twilight | 1 - 10 |
| Corridors, storage | 100 |
| Very dark overcast day | 100 - 500 |
| Sunrise or sunset (clear) | 300 - 500 |
| Office / classroom | 300 - 500 |
| Overcast day | 1,000 - 2,000 |
| Shade (sunny day) | 5,000 - 10,000 |
| Full daylight (indirect) | 10,000 - 25,000 |
| Direct sunlight | 30,000 - 100,000 |
Plant growing reference:
| Light Level | Lux | Suitable For |
|---|---|---|
| Low light | 500 - 2,500 | ZZ plant, pothos, snake plant, ferns |
| Medium light | 2,500 - 10,000 | Philodendron, peace lily |
| Bright indirect | 10,000 - 20,000 | Fiddle leaf fig, monstera, rubber plant |
| Direct sunlight | 20,000+ | Succulents, cacti, herbs |
This module requires camera permission because it uses the device's camera to measure ambient light. The permission is requested automatically when you call requestPermissionsAsync().
Note: The module prioritizes the back camera (rear-facing camera) for measurements. If the back camera is unavailable, it will fall back to the default camera. Make sure the camera is not obstructed and is facing the light source you want to measure.
iOS: Add to app.json:
{
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "This app needs access to the camera to measure ambient light levels."
}
}
}Android: The CAMERA permission is automatically added to your app's AndroidManifest.xml when you install this module. The module's AndroidManifest.xml is automatically merged with your app's manifest during the build process. You don't need to add it manually in app.json.
If you get a permission error:
- Make sure you've requested permissions using
requestPermissionsAsync() - Check that the permission is granted before calling
startLuxUpdatesAsync() - On iOS, verify
NSCameraUsageDescriptionis set inapp.json
If you're not receiving lux values:
- Ensure the sensor is started with
startLuxUpdatesAsync() - Check that you've added a listener with
addLuxListener() - Verify camera permission is granted
- Make sure the app is running on a physical device (camera access may not work in simulators)
The sensor uses the camera continuously, which can drain battery. Consider:
- Increasing the
updateIntervalto reduce update frequency - Stopping the sensor when not needed with
stopLuxUpdatesAsync() - Only starting the sensor when the screen is visible
Contributions are welcome! Please feel free to submit a Pull Request.
MIT