반응형
⌚ Wear OS | 센서 수명주기·권한·동기화 연결로 기본 데이터 파이프 완성

개요 (Intro)
- 오늘의 목표: Wear OS에서 걸음 수/헤딩/고도/위치를 안정적으로 수집하고, 폰과 동기화까지 이어지는 파이프를 작동 상태로 만들기
- 배경: 기존 코드에 센서 시작/중지, 런타임 권한, 폰-웨어 메시지 등록이 일부 누락되어 실사용에 불편함
- 사용 기술: Kotlin, Jetpack Compose, Hilt, Google Play Services (Wearable/Location), SensorManager
📅 날짜: 2025.12.05
🎯 목표: 센서 수명주기 연결 + 권한 요청 + 동기화 작동 상태 만들기
🧰 기술: Kotlin, Compose, Hilt, Wearable Data Layer, SensorManager
문제 정의 (Problem / Motivation)
- 센서 매니저(PassiveSteps/Heading/Altitude)가 start()/stop()로 생명주기에 연결되지 않음
- WearDataSyncManager의 register()/unregister() 미호출로 메시지 수신/전송 상태 반영이 어려움
- 걸음 업데이트와 PDR(보행자 추정항법)·스텝 전송의 연계가 빠져 있음
- 런타임 권한(특히
ACTIVITY_RECOGNITION) 요청 흐름이 없어 첫 실행 경험이 매끄럽지 않음
// 예시: LiveData가 아니라 Compose state를 쓰고 있으나, 문제의 본질은 동일
// "센서 시작/중지"와 "동기화 리스너"가 Activity 수명주기에 묶여야 한다는 점.
val passiveStepsManager = remember(context) { PassiveStepsManager(context, scope) { total, delta ->
// 1) UI 업데이트
steps = total
lastDelta = delta
// 2) PDR 누적 (현재 헤딩 반영)
pdr.onStep(delta, headingRad)
// 3) 폰으로 스텝 동기화 (내부 스로틀 보유)
syncManager.sendSteps(total)
} }
해결 과정 (How I Solved It)
- 런타임 권한 요청을 Activity에 추가: 위치 + 활동 인식 분리 요청
- 센서·동기화 수명주기 연결: Compose에서
DisposableEffect로 start/stop, register/unregister 보장 - 걸음 → PDR → 동기화 파이프:
onStepsUpdate에서 PDR 누적과sendSteps호출 - 위치 스트리밍은 임계치/간격/enable 토글 기준으로
collect하며 폰으로 전송
// 1) 권한 요청 런처(위치 + 활동 인식)
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { result ->
val locGranted = result[Manifest.permission.ACCESS_FINE_LOCATION] == true ||
result[Manifest.permission.ACCESS_COARSE_LOCATION] == true
val actGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
result[Manifest.permission.ACTIVITY_RECOGNITION] == true else true
hasLocationPermission = locGranted
hasActivityRecognitionPermission = actGranted
}
// 2) 센서와 동기화 수명주기 연결 (Activity 내 Composable)
DisposableEffect(Unit) {
// 센서 시작
headingProvider.start() // 회전벡터 → heading
altitudeMonitor.startMonitoring() // 기압 → 고도
passiveStepsManager.start() // 스텝 카운터
// 동기화 리스너 등록 + 현지 스텝 제공자 설정
syncManager.localStepsProvider = { steps }
syncManager.register()
// 컴포저블이 사라질 때 정리
onDispose {
passiveStepsManager.stop()
headingProvider.stop()
altitudeMonitor.stopMonitoring()
syncManager.unregister()
}
}
// 3) 걸음 업데이트 -> PDR 누적 및 스텝 전송
val passiveStepsManager = remember(context) {
PassiveStepsManager(context, scope) { total, delta ->
steps = total
lastDelta = delta
pdr.onStep(delta, headingRad) // 보행자 경로 누적
syncManager.sendSteps(total) // 폰으로 전송(스로틀 내장)
}
}
// 4) 위치 ENU 흐름을 폰으로 전송 (임계/간격/토글 기준)
LaunchedEffect(enuFlow, xyzIntervalMs, streamEnabled) {
if (enuFlow == null || !streamEnabled) return@LaunchedEffect
var lastSentX = Double.NaN
var lastSentY = Double.NaN
var lastSentZ = Double.NaN
var lastSentHeading = Double.NaN
var lastSentAt = 0L
val minIntervalMs = 2000L
val posThresh = 0.3
val headingThreshDeg = 5.0
enuFlow.collect { enu ->
val baro = altitude
val fusedZ = baro?.let { b -> altitudeFusionRatio * enu.zUp + (1 - altitudeFusionRatio) * b } ?: enu.zUp
val headingDegNow = Math.toDegrees(headingRad)
val now = System.currentTimeMillis()
val shouldSend = now - lastSentAt >= minIntervalMs ||
lastSentX.isNaN() || kotlin.math.abs(enu.xEast - lastSentX) > posThresh ||
lastSentY.isNaN() || kotlin.math.abs(enu.yNorth - lastSentY) > posThresh ||
lastSentZ.isNaN() || kotlin.math.abs(fusedZ - lastSentZ) > posThresh ||
lastSentHeading.isNaN() || kotlin.math.abs(headingDegNow - lastSentHeading) > headingThreshDeg
if (shouldSend) {
val payload = LocationPayload(
xEast = enu.xEast,
yNorth = enu.yNorth,
zFused = fusedZ,
headingDeg = headingDegNow,
pdrX = pdrState.value.xEast,
pdrY = pdrState.value.yNorth,
timestamp = now,
deviceId = Build.MODEL ?: "wear",
source = "wear"
)
syncManager.sendLocation(payload)
lastSentX = enu.xEast; lastSentY = enu.yNorth; lastSentZ = fusedZ
lastSentHeading = headingDegNow; lastSentAt = now
}
}
}
결과 (Result)
- 실시간 센서 → UI 반응: Steps/Heading/Altitude가 1–2초 내 반응
- 폰-웨어 동기화 가시화: /steps/push, /steps/state, /location/update 정상 송수신
- 권한 거부 시에도 크래시 없이 안내 UI 유지
- 스트리밍 프로파일(배터리/일반/고속)을 메시지로 수신해 샘플링 주기 동적 변경
✅ 기본 데이터 파이프(센서→처리→동기화) 작동 확인
📱 폰 로그에서 메시지 수신 경로 동작 검증(/steps/*, /location/*)
느낀 점 / 회고 (Reflection)
- Compose 환경에서도
DisposableEffect로 수명주기 제어가 간결하고 확실했다. - 데이터 클래스는 공통 모듈로 분리하면 스키마 드리프트 사고를 줄일 수 있겠다.
- Fused Location으로 전환하면 실내 가용성과 배터리가 더 좋아질 듯. 다음 스프린트에 포함.
참고자료 (References)
본 문서는 자동 생성된 개발일기이며, 예시 코드는 초보자도 이해할 수 있도록 상세 주석을 포함합니다.
반응형
'모바일 앱(안드로이드)' 카테고리의 다른 글
| 🛰️ Android | 워치-폰 동기화 + 위치(x,y,z)+고도 융합(칼만/EMA) 기능 통합 작업 기록 (0) | 2025.12.03 |
|---|---|
| 🛠 Wear + Phone Altitude 동기화 & 설정화면 컴파일 오류 해결 요약 (1) | 2025.12.01 |
| 🖐 Wear OS | 폰-워치 동기화와 큰절 제스처 시작 표식, 설정 화면 표시까지 (0) | 2025.11.29 |
| 🛠 Android | Coupang API + Hilt DI + AdsScreen UX/포맷 개선 작업 기록 (1) | 2025.11.25 |
| 🦾 Android | 하단 바 + Navigation Compose + 보안 키 주입(ResValue) 적용기 (1) | 2025.11.23 |