آموزش کار با سنسورها در اندروید با SensorManager و کاتلین

برنامه نویسی اندروید
آموزش کار با سنسورها در اندروید با SensorManager و کاتلین

تلفن های هوشمند امروزی صرفاً ابزار هایی برای پردازش متن و تصویر نیستند، بلکه به لطف مجموعه ای غنی از سنسورهای سخت افزاری پیشرفته، جهان فیزیکی پیرامون خود را با دقت میلی متری درک می کنند.

از گام شمارهای دقیق و ردیاب های سلامت گرفته تا چرخش خودکار صفحه نمایش، واقعیت افزوده (AR) و بازی های حساس به حرکت، همگی بر پایه بستر قدرتمند سنسورهای سیستم عامل اندروید بنا شده اند.

سیستم عامل اندروید از طریق کلاس سیستمی SensorManager و چارچوب یکپارچه سنسورها، دسترسی سطح بالا و استانداردی به حسگرهای فیزیکی و ترکیبی دستگاه فراهم می آورد.

با این حال، کار با سنسورها در سطح پروداکشن با چالش های مهندسی پیچیده ای مانند نویزهای داده های خام، مصرف وحشتناک باتری در صورت عدم مدیریت چرخه حیات، و محاسبات برداری ماتریس های دوران همراه است.

در این راهنمای جامع و تخصصی، از کالبدشکافی معماری سخت افزاری لایه HAL تا پیاده سازی فیلترهای ریاضی حذف نویز، تبدیل رویدادها به جریان های CallbackFlow کاتلین و نمایش بلادرنگ در Jetpack Compose را با کدهای عملیاتی بررسی می کنیم.

پاسخ سریع و معماری خلاصه (GEO Summary): کار با سنسورها در اندروید از طریق سرویس SensorManager انجام می شود. با ثبت شنونده SensorEventListener برای حسگرهای حرکتی، محیطی یا وضعیتی، مقادیر فیزیکی در متد onSensorChanged دریافت شده و پس از اعمال فیلتر پایین گذر (Low-Pass Filter) جهت حذف نویز، به رابط کاربری متصل می گردند.

۱. اکوسیستم سنسورها در اندروید و کالبدشکافی معماری لایه HAL

ارتباط میان اپلیکیشن و حسگر سخت افزاری در سیستم عامل اندروید از یک پایپ لاین چند لایه ساختاریافته عبور می کند تا ایمنی و پایداری سیستم تضمین گردد.

جریان دریافت رویداد از سیلیکون حسگر، عبور از درایور کرنل لینوکس، لایه انتزاعی سخت افزار و تحویل به کاتلین

در پایین ترین سطح، تراشه های حسگر (MEMS) داده های آنالوگ را به سیگنال های دیجیتال تبدیل کرده و به درایورهای کرنل لینوکس ارسال می نمایند.

سپس لایه انتزاعی سخت افزار (Hardware Abstraction Layer یا HAL) این داده ها را با فرمت استاندارد اندروید بسته بندی کرده و در اختیار سرویس سیستمی SensorService قرار می دهد.

اپلیکیشن های اندروید هرگز مستقیماً با درایورها تعامل ندارند، بلکه از طریق پروکسی SensorManager در لایه فریمورک، رویدادهای ثبت شده را در قالب اشیای SensorEvent دریافت می کنند.

جهت درک عمیق ساختارهای پایه چرخه حیات و معماری پکیج ها، مطالعه راهنمای جامع آموزش برنامه نویسی اندروید به عنوان پیش نیاز توصیه می گردد.

پیشنهاد مطالعه
آموزش برنامه نویسی اندروید ۲۰۲۶: نقشه راه از صفر تا استخدام
آموزش برنامه نویسی اندروید در سال ۲۰۲۶؛ نقشه راه جامع مهندسی نرم افزار از مبانی کاتلین و Jetpack Compose تا معماری تمیز، مدیریت داده آفلاین، تست نویس…
مشاهده مقاله ←

نمودار معماری لایه‌ای سنسورها در اندروید: انتقال سیگنال از سخت افزار MEMS به لایه HAL و نگاشت به دستگاه مختصات استاندارد دکارتی

نمودار ۳ محوره دکارتی سنسورهای شتاب سنج و ژیروسکوپ در اندروید با SensorManager و Kotlin Flow

دستگاه مختصات استاندارد ۳ محوره (X, Y, Z) سنسورها در اندروید: نگاشت بردارهای فیزیکی شتاب‌سنج و ژیروسکوپ به فریم ورک SensorManager و کاتلین فلو

۲. دسته بندی جامع سنسورهای سخت افزاری و نرم افزاری (Synthetic)

سیستم عامل اندروید سنسورها را بر اساس ماهیت داده ها و شیوه تولید اطلاعات به سه شاخه اصلی تقسیم بندی می کند:

  • سنسورهای حرکتی (Motion Sensors): شامل شتاب سنج (Accelerometer)، ژیروسکوپ (Gyroscope) و سنسور گرانش (Gravity) که شتاب خطی و سرعت زاویه ای را در سه محور X، Y و Z اندازه گیری می کنند.
  • سنسورهای وضعیتی (Position Sensors): شامل قطب نمای دیجیتال (Magnetic Field)، مجاورت (Proximity) و وکتور دوران (Rotation Vector) که جهت گیری فیزیکی دستگاه نسبت به قطب شمال و فاصله اشیا را می سنجند.
  • سنسورهای محیطی (Environmental Sensors): شامل سنسور نور (Light)، فشارسنج (Barometer)، دماسنج (Ambient Temperature) و رطوبت سنج که شرایط فیزیکی محیط پیرامون کاربر را ارزیابی می کنند.

برخی از سنسورها مانند Rotation Vector سنسورهای نرم افزاری یا ترکیبی (Synthetic) نامیده می شوند، زیرا اطلاعات خود را از ادغام داده های شتاب سنج، ژیروسکوپ و مغناطیس سنج با الگوریتم های فیوژن استخراج می نمایند.

۳. جدول مقایسه فنی: سنسورهای پرکاربرد، مقادیر خروجی و میزان مصرف باتری

برای انتخاب سنسور مناسب و آگاهی از تاثیر آن بر منابع سیستم، مشخصات فنی حسگرهای کلیدی را مقایسه می کنیم:

نام سنسور در فریمورک نوع حسگر واحد اندازه گیری خروجی میزان مصرف انرژی کاربرد متداول در اپلیکیشن
TYPE_ACCELEROMETER فیزیکی (سخت افزاری) متر بر مجذور ثانیه (m/s²) کم تا متوسط تشخیص تکان دادن گوشی، گام شمار
TYPE_GYROSCOPE فیزیکی (سخت افزاری) رادیان بر ثانیه (rad/s) زیاد بازی های حرکتی، عکاسی ۳۶۰ درجه
TYPE_MAGNETIC_FIELD فیزیکی (سخت افزاری) میکروتسلا (µT) متوسط قطب نما، نقشه یابی و جهت یابی
TYPE_PROXIMITY فیزیکی (سخت افزاری) سانتی متر (cm) بسیار کم خاموش کردن صفحه هنگام مکالمه
TYPE_LIGHT فیزیکی (سخت افزاری) لوکس (SI lux) بسیار کم تنظیم خودکار روشنایی و تم تیره
TYPE_ROTATION_VECTOR ترکیبی (سافت سنسور) بردارهای زاویه ای بدون بعد زیاد واقعیت افزوده (AR)، شبیه ساز پرواز

همان طور که جدول نشان می دهد، سنسورهایی مانند ژیروسکوپ به دلیل فرکانس کاری بالا باتری زیادی مصرف می کنند و نباید به طور دائمی باز بمانند.

۴. دریافت سرویس SensorManager و استعلام حسگرهای موجود در دستگاه

پیش از ثبت هرگونه شنونده، باید بررسی کنیم که آیا سنسور مورد نظر در سخت افزار تلفن همراه کاربر تعبیه شده است یا خیر:

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager

fun checkDeviceSensors(context: Context) {
    val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    if (accelerometer != null) {
        println("شتاب سنج سخت افزاری در این دستگاه پشتیبانی می شود")
    } else {
        println("خطا: سخت افزار شتاب سنج در این دستگاه یافت نشد")
    }
}

متد getDefaultSensor اولین و بهینه ترین سنسور فیزیکی متناظر با نوع درخواستی را باز می گرداند. در صورت عدم وجود سنسور، مقدار null برگشت داده می شود که باید به دقت مدیریت گردد.

۵. پیاده سازی اینترفیس SensorEventListener و تنظیم نرخ نمونه برداری

دریافت رویدادهای خام حرکتی از طریق پیاده سازی اینترفیس SensorEventListener انجام می پذیرد:

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener

class MotionSensorListener(
    private val onValuesChanged: (FloatArray) -> Unit
) : SensorEventListener {

    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            if (it.sensor.type == Sensor.TYPE_ACCELEROMETER) {
                onValuesChanged(it.values)
            }
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        // تغییر در دقت کالیبراسیون حسگر
    }
}

متد onSensorChanged با هر بار ایجاد تغییر در شتاب یا حرکت فراخوانی می شود. آرایه values حاوی سه مولفه برداری شتاب در راستای محورهای سه گانه مختصات است.

۶. بهینه سازی مصرف باتری: انتخاب هوشمندانه بازه زمانی نمونه برداری (Sampling Rate)

هنگام ثبت شنونده با متد registerListener، انتخاب نرخ نمونه برداری تاثیری مستقیم بر طول عمر باتری دستگاه می گذارد:

import android.hardware.Sensor
import android.hardware.SensorManager

fun registerAccelerometer(sensorManager: SensorManager, listener: MotionSensorListener) {
    val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    sensor?.let {
        // نرخ مناسب برای انیمیشن های رابط کاربری و تعاملات معمولی
        sensorManager.registerListener(
            listener,
            it,
            SensorManager.SENSOR_DELAY_UI
        )
    }
}

اندروید چهار ثابت استاندارد برای تاخیر نمونه برداری ارائه می دهد:

  • SENSOR_DELAY_FASTEST (تاخیر صفر میلی ثانیه): بدون هیچ تاخیری و با حداکثر توان سخت افزار رویدادها را تولید می کند؛ مصرف باتری به شدت بالا است.
  • SENSOR_DELAY_GAME (حدود ۲۰ میلی ثانیه / ۵۰ هرتز): نرخ بهینه برای بازی های اکشن و رندر بلادرنگ سه بعدی.
  • SENSOR_DELAY_UI (حدود ۶۰ میلی ثانیه / ۱۶ هرتز): مناسب ترین گزینه برای به روزرسانی المان های رابط کاربری و انیمیشن های متداول.
  • SENSOR_DELAY_NORMAL (حدود ۲۰۰ میلی ثانیه / ۵ هرتز): کم مصرف ترین حالت برای پایش تغییرات کلان جهت گیری دستگاه و قطب نما.

۷. فیلترهای ریاضی حذف نویز: پیاده سازی فیلتر پایین گذر (Low-Pass Filter) در کاتلین

داده های خام سنسور شتاب سنج همواره حاوی نویزهای لرزشی دست کاربر و خطاهای مکانیکی است. برای جداسازی نیروی ثابت گرانش زمین از شتاب لحظه ای کاربر، از فیلتر پایین گذر استفاده می کنیم:

class LowPassFilter(private val alpha: Float = 0.8f) {
    private var gravity = FloatArray(3)

    fun filter(rawValues: FloatArray): FloatArray {
        gravity[0] = alpha * gravity[0] + (1 - alpha) * rawValues[0]
        gravity[1] = alpha * gravity[1] + (1 - alpha) * rawValues[1]
        gravity[2] = alpha * gravity[2] + (1 - alpha) * rawValues[2]

        // محاسبه شتاب خالص کاربر با حذف اثر گرانش
        val linearAcceleration = FloatArray(3)
        linearAcceleration[0] = rawValues[0] - gravity[0]
        linearAcceleration[1] = rawValues[1] - gravity[1]
        linearAcceleration[2] = rawValues[2] - gravity[2]

        return linearAcceleration
    }
}

با این فرمول ساده و پرسرعت، مقادیر پایدار شتاب بدون لرزش های میکروسکوپی استخراج شده و دقت تشخیص گام ها یا حرکات معنادار به طور چشمگیری افزایش می یابد.

پردازش سیگنال دیجیتال (DSP) در اندروید: مقایسه نوسانات پرنویز ورودی خام شتاب‌سنج با موج هموار خروجی فیلتر کالمن جهت تعیین بردار گرانش واقعی

نمودار تلمتری حذف نویز شتاب سنج با فیلتر کالمن در اندروید

پردازش سیگنال شتاب‌سنج: تفکیک نوسانات پرنویز فرکانس بالا (Raw Sensor Signal) از سیگنال هموار بردار گرانش با فیلتر کالمن (Kalman Filter)

۸. محاسبات برداری جهت یابی: استخراج زوایای Pitch ،Roll و Azimuth با ماتریس دوران

برای ساخت قطب نما یا هدایت کاراکتر در بازی، ادغام داده های شتاب سنج و میدان مغناطیسی الزامی است:

import android.hardware.SensorManager

fun calculateOrientationAngles(
    accelerometerReading: FloatArray,
    magnetometerReading: FloatArray
): FloatArray {
    val rotationMatrix = FloatArray(9)
    val orientationAngles = FloatArray(3)

    // محاسبه ماتریس دوران بر پایه گرانش و قطب مغناطیسی
    val success = SensorManager.getRotationMatrix(
        rotationMatrix,
        null,
        accelerometerReading,
        magnetometerReading
    )

    if (success) {
        // استخراج زوایای سه گانه آزیموت، پیچ و رول
        SensorManager.getOrientation(rotationMatrix, orientationAngles)
    }

    return orientationAngles
}

زاویه orientationAngles[0] زاویه آزیموت نسبت به قطب شمال مغناطیسی (بین منفی پی تا مثبت پی رادیان) است که می توان آن را به درجه تبدیل کرده و به عنوان زاویه چرخش عقربه قطب نما استفاده نمود.

هنگامی که دستگاه به حالت افقی (Landscape) می چرخد، محورهای مختصات جابجا می شوند. متد SensorManager.remapCoordinateSystem به توسعه دهنده اجازه می دهد محورهای برداری را با جهت جدید صفحه بازنگاشت کند.

۹. جبران خطای دریفت ژیروسکوپ با فیلتر مکمل (Complementary Filter) در کاتلین

ژیروسکوپ زاویه دوران را با دقت لحظه ای بالا اندازه می گیرد اما در گذر زمان با پدیده انحراف یا دریفت مواجه می شود. شتاب سنج دریفت ندارد اما نویزهای لرزشی شدیدی دارد. فیلتر مکمل بهترین ترکیب از این دو سنسور را می سازد:

class ComplementaryOrientationFilter(private val alpha: Float = 0.98f) {
    private var angle: Float = 0f

    fun update(gyroRate: Float, accelAngle: Float, dt: Float): Float {
        // ادغام انتگرال ژیروسکوپ با زاویه شتاب سنج
        angle = alpha * (angle + gyroRate * dt) + (1 - alpha) * accelAngle
        return angle
    }
}

ضریب ۰.۹۸ وزن اصلی را به سرعت زاویه ای ژیروسکوپ می دهد و ضریب ۰.۰۲ به آرامی هرگونه انحراف زمانی را به کمک شتاب سنج خنثی می نماید. این تکنیک پایه اصلی پایدارسازهای پرواز کوادکوپتر و دوربین های VR است.

۱۰. تبدیل کالبک های سنسور به جریان های واکنشی مدرن با CallbackFlow در کاتلین

برای انطباق معماری برنامه با الگو های مدرن کاتلین، کالبک های سنتی SensorEventListener را به یک Flow ایمن تبدیل می کنیم:

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

fun getSensorFlow(sensorManager: SensorManager, sensorType: Int): Flow<FloatArray> = callbackFlow {
    val sensor = sensorManager.getDefaultSensor(sensorType)
    if (sensor == null) {
        close(IllegalStateException("سنسور درخواستی در این دستگاه وجود ندارد"))
        return@callbackFlow
    }

    val listener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            event?.let { trySend(it.values.clone()) }
        }
        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
    }

    sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)

    awaitClose {
        // آزادسازی قطعی سنسور هنگام لغو اسکوپ کورتین
        sensorManager.unregisterListener(listener)
    }
}

دستور awaitClose تضمین می کند که به محض متوقف شدن جمع آوری اطلاعات توسط رابط کاربری، شنونده سنسور بلافاصله لغو ثبت (Unregister) شود تا از نشت انرژی باتری جلوگیری گردد.

جهت درک عمیق ساختار تفکیک مسئولیت ها و معماری ماژولار، راهنمای معماری تمیز در مهندسی نرم افزار را در طراحی این لایه ها به کار ببندید.

معماری واکنشی سنسور با کاتلین فلو: خط لوله ۴ مرحله‌ای ثبت لیسنر، بافر کانفلیشن ۱۰۰ هرتز و آزادسازی قطعی منابع با awaitClose

معماری واکنشی callbackFlow سنسورها در کاتلین با مدیریت لایف سایکل و آزادسازی منابع

خط لوله واکنشی سنسور در کاتلین: تبدیل رویدادهای ناهمگام SensorEventListener به جریان واکنشی callbackFlow و بستن ایمن شنونده با awaitClose جهت پیشگیری از تخلیه باتری

۱۱. مدیریت توان با سنسورهای بافرشده و صف سخت افزاری FIFO در اندروید

اگر اپلیکیشن در پس زمینه نیاز به ثبت رویدادها (مانند گام شمار یا پایش خواب) داشته باشد، بیدار شدن مکرر پردازنده اصلی (AP) باتری را در چند ساعت تخلیه می کند.

اندروید با مکانیزم Sensor Batching و استفاده از صف سخت افزاری FIFO چیپست، رویدادها را در حافظه سنسور بافر می کند:

import android.hardware.Sensor
import android.hardware.SensorEventListener
import android.hardware.SensorManager

fun registerBatchedSensor(
    sensorManager: SensorManager,
    listener: SensorEventListener,
    sensor: Sensor
) {
    // گزارش دسته ای هر ۱۰ ثانیه (۱۰ میلیون میکروثانیه)
    val maxReportLatencyUs = 10_000_000

    sensorManager.registerListener(
        listener,
        sensor,
        SensorManager.SENSOR_DELAY_NORMAL,
        maxReportLatencyUs
    )
}

با این روش، پردازنده مرکزی گوشی در خواب عمیق (Deep Sleep) باقی می ماند و چیپست سنسور داده ها را تا ۱۰ ثانیه جمع آوری کرده و سپس همه را در قالب یک بسته تحویل می دهد که مصرف باتری را تا ۸۰ درصد کاهش خواهد داد.

۱۲. پیاده سازی الگوریتم تشخیص سقوط آزاد و ضربه شدید با شتاب سنج خطی

در اپلیکیشن های ایمنی فردی یا ورزشی، تشخیص سقوط آزاد (Free-Fall) کاربردی حیاتی دارد:

import kotlin.math.sqrt

class FallDetector(
    private val onFallDetected: () -> Unit,
    private val onImpactDetected: () -> Unit
) {
    fun processAcceleration(values: FloatArray) {
        val x = values[0]
        val y = values[1]
        val z = values[2]
        val totalMagnitude = sqrt(x * x + y * y + z * z)

        // در هنگام سقوط آزاد، برآیند شتاب به نزدیک صفر افت می کند
        if (totalMagnitude  30.0f) {
            // برخورد و ضربه شدید پس از سقوط
            onImpactDetected()
        }
    }
}

محاسبه مجذور برآیند بردار شتاب در فضای سه بعدی، الگوریتمی بسیار سبک و سریع است که بدون افت فریم در بالاترین فرکانس های سنسور شتاب سنج اجرا می شود.

۱۳. دوران سه بعدی با کواترنیون ها (Quaternions) و رفع مشکل قفل گیمبال (Gimbal Lock)

استفاده از زوایای اویلر (Euler Angles) در چرخش های فضایی ۳۶۰ درجه به علت هم راستا شدن دو محور مختصات، دچار پدیده قفل گیمبال (Gimbal Lock) می شود که در آن یک درجه آزادی دوران از دست می رود.

اندروید برای سیستم های واقعیت مجازی و موتورهای بازی، سنسور TYPE_ROTATION_VECTOR را با نمایش ۴ بعدی کواترنیون ها ارائه می دهد:

import android.hardware.SensorManager

fun extractQuaternion(rotationVectorValues: FloatArray): FloatArray {
    val quaternion = FloatArray(4)
    // استخراج چهار مولفه بردار دوران کواترنیون [w, x, y, z]
    SensorManager.getQuaternionFromVector(quaternion, rotationVectorValues)
    return quaternion
}

کواترنیون های واحد با حذف ناپیوستگی های ریاضیاتی، امکان درون یابی نرم (SLERP) میان وضعیت های زاویه ای مختلف را فراهم ساخته و چرخش های بصری در Jetpack Compose و موتورهای گرافیکی را کاملاً بدون پرش و سینمایی می سازند.

۱۴. کالیبراسیون سنسور مغناطیس سنج و خنثی سازی اثرات Hard Iron و Soft Iron

سنسور مغناطیس سنج (Magnetometer) برای تعیین جهت شمال مغناطیسی و ساخت قطب نما حیاتی است؛ اما میدان های مغناطیسی مزاحم محیطی و فلزات داخلی بدنه گوشی دقت آن را به شدت تخریب می کنند.

اعوجاج های مغناطیسی به دو دسته اصلی تقسیم می شوند: اعوجاج هارد آیرون (Hard Iron) ناشی از قطعات مغناطیسی دائمی درون گوشی مانند بلندگو و موتور ویبره که یک شیفت ثابت برداری به داده ها اضافه می کنند، و اعوجاج سافت آیرون (Soft Iron) ناشی از فلزات اطراف که خطوط میدان را خمیده می سازند.

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager

class CalibratedCompass(private val sensorManager: SensorManager) : SensorEventListener {
    private val hardIronOffset = floatArrayOf(2.5f, -1.8f, 0.4f)

    override fun onSensorChanged(event: SensorEvent?) {
        if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) {
            val calibratedX = event.values[0] - hardIronOffset[0]
            val calibratedY = event.values[1] - hardIronOffset[1]
            val calibratedZ = event.values[2] - hardIronOffset[2]
            // استفاده از مقادیر کالیبره شده در ماتریس دوران
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
        if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
            // اعلام پیام لزوم چرخاندن دستگاه به شکل عدد هشت انگلیسی
        }
    }
}

در سیستم عامل اندروید، سنسور ترکیبی TYPE_MAGNETIC_FIELD_UNCALIBRATED مقادیر خام همراه با بایاس تخمین زده شده توسط فریمورک را در ۳ مولفه پایانی برمی گرداند تا الگوریتم های پیشرفته ناوبری قادر به اعمال جبران سازهای اختصاصی باشند.

۱۵. ساخت گام شمار هوشمند و مدیریت مجوز دسترسی ACTIVITY_RECOGNITION

از نسخه اندروید ۱۰ به بعد، دسترسی به سنسورهای گام شمار (Step Counter و Step Detector) نیازمند کسب اجازه صریح در زمان اجرا از طریق مجوز ACTIVITY_RECOGNITION است:

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat

fun hasActivityRecognitionPermission(context: Context): Boolean {
    return ContextCompat.checkSelfPermission(
        context,
        Manifest.permission.ACTIVITY_RECOGNITION
    ) == PackageManager.PERMISSION_GRANTED
}

سنسور TYPE_STEP_COUNTER تعداد کل گام های برداشته شده توسط کاربر از زمان آخرین روشن شدن گوشی را برمی گرداند، در حالی که سنسور TYPE_STEP_DETECTOR با هر بار برداشتن یک گام، یک رویداد منفرد صادر می کند.

۱۶. پیاده سازی سنسورهای علائم حیاتی و ضربان قلب با Health Services API

در حوزه اپلیکیشن های ورزشی و گجت های پوشیدنی Wear OS، پایش حسگرهای ضربان قلب (Heart Rate) و سطح اکسیژن خون از طریق چارچوب Health Services مدیریت می شود.

دسترسی به این حسگرهای فیزیولوژیک مستلزم اعلام و درخواست مجوز زمان اجرای BODY_SENSORS و در اندروید ۱۳ به بعد BODY_SENSORS_BACKGROUND برای پایش پیوسته در پس زمینه است:

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener

class HeartRateMonitor(private val onBpmReceived: (Float) -> Unit) : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
        if (event?.sensor?.type == Sensor.TYPE_HEART_RATE) {
            val heartRateBpm = event.values[0]
            val accuracy = event.accuracy
            if (accuracy >= 2) { // دقت متوسط یا بالا
                onBpmReceived(heartRateBpm)
            }
        }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}

سنسور ضربان قلب مبتنی بر فوتوپلتیسموگرافی (PPG) بوده و تغییرات حجم خون مویرگ ها را با بازتابش امواج نوری سبز اندازه گیری می نماید؛ بنابراین در لحظات آغازین اتصال به مچ دست، رویدادها دارای فلگ عدم دقت موقت هستند.

۱۷. پایش هوشمند سنسورهای مجاورت (Proximity) و شدت نور محیط (Light Sensor)

سنسورهای مجاورت و نور برای بهبود تعاملات ارگونومیک کاربرد فراوانی دارند:

import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener

class ProximityHandler(private val onNearStateChanged: (Boolean) -> Unit) : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
        event?.let {
            val distance = it.values[0]
            val maxRange = it.sensor.maximumRange
            // اگر فاصله کمتر از حداکثر برد باشد، جسم به سنسور نزدیک است
            onNearStateChanged(distance 

سنسور نور نیز مقدار شدت نور را بر حسب لوکس (Lux) گزارش می دهد؛ مقادیر کمتر از ۱۰ لوکس نشان دهنده محیط تاریک بوده و می توان تم اپلیکیشن را به طور خودکار به حالت شب (Dark Mode) تغییر داد.

۱۸. پیاده سازی الگوی مخزن داده (Repository Pattern) برای ایزوله سازی سخت افزار

برای رعایت استانداردهای مهندسی نرم افزار، دسترسی به سنسورها را در لایه Data کپسوله می کنیم:

import kotlinx.coroutines.flow.Flow

interface SensorRepository {
    fun observeStepCount(): Flow<Int>
    fun observeOrientation(): Flow<FloatArray>
    fun isSensorAvailable(sensorType: Int): Boolean
}

این الگو به ما اجازه می دهد در زمان اجرای تست های واحد، داده های سنسور را بدون نیاز به دستگاه واقعی موک (Mock) کرده و رفتارهای نرم افزار را بررسی کنیم.

۱۹. اتصال جریان های بلادرنگ سنسور به رابط کاربری فریمورک Jetpack Compose

در فریمورک مدرن کامپوز، مقادیر لحظه ای حسگرها را از ViewModel دریافت کرده و در المان های بصری بازتاب می دهیم:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn

class CompassViewModel(repository: SensorRepository) : ViewModel() {
    val azimuthDegrees: StateFlow<Float> = repository.observeOrientation()
        .map { values -> values[0] }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = 0f
        )
}

با پیکربندی SharingStarted.WhileSubscribed(5000)، به محض اینکه کاربر صفحه را ترک کند، پس از ۵ ثانیه جمع آوری رویدادها متوقف شده و سنسور خاموش می شود تا مصرف انرژی به حداقل برسد.

برای بررسی معماری اعلانی رابط کاربری و شیوه اتصال داده های واکنشی، مقاله مرجع آموزش جامع Jetpack Compose را مطالعه بفرمایید.

پیشنهاد مطالعه
آموزش جامع Jetpack Compose برای توسعه رابط کاربری اندروید
آموزش جت پک کامپوز Jetpack Compose در اندروید؛ معماری رابط کاربری اعلانی، مدیریت State و جریان UDF، فازهای Recomposition، ناوبری تایپ سیف و نمونه کده…
مشاهده مقاله ←

۲۰. مدیریت چرخه حیات سنسورها با DefaultLifecycleObserver جهت پیشگیری از تخلیه باتری

بزرگ ترین اشتباه توسعه دهندگان، فراموش کردن لغو ثبت شنونده در زمان توقف اکتیویتی یا فرگمنت است:

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

class SensorLifecycleObserver(
    private val onStartListening: () -> Unit,
    private val onStopListening: () -> Unit
) : DefaultLifecycleObserver {

    override fun onResume(owner: LifecycleOwner) {
        onStartListening()
    }

    override fun onPause(owner: LifecycleOwner) {
        onStopListening()
    }
}

با متصل ساختن این ناظر به چرخه حیات کامپوننت، حسگرها دقیقاً در متد onResume فعال شده و در متد onPause فوراً خاموش می شوند.

۲۱. آزمون های خودکار شبیه سازی داده های سنسور در پایپ لاین CI/CD

اجرای تست های خودکار برای منطق های حساس به سنسور نیازمند تزریق داده های موک و اعتبارسنجی خروجی ها است:

import org.junit.Test
import kotlin.test.assertEquals

class SensorFilterTest {
    @Test
    fun lowPassFilter_removesSuddenSpikeSuccessfully() {
        val filter = LowPassFilter(alpha = 0.8f)
        val rawData = floatArrayOf(10f, 0f, 0f)
        val filtered = filter.filter(rawData)

        // بررسی عدم عبور ضربه ناگهانی با ضریب فیلتر
        assertEquals(2.0f, filtered[0], 0.1f)
    }
}

جهت مشاهده ساختارهای تست پایدار با فریمورک های مدرن، مقاله مرجع راهنمای جامع تست نرم افزار در مهندسی مدرن را در ساختار پروژه ها به کار بگیرید.

۲۲. اشتباهات رایج و خطاهای پرتکرار در توسعه ماژول های مبتنی بر سنسور

تجربه دیباگ سیستم های حساس به حسگر، خطاهای پرهزینه زیر را نمایان می سازد:

  • نشت حافظه و تخلیه باتری به دلیل عدم Unregister: اگر شنونده در چرخه حیات لغو نشود، حسگر حتی پس از بسته شدن برنامه به تولید داده و مصرف باتری ادامه می دهد.
  • فرض قطعی وجود سنسور در تمام دستگاه ها: تبلت ها و گوشی های اقتصادی ممکن است فاقد ژیروسکوپ یا فشارسنج باشند؛ همیشه مقدار برگشتی را از نظر null بودن بررسی کنید.
  • پردازش های سنگین محاسباتی در متد onSensorChanged: این متد در فرکانس های بالا صدها بار در ثانیه صدا زده می شود؛ اجرای لاجیک های سنگین در این بخش موجب فریز کامل UI می گردد.
  • نادیده گرفتن جهت گیری صفحه نمایش: محورهای مختصات سنسورها نسبت به ابعاد طبیعی دستگاه تعریف شده اند؛ هنگام چرخش صفحه به افقی (Landscape) باید بردارها را اصلاح نمایید.
هشدار کارایی و پرفورمنس: هرگز درون متد onSensorChanged اقدام به تخصیص حافظه (مانند ایجاد اشیای جدید یا ساخت لیست) نکنید؛ زیرا فعال شدن مکرر Garbage Collector باعث پرش فریم و لگ های شدید در رابط کاربری می شود.

۲۳. چک لیست مهندسی استقرار ماژول های سنسور در محیط های عملیاتی پروداکشن

پیش از انتشار نسخه نهایی اپلیکیشن در مارکت ها، موارد زیر را بازبینی فرمایید:

  • اعتبارسنجی وجود سخت افزار با بررسی مقادیر null برای کلیه سنسورهای مورد نیاز.
  • تنظیم تگ uses-feature در مانیفست با صفت android:required="false" جهت جلوگیری از فیلتر شدن اپلیکیشن در دستگاه های فاقد سنسور.
  • استفاده از بهینه ترین نرخ تاخیر نمونه برداری متناسب با سناریوی کاربری (ترجیحاً SENSOR_DELAY_UI یا NORMAL).
  • جداسازی کامل پردازش های سنگین از ترد اصلی با استفاده از جریان های واکنشی کاتلین.
  • مدیریت بی نقص چرخه حیات با فعال سازی در onResume و غیرفعال سازی قطعی در onPause.
توصیه کالیبراسیون داده ها: در کار با قطب نما و سنسور میدان مغناطیسی، همواره مقدار پارامتر accuracy در متد onAccuracyChanged را بررسی کنید؛ در صورت گزارش مقدار SENSOR_STATUS_UNRELIABLE به کاربر پیام چرخش ۸ انگلیسی (Figure-۸) جهت کالیبره شدن حسگر نمایش دهید.

علاوه بر این، در محیط های شبیه ساز Android Virtual Device (AVD) می توانید از پنل Extended Controls برای تست مقادیر دلخواه شتاب، زوایای دوران و حرکت سنسور مجازی بهره مند شوید تا رفتارهای غیرمنتظره دستگاه ها در محیط آزمایشگاهی ارزیابی گردد.

۲۴. پرسش های متداول مهندسی سنسورها در اندروید

تفاوت سنسور شتاب سنج (Accelerometer) با ژیروسکوپ (Gyroscope) چیست؟

شتاب سنج نرخ تغییرات سرعت خطی در راستای سه محور مختصات را اندازه می گیرد، در حالی که ژیروسکوپ نرخ دوران زاویه ای یا چرخش دستگاه به دور محورهای سه گانه را می سنجد.

چرا مقادیر شتاب سنج در حالت سکون تلفن همراه روی میز صفر نیست؟

زیرا شتاب سنج همواره تحت تاثیر شتاب ثابت گرانش زمین (حدود ۹.۸ متر بر مجذور ثانیه) قرار دارد؛ برای دریافت شتاب خالص کاربر باید از سنسور TYPE_LINEAR_ACCELERATION یا فیلتر پایین گذر استفاده کرد.

آیا برای کار با سنسورها در اندروید اخذ مجوز (Permission) از کاربر الزامی است؟

اکثر سنسورهای حرکتی و محیطی عمومی نیازی به مجوز ندارند؛ اما سنسورهای پایش فعالیت و گام شمار از اندروید ۱۰ به بعد نیازمند مجوز زمان اجرای ACTIVITY_RECOGNITION و سنسورهای ضربان قلب نیازمند BODY_SENSORS هستند.

چرا شنونده سنسور باید در متد onPause لغو ثبت (Unregister) شود؟

اگر شنونده لغو نشود، چیپ های سخت افزاری سنسور همچنان روشن مانده و با تولید بی وقفه داده در پس زمینه، باتری دستگاه را در کمتر از چند ساعت تخلیه می نمایند.

کدام نرخ نمونه برداری برای ساخت بازی های موبایلی مناسب است؟

ثابت SENSOR_DELAY_GAME با تاخیر تقریبی ۲۰ میلی ثانیه (معادل ۵۰ هرتز)، نرخ بروزرسانی ایده آلی برای شبیه سازی روان بازی های تعاملی و حرکتی فراهم می سازد.

سنسورهای ترکیبی (Synthetic Sensors) چگونه کار می کنند؟

این سنسورها قطعه فیزیکی مجزا ندارند؛ بلکه فریمورک اندروید داده های چند سنسور فیزیکی مختلف مانند شتاب سنج، ژیروسکوپ و مغناطیس سنج را ادغام کرده و خروجی های پالایش شده ای نظیر وکتور دوران تولید می کند.

چرا مقادیر سنسور مجاورت (Proximity) در برخی گوشی ها فقط صفر یا پنج است؟

بسیاری از تولیدکنندگان سخت افزار از سنسورهای مجاورت باینری استفاده می کنند که صرفاً دو حالت دور (معمولاً ۵ سانتی متر) یا نزدیک (صفر سانتی متر) را گزارش می دهند.

چگونه می توان عملکرد سنسورها را بدون دستگاه واقعی در ایمولاتور تست کرد؟

در ایمولاتور اندروید استودیو با باز کردن پنجره Extended Controls و بخش Virtual Sensors، می توان موقعیت فیزیکی، زوایای چرخش و مقادیر شتاب مجازی را به صورت لحظه ای تغییر داد.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.