Skip to content
Merged
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
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,9 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Bluetooth Classic (SPP) transport to the ESP32 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

</manifest>
124 changes: 109 additions & 15 deletions app/src/main/java/com/example/trackpro/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.example.trackpro
import com.example.trackpro.extrasForUI.TrackProTheme
import android.Manifest
import android.app.Application
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
Expand Down Expand Up @@ -62,10 +63,12 @@ import androidx.navigation.navArgument
import com.example.trackpro.managerClasses.ESPDatabase
import com.example.trackpro.managerClasses.TrackSeeder
import com.example.trackpro.managerClasses.gpsDataManagers.ESPTcpClient
import com.example.trackpro.managerClasses.gpsDataManagers.BluetoothClassicClient
import com.example.trackpro.managerClasses.JsonReader
import com.example.trackpro.managerClasses.SessionManager
import com.example.trackpro.managerClasses.gpsDataManagers.GpsManager
import com.example.trackpro.managerClasses.gpsDataManagers.PhoneGpsProvider
import com.example.trackpro.models.GpsProviderType
import com.example.trackpro.screens.vehicleScreens.CarCreationScreen
import com.example.trackpro.screens.telemetricScreens.DragRaceScreen
import com.example.trackpro.screens.ESPConnectionTestScreen
Expand Down Expand Up @@ -114,11 +117,76 @@ class TrackProApp : Application() {
ESPTcpClient(serverAddress = config.first, port = config.second)
}

// Lets Settings redirect the WiFi connection to a test simulator (e.g.
// esp32_simulator.py on a dev machine) instead of the real ESP32's fixed
// AP address, without editing config.json and rebuilding. Port always
// comes from config.json (the simulator listens on the same 4210 the
// firmware does) - only the host is swappable.
private val espTargetPrefs by lazy { getSharedPreferences("esp_target_prefs", MODE_PRIVATE) }
val useTestServer by lazy { MutableStateFlow(espTargetPrefs.getBoolean("use_test_server", false)) }
val testServerAddress by lazy { MutableStateFlow(espTargetPrefs.getString("test_server_address", "") ?: "") }

fun setUseTestServer(enabled: Boolean) {
espTargetPrefs.edit().putBoolean("use_test_server", enabled).apply()
useTestServer.value = enabled
applyEspTarget()
}

fun setTestServerAddress(address: String) {
espTargetPrefs.edit().putString("test_server_address", address).apply()
testServerAddress.value = address
if (useTestServer.value) applyEspTarget()
}

private fun applyEspTarget() {
val (realIp, port) = JsonReader.loadConfig(this)
val target = if (useTestServer.value && testServerAddress.value.isNotBlank()) {
testServerAddress.value
} else {
realIp
}
espTcpClient.updateTarget(target, port)
}

val bluetoothClassicClient: BluetoothClassicClient by lazy {
BluetoothClassicClient(this)
}

val phoneGpsProvider: PhoneGpsProvider by lazy {
PhoneGpsProvider(this)
}

val useExternalGps = MutableStateFlow(true)
// Persisted like useDarkTheme/useMetricUnits below (unlike the old useExternalGps,
// which reset to WiFi every launch) — avoids surprising the user mid-track-day.
private val gpsSourcePrefs by lazy { getSharedPreferences("gps_source_prefs", MODE_PRIVATE) }
val gpsSource by lazy {
val stored = gpsSourcePrefs.getString("source", GpsProviderType.WIFI.name)
val initial = runCatching { GpsProviderType.valueOf(stored ?: GpsProviderType.WIFI.name) }
.getOrDefault(GpsProviderType.WIFI)
MutableStateFlow(initial)
}

fun setGpsSource(source: GpsProviderType) {
gpsSourcePrefs.edit().putString("source", source.name).apply()
gpsSource.value = source
}

private val ratePrefs by lazy { getSharedPreferences("gps_rate_prefs", MODE_PRIVATE) }
val selectedRateHz by lazy { MutableStateFlow(ratePrefs.getInt("rate_hz", 10)) }

fun setRateHz(hz: Int) {
ratePrefs.edit().putInt("rate_hz", hz).apply()
selectedRateHz.value = hz
gpsManager.sendCommandToActive("RATE:$hz\n")
}

private val btDevicePrefs by lazy { getSharedPreferences("bluetooth_prefs", MODE_PRIVATE) }
val selectedBtDeviceMac by lazy { MutableStateFlow(btDevicePrefs.getString("device_mac", null)) }

fun setSelectedBtDevice(mac: String) {
btDevicePrefs.edit().putString("device_mac", mac).apply()
selectedBtDeviceMac.value = mac
}

private val themePrefs by lazy { getSharedPreferences("theme_prefs", MODE_PRIVATE) }
val useDarkTheme by lazy { MutableStateFlow(themePrefs.getBoolean("dark_theme", true)) }
Expand All @@ -138,15 +206,21 @@ class TrackProApp : Application() {

val gpsManager: GpsManager by lazy {
GpsManager(
espProvider = espTcpClient,
wifiProvider = espTcpClient,
bluetoothProvider = bluetoothClassicClient,
phoneProvider = phoneGpsProvider,
useExternalGps = useExternalGps
gpsSource = gpsSource,
selectedRateHz = selectedRateHz
)
}

override fun onCreate() {
super.onCreate()
MapLibre.getInstance(this)
// Apply a persisted test-server redirect (if any) before the first
// connection attempt, so a restart doesn't briefly dial the real ESP32
// before switching over.
applyEspTarget()
// Start the active provider immediately at app launch
gpsManager.startActiveProvider()

Expand Down Expand Up @@ -176,6 +250,23 @@ class MainActivity : ComponentActivity() {
}
}

// Requested contextually (only when the user opens the Bluetooth device
// picker in Settings), unlike the eager location request above — Bluetooth
// is opt-in/rare, location is core to the app on every launch.
private val bluetoothPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (permissions[Manifest.permission.BLUETOOTH_CONNECT] != true) {
Log.w("Permissions", "Bluetooth permission denied — Bluetooth GPS source unavailable")
}
}

private fun requestBluetoothPermissionIfNeeded() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bluetoothPermissionRequest.launch(arrayOf(Manifest.permission.BLUETOOTH_CONNECT))
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -220,7 +311,7 @@ class MainActivity : ComponentActivity() {
DragRaceScreen(database, sessionManager, vehicleFULLViewModel)
}
composable("esptest") {
ESPConnectionTestScreen()
ESPConnectionTestScreen(onNavigateToSettings = { navController.navigate("settings") })
}
composable(
"track/{trackId}",
Expand Down Expand Up @@ -286,7 +377,10 @@ class MainActivity : ComponentActivity() {
)
}
composable(route = "settings") {
SettingsScreen(onBack = { navController.popBackStack() })
SettingsScreen(
onBack = { navController.popBackStack() },
onRequestBluetoothPermission = { requestBluetoothPermissionIfNeeded() }
)
}
// In your NavHost setup
composable("lap_detail/{sessionId}/{lapId}") { backStackEntry ->
Expand Down Expand Up @@ -337,7 +431,7 @@ fun MainScreen(
Box(
modifier = Modifier
.size(6.dp)
.background(TrackProTheme.colors.accentCyan, CircleShape)
.background(TrackProTheme.colors.accent, CircleShape)
)
Spacer(Modifier.width(10.dp))
Column {
Expand All @@ -361,13 +455,13 @@ fun MainScreen(
DrawerItem(
icon = Icons.Default.RocketLaunch,
label = "Drag Sessions",
tint = TrackProTheme.colors.accentCyan,
tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToDragTimesList(); scope.launch { drawerState.close() } }
)
DrawerItem(
icon = Icons.Default.FlagCircle,
label = "Track Sessions",
tint = TrackProTheme.colors.accentBlue,
tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToTimeAttackListView(); scope.launch { drawerState.close() } }
)
}
Expand All @@ -382,13 +476,13 @@ fun MainScreen(
DrawerItem(
icon = Icons.Default.Timelapse,
label = "My Tracks",
tint = TrackProTheme.colors.accentAmber,
tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToTrackListScreen(); scope.launch { drawerState.close() } }
)
DrawerItem(
icon = Icons.Default.CarRepair,
label = "My Vehicles",
tint = TrackProTheme.colors.accentAmber,
tint = TrackProTheme.colors.accentMuted,
onClick = { onNavigateToVehicleList(); scope.launch { drawerState.close() } }
)
}
Expand Down Expand Up @@ -463,7 +557,7 @@ fun MainScreen(
modifier = Modifier
.width(28.dp)
.height(2.dp)
.background(TrackProTheme.colors.accentCyan)
.background(TrackProTheme.colors.accent)
)
Spacer(Modifier.height(10.dp))
Text(
Expand Down Expand Up @@ -496,7 +590,7 @@ fun MainScreen(
icon = Icons.Default.RocketLaunch,
title = "DRAG TIMING",
subtitle = "0–100 · ¼ mile · speed trace",
accentColor = TrackProTheme.colors.accentCyan,
accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToDragRace,
fullWidth = true
)
Expand All @@ -505,7 +599,7 @@ fun MainScreen(
icon = Icons.Default.FlagCircle,
title = "LAP TIMING",
subtitle = "Circuit & sprint · live delta · best lap",
accentColor = TrackProTheme.colors.accentBlue,
accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToTrackVehicleSelector,
fullWidth = true
)
Expand All @@ -526,7 +620,7 @@ fun MainScreen(
icon = Icons.Default.CarRepair,
title = "ADD VEHICLES",
subtitle = "Create your own vehicles",
accentColor = TrackProTheme.colors.accentAmber,
accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToVehicleCreatorScreen,
halfWidth = true
)
Expand All @@ -536,7 +630,7 @@ fun MainScreen(
icon = Icons.Default.Timelapse,
title = "TRACK\nBUILDER",
subtitle = "Define tracks",
accentColor = TrackProTheme.colors.accentAmber,
accentColor = TrackProTheme.colors.accentMuted,
onClick = onNavigateToTrackBuilder,
halfWidth = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data class RawGPSData(
val altitude: Double?,
val timestamp: Long,
val speed: Float?,
val fixQuality: Int?
val fixQuality: Int?,
val valid: Boolean? = null
)

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fun CustomTextField(
value: String,
isNumber: Boolean = false,
leadingIcon: ImageVector? = null,
accent: Color = TrackProTheme.colors.accentCyan,
accent: Color = TrackProTheme.colors.accent,
onValueChange: (String) -> Unit
) {
OutlinedTextField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fun <T> AppDropdownField(
itemLabel: (T) -> String,
onSelect: (T) -> Unit,
modifier: Modifier = Modifier,
accent: Color = TrackProTheme.colors.accentCyan,
accent: Color = TrackProTheme.colors.accent,
emptyMessage: String = "No options available"
) {
var expanded by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -103,7 +103,7 @@ fun DropdownMenuFieldMulti(label: String, options: List<VehiclePair>, selectedOp
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = TrackProTheme.colors.textPrimary,
unfocusedTextColor = TrackProTheme.colors.textPrimary,
focusedBorderColor = TrackProTheme.colors.accentCyan,
focusedBorderColor = TrackProTheme.colors.accent,
unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier
Expand Down Expand Up @@ -151,7 +151,7 @@ fun TrackDropdownMenu(
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = TrackProTheme.colors.textPrimary,
unfocusedTextColor = TrackProTheme.colors.textPrimary,
focusedBorderColor = TrackProTheme.colors.accentCyan,
focusedBorderColor = TrackProTheme.colors.accent,
unfocusedBorderColor = TrackProTheme.colors.sectorLine
),
modifier = Modifier.fillMaxWidth()
Expand Down
29 changes: 16 additions & 13 deletions app/src/main/java/com/example/trackpro/extrasForUI/TrackProTheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ object TrackProTheme {
private fun TrackProColorScheme.toMaterialColorScheme(dark: Boolean): androidx.compose.material3.ColorScheme =
if (dark) {
darkColorScheme(
primary = accentCyan,
onPrimary = Color.Black,
secondary = accentBlue,
onSecondary = Color.Black,
tertiary = accentAmber,
onTertiary = Color.Black,
// primary/secondary/tertiary intentionally all map to the same accent - the
// palette has exactly one. Material needs three slots filled; that is not a
// reason to invent two more brand colors.
primary = accent,
onPrimary = onAccent,
secondary = accent,
onSecondary = onAccent,
tertiary = accent,
onTertiary = onAccent,
background = bgDeep,
onBackground = textPrimary,
surface = bgCard,
Expand All @@ -46,16 +49,16 @@ private fun TrackProColorScheme.toMaterialColorScheme(dark: Boolean): androidx.c
onSurfaceVariant = textMuted,
outline = sectorLine,
error = deltaBad,
onError = Color.White
onError = Color.Black
)
} else {
lightColorScheme(
primary = accentCyan,
onPrimary = Color.Black,
secondary = accentBlue,
onSecondary = Color.Black,
tertiary = accentAmber,
onTertiary = Color.Black,
primary = accent,
onPrimary = onAccent,
secondary = accent,
onSecondary = onAccent,
tertiary = accent,
onTertiary = onAccent,
background = bgDeep,
onBackground = textPrimary,
surface = bgCard,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import com.example.trackpro.dataClasses.VehicleInformationData
LapTimeData::class,
LapInfoData::class,
SectorTimeData::class
], version = 3, exportSchema = false)
], version = 4, exportSchema = false)
abstract class ESPDatabase : RoomDatabase() {
abstract fun sessionDataDao(): SessionDataDao
abstract fun rawGPSDataDao(): RawGPSDataDao
Expand Down
Loading
Loading