מעקב אימונים באפליקציות מובייל: GPS, דופק, BLE
כשמפתחים אפליקציית מעקב אימונים, האתגרים המרכזיים הם דיוק GPS והמשכיות הקלטה ברקע. אפילו במכשירי דגל, GPS גולמי מייצר רעש של 5–15 מטרים, והמערכת עלולה להרוג את תהליך האפליקציה תוך 15 שניות לאחר מזעורה. ללא סינון מתאים, המסלול מלא בזיגזגים, ואובדן נתונים בקריסה משמעותו אובדן מוטיבציה של המשתמש. בנוסף, באנדרואיד ללא שירות רקע (Foreground Service), האפליקציה עלולה להיהרג בכל רגע; ב-iOS, אפילו עם הרשאת "תמיד", המערכת עלולה להשהות עדכונים כשהסוללה חלשה. מקרה קשה במיוחד הוא כשספורטאי רץ באזור מיוער—אות ה-GPS נחלש והמסלול מתחיל לסטות ללא פילטר קלמן.
אנו מציעים מודול מעקב מוכן לשימוש הפותר בעיות אלה: מאיסוף נתונים ועד אינטגרציה עם חנויות הבריאות של הפלטפורמות. הצוות שלנו עם 5 שנות ניסיון בפיתוח מובייל מבטיח הקלטה יציבה ב-iOS ואנדרואיד. תוך 3–5 שבועות, תקבלו אפליקציית ריצה מוכנה עם GPS, מרחק וקצב. למשימות מורכבות יותר—עם דופק, חיישני BLE ומספר סוגי פעילויות—לוח הזמנים הוא 2–4 חודשים. אנו משתמשים באלגוריתמים ובתצורות מוכחות שהראו דיוק גבוה ביותר מ-15 פרויקטים מסחריים.
בואו נצלול לאיך אנו מבטיחים דיוק מסלול, פעילות ברקע ואינטגרציה עם מכשירים לבישים.
ארכיטקטורת סשן
המרכיב המרכזי הוא WorkoutSession (או איך שתקראו לו), מכונת מצבים עם מצבים:
Idle → Preparing → Active → Paused → Active → Finishing → Saved מעברים מופעלים על ידי המשתמש (כפתורי התחל/השהה/סיום) והמערכת (אובדן GPS, ירידת סוללה). כל המצב נשמר ב-Idle → Preparing → Active → Paused → Active → Finishing → Saved , נשמר דרך Room (אנדרואיד) או CoreData/SQLite (iOS) לאחר כל עדכון—כך שבקריסה, ניתן לשחזר את הסשן.
@Entity
data class WorkoutPoint(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val sessionId: String,
val timestamp: Long,
val latitude: Double?,
val longitude: Double?,
val altitude: Double?,
val heartRate: Int?,
val speed: Double?,
val distance: Double
)כל 5 שניות אנו מכניסים רשומה חדשה למסד הנתונים. כשהאימון מסתיים, אנו מצרפים הכל ל-WorkoutRepository. נקודות ביניים לא נמחקות—הן נחוצות להצגת המסלול.
איך להבטיח דיוק מסלול GPS?
איך לסנן רעש GPS?
נתוני GPS גולמיים מכילים רעש של ±5–15 מ'. במסלול, זה נראה ויזואלית כמו זיגזגים במקום קו ישר. אנו מסננים באמצעות פילטר קלמן—הוא מדויק יותר מממוצע נע ומסתגל לדינמיקת התנועה. בקטעים ישרים, קלמן נותן דיוק של ±2 מ' לעומת ±5 מ' לממוצע נע; בפניות, ±4 מ' לעומת ±10 מ'. ב-80% מהפרויקטים שלנו, אנו משתמשים בפילטר קלמן—הוא משפר את הדיוק ב-60% בפניות.
לפיתוח מובייל, אין צורך ליישם קלמן מאפס. באנדרואיד, @Entity data class WorkoutPoint( @PrimaryKey(autoGenerate = true) val id: Long = 0, val sessionId: String, val timestamp: Long, val latitude: Double?, val longitude: Double?, val altitude: Double?, val heartRate: Int?, val speed: Double?, val distance: Double ) כבר מפעיל סינון פנימי. ב-iOS, WorkoutSummary עם FusedLocationProviderClient משתמש במיזוג חיישנים. בנוסף, אנו משליכים נקודות עם CLLocationManager.
שלבים להגדרת סינון:
- אתחול LocationManager עם דיוק
kCLLocationAccuracyBestForNavigation(iOS) או יצירת FusedLocationProviderClient (אנדרואיד). - הגדרת עדכון מרחק מינימלי של 5 מ'.
- בקריאה החוזרת, השלכת נקודות עם
horizontalAccuracy > 20 м. - הפעלת פילטר קלמן להחלקת הנקודות הנותרות.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last, location.horizontalAccuracy <= 20, location.horizontalAccuracy >= 0 else { return }
if let previous = lastLocation {
let segment = location.distance(from: previous)
totalDistance += segment
}
lastLocation = location
trackPoints.append(location)
} חישוב מרחק וקצב
מרחק הוא סכום המרחקים בין נקודות GPS עוקבות (kCLLocationAccuracyBestForNavigation ב-iOS, horizontalAccuracy > 20 באנדרואיד). קצב (דקות/ק"מ) = 1000 / מהירות (מ'/ש') / 60. המהירות נלקחת מ-func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last, location.horizontalAccuracy <= 20, location.horizontalAccuracy >= 0 else { return } if let previous = lastLocation { let segment = location.distance(from: previous) totalDistance += segment } lastLocation = location trackPoints.append(location) } / CLLocation.distance(from:)—הם מחושבים מהזזת דופלר, מדויקים יותר מהפרשי קואורדינטות.
כש-Location.distanceTo() (אין נתונים אמינים), אנו משתמשים במהירות המחושבת מקואורדינטות.
למה מצב רקע חשוב?
ללא פעילות ברקע, GPS נכבה 15 שניות לאחר מזעור האפליקציה. ב-iOS, אנו מוסיפים CLLocation.speed ב-Location.speed ומבקשים הרשאת "תמיד". באנדרואיד, אנו מתחילים שירות רקע (Foreground Service) עם התראה—אחרת, המערכת הורגת את התהליך כשהזיכרון נמוך.
class WorkoutTrackingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildTrackingNotification()
startForeground(NOTIFICATION_ID, notification)
startLocationUpdates()
return START_STICKY
}
}speed < 0 מבטיח שהמערכת תפעיל מחדש את השירות אם היא הורגת אותו, עם intent של UIBackgroundModes = location. אנו מטפלים ב-Info.plist ומשחזרים מצב מ-Room.
רשימת בדיקה להגדרת מצב רקע ב-iOS ואנדרואיד
iOS:
- הוספת UIBackgroundModes = location ל-Info.plist
- בקשת הרשאת תמיד (requestAlwaysAuthorization)
- הגדרת allowsBackgroundLocationUpdates = true
- להתחלת סשן, שימוש ב-CLMonitor (iOS 17+)
אנדרואיד:
- הצהרת הרשאת FOREGROUND_SERVICE_LOCATION (API 29+)
- קריאה ל-startForeground עם ערוץ התראות
- שימוש ב-START_STICKY להפעלה מחדש לאחר הרג
- טיפול ב-Intent null ב-onStartCommand
השוואת שיטות סינון GPS
| שיטה | דיוק בקטע ישר | דיוק בפניות | מורכבות חישובית |
|---|---|---|---|
| ממוצע נע | ±5 מ' | ±10 מ' | נמוכה |
| פילטר קלמן | ±2 מ' | ±4 מ' | בינונית |
| FusedLocation מערכתי | ±3 מ' | ±5 מ' | נמוכה (מובנה) |
פעילות ברקע: iOS לעומת אנדרואיד
| פלטפורמה | מנגנון | דרישות | אמינות |
|---|---|---|---|
| iOS | מיקום ברקע | הרשאת "תמיד", UIBackgroundModes | גבוהה, אך עלולה להשהות בסוללה חלשה |
| אנדרואיד | שירות רקע | התראה, START_STICKY | גבוהה מאוד, מופעל מחדש לאחר הרג |
אינטגרציה עם מכשירים לבישים
דופק מ-Apple Watch—דרך class WorkoutTrackingService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = buildTrackingNotification() startForeground(NOTIFICATION_ID, notification) startLocationUpdates() return START_STICKY } } ב-iOS (השעון מוסיף דגימות אוטומטית). באנדרואיד, Wear OS דרך START_STICKY או Bluetooth GATT עם פרופיל דופק (UUID null).
Bluetooth GATT לחיישנים חיצוניים (רצועת חזה Polar H10, Wahoo TICKR):
val hrServiceUUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")
val hrCharacteristicUUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
if (characteristic.uuid == hrCharacteristicUUID) {
val flag = characteristic.properties
val format = if (flag and 0x01 != 0) {
BluetoothGattCharacteristic.FORMAT_UINT16
} else {
BluetoothGattCharacteristic.FORMAT_UINT8
}
val heartRate = characteristic.getIntValue(format, 1) ?: 0
onHeartRateReceived(heartRate)
}
} שמירה ל-HealthKit / Health Connect
בסיום האימון, אנו כותבים null מלא (iOS) או HKWorkoutBuilder (אנדרואיד) עם כל המדדים המקוננים: מרחק, דופק, מסלול. ב-iOS, אנו משתמשים ב-HealthServicesClient למסלול GPS. המשתמש צריך לראות את האימון באפליקציית הבריאות של המערכת או ב-Health Connect. ממשק ה-API של Health Connect זמין באנדרואיד 10+.
מה כלול
- תיעוד ארכיטקטורה ודיאגרמות מצבי סשן
- קוד מקור עם הערות ובדיקות יחידה
- אינטגרציה עם חיישנים נבחרים (BLE, מכשירים לבישים)
- הגדרת מצב רקע והתראות
- הוראות פריסה ל-App Store / Google Play
- חודשיים של תמיכה לאחר מסירה
לוחות זמנים
אפליקציית ריצה בסיסית עם GPS, מרחק וקצב: 3–5 שבועות. אפליקציה מלאה עם דופק, חיישני BLE, מספר סוגי פעילויות, ייצוא GPX ואינטגרציה עם חנות הבריאות של הפלטפורמה: 2–4 חודשים.
צרו קשר כדי להעריך את הפרויקט שלכם—נמצא את הפתרון האופטימלי למחסנית הטכנולוגית ולתקציב שלכם. בקשו פיתוח מעקב אימונים מותאם אישית וקבלו מודול יציב עם הבטחת איכות.







