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
1 change: 1 addition & 0 deletions dashpilot-android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.navigation.toRoute
import androidx.startup.AppInitializer
import app.rive.runtime.kotlin.RiveInitializer
import com.softwiredtech.dashpilot.ble.FirmwareUpdateManager
import com.softwiredtech.dashpilot.ble.TeslaStatusSource
import com.softwiredtech.dashpilot.datamodel.dash.DashboardType
import com.softwiredtech.dashpilot.datamodel.dash.ManifestLoader
import com.softwiredtech.dashpilot.datamodel.dash.availableDashboards
Expand All @@ -48,6 +49,7 @@ import com.softwiredtech.dashpilot.navigation.DashboardRoute
import com.softwiredtech.dashpilot.navigation.OnboardingRoute
import com.softwiredtech.dashpilot.navigation.SettingsRoute
import com.softwiredtech.dashpilot.navigation.SetupRoute
import com.softwiredtech.dashpilot.navigation.TeslaEnrollRoute
import com.softwiredtech.dashpilot.navigation.ThemePickerRoute
import com.softwiredtech.dashpilot.ui.AutomationsScreen
import com.softwiredtech.dashpilot.ui.ControlScreen
Expand All @@ -57,6 +59,7 @@ import com.softwiredtech.dashpilot.ui.LOCAL_ASSET_BASE_URL
import com.softwiredtech.dashpilot.ui.SettingsScreen
import com.softwiredtech.dashpilot.ui.ThemePickerScreen
import com.softwiredtech.dashpilot.ui.onboarding.OnboardingScreen
import com.softwiredtech.dashpilot.ui.tesla.TeslaEnrollFlow
import com.softwiredtech.dashpilot.ui.theme.DashPilotTheme
import com.softwiredtech.dashpilot.util.NetworkUtil
import com.softwiredtech.dashpilot.viewmodel.ConnectionViewModel
Expand Down Expand Up @@ -174,6 +177,13 @@ class MainActivity : ComponentActivity() {
onDispose { dashkitUpdateManager?.dispose() }
}

// Phase 4: Tesla status/command channel rides the same bond.
val teslaSource = remember(bleManager) { bleManager?.let { TeslaStatusSource(it) } }
val teslaStatus = teslaSource?.status
val teslaResetPending = teslaSource?.resetPending
LaunchedEffect(teslaSource) { teslaSource?.start() }
DisposableEffect(teslaSource) { onDispose { teslaSource?.stop() } }

LaunchedEffect(startupTarget) {
when (startupTarget.route) {
ConnectionViewModel.StartupRoute.ONBOARDING_DASHKIT -> {
Expand Down Expand Up @@ -247,6 +257,9 @@ class MainActivity : ComponentActivity() {
bleManager = manager,
dashState = dashStateFlow,
pinnedControlId = pinnedControl,
teslaStatus = teslaStatus,
teslaResetPending = teslaResetPending,
onEnrollTesla = { navController.navigate(TeslaEnrollRoute) },
onConnect = { serverAddress, dataSourceType ->
connectionVM.connect(
context, serverAddress, dataSourceType,
Expand Down Expand Up @@ -312,6 +325,10 @@ class MainActivity : ComponentActivity() {
onDisplaySettingsChanged = { connectionVM.updateDisplaySettings(it) },
bleManager = manager,
dashkitUpdateManager = dashkitUpdateManager,
teslaStatus = teslaStatus,
teslaResetPending = teslaResetPending,
onRemoveTesla = { teslaSource?.requestReset() == true },
onEnrollTesla = { navController.navigate(TeslaEnrollRoute) },
onReplayOnboarding = {
connectionVM.disconnect()
setOnboardingCompleted(context, false)
Expand All @@ -324,6 +341,15 @@ class MainActivity : ComponentActivity() {
}
)
}
composable<TeslaEnrollRoute> {
val manager by connectionVM.bleManager.collectAsState()
TeslaEnrollFlow(
manager = manager,
statusFlow = teslaStatus,
vinState = connectionVM.vehicleVin,
onClose = { navController.popBackStack() },
)
}
composable<ThemePickerRoute> {
ThemePickerScreen(
onBack = { navController.popBackStack(ThemePickerRoute, inclusive = true) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ object SetupRoute
@Serializable
object SettingsRoute

@Serializable
object TeslaEnrollRoute

@Serializable
object ThemePickerRoute

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
Expand All @@ -42,16 +43,20 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.softwiredtech.dashpilot.ble.TeslaStatus
import com.softwiredtech.dashpilot.datamodel.dash.CarState
import com.softwiredtech.dashpilot.datamodel.dash.DashState
import com.softwiredtech.dashpilot.datasource.ConnectionStatus
import com.softwiredtech.dashpilot.datasource.DashKitBleManager
import com.softwiredtech.dashpilot.datasource.DataSourceType
import com.softwiredtech.dashpilot.ui.controls.ControlActionButton
import com.softwiredtech.dashpilot.ui.controls.controlById
import com.softwiredtech.dashpilot.ui.tesla.TeslaTile
import com.softwiredtech.dashpilot.ui.theme.AccentColor
import com.softwiredtech.dashpilot.ui.theme.DarkColors
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOf
import kotlin.math.roundToInt

Expand All @@ -66,6 +71,9 @@ fun HomeScreen(
bleManager: DashKitBleManager?,
dashState: Flow<DashState>?,
pinnedControlId: String?,
teslaStatus: StateFlow<TeslaStatus>?,
teslaResetPending: StateFlow<Boolean>?,
onEnrollTesla: () -> Unit,
onConnect: (serverAddress: String, dataSourceType: String) -> Unit,
onDisconnect: () -> Unit,
onNext: () -> Unit,
Expand All @@ -87,6 +95,9 @@ fun HomeScreen(
bleManager = bleManager,
connectionStatus = connectionStatus,
pinnedControlId = pinnedControlId,
teslaStatus = teslaStatus,
teslaResetPending = teslaResetPending,
onEnrollTesla = onEnrollTesla,
onConnect = onConnect,
onDisconnect = onDisconnect,
onSelectDataSource = { selectedDataSource = it },
Expand Down Expand Up @@ -115,6 +126,9 @@ private fun ConnectedHomeContent(
bleManager: DashKitBleManager?,
connectionStatus: ConnectionStatus,
pinnedControlId: String?,
teslaStatus: StateFlow<TeslaStatus>?,
teslaResetPending: StateFlow<Boolean>?,
onEnrollTesla: () -> Unit,
onConnect: (serverAddress: String, dataSourceType: String) -> Unit,
onDisconnect: () -> Unit,
onSelectDataSource: (String) -> Unit,
Expand All @@ -127,6 +141,10 @@ private fun ConnectedHomeContent(
val state by (dashState ?: flowOf(fallback)).collectAsState(initial = fallback)
val car = state.carState
val useImperial = state.displaySettings.useImperial
val idleTesla = remember { MutableStateFlow(TeslaStatus.Idle) }
val tesla by (teslaStatus ?: idleTesla).collectAsState()
val idleReset = remember { MutableStateFlow(false) }
val resetPending by (teslaResetPending ?: idleReset).collectAsState()

Column(
modifier = Modifier
Expand Down Expand Up @@ -199,6 +217,8 @@ private fun ConnectedHomeContent(

Spacer(modifier = Modifier.height(32.dp))

TeslaTile(status = tesla, resetPending = resetPending, onEnroll = onEnrollTesla)

controlById(pinnedControlId)?.let { action ->
Text(
text = "Pinned",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ import com.softwiredtech.dashpilot.datasource.ConnectionStatus
import com.softwiredtech.dashpilot.datasource.DashKitBleManager
import com.softwiredtech.dashpilot.ble.FirmwareUpdateManager
import com.softwiredtech.dashpilot.ble.OtaState
import com.softwiredtech.dashpilot.ble.TeslaLinkState
import com.softwiredtech.dashpilot.ble.TeslaStatus
import com.softwiredtech.dashpilot.ble.VehicleControl
import com.softwiredtech.dashpilot.ui.tesla.teslaTileSummary
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

import com.softwiredtech.dashpilot.BuildConfig
Expand Down Expand Up @@ -120,6 +125,10 @@ fun SettingsScreen(
onDisplaySettingsChanged: (DisplaySettings) -> Unit = {},
bleManager: DashKitBleManager? = null,
dashkitUpdateManager: FirmwareUpdateManager? = null,
teslaStatus: StateFlow<TeslaStatus>? = null,
teslaResetPending: StateFlow<Boolean>? = null,
onRemoveTesla: () -> Boolean = { false },
onEnrollTesla: () -> Unit = {},
onReplayOnboarding: () -> Unit = {},
onThemeClick: () -> Unit = {}
) {
Expand Down Expand Up @@ -457,7 +466,14 @@ fun SettingsScreen(

if (selectedTab.intValue == 1) {
if (bleManager != null && dashkitUpdateManager != null) {
DashKitSettingsContent(bleManager, dashkitUpdateManager)
DashKitSettingsContent(
bleManager,
dashkitUpdateManager,
teslaStatus,
teslaResetPending,
onRemoveTesla,
onEnrollTesla,
)
} else {
Text(
text = stringResource(R.string.settings_dashkit_not_connected),
Expand Down Expand Up @@ -540,6 +556,13 @@ private fun PairNewDeviceSection(bleManager: DashKitBleManager) {
Text(text = stringResource(R.string.settings_pair_new_device), fontSize = 16.sp)
}

Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.settings_pair_caption),
color = DarkColors.TextMuted,
fontSize = 12.sp
)

if (!connected) {
Spacer(modifier = Modifier.height(8.dp))
Text(
Expand Down Expand Up @@ -581,7 +604,11 @@ private fun PairNewDeviceSection(bleManager: DashKitBleManager) {
@Composable
private fun DashKitSettingsContent(
bleManager: DashKitBleManager,
updateManager: FirmwareUpdateManager
updateManager: FirmwareUpdateManager,
teslaStatus: StateFlow<TeslaStatus>?,
teslaResetPending: StateFlow<Boolean>?,
onRemoveTesla: () -> Boolean,
onEnrollTesla: () -> Unit,
) {
val connectionState by bleManager.connectionState.collectAsState()
val connected = connectionState == ConnectionStatus.Connected
Expand All @@ -603,6 +630,15 @@ private fun DashKitSettingsContent(
PairNewDeviceSection(bleManager)
Spacer(modifier = Modifier.height(24.dp))

TeslaKeySection(
bleManager,
teslaStatus,
teslaResetPending,
onRemoveTesla,
onEnrollTesla,
)
Spacer(modifier = Modifier.height(24.dp))

DashKitMaintenanceSection(bleManager, connected)
}

Expand Down Expand Up @@ -688,6 +724,103 @@ private fun DashKitMaintenanceSection(bleManager: DashKitBleManager, connected:
}
}

@Composable
private fun TeslaKeySection(
bleManager: DashKitBleManager,
teslaStatus: StateFlow<TeslaStatus>?,
teslaResetPending: StateFlow<Boolean>?,
onRemoveTesla: () -> Boolean,
onEnrollTesla: () -> Unit,
) {
val context = LocalContext.current
val connectionState by bleManager.connectionState.collectAsState()
val connected = connectionState == ConnectionStatus.Connected
val idleTesla = remember { MutableStateFlow(TeslaStatus.Idle) }
val status by (teslaStatus ?: idleTesla).collectAsState()
val idleReset = remember { MutableStateFlow(false) }
val resetPending by (teslaResetPending ?: idleReset).collectAsState()
val showResetDialog = remember { mutableStateOf(false) }

SectionHeader(stringResource(R.string.settings_section_tesla_key))
Text(
text = stringResource(R.string.settings_tesla_key_caption),
color = DarkColors.TextMuted,
fontSize = 12.sp,
)
Spacer(modifier = Modifier.height(12.dp))
val statusLabel = if (resetPending) {
stringResource(R.string.tesla_connection_removing)
} else {
teslaStatusText(status)
}
InfoRow(label = stringResource(R.string.settings_tesla_key_status), value = statusLabel)
Spacer(modifier = Modifier.height(12.dp))

val hasKey = status.linkState.hasKey
Button(
onClick = if (hasKey) ({ showResetDialog.value = true }) else onEnrollTesla,
enabled = connected && !resetPending,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = if (hasKey) DarkColors.SurfaceSelected else AccentColor,
contentColor = Color.White,
disabledContainerColor = DarkColors.Border,
disabledContentColor = DarkColors.TextMuted
)
) {
Text(
text = stringResource(
when {
resetPending -> R.string.tesla_connection_removing
hasKey -> R.string.settings_tesla_reset
else -> R.string.settings_tesla_enroll
}
),
fontSize = 16.sp
)
}

if (showResetDialog.value) {
AlertDialog(
onDismissRequest = { showResetDialog.value = false },
title = { Text(stringResource(R.string.settings_tesla_reset_dialog_title)) },
text = { Text(stringResource(R.string.settings_tesla_reset_dialog_body)) },
confirmButton = {
TextButton(onClick = {
showResetDialog.value = false
val ok = onRemoveTesla()
val msg = if (ok) {
context.getString(R.string.settings_tesla_reset_sent)
} else {
context.getString(R.string.settings_tesla_reset_failed)
}
android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_LONG).show()
}) {
Text(stringResource(R.string.settings_tesla_reset_dialog_confirm))
}
},
dismissButton = {
TextButton(onClick = { showResetDialog.value = false }) {
Text(stringResource(R.string.settings_tesla_reset_dialog_cancel))
}
}
)
}
}

@Composable
private fun teslaStatusText(status: TeslaStatus): String = when (status.linkState) {
TeslaLinkState.NeverEnrolled -> stringResource(R.string.tesla_tile_key_not_set_up)
TeslaLinkState.Staged -> stringResource(R.string.tesla_tile_staged)
TeslaLinkState.Connecting -> stringResource(R.string.tesla_enroll_connecting_body)
TeslaLinkState.EnrolledNotConnected -> stringResource(R.string.tesla_tile_not_connected)
TeslaLinkState.EnrolledConnected -> teslaTileSummary(status)
.ifBlank { stringResource(R.string.tesla_tile_connected) }
TeslaLinkState.PairingWindow -> stringResource(R.string.tesla_status_pairing)
TeslaLinkState.EnrollmentFault -> stringResource(R.string.tesla_status_fault)
TeslaLinkState.Unknown -> "—"
}

@Composable
private fun FirmwareUpdateSection(updateManager: FirmwareUpdateManager) {
val scope = rememberCoroutineScope()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ enum class PairingState { Idle, Searching, Paired }
@Composable
fun DevicePuck(
state: PairingState,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
accent: Color = OnboardingColors.Accent
) {
val infinite = rememberInfiniteTransition(label = "puck")

Expand Down Expand Up @@ -94,7 +95,7 @@ fun DevicePuck(
if (state == PairingState.Searching) {
ringAnims.forEach { (scaleAnim, alphaAnim) ->
drawCircle(
color = OnboardingColors.Accent.copy(alpha = alphaAnim.value),
color = accent.copy(alpha = alphaAnim.value),
radius = puckRadius * scaleAnim.value,
center = center,
style = Stroke(width = 1.dp.toPx())
Expand Down
Loading
Loading