Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

// Add this to prevent build errors with ARCore
isCoreLibraryDesugaringEnabled = true // <--- ADD THIS
}

kotlinOptions {
Expand All @@ -48,6 +51,17 @@ android {
}

dependencies {
// --- ADD ALL OF THESE --- //
// ARCore (SLAM)
implementation("com.google.ar:core:1.44.0")
implementation("com.google.ar.sceneform:core:1.17.1")
implementation("com.google.ar.sceneform.ux:sceneform-ux:1.17.1")

// Desugaring, required for ARCore
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// --- END OF ADDITIONS --- //


// Core Android
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand All @@ -63,7 +77,7 @@ dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)

// CameraX for SLAM
// CameraX for SLAM (These are from your original file, now replaced by ARCore)
implementation("androidx.camera:camera-core:1.3.3")
implementation("androidx.camera:camera-camera2:1.3.3")
implementation("androidx.camera:camera-lifecycle:1.3.3")
Expand All @@ -87,4 +101,4 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
}
9 changes: 7 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
<uses-permission android:name="android.permission.BODY_SENSORS" />

<!-- ARCore Required -->
<uses-feature android:name="android.hardware.camera.ar" android:required="false" />
<!-- CHANGE android:required="false" to "true" -->
<uses-feature android:name="android.hardware.camera.ar" android:required="true" />
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true" />
<uses-feature android:name="android.hardware.sensor.gyroscope" android:required="true" />
<uses-feature android:name="android.hardware.camera" />
Expand All @@ -23,6 +24,10 @@
android:supportsRtl="true"
android:theme="@style/Theme.SmartNavigation">

<!-- ADD THIS META-DATA FOR ARCore -->
<meta-data
android:name="com.google.ar.core"
android:value="required" />

<activity
android:name=".MainActivity"
Expand All @@ -37,4 +42,4 @@

</application>

</manifest>
</manifest>
189 changes: 110 additions & 79 deletions app/src/main/java/com/example/smartnavigation/DeadReckoningScreen.kt
Original file line number Diff line number Diff line change
@@ -1,108 +1,97 @@
package com.example.smartnavigation


import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.font.FontWeight
import kotlin.math.*
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
import kotlin.math.sqrt

// --- PDR Constants ---
private const val STRIDE_LENGTH_METERS = 0.7f // Average stride length
private const val STEP_THRESHOLD = 10.5f // Accelerometer magnitude to trigger a step
private const val STEP_DELAY_MS = 250 // Minimum time between steps

@Composable
fun DeadReckoningScreen() {
val context = androidx.compose.ui.platform.LocalContext.current
fun DeadReckoningScreen(viewModel: SharedNavViewModel) {
val context = LocalContext.current
val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager }

val position = remember { mutableStateListOf(0f, 0f, 0f) }
val velocity = remember { mutableStateListOf(0f, 0f, 0f) }
val pathPoints = remember { mutableStateListOf<Pair<Float, Float>>() }
// Position is now only (X, Z)
val position = remember { mutableStateOf(0f to 0f) }
val pathPoints = remember { mutableStateListOf(0f to 0f) }
var isRunning by remember { mutableStateOf(false) }

val pinPosition = viewModel.pinPosition.value

val sensorListener = remember {
object : SensorEventListener {
var accelRaw = FloatArray(3)
var rotationVector = FloatArray(4)
var lastTimestamp = 0L
val alpha = 0.6f
private var rotationMatrix = FloatArray(9)
private var orientationAngles = FloatArray(3)
private var lastStepTime = 0L

override fun onSensorChanged(event: SensorEvent) {
when (event.sensor.type) {
Sensor.TYPE_LINEAR_ACCELERATION -> {
val raw = event.values
for (i in 0..2)
accelRaw[i] = alpha * accelRaw[i] + (1 - alpha) * raw[i]
if (isRunning) integrate(event.timestamp)
}
if (!isRunning) return

when (event.sensor.type) {
Sensor.TYPE_ROTATION_VECTOR -> {
if (event.values.size >= 4)
rotationVector = event.values.clone()
else {
val x = event.values[0]
val y = event.values[1]
val z = event.values[2]
rotationVector = floatArrayOf(x, y, z, sqrt(max(0f, 1f - x * x - y * y - z * z)))
SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
SensorManager.getOrientation(rotationMatrix, orientationAngles)
}
Sensor.TYPE_ACCELEROMETER -> {
val (x, y, z) = event.values
val magnitude = sqrt(x * x + y * y + z * z)

val currentTime = System.currentTimeMillis()
if (magnitude > STEP_THRESHOLD && (currentTime - lastStepTime) > STEP_DELAY_MS) {
lastStepTime = currentTime
onStepDetected()
}
}
}
}

override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

private fun integrate(ts: Long) {
if (lastTimestamp == 0L) {
lastTimestamp = ts
return
}
val dt = (ts - lastTimestamp) / 1e9f
lastTimestamp = ts
if (dt <= 0f || dt > 0.5f) return

val aWorld = deviceToWorldAccel(accelRaw, rotationVector)
for (i in 0..2) {
val ax = if (abs(aWorld[i]) < 0.02f) 0f else aWorld[i]
velocity[i] += ax * dt
position[i] += velocity[i] * dt
}
pathPoints.add(position[0] to position[1])
if (pathPoints.size > 2000) pathPoints.removeAt(0)
}

private fun deviceToWorldAccel(accel: FloatArray, q: FloatArray): FloatArray {
val qx = q[0].toDouble(); val qy = q[1].toDouble(); val qz = q[2].toDouble(); val qw = q[3].toDouble()
val vx = accel[0].toDouble(); val vy = accel[1].toDouble(); val vz = accel[2].toDouble()
private fun onStepDetected() {
// orientationAngles[0] is the azimuth (yaw) in radians
val heading = orientationAngles[0]

val ix = qw * vx + qy * vz - qz * vy
val iy = qw * vy + qz * vx - qx * vz
val iz = qw * vz + qx * vy - qy * vx
val iw = -qx * vx - qy * vy - qz * vz
// Update position based on heading and stride length
val (oldX, oldZ) = position.value
val newX = oldX + STRIDE_LENGTH_METERS * cos(heading)
val newZ = oldZ - STRIDE_LENGTH_METERS * sin(heading) // Subtract Z because screen Y is down

val wx = ix * qw - iw * qx + iy * qz - iz * qy
val wy = iy * qw - iw * qy + iz * qx - ix * qz
val wz = iz * qw - iw * qz + ix * qy - iy * qx
return floatArrayOf(wx.toFloat(), wy.toFloat(), wz.toFloat())
position.value = newX to newZ
pathPoints.add(newX to newZ)
if (pathPoints.size > 2000) pathPoints.removeAt(0)
}
}
}

DisposableEffect(isRunning) {
if (isRunning) {
sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)?.also {
sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_GAME)
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_UI)
}
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)?.also {
sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_GAME)
sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_UI)
}
} else {
sensorManager.unregisterListener(sensorListener)
Expand All @@ -117,45 +106,87 @@ fun DeadReckoningScreen() {
Button(onClick = { isRunning = true }, Modifier.weight(1f)) { Text("Start") }
Button(onClick = { isRunning = false }, Modifier.weight(1f)) { Text("Stop") }
Button(onClick = {
for (i in 0..2) { position[i] = 0f; velocity[i] = 0f }
position.value = 0f to 0f
pathPoints.clear()
pathPoints.add(0f to 0f) // Reset path to origin
}, Modifier.weight(1f)) { Text("Reset") }
}
Spacer(Modifier.height(10.dp))
Text("Position: X=${position[0]} Y=${position[1]} Z=${position[2]}")
TrajectoryCanvas(points = pathPoints.toList())
Text("DR Position: X=${"%.2f".format(position.value.first)} Z=${"%.2f".format(position.value.second)}")

if (pinPosition != null) {
Text(
"SLAM Pin: X=${"%.2f".format(pinPosition.first)} Z=${"%.2f".format(pinPosition.second)}",
color = Color(0xFF006400) // Dark Green
)
} else {
Text("No SLAM pin set. Go to SLAM screen to set one.", color = Color.Gray)
}

TrajectoryCanvas(
points = pathPoints.toList(),
pin = pinPosition
)
}
}

@Composable
fun TrajectoryCanvas(points: List<Pair<Float, Float>>) {
fun TrajectoryCanvas(points: List<Pair<Float, Float>>, pin: Pair<Float, Float>?) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(380.dp)
.padding(8.dp)
) {
if (points.isEmpty()) return@Canvas
val xs = points.map { it.first }
val ys = points.map { it.second }
val minX = xs.minOrNull() ?: 0f
val maxX = xs.maxOrNull() ?: 0f
val minY = ys.minOrNull() ?: 0f
val maxY = ys.maxOrNull() ?: 0f
val scale = min(size.width / (maxX - minX + 0.1f), size.height / (maxY - minY + 0.1f))
if (points.isEmpty() && pin == null) return@Canvas

val allPoints = points.toMutableList()
if (pin != null) {
allPoints.add(pin)
}

val xs = allPoints.map { it.first }
val ys = allPoints.map { it.second }
val minX = (xs.minOrNull() ?: -1f) - 1f
val maxX = (xs.maxOrNull() ?: 1f) + 1f
val minY = (ys.minOrNull() ?: -1f) - 1f
val maxY = (ys.maxOrNull() ?: 1f) + 1f

val rangeX = if (abs(maxX - minX) < 0.1f) 2f else (maxX - minX)
val rangeY = if (abs(maxY - minY) < 0.1f) 2f else (maxY - minY)

val scale = min(size.width / rangeX, size.height / rangeY) * 0.9f
val cx = size.width / 2f
val cy = size.height / 2f
val midX = (minX + maxX) / 2f
val midY = (minY + maxY) / 2f

// Draw the Dead Reckoning path
for (i in 0 until points.size - 1) {
val (x1, y1) = points[i]
val (x2, y2) = points[i + 1]
val (x2, y2) = points[i]
drawLine(
color = Color.Blue,
start = Offset(cx + (x1 - (minX + maxX) / 2f) * scale, cy - (y1 - (minY + maxY) / 2f) * scale),
end = Offset(cx + (x2 - (minX + maxX) / 2f) * scale, cy - (y2 - (minY + maxY) / 2f) * scale),
start = Offset(cx + (x1 - midX) * scale, cy + (y1 - midY) * scale),
end = Offset(cx + (x2 - midX) * scale, cy + (y2 - midY) * scale),
strokeWidth = 4f
)
}
val (lx, ly) = points.last()
drawCircle(Color.Red, 8f, Offset(cx + (lx - (minX + maxX) / 2f) * scale, cy - (ly - (minY + maxY) / 2f) * scale))

// Draw the current DR position
if (points.isNotEmpty()) {
val (lx, ly) = points.last()
drawCircle(Color.Red, 8f, Offset(cx + (lx - midX) * scale, cy + (ly - midY) * scale))
}

// Draw the "true" SLAM pin location
if (pin != null) {
val (px, py) = pin
val pinX = cx + (px - midX) * scale
val pinY = cy + (py - midY) * scale
drawCircle(Color(0xFF006400), 12f, Offset(pinX, pinY), style = Stroke(width = 5f))
drawLine(Color(0xFF006400), Offset(pinX - 10, pinY), Offset(pinX + 10, pinY), strokeWidth = 5f)
drawLine(Color(0xFF006400), Offset(pinX, pinY - 10), Offset(pinX, pinY + 10), strokeWidth = 5f)
}
}
}
22 changes: 16 additions & 6 deletions app/src/main/java/com/example/smartnavigation/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,38 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel // <-- ADD THIS IMPORT
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.smartnavigation.ui.theme.SmartNavigationTheme // <-- Ensure this is imported

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
// Use your project's theme
SmartNavigationTheme {
val navController = rememberNavController()

// Create the ViewModel that will be shared
val sharedViewModel: SharedNavViewModel = viewModel()

NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("dead_reckoning") { DeadReckoningScreen() }
composable("slam") { SlamScreen() }

// Pass the ViewModel to both screens
composable("dead_reckoning") { DeadReckoningScreen(sharedViewModel) }
composable("slam") { SlamScreen(sharedViewModel) }
}
}
}
}
}

@Composable
fun HomeScreen(navController: androidx.navigation.NavController) {
fun HomeScreen(navController: NavController) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
Expand All @@ -52,7 +62,7 @@ fun HomeScreen(navController: androidx.navigation.NavController) {
Button(
onClick = { navController.navigate("slam") },
modifier = Modifier.fillMaxWidth()
) { Text("SLAM (Camera)") }
) { Text("SLAM (AR Pin)") } // <-- Changed text
}
}
}
}
Loading