Navigation¶
Overview¶
The Navigation feature is the core of the MINE Indoor Navigation Engine, providing real-time, turn-by-turn guidance for users navigating complex indoor environments. Using advanced positioning technologies including Bluetooth beacons, Wi-Fi RTT, and sensor fusion, MINE delivers accurate location tracking and intelligent route guidance.
Key Features
- π Real-time Positioning: Sub-meter accuracy with multiple positioning systems
- π§ Turn-by-Turn Guidance: Visual and audio navigation instructions
- π Automatic Rerouting: Smart path recalculation when users deviate
- πΆ Step Tracking: Pedometer-based distance calculation
- π’ Multi-Floor Navigation: Seamless transitions between building levels
- βΏ Accessibility Support: Accessible route options and guidance
Navigation Architecture¶
-
Bluetooth Beacons
Primary positioning using BLE beacon trilateration for 1-3m accuracy
-
Wi-Fi RTT
IEEE 802.11mc Round-Trip-Time for 1-2m accuracy without beacons
-
Sensor Fusion
IMU sensors (accelerometer, gyroscope, magnetometer) for dead reckoning
-
Hybrid Positioning
Combines multiple sources using Kalman filtering for optimal accuracy
Positioning Technologies¶
Bluetooth Beacon Positioning¶
MINE uses trilateration to calculate user position based on signal strength (RSSI) from multiple Bluetooth Low Energy (BLE) beacons.
How Trilateration Works¶
Trilateration determines position by measuring distances from three or more known reference points (beacons):
Distance = 10 ^ ((Measured Power - RSSI) / (10 * Path Loss Exponent))
Trilateration Explained
When you know the distance to three beacons, each distance creates a circle around that beacon. The point where all three circles intersect is your location. Learn more about Trilateration on Wikipedia.
Beacon Configuration¶
1. Beacon JSON Format
{
"beacons": [
{
"id": "beacon_001",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": 100,
"minor": 1,
"position": {
"x": 10.5,
"y": 20.3,
"floor": 1
},
"measuredPower": -59,
"pathLossExponent": 2.0,
"metadata": {
"name": "Entrance Beacon",
"installDate": "2024-01-15",
"batteryLevel": 85
}
},
{
"id": "beacon_002",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": 100,
"minor": 2,
"position": {
"x": 50.0,
"y": 20.3,
"floor": 1
},
"measuredPower": -59,
"pathLossExponent": 2.0,
"metadata": {
"name": "Center Beacon"
}
},
{
"id": "beacon_003",
"uuid": "f7826da6-4fa2-4e98-8024-bc5b71e0893e",
"major": 100,
"minor": 3,
"position": {
"x": 30.0,
"y": 60.5,
"floor": 1
},
"measuredPower": -59,
"pathLossExponent": 2.0,
"metadata": {
"name": "Back Beacon"
}
}
],
"config": {
"scanInterval": 1000,
"scanWindow": 500,
"rssiThreshold": -100,
"minBeacons": 3,
"kalmanFilterEnabled": true
}
}
2. Beacon Placement Guidelines
Best Practices for Beacon Deployment
Coverage Requirements:
- β Place beacons 8-12 meters apart for optimal coverage
- β Ensure at least 3 beacons are visible from any location
- β Mount beacons at 2-3 meters height for best signal propagation
- β Avoid placing beacons near metal objects or dense walls
- β Use more beacons in areas with many obstacles
Layout Pattern:
Recommended Triangle/Grid Pattern:
B1 -------- B2 -------- B3
| | |
| [Coverage Area] |
| | |
B4 -------- B5 -------- B6
3. Loading Beacon Configuration
import com.machinestalk.indoornavigationengine.resources.BeaconConfigLoader
import com.machinestalk.indoornavigationengine.components.BeaconPositioningSystem
class NavigationActivity : AppCompatActivity() {
private lateinit var beaconSystem: BeaconPositioningSystem
private lateinit var navigationManager: NavigationManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
setupBeaconPositioning()
}
private fun setupBeaconPositioning() {
// Load beacon configuration
val beaconConfig = BeaconConfigLoader.loadFromAssets(
context = this,
path = "beacons/venue_beacons.json"
)
// Initialize beacon positioning system
beaconSystem = BeaconPositioningSystem(
context = this,
config = beaconConfig,
options = PositioningOptions(
scanInterval = 1000L,
rssiThreshold = -100,
minBeaconsRequired = 3,
useKalmanFilter = true,
pathLossExponent = 2.0
)
)
// Start scanning for beacons
beaconSystem.startScanning()
// Listen for position updates
beaconSystem.onPositionUpdate = { position ->
updateUserLocation(position)
}
}
}
Beacon Calibration¶
// Calibrate beacon RSSI for environment
private suspend fun calibrateBeacons() {
val calibrator = BeaconCalibrator(beaconSystem)
// Collect samples at known distances
val calibrationPoints = listOf(
CalibrationPoint(distance = 1.0, location = Location(10.0, 20.0)),
CalibrationPoint(distance = 3.0, location = Location(13.0, 20.0)),
CalibrationPoint(distance = 5.0, location = Location(15.0, 20.0))
)
val results = calibrator.calibrate(
points = calibrationPoints,
samplesPerPoint = 100
)
// Update beacon parameters
results.forEach { (beaconId, params) ->
beaconSystem.updateBeaconParameters(
beaconId = beaconId,
measuredPower = params.measuredPower,
pathLossExponent = params.pathLossExponent
)
}
// Save calibrated config
beaconSystem.saveConfiguration()
}
Wi-Fi RTT Positioning¶
Wi-Fi Round-Trip-Time (IEEE 802.11mc) provides accurate indoor positioning without additional hardware:
import android.net.wifi.rtt.RangingRequest
import android.net.wifi.rtt.RangingResult
import android.net.wifi.rtt.WifiRttManager
class WiFiRTTPositioning(private val context: Context) {
private val rttManager = context.getSystemService(WifiRttManager::class.java)
fun startRTTPositioning() {
// Check if RTT is supported
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_RTT)) {
Log.e("WiFiRTT", "WiFi RTT not supported on this device")
return
}
lifecycleScope.launch {
// Get RTT-capable access points
val accessPoints = getWiFiRTTAccessPoints()
if (accessPoints.size < 3) {
Log.w("WiFiRTT", "Not enough RTT APs available")
return@launch
}
// Create ranging request
val request = RangingRequest.Builder()
.addAccessPoints(accessPoints)
.build()
// Start ranging
rttManager.startRanging(
request,
context.mainExecutor,
object : RangingResultCallback() {
override fun onRangingResults(results: List<RangingResult>) {
processRangingResults(results)
}
override fun onRangingFailure(code: Int) {
Log.e("WiFiRTT", "Ranging failed: $code")
}
}
)
}
}
private fun processRangingResults(results: List<RangingResult>) {
val distances = results.mapNotNull { result ->
if (result.status == RangingResult.STATUS_SUCCESS) {
AccessPointDistance(
macAddress = result.macAddress,
distanceMeters = result.distanceMm / 1000.0,
accuracy = result.distanceStdDevMm / 1000.0
)
} else null
}
// Calculate position using trilateration
val position = calculatePositionFromDistances(distances)
updateUserLocation(position)
}
}
Sensor Fusion (PDR)¶
Pedestrian Dead Reckoning (PDR) uses device sensors to track movement:
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
class SensorFusionPositioning(private val context: Context) : SensorEventListener {
private val sensorManager = context.getSystemService(SensorManager::class.java)
private var currentPosition = Position(0.0, 0.0, 0.0)
private var currentHeading = 0.0
private var stepCount = 0
private val stepLength = 0.7 // meters, can be calibrated
fun startTracking() {
// Register sensors
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
}
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
}
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
}
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR)?.let {
sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME)
}
}
override fun onSensorChanged(event: SensorEvent) {
when (event.sensor.type) {
Sensor.TYPE_STEP_DETECTOR -> {
onStepDetected()
}
Sensor.TYPE_MAGNETIC_FIELD -> {
updateHeading(event.values)
}
Sensor.TYPE_ACCELEROMETER -> {
processAccelerometer(event.values)
}
}
}
private fun onStepDetected() {
stepCount++
// Update position based on heading and step length
val dx = stepLength * sin(Math.toRadians(currentHeading))
val dy = stepLength * cos(Math.toRadians(currentHeading))
currentPosition = Position(
x = currentPosition.x + dx,
y = currentPosition.y + dy,
z = currentPosition.z
)
notifyPositionUpdate(currentPosition)
}
private fun updateHeading(magneticField: FloatArray) {
// Calculate device orientation
// Combine with accelerometer for accurate heading
currentHeading = calculateHeading(magneticField)
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
// Handle accuracy changes
}
fun stopTracking() {
sensorManager.unregisterListener(this)
}
}
Navigation Implementation¶
Initialize Navigation¶
import com.machinestalk.indoornavigationengine.components.NavigationManager
import com.machinestalk.indoornavigationengine.models.Route
import com.machinestalk.indoornavigationengine.models.NavigationConfig
class NavigationActivity : AppCompatActivity() {
private lateinit var navigationManager: NavigationManager
private lateinit var sceneView: MineSceneView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_navigation)
sceneView = findViewById(R.id.scene_view)
// Initialize navigation manager
navigationManager = NavigationManager(
context = this,
config = NavigationConfig(
positioningMode = PositioningMode.BLUETOOTH_BEACONS,
enableVoiceGuidance = true,
enableHapticFeedback = true,
reroutingEnabled = true,
offTrackThreshold = 5.0, // meters
instructionDistance = 10.0 // meters before turn
)
)
// Set up positioning
setupPositioning()
// Start navigation
startNavigation()
}
private fun setupPositioning() {
// Use beacon positioning
val beaconSystem = BeaconPositioningSystem(this)
beaconSystem.loadConfiguration("beacons/venue_beacons.json")
navigationManager.setPositioningProvider(beaconSystem)
// Add sensor fusion for improved accuracy
val sensorFusion = SensorFusionPositioning(this)
navigationManager.addSecondaryPositioningProvider(sensorFusion)
// Listen for position updates
navigationManager.onPositionUpdate = { position ->
updateUserMarker(position)
checkNavigationProgress(position)
}
}
}
Calculate and Start Navigation¶
private fun startNavigation() {
lifecycleScope.launch {
try {
// Get user's current location
val currentLocation = navigationManager.getCurrentLocation()
if (currentLocation == null) {
showError("Unable to determine your location")
return@launch
}
// Calculate route to destination
val destination = getSelectedDestination()
val routeRequest = RouteRequest(
origin = currentLocation,
destination = destination,
preferences = RoutePreferences(
avoidStairs = false,
preferElevators = false,
optimizeFor = OptimizationCriteria.SHORTEST
)
)
val route = navigationManager.calculateRoute(routeRequest)
// Display route on map
sceneView.displayRoute(route)
// Start turn-by-turn navigation
navigationManager.startNavigation(route)
// Listen for navigation events
observeNavigationEvents()
} catch (e: NavigationException) {
handleNavigationError(e)
}
}
}
private fun observeNavigationEvents() {
navigationManager.navigationEvents.collect { event ->
when (event) {
is NavigationEvent.InstructionUpdate -> {
displayInstruction(event.instruction)
if (event.playAudio) {
speakInstruction(event.instruction.text)
}
}
is NavigationEvent.OffRoute -> {
showOffRouteWarning()
if (navigationManager.config.reroutingEnabled) {
recalculateRoute()
}
}
is NavigationEvent.RouteProgress -> {
updateProgressUI(
distanceRemaining = event.distanceRemaining,
timeRemaining = event.estimatedTimeRemaining
)
}
is NavigationEvent.FloorChange -> {
showFloorChangeInstruction(
fromFloor = event.fromFloor,
toFloor = event.toFloor,
transitionType = event.transitionType // stairs, elevator, escalator
)
// Switch map view to new floor
sceneView.switchToFloor(event.toFloor)
}
is NavigationEvent.DestinationReached -> {
showArrivalDialog()
stopNavigation()
}
is NavigationEvent.PositionAccuracyChanged -> {
updateAccuracyIndicator(event.accuracy)
}
}
}
}
Turn-by-Turn Instructions¶
data class NavigationInstruction(
val type: InstructionType,
val text: String,
val distance: Double,
val direction: Direction,
val nextInstruction: NavigationInstruction? = null
)
enum class InstructionType {
START,
TURN_LEFT,
TURN_RIGHT,
CONTINUE_STRAIGHT,
SLIGHT_LEFT,
SLIGHT_RIGHT,
SHARP_LEFT,
SHARP_RIGHT,
USE_STAIRS_UP,
USE_STAIRS_DOWN,
USE_ELEVATOR,
USE_ESCALATOR,
ARRIVE
}
private fun displayInstruction(instruction: NavigationInstruction) {
// Update UI
binding.instructionText.text = instruction.text
binding.instructionIcon.setImageResource(
getInstructionIcon(instruction.type)
)
// Show distance
binding.distanceText.text = when {
instruction.distance < 10 -> "In ${instruction.distance.toInt()} meters"
instruction.distance < 100 -> "In ${(instruction.distance / 10).toInt() * 10} meters"
else -> "In ${(instruction.distance / 100).toInt() * 100} meters"
}
// Highlight next turn on map
sceneView.highlightNextTurn(instruction)
}
private fun speakInstruction(text: String) {
textToSpeech.speak(
text,
TextToSpeech.QUEUE_FLUSH,
null,
"NAVIGATION_INSTRUCTION"
)
}
User Location Tracking¶
private fun updateUserMarker(position: Position) {
// Update user marker on map
sceneView.updateUserMarker(
position = position,
heading = position.heading,
accuracy = position.accuracy
)
// Update camera to follow user
if (binding.followUserButton.isChecked) {
sceneView.animateCameraToPosition(
position = position,
duration = 300,
smoothing = true
)
}
}
// User marker configuration
sceneView.configureUserMarker(
markerStyle = UserMarkerStyle(
icon = R.drawable.ic_user_location,
size = 24.dp,
showAccuracyCircle = true,
accuracyCircleColor = Color(0x330000FF),
showHeadingIndicator = true,
headingIndicatorColor = Color(0xFF0000FF)
)
)
Advanced Features¶
Automatic Rerouting¶
private suspend fun recalculateRoute() {
try {
showReroutingIndicator()
val currentLocation = navigationManager.getCurrentLocation()!!
val remainingRoute = navigationManager.getRemainingRoute()
// Calculate new route to destination
val newRoute = navigationManager.calculateRoute(
RouteRequest(
origin = currentLocation,
destination = remainingRoute.destination,
preferences = navigationManager.routePreferences
)
)
// Update navigation with new route
navigationManager.updateRoute(newRoute)
sceneView.displayRoute(newRoute)
hideReroutingIndicator()
showToast("Route recalculated")
} catch (e: Exception) {
Log.e("Navigation", "Rerouting failed", e)
showError("Unable to recalculate route")
}
}
Multi-Floor Navigation¶
private fun handleFloorTransition(event: NavigationEvent.FloorChange) {
when (event.transitionType) {
TransitionType.ELEVATOR -> {
showElevatorInstructions(
fromFloor = event.fromFloor,
toFloor = event.toFloor
)
}
TransitionType.STAIRS -> {
val direction = if (event.toFloor > event.fromFloor) "up" else "down"
speakInstruction("Take the stairs $direction to floor ${event.toFloor}")
}
TransitionType.ESCALATOR -> {
speakInstruction("Take the escalator to floor ${event.toFloor}")
}
}
// Wait for user to change floors
waitForFloorChange(event.toFloor) {
speakInstruction("Floor change detected. Continuing navigation.")
}
}
private fun waitForFloorChange(targetFloor: Int, onFloorChanged: () -> Unit) {
var floorDetected = false
val floorChangeListener = object : FloorChangeListener {
override fun onFloorDetected(floor: Int) {
if (floor == targetFloor && !floorDetected) {
floorDetected = true
onFloorChanged()
navigationManager.removeFloorChangeListener(this)
}
}
}
navigationManager.addFloorChangeListener(floorChangeListener)
}
Step-by-Step Progress Tracking¶
class StepTrackingNavigationManager : NavigationManager() {
private var totalSteps = 0
private var routeSteps = listOf<RouteStep>()
fun startNavigationWithSteps(route: Route) {
// Break route into steps
routeSteps = generateRouteSteps(route)
totalSteps = routeSteps.size
startNavigation(route)
}
private fun generateRouteSteps(route: Route): List<RouteStep> {
val steps = mutableListOf<RouteStep>()
route.segments.forEach { segment ->
when (segment) {
is StraightSegment -> {
steps.add(RouteStep(
instruction = "Continue straight for ${segment.distance}m",
distance = segment.distance,
type = StepType.CONTINUE
))
}
is TurnSegment -> {
steps.add(RouteStep(
instruction = "Turn ${segment.direction} at ${segment.landmark}",
distance = 0.0,
type = StepType.TURN
))
}
is FloorChangeSegment -> {
steps.add(RouteStep(
instruction = "Use ${segment.transitionMethod} to floor ${segment.toFloor}",
distance = 0.0,
type = StepType.FLOOR_CHANGE
))
}
}
}
return steps
}
fun getCurrentStep(): Int {
val currentPosition = getCurrentLocation() ?: return 0
// Find which step user is on based on position
return routeSteps.indexOfFirst { step ->
step.isUserAtStep(currentPosition)
}
}
fun getProgressPercentage(): Float {
val currentStep = getCurrentStep()
return (currentStep.toFloat() / totalSteps) * 100
}
}
Voice Guidance¶
Text-to-Speech Integration¶
class VoiceGuidanceManager(private val context: Context) {
private val tts = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
tts.language = Locale.getDefault()
tts.setSpeechRate(1.0f)
}
}
fun speak(instruction: String, urgent: Boolean = false) {
val queueMode = if (urgent) {
TextToSpeech.QUEUE_FLUSH
} else {
TextToSpeech.QUEUE_ADD
}
tts.speak(instruction, queueMode, null, UUID.randomUUID().toString())
}
fun speakWithDistance(instruction: String, distance: Double) {
val distanceText = formatDistance(distance)
speak("In $distanceText, $instruction")
}
private fun formatDistance(meters: Double): String {
return when {
meters < 10 -> "${meters.toInt()} meters"
meters < 100 -> "${(meters / 10).toInt() * 10} meters"
meters < 1000 -> "${(meters / 100).toInt() * 100} meters"
else -> "${(meters / 1000).roundToInt()} kilometers"
}
}
fun shutdown() {
tts.stop()
tts.shutdown()
}
}
Best Practices¶
Recommended Practices
1. Beacon Deployment
β
Use at least 3 beacons for accurate positioning
β
Mount beacons at consistent heights (2-3m)
β
Test signal coverage before production deployment
β
Calibrate beacons for your specific environment
β
Monitor beacon battery levels regularly
2. User Experience
// Provide clear feedback about positioning accuracy
when (accuracy) {
Accuracy.HIGH -> showGreenIndicator()
Accuracy.MEDIUM -> showYellowIndicator()
Accuracy.LOW -> showRedIndicator()
}
// Give advance notice of turns
val noticeDistance = calculateNoticeDistance(walkingSpeed)
announceInstructionAt(noticeDistance)
3. Error Handling
// Always handle positioning failures gracefully
navigationManager.onPositioningLost = {
showWarning("GPS signal lost. Using last known location.")
switchToDeadReckoning()
}
4. Battery Optimization
// Adjust scanning based on navigation state
when (navigationState) {
NavigationState.ACTIVE -> {
beaconSystem.setScanInterval(1000L) // Frequent
}
NavigationState.PAUSED -> {
beaconSystem.setScanInterval(5000L) // Less frequent
}
NavigationState.IDLE -> {
beaconSystem.stopScanning() // Save battery
}
}
Common Pitfalls
- Don't rely on beacon positioning alone in areas with interference
- Don't forget to handle permission requests for location and Bluetooth
- Always provide fallback positioning methods
- Always test navigation in actual venue conditions
- Consider implementing offline map support for poor connectivity
Troubleshooting¶
Inaccurate Positioning¶
Problem: User location jumps or is inaccurate
Solutions:
-
Check Beacon Coverage
// Verify beacon visibility fun checkBeaconCoverage(location: Location): Int { val visibleBeacons = beaconSystem.getVisibleBeacons() if (visibleBeacons.size < 3) { Log.w("Positioning", "Insufficient beacons: ${visibleBeacons.size}") showWarning("Position accuracy may be reduced") } return visibleBeacons.size } -
Calibrate Path Loss Exponent
// Adjust for environment beaconSystem.setPathLossExponent( when (environmentType) { EnvironmentType.OPEN_SPACE -> 2.0 EnvironmentType.OFFICE -> 2.5 EnvironmentType.DENSE_OBSTACLES -> 3.0 else -> 2.0 } ) -
Enable Kalman Filtering
// Smooth position updates navigationManager.enableKalmanFilter( processNoise = 0.1, measurementNoise = 1.0 )
Navigation Not Starting¶
Problem: Navigation fails to start
Diagnostic Steps:
fun diagnoseNavigationIssues(): DiagnosticReport {
val report = DiagnosticReport()
// Check location permissions
report.hasLocationPermission = checkLocationPermission()
// Check Bluetooth
report.isBluetoothEnabled = checkBluetoothEnabled()
// Check beacon availability
report.visibleBeacons = beaconSystem.getVisibleBeacons().size
// Check map loaded
report.isMapLoaded = sceneView.isMapLoaded()
// Check positioning provider
report.isPositioningActive = navigationManager.isPositioningActive()
return report
}
Navigation Frequently Rerouting¶
Problem: Navigation constantly recalculates route
Solutions:
// Increase off-track threshold
navigationManager.config = navigationManager.config.copy(
offTrackThreshold = 8.0, // Increase from 5.0 to 8.0 meters
reroutingDelay = 5000L // Wait 5 seconds before rerouting
)
// Improve positioning accuracy
beaconSystem.apply {
setScanInterval(500L) // More frequent scanning
setRSSISmoothing(true) // Smooth RSSI values
}
Performance Metrics¶
| Metric | Target | Notes |
|---|---|---|
| Position Update Rate | 1-2 Hz | Updates per second |
| Position Accuracy | < 3 meters | With 3+ beacons |
| Route Calculation | < 500ms | For typical venue |
| Rerouting Time | < 300ms | When off-track |
| Battery Drain | < 5% per hour | During active navigation |
| Memory Usage | < 100 MB | Including map data |
Related Documentation¶
- Path Finding - Route calculation algorithms
- Map Loading - Loading venue maps
- UI Components - Navigation UI widgets
- API Reference - Complete API docs
Next Steps¶
Now that you understand navigation implementation:
- Implement Path Finding - Set up route calculation
- Add UI Components - Build navigation interface
- Customize Themes - Brand your navigation experience