DEV Community

HarmonyOS
HarmonyOS

Posted on

Using Device Sensors in ArkTS for HarmonyOS — a Step-by-Step Guide

Read the original article:Using Device Sensors in ArkTS for HarmonyOS — a Step-by-Step Guide

Introduction

Want to make your HarmonyOS app move, tilt, count steps, or even read heart rate? In this hands-on guide, you’ll learn how to access and visualize multiple device sensors in an ArkTS app using the @kit.SensorServiceKit. We’ll walk through permissions, subscriptions, UI binding, power-saving unsubscriptions, and a simple dashboard that streams live sensor data.

What you'll build

A tiny Sensor Dashboard page that:

· Requests the right permissions

· Subscribes to a bunch of common sensors (accelerometer, gyroscope, light, pressure, humidity, temperature, gravity, orientation, heart rate, pedometer)

· Streams live readings onto the screen

· Cleans up listeners to save battery

TL;DR (for the impatient)

  1. Import the kits you need (SensorServiceKit, AbilityKit, etc.).

  2. Ask for permissions (e.g., ACCELEROMETER, READ_HEALTH_DATA).

  3. Subscribe to sensors with sensor.on(...) (or sensor.once(...)).

  4. Render the data with ArkTS @State variables.

  5. Unsubscribe in aboutToDisappear() with sensor.off(...).

  6. Ship — but remember: not every device has every sensor.

Background: How HarmonyOS sensors work

HarmonyOS exposes sensors via the @kit.SensorServiceKit module. Each physical sensor is identified by a sensor.SensorId constant and delivers data through a typed response (e.g., AccelerometerResponse, OrientationResponse).

Typical use cases:
· Fitness & Health: Steps, heart rate, movement, sleep

· Environment: Ambient light, humidity, pressure, temperature

· UX & Games: Gestures, tilt, orientation

· Automation: Flip, shake, or motion-triggered actions

Data arrives asynchronously via listeners, and you can set sampling intervals to balance responsiveness and battery life.

Prerequisites

· HarmonyOS dev environment set up

· Basic ArkTS + declarative UI understanding

· A device (or emulator — don’t forget you cant use every sensor value with emulator) that actually has the sensors you want to read

Step 1 — Create a tiny permission helper

Create utils/PermissionsUtil.ets to centralize permission checks and prompts:

import { abilityAccessCtrl, bundleManager, common, PermissionRequestResult, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const domain = 0x0000;

class PermissionsUtil {
  async checkPermissions(permissions: Array<Permissions>): Promise<void> {
    let applyResult: boolean = false;
    for (let permission of permissions) {
      let grantStatus: abilityAccessCtrl.GrantStatus = await this.checkAccessToken(permission);
      if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        hilog.info(domain, 'testTag', '%{public}s', 'CheckPermission is granted');
        applyResult = true;
      } else {
        hilog.info(domain, 'testTag', '%{public}s', 'CheckPermission is not granted');
        applyResult = false;
      }
    }
    if (!applyResult) {
      this.requestPermissions(permissions);
    }
  }

  async checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;

    let tokenId: number = 0;
    try {
      let bundleInfo: bundleManager.BundleInfo =
        await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
      tokenId = appInfo.accessTokenId;
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      hilog.error(0x0000,'',`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
    }

    try {
      grantStatus = await atManager.checkAccessToken(tokenId, permission);
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      hilog.error(0x0000,'',`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
    }
    return grantStatus;
  }

  requestPermissions(permissions: Array<Permissions>): void {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    atManager.requestPermissionsFromUser(getContext() as common.UIAbilityContext, permissions)
      .then((data: PermissionRequestResult) => {
        hilog.info(0x0000,'','request Permissions success')
      })
      .catch((err: BusinessError) => {
        hilog.error(0x0000,'',`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
      })
  }

  async requestPermission(permissions: Permissions[],
    context: common.UIAbilityContext): Promise<boolean> {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    const result = await atManager.requestPermissionsFromUser(context, permissions);
    return !!result.authResults.length && result.authResults.every(authResults => authResults === 0);
  }
}

export default new PermissionsUtil();
Enter fullscreen mode Exit fullscreen mode

Step 2 — Declare the permissions in module.json5

Add only what you need;

...
"requestPermissions": [
  {
    "name": "ohos.permission.ACCELEROMETER",
    "reason": "$string:EntryAbility_desc",
    "usedScene": {
      "abilities": [ "EntryAbility" ],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.ACTIVITY_MOTION",
    "reason": "$string:EntryAbility_desc",
    "usedScene": {
      "abilities": [ "EntryAbility" ],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.GYROSCOPE",
    "reason": "$string:EntryAbility_desc",
    "usedScene": {
      "abilities": [ "EntryAbility" ],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.READ_HEALTH_DATA",
    "reason": "$string:EntryAbility_desc",
    "usedScene": {
      "abilities": [ "EntryAbility" ],
      "when": "always"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

Tip: READ_HEALTH_DATA is required for things like heart rate and pedometer.

Step 3 — Build the Sensor Dashboard page

Create your Index component and wire everything together.

Key ideas

· Use @State to bind sensor values directly to the UI.

· Use sensor.on(...) with an interval (specified in nanoseconds).

· Always call sensor.off(...) to unsubscribe when the page disappears.

import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
import PermissionsUtil from '../utils/PermissionsUtil';

@Entry
@Component
struct Index {
  @State accelerometer: string = '';
  @State gyroscope: string = '';
  @State light: string = '';
  @State pressure: string = '';
  @State humidity: string = '';
  @State temperature: string = '';
  @State orientation: string = '';
  @State gravity: string = '';
  @State heartData: number = 111;
  @State pedometer: number = 111;

  aboutToAppear(): void {
    const INTERVAL = 100_000_000; // 100 ms in nanoseconds

    PermissionsUtil.requestPermissions([
      'ohos.permission.ACCELEROMETER',
      'ohos.permission.ACTIVITY_MOTION',
      'ohos.permission.GYROSCOPE',
      'ohos.permission.READ_HEALTH_DATA'
    ]);

    try {
      sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
        this.accelerometer = `x:${data.x.toFixed(2)}, y:${data.y.toFixed(2)}, z:${data.z.toFixed(2)}`;
      }, { interval: INTERVAL });
    } catch (error) {
      let e: BusinessError = error as BusinessError;
      console.error(`Accelerometer error: ${e.message}`);
    }

    try {
      sensor.on(sensor.SensorId.PEDOMETER, (data: sensor.PedometerResponse) => {
        this.pedometer = data.steps;
        console.info('Succeeded in invoking once. Step count: ' + data.steps);
      });
    } catch (error) {
      let e: BusinessError = error as BusinessError;
      console.error(`Failed to invoke once. Code: ${e.code}, message: ${e.message}`);
    }

    try {
      sensor.on(sensor.SensorId.GYROSCOPE, (data) => {
        this.gyroscope = `x:${data.x.toFixed(2)}, y:${data.y.toFixed(2)}, z:${data.z.toFixed(2)}`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Gyroscope failed.");
    }

    try {
      sensor.on(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => {
        this.gravity = `x:${data.x.toFixed(2)}, y:${data.y.toFixed(2)}, z:${data.z.toFixed(2)}`;
        console.info('Gravity: x=' + data.x + ', y=' + data.y + ', z=' + data.z);
      }, { interval: INTERVAL });
      setTimeout(() => {
        sensor.off(sensor.SensorId.GRAVITY);
      }, 500);
    } catch (error) {
      let e: BusinessError = error as BusinessError;
      console.error(`Failed to invoke on. Code: ${e.code}, message: ${e.message}`);
    }

    try {
      sensor.on(sensor.SensorId.HEART_RATE, (data: sensor.HeartRateResponse) => {
        this.heartData = data.heartRate;
      }, { interval: INTERVAL });
      setTimeout(() => {
        sensor.off(sensor.SensorId.HEART_RATE);
      }, 500);
    } catch (error) {
      let e: BusinessError = error as BusinessError;
      console.error(`Failed to invoke on. Code: ${e.code}, message: ${e.message}`);
    }

    try {
      sensor.on(sensor.SensorId.AMBIENT_LIGHT, (data) => {
        this.light = `Light: ${data.intensity.toFixed(2)} lx`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Light sensor failed.");
    }

    try {
      sensor.on(sensor.SensorId.BAROMETER, (data) => {
        this.pressure = `Pressure: ${data.pressure.toFixed(2)} hPa`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Barometer failed.");
    }

    try {
      sensor.on(sensor.SensorId.HUMIDITY, (data) => {
        this.humidity = `Humidity: ${data.humidity.toFixed(2)}%`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Humidity sensor failed.");
    }

    try {
      sensor.on(sensor.SensorId.AMBIENT_TEMPERATURE, (data) => {
        this.temperature = `Temp: ${data.temperature.toFixed(2)} °C`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Temperature sensor failed.");
    }

    try {
      sensor.on(sensor.SensorId.ORIENTATION, (data) => {
        this.orientation = `Pitch: ${data.alpha.toFixed(2)}, Roll: ${data.beta.toFixed(2)}, Yaw: ${data.gamma.toFixed(2)}`;
      }, { interval: INTERVAL });
    } catch (error) {
      console.error("Orientation sensor failed.");
    }
  }

  aboutToDisappear(): void {
    sensor.off(sensor.SensorId.ACCELEROMETER);
    sensor.off(sensor.SensorId.GRAVITY);
    sensor.off(sensor.SensorId.GYROSCOPE);
    sensor.off(sensor.SensorId.AMBIENT_LIGHT);
    sensor.off(sensor.SensorId.BAROMETER);
    sensor.off(sensor.SensorId.HUMIDITY);
    sensor.off(sensor.SensorId.AMBIENT_TEMPERATURE);
    sensor.off(sensor.SensorId.ORIENTATION);
    sensor.off(sensor.SensorId.HEART_RATE);
  }

  build() {
    Scroll() {
      Column({ space: 15 }) {
        Text('Sensor Dashboard').fontSize(24).fontWeight(FontWeight.Bold)
        Text('accelerometer ' + this.accelerometer)
        Text('gyroscope ' + this.gyroscope)
        Text('light ' + this.light)
        Text('pressure ' + this.pressure)
        Text('humidity ' + this.humidity)
        Text('temperature ' + this.temperature)
        Text('orientation ' + this.orientation)
        Text('gravity ' + this.gravity)
        Text('heartData ' + this.heartData)
        Text('pedometer ' + this.pedometer)
      }.padding(20)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4 — Test it

Run on a real HarmonyOS phone (recommended) that actually exposes the sensors.

My results

Step 5 — Clean up & optimize

· Unsubscribe aggressively: call sensor.off() as soon as you don’t need a stream.

· Batch updates/UI: If you’re subscribing to many sensors, consider throttling UI updates.

· Filter noise: Use smoothing/filters (e.g., simple moving average) for nicer graphs and less jumpy values.

· Guard for missing sensors: Not all hardware is equal. Wrap every subscription in try/catch (as shown) and/or use capability checks if available.

Common pitfalls (and how to avoid them)

  1. Forgetting permissions
    You’ll get crashes or silent failures. Always request (and justify) what you need.

  2. High-frequency sampling = Drain battery
    Don’t use tiny intervals unless you truly need them. Unsubscribe on aboutToDisappear().

  3. Assuming a sensor exists
    Many devices don’t have barometers, heart rate monitors, or ambient temperature sensors. Code defensively.

  4. UI jank from rapid updates
    Use coarser intervals or debounce updates to keep the UI smooth.

Conclusion

With just a few dozen lines of ArkTS, HarmonyOS gives you powerful access to real-time sensor data. The key is permissions, defensive coding, and power management. Start small, verify sensor availability, and build rich, sensor-driven experiences on top.

Written by Fatih Turan Gundogdu

Top comments (0)