Wi‐fi 기반 위치 추적 시스템과 INS(관성 항법 장치) - YangJJune/U-Compass GitHub Wiki

기본 GPS 활용

val context = LocalContext.current
    val fusedLocationClient: FusedLocationProviderClient = remember {
        LocationServices.getFusedLocationProviderClient(context)
    }

    var currentLocation by remember { mutableStateOf<Location?>(null) }

    // 위치 업데이트 콜백
    val locationCallback = remember {
        object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                for (location in locationResult.locations) {
                    currentLocation = location
                    break
                }
            }
        }
    }

    LaunchedEffect(key1 = locationPermissionState.status.isGranted) {
        try {
            fusedLocationClient.lastLocation
                .addOnSuccessListener { loc ->
                    if (loc != null) {
                        currentLocation = loc
                        Log.d("LocationPermission", "Last location received: $loc")
                    } else {
                        Log.d("LocationPermission", "Last location is null")
                    }
                }
                .addOnFailureListener { e ->
		                Log.e("LocationPermission", "Failed to get location: ${e.message}", e)
                }

            // 지속적인 위치 업데이트 요청
            val locationRequest = LocationRequest.Builder(1000)
                .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
                .build()

            fusedLocationClient.requestLocationUpdates(
                locationRequest,
                locationCallback,
                context.mainLooper
            )
            Log.d("LocationPermission", "Location updates requested")
        } catch (e: SecurityException) {
            Log.e("LocationPermission", "Security exception: ${e.message}", e)
            errorMessage = "권한 오류: ${e.message}"
            e.printStackTrace()
        } catch (e: Exception) {
            Log.e("LocationPermission", "Unexpected error: ${e.message}", e)
            errorMessage = "예상치 못한 오류: ${e.message}"
            e.printStackTrace()
        }
    }
    }

결과

  • 현재 위치가 신뢰할 만한 위치로 추정되긴 함
  • 하지만 실내 기준 계속 위도 경도가 바뀜
    • (휴대폰은 가만히 있는 상태)
    • 축척을 10m 로 낮추면 위치 마커가 계속해서 변동됨. (GPS 위도경도가 계속 바뀜)
  • GPS 만으로는 실내에서 위치를 신뢰하기 어려움.

image

image

Wifi

FingerPrint

  • 미리 수집해둔 Wi-Fi 신호 패턴 데이터베이스와, 실시간 스캔 데이터를 비교해서 사용자의 대략적인 위치를 추정하는 기법.
  • 기준 위치에서 RSSI(신호 세기) 데이터를 수집해 해당 데이터를 저장.
  • 사용자가 요청을 할 때, 주기적으로 주변 AP 의 RSSI 를 스캔하고, DB 의 신호 벡터와 비교해 가장 유사도가 높은 위치를 현재 위치로 결정함.

한계점

  • [Wifi FingerPrint 데이터 수집 및 관리 문제](https://koreascience.kr/article/JAKO201320360167437.pdf)
    • FingerPrint 데이터베이스를 구축하려면 1 미터제곱 당 4~6개의 지점에서 AP 신호를 측정해야 함. 500 미터제곱의 경우에는 최소 2,000 회의 측정 필요 + 그만큼의 시간 또한 추가로 소요됨.(500 미터제곱 규모 공간에서 약 72시간 소요)
    • 데이터베이스에 존재하는 Wifi 의 스펙이 변경되거나 건물 구조가 변경되었을 때, 데이터베이스 상 이 변경사항에 관련된 모든 데이터를 수정해야 합니다. 단순 가구 이동, 벽 설치 시 기존의 데이터베이스로는 심한 오차가 생겨 기대되는 위치를 제공할 수 없습니다.
  • 정확도 문제
    • [지역별 AP 밀도 편차가 존재함](https://hoonstudio.tistory.com/3)(ex) 강남 → 0.32 , 신안 → 0.002), 따라서 지역간 위치 정확도의 편차가 존재
    • 전파를 기반으로 측정하기 때문에 금속 구조물이 많은 공간에서 신호 반사로 인해서 오차가 발생할 수 있음. 금속 구조물이 많을 수록 오차가 누적되는 Drift 문제가 발생해 위치 좌표의 오차가 점점 커질 수 있음.

삼변측위

  • Wi-Fi RTT (Round-Trip-Time) API 가 제공하는 Wi-Fi 위치 기능을 사용하여 주변의 RTT 지원 Wi-Fi 에 액세스해서 해당 기기와의 거리를 측정 가능하다.
  • 이 때 기기(핸드폰)에서 3개 이상의 AP(Wi-Fi Access Point) 에 접근해 거리를 측정하는 경우에, 다변측정(MLAT) 알고리즘을 사용해 거리 측정치를 기준으로 기기의 위치를 특정할 수 있다. 이 때 오차는 1~2 미터로 계산된다. by [[Android Developer Wifi RTT](https://developer.android.com/develop/connectivity/wifi/wifi-rtt?hl=ko&_gl=112z2spc_up*MQ)]
  • WiFi-RTT 와 관련한 FTM(Fine-Time-Measurement) 기능은 IEEE 802.11-2016 표준에서 지정했다.
    • 이를 통해 패킷이 두 기기를 왕복하는 시간을 측정하고 그 시간을 활용해 거리를 계산함.
  • Android 10 이상부터 기기가 Wi-Fi AP 정보를 미리 저장할 필요 없이 AP 를 쿼리해서 위치 정보(위도, 경도, 고도 등)를 직접 요청할 수 있다.

요구사항

  • scanResults() 을 통해 얻은 List<ScanResult> 에서 Wi-Fi RTT 를 지원하는 Wi-Fi 를 찾아내서 이 Wi-Fi 들로 작업을 해야 한다.

    val scanResults : List<ScanResult> = wifiManager.scanResults
    val rttCapableAps = scanResults.filter { it.is80211mcResponder }
  • Android 9(28 API) 이상에서부터 실행 가능, IEEE 802.11az 기반 측정은 Android 15(35 API) 부터 사용 가능

  • Android 13(33 API) 이상을 타겟팅하는 경우 NEARBY_WIFI_DEVICES 권한이 있어야 함.

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission
    android:name="android.permission.NEARBY_WIFI_DEVICES"
    android:usesPermissionFlags="neverForLocation" />
  • 기기가 Wi-Fi RTT 를 지원하는 지 확인
Log.d("TAG", context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_RTT).toString)
  • 기기가 Wi-Fi RTT 를 사용할 수 있는 지 확인
Log.d("TAG", wifiRttManager.isAvailable)
  • BroadcastReceiver 를 이용해 사용 가능 여부를 계속 추적하기
val filter = IntentFilter(WifiRttManager.ACTION_WIFI_RTT_STATE_CHANGED)
val myReceiver = object: BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (wifiRttManager.isAvailable) {
            …
        } else {
            …
        }
    }
}
context.registerReceiver(myReceiver, filter)

사용법

  • 권한 요청
val requiredPermissions = remember {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            arrayOf(
		            Manifest.permission.ACCESS_COARSE_LOCATION
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.NEARBY_WIFI_DEVICES
            )
        } else {
            arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            )
        }
    }
  • Wi-Fi AP 에 요청할 수 있는 최대 개수 구하기
val maxRttDevices = RangingRequest.getMaxPeers()
  • 주변에 접근할 수 있는 Wi-Fi 스캔하기 후 RTT 기능이 있는 Wi-Fi 추출하기
val scanResults : List<ScanResult> = wifiManager.scanResults
val rttCapableAps = scanResults.filter { it.is80211mcResponder }
  • Wifi RTT 기술을 지원하는 Wifi AP 3대를 찾아서, 나와 그 AP들 사이의 거리를 측정함.
  • AP 3개 ↔ 내 위치 사이의 거리를 사용해 삼변측위 기술로 내 현재 위치를 특정한다.

한계점 및 제한사항

  • Wi-Fi RTT 를 지원하는 wifi 가 많이 없음.
// 사용 가능한 AP 스캔
    val success = wifiManager.startScan()
    if (!success) {
        Log.w("PositionLog", "WiFi 스캔 시작 실패, 이전 결과를 사용합니다")
    }
    
    val scanResults = wifiManager.scanResults
    Log.d("PositionLog", "스캔된 AP 개수: ${scanResults.size}")
    
    // 모든 AP 정보 로깅
    Log.d("PositionLog", "==== 스캔된 모든 AP 정보 ====")
    scanResults.forEachIndexed { index, result ->
        Log.d("PositionLog", "AP[$index]: SSID='${result.SSID}', BSSID=${result.BSSID}, " +
                "RSSI=${result.level}dBm, 주파수=${result.frequency}MHz, " +
                "RTT지원=${result.is80211mcResponder}, 채널폭=${result.channelWidth}")
    }
    
    // 802.11mc 지원 AP 필터링
    val rttCapableAps = scanResults
        .filter { result ->
        result.is80211mcResponder
    }
    
    Log.d("PositionLog", "==== RTT 지원 AP 정보 ====")
    Log.d("PositionLog", "RTT 지원 AP 개수: ${rttCapableAps.size}")

아래 사진은 건국대학교 경영관에서 측정한 주변 AP 정보들이다. 하지만 조사한 상태로는, 주변 39개의 AP 중 RTT 를 지원하는 Wifi 가 단 한 개도 없음을 알 수 있다. image

INS (Inertial Navigation System)

  • GPS 위치와 스마트폰 내장 센서를 결합해서, 실내에서도 비교적 정확한 위치 추적을 하기 위해 고안된 시스템
  • 실내에 들어가면, 마지막 GPS 좌표를 바탕으로 기준점을 정하고 기기의 방향과 이동거리를 조합해 실내 위치를 특정합니다.

요구사항

  • 권한 관리

    • 위치 권한 : ACCESS_FINE_LOCATION , ACCESS_COARSE_LOACATION
  • GPS 기반 위치 추적

    val locationCallback = remember { object : LocationCallback() {
    		override fun onLocationResult(locationResult: LocationResult) {
            for (location in locationResult.locations) {
                if (initialLocation == null) {
                    initialLocation = location
                    currentLatitude = location.latitude
                    currentLongitude = location.longitude
                } else if (!insActive) {
                    // INS가 활성화되지 않은 경우에만 GPS 위치 업데이트
                    currentLatitude = location.latitude
                    currentLongitude = location.longitude
                }
                break
            }
        }
    }
  • 센서 선언 : *TYPE_ACCELEROMETER, TYPE_MAGNETIC_FIELD*

    val sensorManager = remember { 
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager 
    }
    val accelerometer = remember { 
        sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) 
    }
    val magnetometer = remember { 
        sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) 
    }
  • 센서 등록

    sensorManager.registerListener(
        sensorListener, 
        accelerometer, 
        SensorManager.SENSOR_DELAY_NORMAL
    )
    sensorManager.registerListener(
        sensorListener, 
        magnetometer, 
        SensorManager.SENSOR_DELAY_NORMAL
    )
  • 실내에서 이동하는 경우와 실외에서 이동하는 경우를 분리해야 한다.

    • 실외에서 이동하는 경우 : GPS 가 정상 작동되므로 GPS 를 사용함.
    • 실내에서 이동하는 경우 : 마지막으로 얻은 GPS 좌표를 기반으로, 센서기능을 조합해 기기가 움직이는 방향과 거리만큼 위도 경도 좌표를 업데이트한다.

사용법

  • GPS 위치 요청 콜백 등록

    val fusedLocationClient = remember {
        LocationServices.getFusedLocationProviderClient(context)
    }
    
    val locationRequest = LocationRequest.Builder(1000) // 1초마다 업데이트
        .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
        .build()
    
    fusedLocationClient.requestLocationUpdates(
        locationRequest,
        locationCallback,
        Looper.getMainLooper()
    )
    
    fusedLocationClient.lastLocation.addOnSuccessListener { location ->
        if (location != null) {
            initialLocation = location
            currentLatitude = location.latitude
            currentLongitude = location.longitude
            logMessages = addLogMessage(logMessages) { "초기 위치 설정: 위도 ${location.latitude}, 경도 ${location.longitude}" }
        }
    }
    • Location Callback 이 성공적으로 호출되었으면 current 좌표가 정상적으로 업데이트 됨 (실외 GPS 기반 기준)
  • 센서 기반 걸음 수 측정

override fun onSensorChanged(event: SensorEvent) {
		when (event.sensor.type) {
				Sensor.TYPE_STEP_COUNTER -> {
		        val steps = event.values[0].toLong()
		        
            stepCount++ // 세션 내 걸음 수만 증가
            logMessages = addLogMessage(logMessages) { "걸음 감지 (카운터): $stepCount" }
            
            // 위치 업데이트
            updatePositionWithINS(
                currentAzimuth,
                stepLength,
                currentLatitude,
                currentLongitude
            ) { lat, lng ->
                currentLatitude = lat
                currentLongitude = lng
            }
		    }
		}
}
  • 기기 방위각 계산

    override fun onSensorChanged(event: SensorEvent) {
    		when (event.sensor.type) {
    		    Sensor.TYPE_MAGNETIC_FIELD -> {
    		        magnetValues = event.values.clone()
    		        // 방위각 계산
    		        calculateOrientation(accelValues, magnetValues) { azimuth ->
    		            currentAzimuth = azimuth
    		        }
    		    }
    		}
    }
    
    private fun calculateOrientation(
        accelValues: FloatArray,
        magnetValues: FloatArray,
        onResult: (Float) -> Unit
    ) {
        val rotationMatrix = FloatArray(9)
        val orientationAngles = FloatArray(3)
        
        val success = SensorManager.getRotationMatrix(
            rotationMatrix,
            null,
            accelValues,
            magnetValues
        )
        
        if (success) {
            SensorManager.getOrientation(rotationMatrix, orientationAngles)
            // orientationAngles[0]는 방위각(라디안)
            val azimuthRad = orientationAngles[0]
            var azimuthDegrees = Math.toDegrees(azimuthRad.toDouble()).toFloat()
            
            // 음수 값을 0~360도로 변환
            if (azimuthDegrees < 0) {
                azimuthDegrees += 360f
            }
            
            onResult(azimuthDegrees)
        }
    }
  • 현재 위치 좌표 업데이트

    override fun onSensorChanged(event: SensorEvent) {
    		when (event.sensor.type) {
    		    Sensor.TYPE_ACCELEROMETER -> {
    		        accelValues = event.values.clone()
    		        detectStep(accelValues, lastUpdateTime) { isStep ->
    		            if (isStep && insActive) {
    		                stepCount++
    		                updatePositionWithINS(
    		                    currentAzimuth,
    		                    stepLength,
    		                    currentLatitude,
    		                    currentLongitude
    		                ) { lat, lng ->
    		                    currentLatitude = lat
    		                    currentLongitude = lng
    		                }
    		            }
    		            lastUpdateTime = System.currentTimeMillis()
    		        }
    		    }
        }
    }
    
    private fun updatePositionWithINS(
        azimuth: Float,
        stepLength: Float,
        currentLat: Double,
        currentLng: Double,
        onUpdate: (Double, Double) -> Unit
    ) {
        // 방위각을 라디안으로 변환
        val azimuthRad = Math.toRadians(azimuth.toDouble())
        
        // 북쪽 및 동쪽 방향으로의 이동량 (미터)
        val northMeter = stepLength * cos(azimuthRad)
        val eastMeter = stepLength * sin(azimuthRad)
        
        // 지구 반경 (미터)
        val earthRadius = 6378137.0
        
        // 위도 변화량 (라디안)
        val latRad = Math.toRadians(currentLat)
        val latChange = northMeter / earthRadius
        
        // 경도 변화량 (라디안)
        val lngChange = eastMeter / (earthRadius * cos(latRad))
        
        // 라디안에서 도(degree)로 변환
        val newLat = currentLat + Math.toDegrees(latChange)
        val newLng = currentLng + Math.toDegrees(lngChange)
        
        onUpdate(newLat, newLng)
    }

한계점 및 제한사항

최초의 GPS 좌표를 기반으로 기기의 방향걸음 에 기반해 위치 좌표를 업데이트함.

  • 방향 문제: 사용자가 앞으로 걷고 있더라도, 기기가 정확히 앞 방향을 바라보지 않고, 옆이나 뒤를 바라보고 있다면 실제 걸음 방향은 옆이나 뒤를 향해 가고 있다고 인식하게 됨.
  • GPS 를 끄고 있고, 의존하는 것은 기기의 각센서와 자기장 센서 이므로 이에 대한 해결책은 사용자가 핸드폰을 똑바로 앞으로 향하도록 기대하는 것 으로 추정..
  • 걸음 문제: 사용자의 걸음 수만 인식할 수 있음. -> 실제 걸음 수에 따른 이동거리(걸음 수 * 보폭) 은 사용자마다 다를 것임.
  • 의존하는 것은 내장된 사용자의 걸음 수 센서이므로 사용자마다 적용하는 보폭 거리를 적용할 수 있도록 한다면 사용자 간 보폭 차이를 줄일 수 있음.
  • 오차 누적(Drift) 문제 :
  • 실내에서는 GPS 좌표가 적합한 위치인지, INS 로 업데이트된 위치좌표가 적합한 위치인지 파악 불가능
  • 사용자가 실내에서는 INS 시스템을 쓰도록하고, 외부에서는 GPS 를 쓰도록 사용자에게 맡겨야할 듯함.

Reference

image.png

https://www.ctman.kr/26343

⚠️ **GitHub.com Fallback** ⚠️