Wear OS 기반 러닝 트래킹 워치 애플리케이션으로, 실시간 GPS, 심박수, 페이스 데이터를 수집하고 페어링된 스마트폰으로 전송합니다.
이 프로젝트는 Wear OS 스마트워치에서 러닝 세션 데이터를 수집하여 페어링된 스마트폰 앱으로 전송하는 기능을 제공합니다. Google Wearable API를 사용하여 워치와 폰 간의 실시간 데이터 동기화를 지원합니다.
- GPS 위치 추적: 1초마다 위치 데이터 수집
- 심박수 모니터링: 워치 하드웨어 센서를 직접 사용한 실시간 심박수 측정 (EMA 필터링 적용)
- 거리 계산: Health Services 거리 또는 GPS 기반 거리 계산
- 페이스 계산: 최근 100m 기준 즉시 페이스 계산
- 칼로리 계산: 거리 기반 칼로리 소모량 추정 (1km당 60kcal)
- 일시정지/재개: 러닝 중 일시정지 및 재개 기능
- 실시간 UI 업데이트: StateFlow 기반 반응형 UI로 데이터 자동 반영
- 명령 수신: 폰에서 시작/정지/일시정지/재개 명령 수신
- 실시간 업데이트: 10초마다 현재 러닝 상태 전송
- 최종 세션 전송: 러닝 종료 시 전체 세션 데이터 전송
- 상태 응답: 각 명령에 대한 성공/실패 응답 전송
- 워치 주도 상태 알림: 워치에서 직접 일시정지/재개 시 폰으로 상태 알림
- Message API: 실시간 명령 및 업데이트 전송
- Data Layer API: 최종 세션 데이터 영구 저장 및 전송
┌─────────────────────────────────────────────────┐
│ Wear OS Watch App │
├─────────────────────────────────────────────────┤
│ Presentation Layer │
│ ├─ MainActivity (Compose) │
│ ├─ RunningScreen (UI - StateFlow 구독) │
│ └─ PermissionScreen (권한 요청) │
├─────────────────────────────────────────────────┤
│ Manager Layer │
│ └─ RunningManager (세션 관리, StateFlow 상태) │
├─────────────────────────────────────────────────┤
│ Service Layer │
│ ├─ PhoneCommunicationService (폰 통신) │
│ ├─ WatchCommandReceiver (명령 수신) │
│ ├─ LocationService (GPS) │
│ ├─ HeartRateService (심박수) │
│ └─ HealthMetricsService (통합 메트릭) │
├─────────────────────────────────────────────────┤
│ Data Layer │
│ ├─ RunningSession (세션 데이터 모델) │
│ └─ RoutePoint (경로 포인트 데이터 모델) │
└─────────────────────────────────────────────────┘
↕️
(Google Wearable API)
↕️
┌─────────────────────────────────────────────────┐
│ Paired Phone App │
└─────────────────────────────────────────────────┘
| Path | 설명 | Payload |
|---|---|---|
/waytoearth/command/start |
러닝 시작 | {sessionId, runningType} |
/waytoearth/command/stop |
러닝 종료 | {sessionId} |
/waytoearth/command/pause |
러닝 일시정지 | {sessionId} |
/waytoearth/command/resume |
러닝 재개 | {sessionId} |
| Path | 설명 | Payload |
|---|---|---|
/waytoearth/response/started |
시작 완료 | {success, sessionId, timestamp} |
/waytoearth/response/stopped |
종료 완료 | {success, sessionId} |
/waytoearth/response/paused |
일시정지 완료 (명령 응답 또는 워치 주도) | {success, sessionId, fromWatch?} |
/waytoearth/response/resumed |
재개 완료 (명령 응답 또는 워치 주도) | {success, sessionId, fromWatch?} |
참고: fromWatch: true가 포함된 경우 워치에서 사용자가 직접 일시정지/재개 버튼을 눌렀음을 의미합니다.
Path: /waytoearth/realtime/update
{
"sessionId": "watch-uuid",
"distanceMeters": 1234,
"durationSeconds": 180,
"heartRate": 145,
"paceSeconds": 320,
"averagePaceSeconds": 310,
"calories": 74,
"currentPoint": {
"latitude": 37.5665,
"longitude": 126.9780,
"sequence": 181,
"t": 180,
"acc": 5.2
},
"timestamp": 1234567890
}Path: /waytoearth/running/complete
{
"sessionId": "watch-uuid",
"distanceMeters": 5000,
"durationSeconds": 1800,
"averagePaceSeconds": 360,
"calories": 300,
"averageHeartRate": 150,
"maxHeartRate": 175,
"routePoints": [
{
"latitude": 37.5665,
"longitude": 126.9780,
"sequence": 1,
"timestampSeconds": 0,
"heartRate": 145,
"paceSeconds": 320,
"altitude": 50.5,
"accuracy": 5.2,
"cumulativeDistanceMeters": 0
}
],
"endedAt": 1234567890
}워치와 폰 간의 양방향 통신을 담당하는 핵심 서비스입니다.
위치: app/src/main/java/cloud/waytoearth/watch/service/PhoneCommunicationService.kt
주요 기능:
sendRealtimeUpdate(): 10초마다 실시간 러닝 데이터 전송sendRunningCompleteTransformed(): 최종 세션 데이터 전송sendResponseStarted/Stopped/Paused/Resumed(): 명령 응답 전송
사용 API:
MessageClient: 실시간 메시지 전송 (양방향)DataClient: 영구 데이터 저장 및 동기화
폰에서 전송된 명령을 수신하고 처리하는 서비스입니다.
위치: app/src/main/java/cloud/waytoearth/watch/service/WatchCommandReceiver.kt
주요 기능:
WearableListenerService상속- 폰의 명령 메시지 수신 및 파싱
RunningManager를 통한 세션 제어- 명령 처리 결과 응답
러닝 세션의 전체 생명주기를 관리하는 핵심 매니저입니다.
위치: app/src/main/java/cloud/waytoearth/watch/manager/RunningManager.kt
주요 기능:
startRunning(): 러닝 세션 시작, GPS/심박수 추적 시작stopRunning(): 세션 종료, 통계 계산pause()/resume(): 일시정지/재개startRealtimeSync(): 10초마다 폰으로 데이터 전송onLocationUpdate(): GPS 위치 업데이트 처리 (1초마다)
상태 관리 (StateFlow):
runningState: UI가 구독하는 실시간 상태 스트림RunningState: 현재 세션, 일시정지 여부, 실행 여부를 담는 데이터 클래스updateRunningState(): 상태 변경 시 자동으로 UI 업데이트 트리거
데이터 수집:
- GPS 위치: 1초마다 수집
- 심박수: 워치 하드웨어 센서(TYPE_HEART_RATE)를 직접 사용, EMA 필터링으로 노이즈 제거
- 거리: Health Services 거리 우선, GPS 증분 계산
- 페이스: 최근 100m 기준 즉시 페이스 계산
- 일시정지 시 위치 업데이트는 수신하지만 거리 계산 중지
GPS 위치 추적을 담당하는 서비스입니다.
위치: app/src/main/java/cloud/waytoearth/watch/service/LocationService.kt
주요 기능:
FusedLocationProviderClient사용- 1초 간격 위치 업데이트
Flow기반 위치 스트림 제공
심박수 모니터링을 담당하는 서비스입니다.
위치: app/src/main/java/cloud/waytoearth/watch/service/HeartRateService.kt
주요 기능:
- 워치 하드웨어 센서 직접 사용:
SensorManager와Sensor.TYPE_HEART_RATE로 센서 직접 제어 - EMA 필터링: Exponential Moving Average (alpha=0.3)로 센서 노이즈 제거
- 1초 간격 업데이트:
SensorEventListener를 통해 실시간 심박수 수집 - Health Services 운동 세션: 배터리 최적화를 위해 ExerciseClient로 RUNNING 모드 활성화
- Flow 기반 스트림: 심박수 데이터를 Flow로 제공하여 RunningManager와 연동
Health Services의 통합 메트릭을 수집하는 서비스입니다.
위치: app/src/main/java/cloud/waytoearth/watch/service/HealthMetricsService.kt
주요 기능:
- 심박수, 거리, 페이스, 속도 통합 수집
- Health Services의 정확한 거리 및 페이스 데이터 활용
러닝 세션의 전체 데이터를 담는 모델입니다.
위치: app/src/main/java/cloud/waytoearth/watch/data/RunningSession.kt
data class RunningSession(
val sessionId: String, // 세션 ID (UUID)
val startTime: Long, // 시작 시각 (epoch millis)
val routePoints: MutableList<RoutePoint>, // 경로 포인트 리스트
var totalDistanceMeters: Int = 0, // 총 거리 (m)
var durationSeconds: Int = 0, // 총 시간 (초)
var averageHeartRate: Int? = null, // 평균 심박수
var maxHeartRate: Int? = null, // 최대 심박수
var calories: Int = 0 // 칼로리
)1초마다 수집되는 경로 포인트 데이터입니다.
위치: app/src/main/java/cloud/waytoearth/watch/data/RoutePoint.kt
data class RoutePoint(
val latitude: Double, // 위도
val longitude: Double, // 경도
val sequence: Int, // 순서 (0부터 시작)
val timestampSeconds: Int, // 시작 시점부터 경과 시간 (초)
val heartRate: Int?, // 심박수 (BPM) - nullable
val paceSeconds: Int?, // 현재 페이스 (초/km) - nullable
val altitude: Double?, // 고도 (m) - nullable
val accuracy: Double?, // GPS 정확도 (m) - nullable
val cumulativeDistanceMeters: Int // 누적 거리 (m)
)UI 업데이트를 위한 실시간 상태 모델입니다.
위치: app/src/main/java/cloud/waytoearth/watch/manager/RunningManager.kt
data class RunningState(
val session: RunningSession? = null, // 현재 러닝 세션 (null이면 미실행)
val isPaused: Boolean = false, // 일시정지 여부
val isRunning: Boolean = false // 러닝 실행 여부
)이 모델은 StateFlow<RunningState>로 UI에 제공되어 Compose가 자동으로 리컴포지션을 수행합니다.
- Kotlin: 1.9+
- Wear OS: minSdk 30 (Android 11)
- Compose for Wear OS: UI 구현
play-services-wearable: 워치-폰 통신play-services-location: GPS 위치 추적
health-services-client: 운동 세션 관리 및 배터리 최적화 (심박수는 센서 직접 사용)
kotlinx-coroutines-android: 비동기 처리kotlinx-coroutines-flow: StateFlow/Flow 기반 반응형 상태 관리gson: JSON 직렬화/역직렬화accompanist-permissions: 런타임 권한 관리
<uses-permission android:name="android.permission.BODY_SENSORS" />
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />- Android Studio Hedgehog 이상
- Wear OS 에뮬레이터 또는 실제 Wear OS 디바이스 (Android 11 이상)
- 페어링된 안드로이드 폰 (데이터 전송 테스트용)
./gradlew build./gradlew installDebug-
WearableListenerService 구현
- 워치에서 전송되는 메시지 수신
-
메시지 수신 처리
override fun onMessageReceived(messageEvent: MessageEvent) { when (messageEvent.path) { "/waytoearth/response/started" -> handleStarted() "/waytoearth/response/stopped" -> handleStopped() "/waytoearth/response/paused" -> handlePaused() "/waytoearth/response/resumed" -> handleResumed() "/waytoearth/realtime/update" -> handleRealtimeUpdate() "/waytoearth/running/complete" -> handleRunningComplete() } }
-
명령 전송
val messageClient = Wearable.getMessageClient(context) val nodes = Wearable.getNodeClient(context).connectedNodes.await() nodes.forEach { node -> val payload = JSONObject().apply { put("sessionId", sessionId) }.toString().toByteArray() messageClient.sendMessage( node.id, "/waytoearth/command/start", payload ).await() }
폰 앱 → [START 명령] → 워치
워치 → [RunningManager.startRunning()] → GPS/심박수 추적 시작
워치 → [STARTED 응답] → 폰 앱
워치 → [10초마다 실시간 업데이트] → 폰 앱
워치 (GPS 1초마다) → RoutePoint 생성 → RunningSession 업데이트
워치 (심박수 실시간) → currentHeartRate 업데이트
워치 (10초마다) → PhoneCommunicationService.sendRealtimeUpdate() → 폰 앱
폰 앱 → [STOP 명령] → 워치
워치 → [RunningManager.stopRunning()] → 통계 계산
워치 → [STOPPED 응답] → 폰 앱
워치 → [최종 세션 데이터] → 폰 앱
워치 → GPS/심박수 추적 중지
폰 앱 → [PAUSE 명령] → 워치
워치 → [RunningManager.pause()] → 거리 계산 중지, StateFlow 업데이트
워치 → [PAUSED 응답] → 폰 앱
워치 UI → 자동 업데이트 (일시정지 상태 표시)
폰 앱 → [RESUME 명령] → 워치
워치 → [RunningManager.resume()] → 거리 계산 재개, StateFlow 업데이트
워치 → [RESUMED 응답] → 폰 앱
워치 UI → 자동 업데이트 (러닝 중 상태 표시)
워치 UI → [일시정지 버튼 클릭]
워치 → [RunningManager.pause()] → StateFlow 업데이트
워치 → [PAUSED 응답 with fromWatch=true] → 폰 앱
워치 UI → 자동 업데이트 (일시정지 상태 표시)
워치 UI → [재개 버튼 클릭]
워치 → [RunningManager.resume()] → StateFlow 업데이트
워치 → [RESUMED 응답 with fromWatch=true] → 폰 앱
워치 UI → 자동 업데이트 (러닝 중 상태 표시)
자세한 테스트 가이드는 TESTING_GUIDE.md를 참조하세요.
- 배터리 소모: GPS와 심박수 센서를 동시에 사용하므로 배터리 소모가 큽니다.
- 권한 요청: 첫 실행 시 위치, 센서, 활동 인식 권한이 필요합니다.
- 폰 연결: 워치와 폰이 페어링되어 있어야 데이터 전송이 가능합니다.
- 데이터 크기: 장시간 러닝 시 RoutePoint 데이터가 매우 커질 수 있습니다 (1시간 = 3600개 포인트).
- 워치와 폰이 페어링되어 있는지 확인
- 폰에서 WearableListenerService가 등록되어 있는지 확인
- AndroidManifest.xml에 서비스가 등록되어 있는지 확인
- 위치 권한이 승인되었는지 확인
- 워치의 위치 서비스가 활성화되어 있는지 확인
- 실외에서 테스트 (GPS 신호 수신 필요)
- BODY_SENSORS 및 BODY_SENSORS_BACKGROUND 권한이 승인되었는지 확인
- 워치를 손목에 착용했는지 확인 (센서가 피부에 밀착되어야 함)
- 워치에 TYPE_HEART_RATE 센서가 있는지 확인
- HeartRateService가 제대로 시작되었는지 로그 확인
이 프로젝트는 WayToEarth의 일부입니다.
프로젝트 관련 문의사항은 이슈를 등록해 주세요.