Skip to content

Android

Ddudduu edited this page Aug 27, 2022 · 4 revisions

Generic badge Generic badge Generic badge Generic badge Generic badge Generic badge

Aos AR-Evacuation Beacon is Android version of the application. AR-Evacuation With Beacons detects the beacons inside of K-SW Square building in the Purdue University. The application estimates user's current indoor location and inform the optimized path to each user to flee safely from fire situation.

How does it works?

BeaconApplication

BeaconApplication is an application detects nearby beacons which are installed on the ceiling or the wall. In this project, iBeacon protocol was used, setBeaconLayout was set. To detect particular beacon, the UUID was set. After the UUId was set, only the beacons with specific UUID are detected.

val beaconManager = BeaconManager.getInstanceForApplication(this)
BeaconManager.setDebug(true)
beaconManager.beaconParsers.clear()

// setBeaconLayout to find iBeacon
beaconManager.beaconParsers.add(BeaconParser().setBeaconLayout("m:2-3=0215,i:4-19,i:20-21,i:22-23,p:24-24"))

// set beacon period
beaconManager.backgroundScanPeriod = 1000
beaconManager.foregroundScanPeriod = 1000

// set UUID
region = Region("all-beacons", Identifier.parse("$beaconID"), null, null)
beaconManager.startMonitoring(region)
beaconManager.startRangingBeacons(region)
 

LocalizationManager

LocalizationManager utilizes RSSI received from the beacons as an input of DNN model and estimates user location from model output. The DNN model is downloaded from firebase. The code block below is how to get model output. Since the each cell location comes out with the probability, the highest probability cell is the estimated location.

private fun getModelOutput(rssiList: MutableList<Float>) {
      val labelNum = 31
      val input = ByteBuffer.allocateDirect(4 * rssiList.size).order(ByteOrder.nativeOrder())

      for (i in 0 until rssiList.size) {
         input.putFloat(rssiList[i])
      }
      // set buffer size
      val bufferSize = labelNum * java.lang.Float.SIZE / java.lang.Byte.SIZE
      val modelOutput = ByteBuffer.allocateDirect(bufferSize).order(ByteOrder.nativeOrder())
      
      // run DNN model
      interpreter?.run(input, modelOutput)
      modelOutput.rewind()
      val probabilities = modelOutput.asFloatBuffer()

      try {
         val probabilityArray = mutableListOf<Float>()

         for (i in 0 until probabilities.capacity()) {
            val probability = probabilities.get(i)
            probabilityArray.add(probability)
         }

         // highest probability is the estimated location
         val maxProbability = probabilityArray.maxOrNull()
         // map probability values ​​to labels 
         var mappedLabel: String = BeaconConstants.labelList[probabilityArray.indexOf(maxProbability)]
      } catch (e: IOException) {
         Log.e("$$$ Output Error $$$", e.toString())
      }

There was a limit to assume the location of the user using only the model output. So, we used the output of the model and user direction together based on the compass sensor of Android device.

private fun filterErrorWithHeading(previousLocation: Position, currentLocation: Position = Position.unknown): Position {
      if (previousLocation == Position.unknown || currentLocation == Position.unknown) return Position.unknown

      val adjacentCellList = mutableListOf<String>()
      val adjacentCells = AdjacentCell.valueOf(previousLocation.position).cell.toList()
      for (cell in adjacentCells) {
         if (cell is List<*>) {
            val list = cell.filterIsInstance<Position>()
            list.forEachIndexed { _, value ->
               adjacentCellList.add(value.position)
            }
         } else {
            adjacentCellList.add(cell.toString())
         }
      }

      val userDirection = directionRepository.userDirection.value

      // if currentLocation is adjacent to previousLocation
      if (adjacentCellList.contains(currentLocation.position)) {
         var candidateCells = adjacentCells[userDirection?.index!!]
         val newCandidateCells = mutableListOf<Position>()

         if (candidateCells is List<*>) {
            val list = candidateCells.filterIsInstance<Position>()
            list.forEach { value ->
               newCandidateCells.add(Position.valueOf(value.position))
            }
         } else {
            newCandidateCells.add(Position.valueOf(candidateCells.toString()))
         }
         newCandidateCells.removeAll { it == Position.unknown }
         // if candidate cell and direction and heading are same, use current cell
         return if (newCandidateCells.contains(currentLocation)) {          
            currentLocation
         } else {
            // if adjacent cell, direction and heading are not same, use previous cell           
            previousLocation
         }
      } else {
         // if currentLocation is not adjacent to previousLocation
         // user heading and angle between previous location and the result are same, the user moves fast
         val currentPair = locationRepository.calculateCenter(currentLocation.position)
         val currentX = currentPair.first
         val currentY = currentPair.second

         val previousPair = locationRepository.calculateCenter(previousLocation.position)
         val previousX = previousPair.first
         val previousY = previousPair.second
         val direction = directionRepository.classifyDirection(directionRepository.vectorBetween2Points(previousX!!, previousY!!, currentX!!, currentY!!))

         // user can go to candidate cell in same direction
         return if (direction == userDirection) {           
            var candidateCells = adjacentCells[direction?.index!!]

            var newCells = mutableListOf<Position>()
            if (candidateCells is List<*>) {
               val list = candidateCells.filterIsInstance<Position>()
               list.forEach { value ->
                  newCells.add(Position.valueOf(value.position))
               }
            } else {
               newCells.add(Position.valueOf(candidateCells.toString()))
            }
            newCells.removeAll { it == Position.unknown }

            if (newCells.isEmpty()) {
               previousLocation
            } else {
               newCells.first()
            }
         } else {    
            previousLocation
         }
      }
   }

DirectionRepository

DirectionRepository saves direction and heading informations. vectorBetween2Points calculates find out angle between two points using atan2 function.

fun vectorBetween2Points(previousX: Float, previousY: Float, currentX: Float, currentY: Float): Float {
      var degree: Float
      val tan = atan2(previousX - currentX, previousY - currentY) * 180 / Math.PI

      degree = if (tan < 0) {
         (-tan).toFloat() + 180f
      } else {
         180f - tan.toFloat()
      }
      return degree
   }

ArRenderable

ArRenderable renders 3d arrow object at the center of the camera angle. It extracts camera position and update the worldposition of the anchorNode every per frame.

fun onUpdateFrame(textView: TextView, frameTime: FrameTime) {
      val frame = arFragment.arSceneView.arFrame ?: return

     val position = frame.camera?.pose?.compose(Pose.makeTranslation(0f, 0f, -2f))?.extractTranslation()
         anchorNode.worldPosition = Vector3(position?.tx()!!, position.ty(), position.tz())
     ...
}

After it renders 3D arrow, it updates angle of the arrow based on direction of DirectionRepository. It supposed that user look towards the south.

 directionRepository.arrowDegree.value?.let {
            val pointDirection = directionRepository.classifyDirection(it)
            val headingDirection = directionRepository.classifyDirection(directionRepository.userCurrentHeading.value!!)
            when (pointDirection.index - headingDirection.index) {
               // forward
               0 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 180f, 0f))
               // right
               -1 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 90f, 0f))
               // left
               1 -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 270f, 0f))
               // backward
               else -> transformableNode.localRotation = Quaternion.eulerAngles(Vector3(0f, 0f, 0f))
            }         
         }
Clone this wiki locally