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 @@ -164,6 +164,8 @@ class MainActivity : ComponentActivity() {
val wiperOffAutomation by connectionVM.wiperOffAutomation.collectAsState()
val climateKeepAutomation by connectionVM.climateKeepAutomation.collectAsState()
val climateKeepMinutes by connectionVM.climateKeepMinutes.collectAsState()
val sportKickdownAutomation by connectionVM.sportKickdownAutomation.collectAsState()
val sportKickdownPercent by connectionVM.sportKickdownPercent.collectAsState()
val fingerActions by connectionVM.fingerActions.collectAsState()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val onDashboard = navBackStackEntry?.destination?.route
Expand Down Expand Up @@ -305,6 +307,14 @@ class MainActivity : ComponentActivity() {
onClimateKeepMinutesChange = {
connectionVM.updateClimateKeepMinutes(context, it)
},
sportKickdownEnabled = sportKickdownAutomation,
onSportKickdownChange = {
connectionVM.updateSportKickdownAutomation(context, it)
},
sportKickdownPercent = sportKickdownPercent,
onSportKickdownPercentChange = {
connectionVM.updateSportKickdownPercent(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 @@ -86,6 +86,12 @@ object VehicleControl {
// car is in reverse; the override drops when it leaves reverse. Value ignored.
const val CMD_MIRROR_DIP_TOGGLE: Int = 0x48

// --- Sport kick-down (UI_powertrainControl 0x334) ---
// 1=enable, 0=disable. Persisted in NVS by the firmware.
const val CMD_SPORT_KICKDOWN_ENABLE: Int = 0x49
// Trigger pedal percent, clamped to 10..95. Persisted in NVS by the firmware.
const val CMD_SPORT_KICKDOWN_THRESHOLD: Int = 0x4A

/** 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 @@ -102,6 +108,12 @@ object VehicleControl {
fun sendClimateKeepDuration(manager: DashKitBleManager, minutes: Int): Boolean =
send(manager, CMD_CLIMATE_KEEP_DURATION, minutes)

fun sendSportKickdown(manager: DashKitBleManager, enabled: Boolean): Boolean =
send(manager, CMD_SPORT_KICKDOWN_ENABLE, if (enabled) 1 else 0)

fun sendSportKickdownThreshold(manager: DashKitBleManager, percent: Int): Boolean =
send(manager, CMD_SPORT_KICKDOWN_THRESHOLD, percent)

/** 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 @@ -24,13 +24,17 @@ const val PREF_PINNED_CONTROL = "pinned_control"
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"
const val PREF_SPORT_KICKDOWN_AUTOMATION = "sport_kickdown_automation"
const val PREF_SPORT_KICKDOWN_PERCENT = "sport_kickdown_percent"
// 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
const val DEFAULT_SPORT_KICKDOWN_AUTOMATION = false
const val DEFAULT_SPORT_KICKDOWN_PERCENT = 80

// Display settings defaults
const val DEFAULT_SHOW_PHONE_BATTERY = true
Expand Down Expand Up @@ -133,6 +137,24 @@ fun setClimateKeepMinutes(context: Context, value: Int) {
.edit { putInt(PREF_CLIMATE_KEEP_MINUTES, value) }
}

fun getSportKickdownAutomation(context: Context): Boolean =
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean(PREF_SPORT_KICKDOWN_AUTOMATION, DEFAULT_SPORT_KICKDOWN_AUTOMATION)

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

fun getSportKickdownPercent(context: Context): Int =
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.getInt(PREF_SPORT_KICKDOWN_PERCENT, DEFAULT_SPORT_KICKDOWN_PERCENT)

fun setSportKickdownPercent(context: Context, value: Int) {
context.getSharedPreferences(DASH_PREFS_NAME, Context.MODE_PRIVATE)
.edit { putInt(PREF_SPORT_KICKDOWN_PERCENT, 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
Expand Up @@ -21,6 +21,7 @@ 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.Speed
import androidx.compose.material.icons.rounded.WaterDrop
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu
Expand Down Expand Up @@ -59,6 +60,10 @@ 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

// Matches the firmware clamp.
private val SPORT_KICKDOWN_PERCENT_RANGE = 10..95
private const val SPORT_KICKDOWN_PERCENT_STEP = 5

/**
* 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 @@ -74,6 +79,10 @@ fun AutomationsScreen(
onClimateKeepChange: (Boolean) -> Unit,
climateKeepMinutes: Int,
onClimateKeepMinutesChange: (Int) -> Unit,
sportKickdownEnabled: Boolean,
onSportKickdownChange: (Boolean) -> Unit,
sportKickdownPercent: Int,
onSportKickdownPercentChange: (Int) -> Unit,
fingerActions: Map<Int, String>,
onSetFingerAction: (fingers: Int, id: String?) -> Unit,
onChangeFingerCount: (from: Int, to: Int) -> Unit,
Expand Down Expand Up @@ -118,9 +127,36 @@ fun AutomationsScreen(
onToggle = { onClimateKeepChange(!climateKeepEnabled) },
extraContent = if (climateKeepEnabled) {
{
ClimateKeepDurationFooter(
minutes = climateKeepMinutes,
onMinutesChange = onClimateKeepMinutesChange
NumberPickerFooter(
label = "Stop after",
value = climateKeepMinutes,
unit = "min",
range = CLIMATE_KEEP_MINUTE_RANGE,
onValueChange = onClimateKeepMinutesChange
)
}
} else null
)

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

SectionLabel("Driving")
Spacer(modifier = Modifier.height(8.dp))
AutomationRow(
icon = Icons.Rounded.Speed,
title = "Sport kick-down",
subtitle = "Switch from Chill to Sport pedal response while the accelerator is pressed past the threshold. Reverts when you ease off.",
checked = sportKickdownEnabled,
onToggle = { onSportKickdownChange(!sportKickdownEnabled) },
extraContent = if (sportKickdownEnabled) {
{
NumberPickerFooter(
label = "Pedal threshold",
value = sportKickdownPercent,
unit = "%",
range = SPORT_KICKDOWN_PERCENT_RANGE,
step = SPORT_KICKDOWN_PERCENT_STEP,
onValueChange = onSportKickdownPercentChange
)
}
} else null
Expand Down Expand Up @@ -175,19 +211,24 @@ private fun SectionLabel(text: String) {
}

@Composable
private fun ClimateKeepDurationFooter(
minutes: Int,
onMinutesChange: (Int) -> Unit
private fun NumberPickerFooter(
label: String,
value: Int,
unit: String,
range: IntRange,
onValueChange: (Int) -> Unit,
step: Int = 1
) {
var showPicker by remember { mutableStateOf(false) }
val choices = remember(range, step) { (range step step).toList() }

Spacer(modifier = Modifier.height(10.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Stop after",
text = label,
color = DarkColors.TextMuted,
fontSize = 14.sp,
modifier = Modifier.weight(1f)
Expand All @@ -200,7 +241,7 @@ private fun ClimateKeepDurationFooter(
.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)
Text(text = "$value $unit", color = Color.White, fontSize = 15.sp)
Icon(
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = null,
Expand All @@ -211,19 +252,20 @@ private fun ClimateKeepDurationFooter(
}

if (showPicker) {
var pending by remember { mutableIntStateOf(minutes) }
var pending by remember { mutableIntStateOf(value) }
AlertDialog(
onDismissRequest = { showPicker = false },
title = { Text("Stop after") },
title = { Text(label) },
text = {
AndroidView(
factory = { ctx ->
NumberPicker(ctx).apply {
minValue = CLIMATE_KEEP_MINUTE_RANGE.first
maxValue = CLIMATE_KEEP_MINUTE_RANGE.last
minValue = 0
maxValue = choices.lastIndex
displayedValues = choices.map { "$it $unit" }.toTypedArray()
wrapSelectorWheel = false
value = minutes
setOnValueChangedListener { _, _, value -> pending = value }
this.value = choices.indexOf(value).coerceAtLeast(0)
setOnValueChangedListener { _, _, idx -> pending = choices[idx] }
}
},
modifier = Modifier.fillMaxWidth()
Expand All @@ -232,7 +274,7 @@ private fun ClimateKeepDurationFooter(
confirmButton = {
TextButton(onClick = {
showPicker = false
onMinutesChange(pending)
onValueChange(pending)
}) { Text("OK") }
},
dismissButton = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ import com.softwiredtech.dashpilot.datamodel.dash.getClimateKeepAutomation
import com.softwiredtech.dashpilot.datamodel.dash.setClimateKeepAutomation
import com.softwiredtech.dashpilot.datamodel.dash.getClimateKeepMinutes
import com.softwiredtech.dashpilot.datamodel.dash.setClimateKeepMinutes
import com.softwiredtech.dashpilot.datamodel.dash.DEFAULT_SPORT_KICKDOWN_PERCENT
import com.softwiredtech.dashpilot.datamodel.dash.getSportKickdownAutomation
import com.softwiredtech.dashpilot.datamodel.dash.setSportKickdownAutomation
import com.softwiredtech.dashpilot.datamodel.dash.getSportKickdownPercent
import com.softwiredtech.dashpilot.datamodel.dash.setSportKickdownPercent
import com.softwiredtech.dashpilot.ui.controls.controlById
import com.softwiredtech.dashpilot.datasource.DataSourceType
import com.softwiredtech.dashpilot.datasource.CommaDataSource
Expand Down Expand Up @@ -119,13 +124,21 @@ class ConnectionViewModel(private var networkUtil: NetworkUtil) : ViewModel() {
private val _climateKeepMinutes = MutableStateFlow(DEFAULT_CLIMATE_KEEP_MINUTES)
val climateKeepMinutes = _climateKeepMinutes.asStateFlow()

private val _sportKickdownAutomation = MutableStateFlow(false)
val sportKickdownAutomation = _sportKickdownAutomation.asStateFlow()

private val _sportKickdownPercent = MutableStateFlow(DEFAULT_SPORT_KICKDOWN_PERCENT)
val sportKickdownPercent = _sportKickdownPercent.asStateFlow()

private val _fingerActions = MutableStateFlow<Map<Int, String>>(emptyMap())
val fingerActions = _fingerActions.asStateFlow()

fun loadAutomations(context: Context) {
_wiperOffAutomation.value = getWiperOffAutomation(context)
_climateKeepAutomation.value = getClimateKeepAutomation(context)
_climateKeepMinutes.value = getClimateKeepMinutes(context)
_sportKickdownAutomation.value = getSportKickdownAutomation(context)
_sportKickdownPercent.value = getSportKickdownPercent(context)
_fingerActions.value = getFingerActions(context)
}

Expand Down Expand Up @@ -158,6 +171,24 @@ class ConnectionViewModel(private var networkUtil: NetworkUtil) : ViewModel() {
}
}

fun updateSportKickdownAutomation(context: Context, value: Boolean) {
setSportKickdownAutomation(context, value)
_sportKickdownAutomation.value = value
_bleManager.value?.let { VehicleControl.sendSportKickdown(it, value) }
}

private var sportKickdownPercentPush: Job? = null

fun updateSportKickdownPercent(context: Context, value: Int) {
setSportKickdownPercent(context, value)
_sportKickdownPercent.value = value
sportKickdownPercentPush?.cancel()
sportKickdownPercentPush = viewModelScope.launch {
delay(400)
_bleManager.value?.let { VehicleControl.sendSportKickdownThreshold(it, value) }
}
}

fun setFingerAction(context: Context, fingers: Int, id: String?) {
val next = _fingerActions.value.toMutableMap()
if (id.isNullOrBlank()) next.remove(fingers) else next[fingers] = id
Expand Down Expand Up @@ -435,6 +466,13 @@ class ConnectionViewModel(private var networkUtil: NetworkUtil) : ViewModel() {
val climateKeepMin = getClimateKeepMinutes(context)
_climateKeepMinutes.value = climateKeepMin
VehicleControl.sendClimateKeepDuration(mgr, climateKeepMin)

val sportKickdown = getSportKickdownAutomation(context)
_sportKickdownAutomation.value = sportKickdown
VehicleControl.sendSportKickdown(mgr, sportKickdown)
val sportKickdownPct = getSportKickdownPercent(context)
_sportKickdownPercent.value = sportKickdownPct
VehicleControl.sendSportKickdownThreshold(mgr, sportKickdownPct)
}
}
}
Expand Down
Loading