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
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ class MainActivity : ComponentActivity() {
val connectionStatus by connectionVM.connectionStatus.collectAsState()
val pinnedControl by connectionVM.pinnedControl.collectAsState()
val wiperOffAutomation by connectionVM.wiperOffAutomation.collectAsState()
val climateKeepAutomation by connectionVM.climateKeepAutomation.collectAsState()
val climateKeepMinutes by connectionVM.climateKeepMinutes.collectAsState()
val fingerActions by connectionVM.fingerActions.collectAsState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val onDashboard = navBackStackEntry?.destination?.route
Expand Down Expand Up @@ -282,6 +284,14 @@ class MainActivity : ComponentActivity() {
onWiperOffChange = {
connectionVM.updateWiperOffAutomation(context, it)
},
climateKeepEnabled = climateKeepAutomation,
onClimateKeepChange = {
connectionVM.updateClimateKeepAutomation(context, it)
},
climateKeepMinutes = climateKeepMinutes,
onClimateKeepMinutesChange = {
connectionVM.updateClimateKeepMinutes(context, it)
},
fingerActions = fingerActions,
onSetFingerAction = { fingers, id ->
connectionVM.setFingerAction(context, fingers, id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ object VehicleControl {
// the firmware drops the link after 60 s of silence. Value is ignored.
const val CMD_PING: Int = 0x45

// --- Keep climate on after leaving the car (UI_hvacRequest 0x2F3) ---
// Arms/disarms the firmware's climate-keep automation. 1=enable, 0=disable.
// The firmware persists it in NVS (climate_keep module).
const val CMD_CLIMATE_KEEP_ENABLE: Int = 0x46

// --- Climate-keep duration ---
// Minutes the climate-keep automation runs after the driver leaves.
// The firmware clamps to 1..60 and persists it in NVS.
const val CMD_CLIMATE_KEEP_DURATION: Int = 0x47

/** Bind (or clear, with actionValue 0) an N-finger tap to a control action. */
fun sendFingerAction(manager: DashKitBleManager, fingers: Int, actionValue: Int): Boolean =
send(manager, CMD_MULTI_FINGER_ACTION, (fingers shl 8) or (actionValue and 0xFF))
Expand All @@ -84,6 +94,14 @@ object VehicleControl {
fun sendWiperOff(manager: DashKitBleManager, enabled: Boolean): Boolean =
send(manager, CMD_WIPER_OFF_ENABLE, if (enabled) 1 else 0)

/** Enable or disable the firmware's keep-climate-on automation. */
fun sendClimateKeep(manager: DashKitBleManager, enabled: Boolean): Boolean =
send(manager, CMD_CLIMATE_KEEP_ENABLE, if (enabled) 1 else 0)

/** Set how many minutes the keep-climate-on automation runs. */
fun sendClimateKeepDuration(manager: DashKitBleManager, minutes: Int): Boolean =
send(manager, CMD_CLIMATE_KEEP_DURATION, minutes)

/** Ask the DashKit to open a pairing window for one new device. */
fun sendEnterPairing(manager: DashKitBleManager): Boolean {
val ok = send(manager, CMD_ENTER_PAIRING, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ const val PREF_PINNED_CONTROL = "pinned_control"

// Automations
const val PREF_WIPER_OFF_AUTOMATION = "wiper_off_automation"
const val PREF_CLIMATE_KEEP_AUTOMATION = "climate_keep_automation"
const val PREF_CLIMATE_KEEP_MINUTES = "climate_keep_minutes"
// Map of finger count (3..5) -> control id, serialized as "3=glovebox;4=frunk".
const val PREF_FINGER_ACTIONS = "finger_actions"
// Legacy single three-finger binding, migrated into PREF_FINGER_ACTIONS.
const val PREF_THREE_FINGER_ACTION = "three_finger_action"
const val DEFAULT_WIPER_OFF_AUTOMATION = false
const val DEFAULT_CLIMATE_KEEP_AUTOMATION = false
const val DEFAULT_CLIMATE_KEEP_MINUTES = 5

// Display settings defaults
const val DEFAULT_SHOW_PHONE_BATTERY = true
Expand Down Expand Up @@ -111,6 +115,24 @@ fun setWiperOffAutomation(context: Context, value: Boolean) {
.edit { putBoolean(PREF_WIPER_OFF_AUTOMATION, value) }
}

fun getClimateKeepAutomation(context: Context): Boolean =
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean(PREF_CLIMATE_KEEP_AUTOMATION, DEFAULT_CLIMATE_KEEP_AUTOMATION)

fun setClimateKeepAutomation(context: Context, value: Boolean) {
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.edit { putBoolean(PREF_CLIMATE_KEEP_AUTOMATION, value) }
}

fun getClimateKeepMinutes(context: Context): Int =
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.getInt(PREF_CLIMATE_KEEP_MINUTES, DEFAULT_CLIMATE_KEEP_MINUTES)

fun setClimateKeepMinutes(context: Context, value: Int) {
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.edit { putInt(PREF_CLIMATE_KEEP_MINUTES, value) }
}

/**
* Returns the bindings of finger count (3..5) -> control id. Performs a one-time
* migration of the legacy single three-finger binding into the new map.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.softwiredtech.dashpilot.ui

import android.widget.NumberPicker
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
Expand All @@ -16,19 +17,23 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AcUnit
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand All @@ -41,6 +46,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.softwiredtech.dashpilot.ui.controls.controlById
import com.softwiredtech.dashpilot.ui.controls.vehicleControls
import com.softwiredtech.dashpilot.ui.theme.AccentColor
Expand All @@ -50,6 +56,9 @@ import com.softwiredtech.dashpilot.ui.theme.DarkColors
// firmware's MULTI_FINGER_MIN/MAX_FINGERS).
private val FINGER_COUNTS = 3..5

// Minutes the keep-climate-on window can run (matches the firmware clamp).
private val CLIMATE_KEEP_MINUTE_RANGE = 1..60

/**
* Automations screen. Lets the user enable the wiper-off automation and bind
* 3-, 4-, and 5-finger infotainment taps each to a vehicle control. Bindings are
Expand All @@ -61,6 +70,10 @@ private val FINGER_COUNTS = 3..5
fun AutomationsScreen(
wiperOffEnabled: Boolean,
onWiperOffChange: (Boolean) -> Unit,
climateKeepEnabled: Boolean,
onClimateKeepChange: (Boolean) -> Unit,
climateKeepMinutes: Int,
onClimateKeepMinutesChange: (Int) -> Unit,
fingerActions: Map<Int, String>,
onSetFingerAction: (fingers: Int, id: String?) -> Unit,
onChangeFingerCount: (from: Int, to: Int) -> Unit,
Expand Down Expand Up @@ -95,6 +108,26 @@ fun AutomationsScreen(

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

SectionLabel("Climate")
Spacer(modifier = Modifier.height(8.dp))
AutomationRow(
icon = Icons.Rounded.AcUnit,
title = "Keep climate on",
subtitle = "Keep the climate on when you leave the car. The automation stops after the set time, or when you return to the car.",
checked = climateKeepEnabled,
onToggle = { onClimateKeepChange(!climateKeepEnabled) },
extraContent = if (climateKeepEnabled) {
{
ClimateKeepDurationFooter(
minutes = climateKeepMinutes,
onMinutesChange = onClimateKeepMinutesChange
)
}
} else null
)

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

SectionLabel("Multi-touch infotainment trigger")
Spacer(modifier = Modifier.height(4.dp))
Text(
Expand Down Expand Up @@ -141,6 +174,74 @@ private fun SectionLabel(text: String) {
)
}

@Composable
private fun ClimateKeepDurationFooter(
minutes: Int,
onMinutesChange: (Int) -> Unit
) {
var showPicker by remember { mutableStateOf(false) }

Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Stop after",
color = DarkColors.TextMuted,
fontSize = 14.sp,
modifier = Modifier.weight(1f)
)
Row(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(DarkColors.Background)
.clickable { showPicker = true }
.padding(start = 12.dp, end = 6.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "$minutes min", color = Color.White, fontSize = 15.sp)
Icon(
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = null,
tint = DarkColors.TextMuted,
modifier = Modifier.size(20.dp)
)
}
}

if (showPicker) {
var pending by remember { mutableIntStateOf(minutes) }
AlertDialog(
onDismissRequest = { showPicker = false },
title = { Text("Stop after") },
text = {
AndroidView(
factory = { ctx ->
NumberPicker(ctx).apply {
minValue = CLIMATE_KEEP_MINUTE_RANGE.first
maxValue = CLIMATE_KEEP_MINUTE_RANGE.last
wrapSelectorWheel = false
value = minutes
setOnValueChangedListener { _, _, value -> pending = value }
}
},
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(onClick = {
showPicker = false
onMinutesChange(pending)
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showPicker = false }) { Text("Cancel") }
}
)
}
}

@Composable
private fun FingerActionRow(
fingers: Int,
Expand Down Expand Up @@ -265,58 +366,65 @@ private fun AutomationRow(
title: String,
subtitle: String?,
checked: Boolean,
onToggle: () -> Unit
onToggle: () -> Unit,
extraContent: (@Composable () -> Unit)? = null
) {
Row(
Column(
modifier = Modifier
.fillMaxWidth()
.background(DarkColors.Surface, RoundedCornerShape(16.dp))
.clickable(onClick = onToggle)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Box(
Row(
modifier = Modifier
.size(36.dp)
.background(
if (checked) AccentColor.copy(alpha = 0.16f)
else Color.White.copy(alpha = 0.08f),
RoundedCornerShape(10.dp)
),
contentAlignment = Alignment.Center
.fillMaxWidth()
.clickable(onClick = onToggle),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (checked) AccentColor else Color.White,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.size(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
if (subtitle != null) {
Box(
modifier = Modifier
.size(36.dp)
.background(
if (checked) AccentColor.copy(alpha = 0.16f)
else Color.White.copy(alpha = 0.08f),
RoundedCornerShape(10.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (checked) AccentColor else Color.White,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.size(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = subtitle,
color = DarkColors.TextMuted,
fontSize = 13.sp
text = title,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
if (subtitle != null) {
Text(
text = subtitle,
color = DarkColors.TextMuted,
fontSize = 13.sp
)
}
}
}
Switch(
checked = checked,
onCheckedChange = { onToggle() },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = AccentColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = DarkColors.Disabled
Switch(
checked = checked,
onCheckedChange = { onToggle() },
colors = SwitchDefaults.colors(
checkedThumbColor = Color.White,
checkedTrackColor = AccentColor,
uncheckedThumbColor = Color.White,
uncheckedTrackColor = DarkColors.Disabled
)
)
)
}
extraContent?.invoke()
}
}
Loading
Loading