2026/05/04

오늘의 이야기

 



⌚ 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)



  1. 런타임 권한 요청을 Activity에 추가: 위치 + 활동 인식 분리 요청

  2. 센서·동기화 수명주기 연결: Compose에서 DisposableEffect로 start/stop, register/unregister 보장

  3. 걸음 → PDR → 동기화 파이프: onStepsUpdate에서 PDR 누적과 sendSteps 호출

  4. 위치 스트리밍은 임계치/간격/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)



본 문서는 자동 생성된 개발일기이며, 예시 코드는 초보자도 이해할 수 있도록 상세 주석을 포함합니다.





댓글 없음:

댓글 쓰기

오늘의 이야기

  ⌚ Wear OS | 센서 수명주기·권한·동기화 연결로 기본 데이터 파이프 완성 워치, 모바일   개요 (Intro) 오늘의 목표: Wear OS에서 걸음 수/헤딩/고도/위치 를 ...