قراءة مستشعرات الجهاز
قراءة مستشعرات الجهاز في Flutter
تحتوي الهواتف الذكية الحديثة على مجموعة غنية من مستشعرات الأجهزة التي تقيس القوى المادية والاتجاه والحركة. توفر Flutter الوصول إلى هذه المستشعرات من خلال حزمة sensors_plus، التي تُغلّف واجهات برمجة التطبيقات الأصلية لنظام iOS (Core Motion) وأندرويد (SensorManager) في واجهة Dart نظيفة قائمة على التدفقات. في هذا الدرس ستتعلم كيفية الاشتراك في تدفقات مقياس التسارع والجيروسكوب، وتفسير مخرجات المحاور الثلاثة، وبناء لوحة تحكم مباشرة للمستشعرات تستجيب في الوقت الفعلي لحركة الجهاز.
إضافة التبعية
أضف sensors_plus إلى ملف pubspec.yaml. لا تُطلب أذونات منصة إضافية على iOS أو أندرويد لقراءة مقياس التسارع والجيروسكوب — فهي تُعدّ مستشعرات غير حساسة.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
sensors_plus: ^4.0.2 # تحقق دائماً من pub.dev للحصول على أحدث إصدار مستقر
شغّل flutter pub get بعد حفظ الملف.
فهم تدفقات المستشعرات
تكشف حزمة sensors_plus كل مستشعر كـ Stream في Dart. أكثر تدفقين استخداماً هما:
- accelerometerEventStream() — يُصدر كائنات
AccelerometerEventتحتوي على قيم x وy وz تُقاس بوحدة m/s². تشمل هذه القيم قوة الجاذبية (حوالي 9.81 م/ث² عند الراحة على سطح مستوٍ). - gyroscopeEventStream() — يُصدر كائنات
GyroscopeEventتحتوي على معدل الدوران حول كل محور بوحدة راديان/ثانية. القيم القريبة من الصفر تعني أن الجهاز ثابت. - userAccelerometerEventStream() — مثل accelerometerEventStream لكن مع طرح الجاذبية، لذا يُبلّغ الجهاز الثابت عن (0, 0, 0).
dispose() لمنع تسربات الذاكرة واستنزاف البطارية بعد إزالة الودجت من الشجرة.اصطلاحات المحاور
يتبع كلا المستشعرين نفس نظام الإحداثيات كما في SDK الخاصة بأندرويد وiOS. عندما يكون الجهاز مسطحاً على طاولة مع الشاشة لأعلى:
- المحور X — يشير إلى يمين الجهاز (اليمين في الوضع الأفقي = موجب)
- المحور Y — يشير نحو أعلى الجهاز (للأعلى = موجب)
- المحور Z — يشير خارج وجه الشاشة (بعيداً عنك = موجب)
userAccelerometerEventStream() بدلاً من accelerometerEventStream() عندما تريد الكشف فقط عن الحركة التي يبدأها المستخدم وتجاهل الجاذبية. هذا ما تستخدمه تطبيقات اللياقة البدنية لحساب الخطوات.الاشتراك في تدفقات المستشعرات
النمط الأنظف هو تخزين كل StreamSubscription كحقل في فئة State، وبدءها في initState()، وإلغاؤها في dispose(). داخل رد نداء الحدث، استدعِ setState() لتشغيل إعادة بناء واجهة المستخدم.
ودجت لوحة تحكم المستشعرات المباشرة
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sensors_plus/sensors_plus.dart';
class SensorDashboard extends StatefulWidget {
const SensorDashboard({super.key});
@override
State<SensorDashboard> createState() => _SensorDashboardState();
}
class _SensorDashboardState extends State<SensorDashboard> {
// أحدث قراءة لمقياس التسارع (m/s²)
double _accelX = 0, _accelY = 0, _accelZ = 0;
// أحدث قراءة للجيروسكوب (rad/s)
double _gyroX = 0, _gyroY = 0, _gyroZ = 0;
late final StreamSubscription<AccelerometerEvent> _accelSub;
late final StreamSubscription<GyroscopeEvent> _gyroSub;
@override
void initState() {
super.initState();
// الاشتراك في مقياس التسارع بمعدل 20 هرتز (معدل تحديث واجهة مستخدم عادي)
_accelSub = accelerometerEventStream(
samplingPeriod: SensorInterval.normalInterval,
).listen((AccelerometerEvent event) {
setState(() {
_accelX = event.x;
_accelY = event.y;
_accelZ = event.z;
});
});
// الاشتراك في الجيروسكوب بنفس المعدل
_gyroSub = gyroscopeEventStream(
samplingPeriod: SensorInterval.normalInterval,
).listen((GyroscopeEvent event) {
setState(() {
_gyroX = event.x;
_gyroY = event.y;
_gyroZ = event.z;
});
});
}
@override
void dispose() {
// ألغِ الاشتراكات دائماً لتجنب تسربات الذاكرة
_accelSub.cancel();
_gyroSub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('لوحة تحكم المستشعرات')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SensorCard(
title: 'مقياس التسارع (m/s²)',
x: _accelX, y: _accelY, z: _accelZ,
),
const SizedBox(height: 16),
_SensorCard(
title: 'الجيروسكوب (rad/s)',
x: _gyroX, y: _gyroY, z: _gyroZ,
),
],
),
),
);
}
}
// بطاقة قابلة لإعادة الاستخدام لعرض بيانات المستشعرات ثلاثية المحاور
class _SensorCard extends StatelessWidget {
final String title;
final double x, y, z;
const _SensorCard({
required this.title,
required this.x,
required this.y,
required this.z,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
Text('X: ${x.toStringAsFixed(3)}'),
Text('Y: ${y.toStringAsFixed(3)}'),
Text('Z: ${z.toStringAsFixed(3)}'),
],
),
),
);
}
}
التحكم في معدل أخذ العينات
تتحكم المعلمة samplingPeriod في عدد مرات إطلاق المستشعر لحدث. توفر الحزمة أربع ثوابت ملائمة عبر SensorInterval:
SensorInterval.normalInterval— حوالي 20 هرتز، مناسب لمعظم تحديثات واجهة المستخدمSensorInterval.uiInterval— يتطابق مع معدل تحديث الشاشة (حوالي 60 هرتز)SensorInterval.gameInterval— حوالي 100 هرتز، للألعاب التي تحتاج استجابة سريعةSensorInterval.fastestInterval— أقصى معدل للأجهزة (يعتمد على الجهاز)
setState(). استخدم normalInterval للشاشات المعلوماتية وزِد المعدل فقط عندما تتطلب حالة الاستخدام ذلك فعلاً (مثل وحدة تحكم للعبة في الوقت الفعلي).تمهيد القراءات الصاخبة بمرشح تمرير منخفض
بيانات المستشعر الخام مشوّشة بطبيعتها. يدمج مرشح التمرير المنخفض البسيط القراءة الجديدة مع القيمة السابقة لتقليل الاهتزاز دون إضافة تأخير ملحوظ. تتحكم معلمة alpha (من 0 إلى 1) في مقدار الوزن المُعطى للعينة الجديدة: 0.1 سلس جداً (بطيء)، و0.9 سريع الاستجابة جداً (صاخب).
مرشح التمرير المنخفض مطبق على بيانات مقياس التسارع
const double _alpha = 0.15; // معامل التمهيد
double _smoothX = 0, _smoothY = 0, _smoothZ = 0;
void _onAccelEvent(AccelerometerEvent event) {
setState(() {
// مرشح التمرير المنخفض: دمج القيمة الجديدة مع المتوسط الجاري
_smoothX = _alpha * event.x + (1 - _alpha) * _smoothX;
_smoothY = _alpha * event.y + (1 - _alpha) * _smoothY;
_smoothZ = _alpha * event.z + (1 - _alpha) * _smoothZ;
});
}
ملخص
في هذا الدرس تعلمت كيفية الوصول إلى مستشعرات الأجهزة باستخدام حزمة sensors_plus. النقاط الرئيسية التي يجب تذكرها هي:
- يتم الكشف عن كل مستشعر كـ
Streamمستمر في Dart من كائنات أحداث مكتوبة تحتوي على قيم x وy وz. - اشترِك في
initState()وألغِ الاشتراك دائماً فيdispose()لمنع تسربات الموارد. - اختر
SensorIntervalالمناسب — يُفضّلnormalIntervalلتحقيق التوازن بين سرعة الاستجابة وعمر البطارية. - طبّق مرشح التمرير المنخفض لتقليل الضوضاء في بيانات المستشعرات الخام عند بناء تجارب بصرية سلسة.