15  Mobile Sensor APIs

15.1 Learning Objectives

  • Access smartphone sensors through Web APIs including Generic Sensor API, Geolocation API, and DeviceOrientation API for browser-based sensing applications
  • Build cross-platform mobile sensor applications using React Native with access to accelerometers, gyroscopes, magnetometers, and GPS
  • Implement Progressive Web Apps (PWAs) with offline support and service workers for IoT data collection
  • Compare Web API limitations versus native mobile API capabilities for background sensing, sampling rates, and hardware access
In 60 Seconds

Modern web browsers provide standardized APIs (Generic Sensor API, Geolocation API, DeviceOrientation) that let you access smartphone accelerometers, GPS, gyroscopes, and microphones directly from JavaScript – no native app required. For deeper access, native frameworks like React Native offer cross-platform sensor integration.

Key Concepts
  • Web Sensor API: A browser-based JavaScript API providing access to device sensors (Geolocation, DeviceMotion, DeviceOrientation, AmbientLightSensor, Proximity) without native app installation; available in Chrome and some mobile browsers
  • DeviceMotionEvent: A JavaScript event providing accelerometer and gyroscope data (accelerationIncludingGravity, rotationRate) with update intervals configurable through event listener options; requires HTTPS and user permission
  • Geolocation API: Browser API returning GPS/Wi-Fi/cell-tower position (latitude, longitude, altitude, accuracy) via navigator.geolocation.getCurrentPosition() and watchPosition() for continuous tracking
  • Web Bluetooth API: Allows web applications to communicate with BLE peripherals directly from a browser using the GATT profile; enables web-based sensor dashboards to connect to custom BLE sensor hardware without native apps
  • Service Worker / PWA: A Progressive Web App with service worker caches sensor API callbacks offline; enables smartphone sensor data collection even without internet connectivity, syncing data when connectivity is restored
  • Android SensorManager API: Native Android Java/Kotlin API providing access to all hardware sensors with configurable sampling rates (SENSOR_DELAY_FASTEST to SENSOR_DELAY_UI); required for high-frequency accelerometer data (>50 Hz) not available through Web APIs
  • iOS CoreMotion Framework: Apple’s native framework for iPhone/iPad motion sensor access; provides CMMotionManager for accelerometer, gyroscope, and magnetometer at up to 100 Hz; no web API equivalent for high-frequency access
  • Sensor Permission Model: Modern mobile OS and browsers require explicit user permission for each sensor type; permissions must be requested in response to a user gesture (button tap), not on page load; denied permissions cannot be re-requested programmatically

An API (Application Programming Interface) is like a menu at a restaurant – it lists what you can order (request) and what you will get back. Mobile sensor APIs let your web page or app request data from the phone’s built-in sensors, like the accelerometer or GPS, using just a few lines of JavaScript. This means you can build sensor-based applications that run in a web browser without needing to create a native phone app.

15.2 Web-Based Mobile Sensing

15.2.1 Generic Sensor API (Web Standards)

Modern web browsers provide standardized APIs for accessing smartphone sensors without requiring native apps.

15.2.1.1 Accelerometer (Web API)

<!DOCTYPE html>
<html>
<head>
    <title>Accelerometer Demo</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            padding: 20px;
        }
        .sensor-data {
            font-size: 24px;
            margin: 20px 0;
        }
        .axis {
            padding: 10px;
            margin: 5px 0;
            background: #f0f0f0;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>Accelerometer Sensor</h1>
    <div id="status">Checking sensor support...</div>

    <div class="sensor-data">
        <div class="axis" id="x-axis">X: 0 m/s²</div>
        <div class="axis" id="y-axis">Y: 0 m/s²</div>
        <div class="axis" id="z-axis">Z: 0 m/s²</div>
    </div>

    <div id="activity">Activity: Unknown</div>

    <script>
        // Check if Generic Sensor API is supported
        if ('Accelerometer' in window) {
            document.getElementById('status').textContent = 'Accelerometer supported!';

            try {
                const accelerometer = new Accelerometer({ frequency: 60 });

                accelerometer.addEventListener('reading', () => {
                    document.getElementById('x-axis').textContent =
                        `X: ${accelerometer.x.toFixed(2)} m/s²`;
                    document.getElementById('y-axis').textContent =
                        `Y: ${accelerometer.y.toFixed(2)} m/s²`;
                    document.getElementById('z-axis').textContent =
                        `Z: ${accelerometer.z.toFixed(2)} m/s²`;

                    // Detect activity
                    const magnitude = Math.sqrt(
                        accelerometer.x ** 2 +
                        accelerometer.y ** 2 +
                        accelerometer.z ** 2
                    );

                    let activity = 'Stationary';
                    if (magnitude > 12) {
                        activity = 'Shaking';
                    } else if (magnitude > 10) {
                        activity = 'Walking';
                    }

                    document.getElementById('activity').textContent =
                        `Activity: ${activity}`;
                });

                accelerometer.addEventListener('error', event => {
                    console.error('Accelerometer error:', event.error.name);
                    document.getElementById('status').textContent =
                        `Error: ${event.error.message}`;
                });

                accelerometer.start();

            } catch (error) {
                console.error('Failed to start accelerometer:', error);
                document.getElementById('status').textContent =
                    `Error: ${error.message}`;
            }

        } else {
            document.getElementById('status').textContent =
                'Accelerometer not supported by your browser';
        }
    </script>
</body>
</html>
Try It: Accelerometer Activity Detector

15.2.1.2 Geolocation API

<!DOCTYPE html>
<html>
<head>
    <title>GPS Location Tracker</title>
</head>
<body>
    <h1>GPS Location Tracker</h1>
    <button onclick="getCurrentLocation()">Get Current Location</button>
    <button onclick="startTracking()">Start Tracking</button>
    <button onclick="stopTracking()">Stop Tracking</button>

    <div id="location-info"></div>

    <script>
        let watchId = null;

        function getCurrentLocation() {
            if ('geolocation' in navigator) {
                navigator.geolocation.getCurrentPosition(
                    displayPosition,
                    handleError,
                    {
                        enableHighAccuracy: true,
                        timeout: 5000,
                        maximumAge: 0
                    }
                );
            } else {
                alert('Geolocation not supported');
            }
        }

        function startTracking() {
            if ('geolocation' in navigator) {
                watchId = navigator.geolocation.watchPosition(
                    displayPosition,
                    handleError,
                    {
                        enableHighAccuracy: true,
                        timeout: 5000,
                        maximumAge: 0
                    }
                );
                console.log('Started tracking with watchId:', watchId);
            }
        }

        function stopTracking() {
            if (watchId !== null) {
                navigator.geolocation.clearWatch(watchId);
                console.log('Stopped tracking');
                watchId = null;
            }
        }

        function displayPosition(position) {
            const lat = position.coords.latitude;
            const lon = position.coords.longitude;
            const accuracy = position.coords.accuracy;
            const altitude = position.coords.altitude;
            const speed = position.coords.speed;
            const heading = position.coords.heading;
            const timestamp = new Date(position.timestamp);

            const info = `
                <h2>Location Data</h2>
                <p><strong>Latitude:</strong> ${lat.toFixed(6)}</p>
                <p><strong>Longitude:</strong> ${lon.toFixed(6)}</p>
                <p><strong>Accuracy:</strong> ±${accuracy.toFixed(1)} meters</p>
                <p><strong>Altitude:</strong> ${altitude ? altitude.toFixed(1) + ' m' : 'N/A'}</p>
                <p><strong>Speed:</strong> ${speed ? speed.toFixed(1) + ' m/s' : 'N/A'}</p>
                <p><strong>Heading:</strong> ${heading ? heading.toFixed(1) + '°' : 'N/A'}</p>
                <p><strong>Timestamp:</strong> ${timestamp.toLocaleString()}</p>
                <p><a href="https://www.google.com/maps?q=${lat},${lon}" target="_blank">View on Google Maps</a></p>
            `;

            document.getElementById('location-info').innerHTML = info;

            // Send to IoT backend
            sendLocationToBackend(lat, lon, accuracy, timestamp);
        }

        function handleError(error) {
            let message = '';
            switch(error.code) {
                case error.PERMISSION_DENIED:
                    message = 'User denied geolocation permission';
                    break;
                case error.POSITION_UNAVAILABLE:
                    message = 'Location information unavailable';
                    break;
                case error.TIMEOUT:
                    message = 'Location request timeout';
                    break;
                default:
                    message = 'Unknown error occurred';
            }

            document.getElementById('location-info').innerHTML =
                `<p style="color: red;">Error: ${message}</p>`;
        }

        async function sendLocationToBackend(lat, lon, accuracy, timestamp) {
            try {
                const response = await fetch('https://iot-backend.example.com/api/location', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        latitude: lat,
                        longitude: lon,
                        accuracy: accuracy,
                        timestamp: timestamp.toISOString(),
                        deviceId: 'web-client-123'
                    })
                });

                if (response.ok) {
                    console.log('Location sent to backend successfully');
                } else {
                    console.error('Failed to send location:', response.statusText);
                }
            } catch (error) {
                console.error('Error sending location:', error);
            }
        }
    </script>
</body>
</html>
Try It: Geolocation Options Explorer

15.2.1.3 Device Orientation (Gyroscope/Magnetometer)

// DeviceOrientation API - combining accelerometer, gyroscope, magnetometer
window.addEventListener('deviceorientation', (event) => {
    const alpha = event.alpha;  // Z-axis rotation (0-360°) - compass
    const beta = event.beta;    // X-axis rotation (-180 to 180°) - front-to-back tilt
    const gamma = event.gamma;  // Y-axis rotation (-90 to 90°) - left-to-right tilt

    console.log(`Alpha: ${alpha}°, Beta: ${beta}°, Gamma: ${gamma}°`);

    // Compass heading (alpha already gives 0° = North, increases clockwise)
    document.getElementById('compass').textContent = `Heading: ${alpha.toFixed(0)}°`;

    // Tilt detection
    if (Math.abs(beta) > 45 || Math.abs(gamma) > 45) {
        console.log('Device tilted significantly');
    }
});

// Request permission on iOS 13+
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
    DeviceOrientationEvent.requestPermission()
        .then(permissionState => {
            if (permissionState === 'granted') {
                window.addEventListener('deviceorientation', handleOrientation);
            }
        })
        .catch(console.error);
}
Try It: Device Orientation Simulator

15.2.1.4 Progressive Web App (PWA) for IoT Sensing

<!DOCTYPE html>
<html>
<head>
    <title>IoT Sensor PWA</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="manifest" href="manifest.json">
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
        }
        .sensor-card {
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            margin: 10px 0;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .value {
            font-size: 32px;
            font-weight: bold;
            color: #007bff;
        }
        button {
            background: #007bff;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            margin: 5px;
        }
        button:hover {
            background: #0056b3;
        }
        #status {
            padding: 10px;
            margin: 10px 0;
            border-radius: 5px;
            background: #d4edda;
            color: #155724;
        }
    </style>
</head>
<body>
    <h1>IoT Multi-Sensor App</h1>
    <div id="status">Offline</div>

    <button onclick="startSensing()">Start Sensing</button>
    <button onclick="stopSensing()">Stop Sensing</button>
    <button onclick="sendData()">Send to Cloud</button>

    <div class="sensor-card">
        <h3>Accelerometer</h3>
        <div class="value" id="accel">0.00 m/s²</div>
    </div>

    <div class="sensor-card">
        <h3>Location</h3>
        <div class="value" id="location">Unknown</div>
    </div>

    <div class="sensor-card">
        <h3>Light Level</h3>
        <div class="value" id="light">Unknown</div>
    </div>

    <div class="sensor-card">
        <h3>Battery Level</h3>
        <div class="value" id="battery">Unknown</div>
    </div>

    <script>
        let sensorData = {
            accelerometer: null,
            location: null,
            light: null,
            battery: null,
            timestamp: null
        };

        let accelerometer = null;
        let lightSensor = null;
        let geoWatchId = null;

        // Service Worker for offline support
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('/sw.js')
                .then(reg => console.log('Service Worker registered'))
                .catch(err => console.error('Service Worker registration failed:', err));
        }

        // Check online status
        window.addEventListener('online', () => {
            document.getElementById('status').textContent = 'Online';
            document.getElementById('status').style.background = '#d4edda';
        });

        window.addEventListener('offline', () => {
            document.getElementById('status').textContent = 'Offline - Data cached locally';
            document.getElementById('status').style.background = '#f8d7da';
        });

        function startSensing() {
            // Start accelerometer
            if ('Accelerometer' in window) {
                accelerometer = new Accelerometer({ frequency: 10 });
                accelerometer.addEventListener('reading', () => {
                    const magnitude = Math.sqrt(
                        accelerometer.x ** 2 +
                        accelerometer.y ** 2 +
                        accelerometer.z ** 2
                    );
                    document.getElementById('accel').textContent =
                        `${magnitude.toFixed(2)} m/s²`;
                    sensorData.accelerometer = {
                        x: accelerometer.x,
                        y: accelerometer.y,
                        z: accelerometer.z,
                        magnitude: magnitude
                    };
                });
                accelerometer.start();
            }

            // Start GPS
            geoWatchId = navigator.geolocation.watchPosition(
                position => {
                    const lat = position.coords.latitude.toFixed(6);
                    const lon = position.coords.longitude.toFixed(6);
                    document.getElementById('location').textContent =
                        `${lat}, ${lon}`;
                    sensorData.location = {
                        latitude: position.coords.latitude,
                        longitude: position.coords.longitude,
                        accuracy: position.coords.accuracy
                    };
                },
                error => console.error('GPS error:', error),
                { enableHighAccuracy: true }
            );

            // Start ambient light sensor
            if ('AmbientLightSensor' in window) {
                lightSensor = new AmbientLightSensor();
                lightSensor.addEventListener('reading', () => {
                    document.getElementById('light').textContent =
                        `${lightSensor.illuminance.toFixed(0)} lux`;
                    sensorData.light = lightSensor.illuminance;
                });
                lightSensor.start();
            }

            // Battery status
            navigator.getBattery().then(battery => {
                function updateBattery() {
                    const level = (battery.level * 100).toFixed(0);
                    const charging = battery.charging ? ' (Charging)' : '';
                    document.getElementById('battery').textContent =
                        `${level}%${charging}`;
                    sensorData.battery = {
                        level: battery.level,
                        charging: battery.charging
                    };
                }

                battery.addEventListener('levelchange', updateBattery);
                battery.addEventListener('chargingchange', updateBattery);
                updateBattery();
            });

            console.log('Sensors started');
        }

        function stopSensing() {
            if (accelerometer) accelerometer.stop();
            if (lightSensor) lightSensor.stop();
            if (geoWatchId) navigator.geolocation.clearWatch(geoWatchId);
            console.log('Sensors stopped');
        }

        async function sendData() {
            sensorData.timestamp = new Date().toISOString();

            try {
                const response = await fetch('https://iot-backend.example.com/api/sensor-data', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify(sensorData)
                });

                if (response.ok) {
                    console.log('Data sent successfully');
                    alert('Data sent to cloud!');
                } else {
                    // Cache data if offline
                    cacheData(sensorData);
                    alert('Data cached - will sync when online');
                }
            } catch (error) {
                console.error('Error sending data:', error);
                cacheData(sensorData);
                alert('Data cached locally');
            }
        }

        function cacheData(data) {
            // Store in IndexedDB or localStorage
            const cachedData = JSON.parse(localStorage.getItem('cachedSensorData') || '[]');
            cachedData.push(data);
            localStorage.setItem('cachedSensorData', JSON.stringify(cachedData));
        }
    </script>
</body>
</html>
Try It: PWA Offline Data Caching Simulator

15.3 Native Mobile Applications

15.3.1 React Native (Cross-Platform)

// React Native IoT Sensor App
import React, { useState, useEffect } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { Accelerometer, Gyroscope, Magnetometer } from 'expo-sensors';
import * as Location from 'expo-location';
import { Audio } from 'expo-av';

export default function IoTSensorApp() {
  const [accelData, setAccelData] = useState({ x: 0, y: 0, z: 0 });
  const [gyroData, setGyroData] = useState({ x: 0, y: 0, z: 0 });
  const [location, setLocation] = useState(null);
  const [recording, setRecording] = useState(null);
  const [isSensing, setIsSensing] = useState(false);

  useEffect(() => {
    if (isSensing) {
      // Start accelerometer
      const accelSubscription = Accelerometer.addListener(data => {
        setAccelData(data);
      });
      Accelerometer.setUpdateInterval(100); // 100ms

      // Start gyroscope
      const gyroSubscription = Gyroscope.addListener(data => {
        setGyroData(data);
      });
      Gyroscope.setUpdateInterval(100);

      // Start location tracking
      startLocationTracking();

      return () => {
        accelSubscription.remove();
        gyroSubscription.remove();
      };
    }
  }, [isSensing]);

  const startLocationTracking = async () => {
    let { status } = await Location.requestForegroundPermissionsAsync();
    if (status !== 'granted') {
      console.log('Location permission denied');
      return;
    }

    Location.watchPositionAsync(
      {
        accuracy: Location.Accuracy.High,
        timeInterval: 5000,
        distanceInterval: 10
      },
      (loc) => {
        setLocation(loc.coords);
      }
    );
  };

  const startAudioRecording = async () => {
    try {
      const { status } = await Audio.requestPermissionsAsync();
      if (status !== 'granted') {
        console.log('Audio permission denied');
        return;
      }

      await Audio.setAudioModeAsync({
        allowsRecordingIOS: true,
        playsInSilentModeIOS: true,
      });

      const { recording } = await Audio.Recording.createAsync(
        Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY
      );
      setRecording(recording);
      console.log('Recording started');
    } catch (err) {
      console.error('Failed to start recording', err);
    }
  };

  const stopAudioRecording = async () => {
    await recording.stopAndUnloadAsync();
    const uri = recording.getURI();
    console.log('Recording saved to:', uri);
    setRecording(null);

    // Upload to server
    uploadAudioFile(uri);
  };

  const uploadAudioFile = async (uri) => {
    const formData = new FormData();
    formData.append('audio', {
      uri: uri,
      type: 'audio/m4a',
      name: 'recording.m4a',
    });

    try {
      const response = await fetch('https://iot-backend.example.com/api/audio', {
        method: 'POST',
        body: formData,
      });
      console.log('Audio uploaded:', response.status);
    } catch (error) {
      console.error('Upload failed:', error);
    }
  };

  const sendSensorData = async () => {
    const data = {
      accelerometer: accelData,
      gyroscope: gyroData,
      location: location,
      timestamp: new Date().toISOString()
    };

    try {
      const response = await fetch('https://iot-backend.example.com/api/sensor-data', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data)
      });

      console.log('Data sent:', response.status);
    } catch (error) {
      console.error('Error sending data:', error);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>IoT Sensor App</Text>

      <View style={styles.sensorCard}>
        <Text style={styles.sensorTitle}>Accelerometer</Text>
        <Text>X: {accelData.x.toFixed(2)} m/</Text>
        <Text>Y: {accelData.y.toFixed(2)} m/</Text>
        <Text>Z: {accelData.z.toFixed(2)} m/</Text>
      </View>

      <View style={styles.sensorCard}>
        <Text style={styles.sensorTitle}>Gyroscope</Text>
        <Text>X: {gyroData.x.toFixed(2)} rad/s</Text>
        <Text>Y: {gyroData.y.toFixed(2)} rad/s</Text>
        <Text>Z: {gyroData.z.toFixed(2)} rad/s</Text>
      </View>

      <View style={styles.sensorCard}>
        <Text style={styles.sensorTitle}>Location</Text>
        {location && (
          <>
            <Text>Lat: {location.latitude.toFixed(6)}</Text>
            <Text>Lon: {location.longitude.toFixed(6)}</Text>
            <Text>Accuracy: ±{location.accuracy.toFixed(1)}m</Text>
          </>
        )}
      </View>

      <Button
        title={isSensing ? "Stop Sensing" : "Start Sensing"}
        onPress={() => setIsSensing(!isSensing)}
      />

      <Button
        title={recording ? "Stop Recording" : "Start Audio Recording"}
        onPress={recording ? stopAudioRecording : startAudioRecording}
      />

      <Button
        title="Send Data to Cloud"
        onPress={sendSensorData}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  sensorCard: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 10,
    marginBottom: 15,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  sensorTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
});
Try It: Web API vs Native API Comparison

Objective: Create a step counting web app using the Accelerometer API that works in any mobile browser.

Step-by-step implementation:

Step 1: Calculate acceleration magnitude

// Smartphone at rest: magnitude ≈ 9.8 m/s² (gravity)
// Walking: magnitude oscillates 8-12 m/s²
// Running: magnitude oscillates 10-15 m/s²

function calculateMagnitude(x, y, z) {
    return Math.sqrt(x*x + y*y + z*z);
}

Step 2: Detect peaks (steps) with threshold and debouncing

let lastPeakTime = 0;
let stepCount = 0;
const STEP_THRESHOLD = 10.5;  // m/s²
const MIN_STEP_INTERVAL = 300; // 300ms between steps

accelerometer.addEventListener('reading', () => {
    const mag = calculateMagnitude(
        accelerometer.x,
        accelerometer.y,
        accelerometer.z
    );

    const now = Date.now();

    // If magnitude exceeds threshold AND enough time passed
    if (mag > STEP_THRESHOLD &&
        now - lastPeakTime > MIN_STEP_INTERVAL) {
        stepCount++;
        lastPeakTime = now;
        console.log(`Step detected! Total: ${stepCount}`);
    }
});

Step 3: Calibrate for individual gait

// Different users have different walking patterns
// Collect 100 steps during calibration phase
let calibrationData = [];

function calibrate() {
    if (calibrationData.length === 0) {
        console.error('No calibration data collected');
        return;
    }
    const avgMagnitude = calibrationData.reduce((a,b) => a+b) / calibrationData.length;
    STEP_THRESHOLD = avgMagnitude * 1.05; // 5% above average
    console.log(`Calibrated threshold: ${STEP_THRESHOLD} m/s²`);
}

Step 4: Calculate derived metrics

Static example (for reference):

const STRIDE_LENGTH = 0.75; // meters (average adult)
const CALORIES_PER_STEP = 0.04; // kcal

function calculateMetrics(steps) {
    const distanceMeters = steps * STRIDE_LENGTH;
    const distanceKm = (distanceMeters / 1000).toFixed(2);
    const calories = (steps * CALORIES_PER_STEP).toFixed(1);

    return {
        steps: steps,
        distance: `${distanceKm} km`,
        calories: `${calories} kcal`
    };
}

Real-world performance:

  • Accuracy: ±10-15% error compared to manual counting (typical for consumer pedometer apps)
  • Sampling rate: 50 Hz sufficient (Nyquist: walking at ~2 Hz needs 4+ Hz)
  • Battery impact: ~5% battery per hour with 50 Hz sampling
  • False positives: Phone in pocket/bag can trigger false steps from vehicle vibrations (add gyroscope to filter rotation-only events)

The Nyquist theorem determines the minimum sampling rate needed to accurately capture periodic motion like walking.

\(f_{sample} \geq 2 \times f_{max}\)

Worked example: Human walking has a typical step frequency of 2 Hz (120 steps/min). To reliably detect each step: - Maximum signal frequency: \(f_{max} = 2\) Hz - Minimum sampling rate (Nyquist): \(f_{sample} = 2 \times 2 = 4\) Hz - Practical sampling rate: 50 Hz (12.5× oversampling for noise immunity and peak detection accuracy)

If you sample at only 3 Hz (below Nyquist), the 2 Hz walking signal aliases to appear as 1 Hz, causing the step counter to miss every other step. The 50 Hz choice provides a 12.5× oversampling margin, enabling accurate peak detection even with signal noise and irregular gait patterns.

Why this approach works: Human walking has characteristic frequency (~2 Hz = 120 steps/min) and amplitude (magnitude peaks 10-12 m/s²). By detecting peaks above a threshold with minimum interval enforcement, we reliably count steps without complex ML models.

Limitations: Misses steps when phone is not on body (on desk), overcounts during driving on bumpy roads, undercounts during very slow walking (<0.5 m/s). Production apps combine accelerometer with gyroscope and GPS to improve accuracy.

Sammy the Sensor had a big question. “I know I live inside a phone, but how do apps actually talk to me?”

Max the Microcontroller explained: “There are two ways! The first is called a Web API – it is like opening a window in your web browser. Any website can peek through the window and ask, ‘Hey Sammy, how fast is the phone moving?’ No app download needed!”

“That sounds easy!” said Lila the LED.

“It is! But the window is small,” Max continued. “Websites can only peek at sensors for a little while, and only when the screen is on. For the full experience, you need a Native App – that is like giving the app its own key to the house. It can check on Sammy anytime, even in the background!”

Bella the Battery sighed. “But native apps that check sensors all the time use up a LOT of my energy!”

“That is the tradeoff,” Sammy nodded. “Web APIs are quick and easy but limited. Native apps are powerful but need more battery and a download from the app store.”

Max summarized: “Use the web window for quick experiments. Use the native key for serious projects!”

The Sensor Squad Lesson: Web APIs are like windows that let websites peek at phone sensors easily. Native APIs are like house keys that give apps full access. Each has its place – pick the right one for your project!

15.4 Knowledge Checks

Question 1: Web API Selection

You want to build a browser-based compass app that shows which direction the phone is pointing. Which Web API should you use?

  1. Generic Sensor API (Accelerometer)
  2. Geolocation API
  3. DeviceOrientation API
  4. Web Audio API

C) DeviceOrientation API. The DeviceOrientation API provides the alpha property (Z-axis rotation, 0-360 degrees), which corresponds to compass heading. It combines data from the magnetometer and gyroscope to determine which direction the device is facing. The Accelerometer only measures linear acceleration, GPS provides location but not heading when stationary, and Web Audio processes sound.

Question 2: Web vs Native APIs

What is the main limitation of using Web APIs for sensor access compared to native mobile APIs?

  1. Web APIs cannot access accelerometers
  2. Web APIs require HTTPS and have limited background execution
  3. Web APIs only work on Android devices
  4. Web APIs have no user permission model

B) Web APIs require HTTPS and have limited background execution. Web-based sensor access mandates a secure HTTPS connection, and browsers restrict background sensor access to conserve battery and protect privacy. Native APIs (Android SensorManager, iOS CoreMotion) allow continuous background sensing and offer higher sampling rates. However, Web APIs work across platforms and require no app installation, making them excellent for prototyping and short-duration data collection.

Key Takeaway

Web APIs offer the fastest path from idea to working sensor app – zero installation, cross-platform, and instant updates. Use them for prototypes, short-term data collection, and PWAs. Switch to native APIs (React Native, Android/iOS SDKs) when you need background sensing, higher sampling rates, or deeper hardware access.

Common Pitfalls

The Generic Sensor API and DeviceMotion API have inconsistent support — Safari on iOS requires iOS 13+ for DeviceMotion and does not support the Generic Sensor API; Chrome on Android requires HTTPS. Always check MDN compatibility tables for your target browser/OS combination before relying on specific web sensor APIs.

Users who deny sensor permissions in browser prompts cannot be re-prompted by the application. If the user denies access, the application must detect this, display a clear explanation, and provide fallback behavior or manual input options — not silently fail or show an error message that forces app restart.

Requesting accelerometer data at SENSOR_DELAY_FASTEST (200 Hz) or keeping Geolocation watchPosition active continuously drains smartphone battery significantly — up to 20-30% per hour for GPS. Implement duty cycling (sample for 10 seconds, pause for 50 seconds) and use SENSOR_DELAY_NORMAL for most use cases.

DeviceMotionEvent timestamps come from the browser’s performance.now() timer, not a hardware clock synchronized to network time. Timestamps may have ±5-100 ms jitter compared to actual event times depending on browser, OS scheduling, and device load. For applications requiring precise event timing, use native APIs with hardware-timestamped sensor events.

15.5 What’s Next

If you want to… Read this
Understand what hardware sensors mobile phones contain Mobile Phone Sensor Assessment
Learn how to collect data from sensors in research studies Participatory and Crowd Sensing
Explore sensor applications beyond mobile phones Sensor Applications Overview
Understand IoT platform integration for mobile sensor data IoT Reference Architectures

Now that you understand how to access smartphone sensors via Web and Native APIs, continue to:

  • Participatory Sensing: Learn about crowdsourcing applications, privacy considerations, and battery optimization
  • Mobile Phone Labs: Practice building mobile sensing applications with hands-on exercises

Previous: Mobile Phone Introduction | Return to: Mobile Phone as a Sensor Overview