diff --git a/bridge/bridge_ios.cpp b/bridge/bridge_ios.cpp index 69c8af52..5ef116b5 100644 --- a/bridge/bridge_ios.cpp +++ b/bridge/bridge_ios.cpp @@ -55,6 +55,13 @@ static void carStateToBridge(const CarState& cs, BridgeCarState* out) { out->tpmsBattFR = cs.tpmsBattFR; out->tpmsBattRL = cs.tpmsBattRL; out->tpmsBattRR = cs.tpmsBattRR; + out->doorOpenFL = cs.doorOpenFL; + out->doorOpenFR = cs.doorOpenFR; + out->doorOpenRL = cs.doorOpenRL; + out->doorOpenRR = cs.doorOpenRR; + out->trunkOpen = cs.trunkOpen; + out->trunkPosition = cs.trunkPosition; + out->frunkOpen = cs.frunkOpen; } static std::atomic receiveLoopRunning{false}; diff --git a/bridge/bridge_ios.h b/bridge/bridge_ios.h index 6d38572c..c12882a2 100644 --- a/bridge/bridge_ios.h +++ b/bridge/bridge_ios.h @@ -64,6 +64,15 @@ typedef struct { double tpmsBattFR; double tpmsBattRL; double tpmsBattRR; + + // Closures: doors/trunk 0/1 (1 = latch not closed), trunk angle in degrees. + double doorOpenFL; + double doorOpenFR; + double doorOpenRL; + double doorOpenRR; + double trunkOpen; + double trunkPosition; + double frunkOpen; } BridgeCarState; // Context diff --git a/bridge/car/car_state.h b/bridge/car/car_state.h index 2fda0025..32f0ce5f 100644 --- a/bridge/car/car_state.h +++ b/bridge/car/car_state.h @@ -61,6 +61,19 @@ struct CarState { double tpmsBattRL = -1; double tpmsBattRR = -1; + // Closures from VCLEFT_doorStatus / VCRIGHT_doorStatus latch state: 1 while + // the latch is anything but closed (opened, opening, ajar, closing), else 0. + double doorOpenFL = 0; + double doorOpenFR = 0; + double doorOpenRL = 0; + double doorOpenRR = 0; + double trunkOpen = 0; + // Powered liftgate angle in degrees from VCLEFT_liftgateStatus (mux 1); + // 0 when closed or when the car has no powered liftgate. + double trunkPosition = 0; + // Frunk latch from VCFRONT_status (mux 0), same rule as the doors; it has no position. + double frunkOpen = 0; + // Vehicle VIN, empty until fully assembled by the mapper. static constexpr size_t VIN_LENGTH = 17; char vin[VIN_LENGTH + 1] = {}; @@ -69,7 +82,9 @@ struct CarState { static constexpr size_t VIN_OFFSET = 36; static constexpr size_t TPMS_OFFSET = VIN_OFFSET + VIN_DOUBLE_COUNT; static constexpr size_t TPMS_COUNT = 12; - static constexpr size_t FIELD_COUNT = TPMS_OFFSET + TPMS_COUNT; + static constexpr size_t DOOR_OFFSET = TPMS_OFFSET + TPMS_COUNT; + static constexpr size_t DOOR_COUNT = 7; + static constexpr size_t FIELD_COUNT = DOOR_OFFSET + DOOR_COUNT; void toArray(double* out) const { // Party bus @@ -134,5 +149,14 @@ struct CarState { out[TPMS_OFFSET + 9] = tpmsBattFR; out[TPMS_OFFSET + 10] = tpmsBattRL; out[TPMS_OFFSET + 11] = tpmsBattRR; + + // Closures (after TPMS): doors FL FR RL RR, trunk open, trunk position, frunk open. + out[DOOR_OFFSET + 0] = doorOpenFL; + out[DOOR_OFFSET + 1] = doorOpenFR; + out[DOOR_OFFSET + 2] = doorOpenRL; + out[DOOR_OFFSET + 3] = doorOpenRR; + out[DOOR_OFFSET + 4] = trunkOpen; + out[DOOR_OFFSET + 5] = trunkPosition; + out[DOOR_OFFSET + 6] = frunkOpen; } }; diff --git a/bridge/car/cars/tesla.h b/bridge/car/cars/tesla.h index bd212ff1..6b8fc596 100644 --- a/bridge/car/cars/tesla.h +++ b/bridge/car/cars/tesla.h @@ -89,6 +89,33 @@ static inline double powerLevel(const CarState& cs) { return std::clamp(kw / limit, -1.0, 1.0) * 100.0; } +// VCLEFT/VCRIGHT_*LatchStatus: 0 SNA, 1 OPENED, 2 CLOSED, 3 CLOSING, 4 OPENING, +// 5 AJAR, 6 TIMEOUT, 7 DEFAULT, 8 FAULT. Open until the latch reports closed; +// unknown/fault states read as closed so a stale bus doesn't light a warning. +static inline double latchOpen(double latchStatus) { + int s = static_cast(latchStatus); + return (s == 1 || s == 3 || s == 4 || s == 5) ? 1.0 : 0.0; +} + +// Per-door, trunk and frunk latch state plus the powered-liftgate angle, from +// the vehicle bus (DashKit forwards 0x102, 0x103, 0x142 and 0x2E1). +static inline void updateClosures(const CANParsers& cp, CarState& cs) { + cs.doorOpenFL = latchOpen(cp.get(1, "VCLEFT_doorStatus", "VCLEFT_frontLatchStatus")); + cs.doorOpenRL = latchOpen(cp.get(1, "VCLEFT_doorStatus", "VCLEFT_rearLatchStatus")); + cs.doorOpenFR = latchOpen(cp.get(1, "VCRIGHT_doorStatus", "VCRIGHT_frontLatchStatus")); + cs.doorOpenRR = latchOpen(cp.get(1, "VCRIGHT_doorStatus", "VCRIGHT_rearLatchStatus")); + cs.trunkOpen = latchOpen(cp.get(1, "VCRIGHT_doorStatus", "VCRIGHT_trunkLatchStatus")); + // VCLEFT_liftgateStatus is multiplexed: the angle lives on mux 1. Keep the + // last angle across mux-0 frames. Closed reads slightly negative on some cars. + if (static_cast(cp.get(1, "VCLEFT_liftgateStatus", "VCLEFT_liftgateStatusIndex")) == 1) { + cs.trunkPosition = std::max(0.0, cp.get(1, "VCLEFT_liftgateStatus", "VCLEFT_liftgatePosition")); + } + // VCFRONT_status is multiplexed: the frunk latch lives on mux 0. + if (static_cast(cp.get(1, "VCFRONT_status", "VCFRONT_statusIndex")) == 0) { + cs.frunkOpen = latchOpen(cp.get(1, "VCFRONT_status", "VCFRONT_frunkLatchStatus")); + } +} + static inline void updateVehicleBus(const CANParsers& cp, CarState& cs) { // Vehicle bus specific signals // BMS_energyStatus is multiplexed: mux 0 has pack/remaining energy, mux 1 has energyBuffer @@ -175,6 +202,8 @@ class TeslaDashKitMapper : public CarStateMapper { cs.buckleStatus = cp.get(1, "VCLEFT_switchStatus", "VCLEFT_frontBuckleSwitch") == 2.0 ? 1.0 : 0.0; } + updateClosures(cp, cs); + // Climate setpoint temperature (degC) from the vehicle bus. cs.acTemp = cp.get(1, "UI_hvacRequest", "UI_hvacReqTempSetpointLeft"); cs.acTempRight = cp.get(1, "UI_hvacRequest", "UI_hvacReqTempSetpointRight"); diff --git a/dash-apps/README.md b/dash-apps/README.md index 530f404a..367d908b 100644 --- a/dash-apps/README.md +++ b/dash-apps/README.md @@ -67,6 +67,10 @@ Values not received yet are `0` / `false`. For speed limit, stop line and energy | `egoSteeringAngle` | degrees | steering wheel angle, negative = left | | `leftBlinker`, `rightBlinker` | 0–2 | 0 off, 1 blinking (lamp dark), 2 blinking (lamp lit) | | `anyDoorOpen` | 0/1 | | +| `doorOpenFL`, `doorOpenFR`, `doorOpenRL`, `doorOpenRR` | 0/1 | per door, 1 until the latch reports closed. **DashKit only** | +| `trunkOpen` | 0/1 | rear trunk latch. **DashKit only** | +| `trunkPosition` | degrees | powered liftgate angle, 0 when closed or not powered. **DashKit only** | +| `frunkOpen` | 0/1 | frunk latch, same rule as the doors (no position). **DashKit only** | | `buckleStatus` | 0/1 | 1 = driver buckled | | `odometer` | number | km, or miles when `isImperial` | diff --git a/dash-apps/web-compose/core/host.js b/dash-apps/web-compose/core/host.js index bdfe88a1..a17edc4d 100644 --- a/dash-apps/web-compose/core/host.js +++ b/dash-apps/web-compose/core/host.js @@ -23,6 +23,13 @@ stopLineDist: "getStopLineDist", trafficLightColor: "getTrafficLightColor", anyDoorOpen: "getAnyDoorOpen", + doorOpenFL: "getDoorOpenFL", + doorOpenFR: "getDoorOpenFR", + doorOpenRL: "getDoorOpenRL", + doorOpenRR: "getDoorOpenRR", + trunkOpen: "getTrunkOpen", + trunkPosition: "getTrunkPosition", + frunkOpen: "getFrunkOpen", buckleStatus: "getBuckleStatus", accSetSpeed: "getAccSetSpeed", fullPackEnergy: "getFullPackEnergy", diff --git a/dash-apps/web-compose/core/i18n.js b/dash-apps/web-compose/core/i18n.js index 9d25426b..a82fb551 100644 --- a/dash-apps/web-compose/core/i18n.js +++ b/dash-apps/web-compose/core/i18n.js @@ -35,6 +35,13 @@ "signal.leftBlindSpot": "Left blind spot", "signal.rightBlindSpot": "Right blind spot", "signal.anyDoorOpen": "Door open", + "signal.doorOpenFL": "Driver door", + "signal.doorOpenFR": "Passenger door", + "signal.doorOpenRL": "Rear left door", + "signal.doorOpenRR": "Rear right door", + "signal.trunkOpen": "Trunk open", + "signal.trunkPosition": "Trunk angle", + "signal.frunkOpen": "Frunk open", "signal.seatbeltUnbuckled": "Seatbelt", "widget.unknown": "Unavailable widget", "editor.title": "Edit dashboard", @@ -100,6 +107,13 @@ "signal.leftBlindSpot": "Bal holttér", "signal.rightBlindSpot": "Jobb holttér", "signal.anyDoorOpen": "Nyitott ajtó", + "signal.doorOpenFL": "Vezetőajtó", + "signal.doorOpenFR": "Utasajtó", + "signal.doorOpenRL": "Bal hátsó ajtó", + "signal.doorOpenRR": "Jobb hátsó ajtó", + "signal.trunkOpen": "Csomagtartó nyitva", + "signal.trunkPosition": "Csomagtartó szöge", + "signal.frunkOpen": "Frunk nyitva", "signal.seatbeltUnbuckled": "Biztonsági öv", "widget.unknown": "Nem elérhető elem", "editor.title": "Műszerfal szerkesztése", @@ -161,6 +175,13 @@ "signal.leftBlindSpot": "Toter Winkel links", "signal.rightBlindSpot": "Toter Winkel rechts", "signal.anyDoorOpen": "Tür offen", + "signal.doorOpenFL": "Fahrertür", + "signal.doorOpenFR": "Beifahrertür", + "signal.doorOpenRL": "Tür hinten links", + "signal.doorOpenRR": "Tür hinten rechts", + "signal.trunkOpen": "Kofferraum offen", + "signal.trunkPosition": "Kofferraumwinkel", + "signal.frunkOpen": "Frunk offen", "signal.seatbeltUnbuckled": "Sicherheitsgurt", "widget.unknown": "Element nicht verfügbar", "editor.title": "Dashboard bearbeiten", diff --git a/dash-apps/web-compose/core/signals.js b/dash-apps/web-compose/core/signals.js index adcaf173..4f74f9eb 100644 --- a/dash-apps/web-compose/core/signals.js +++ b/dash-apps/web-compose/core/signals.js @@ -94,6 +94,8 @@ read: (r) => num(r.maxDischargePower) || undefined }, { id: "odometer", kind: "number", unit: "dist", precision: 0, category: "vehicle", bus: "vehicle", read: (r) => num(r.odometer) || undefined }, + { id: "trunkPosition", kind: "number", unit: "deg", precision: 0, range: () => [0, 95], category: "vehicle", bus: "vehicle", + read: (r) => num(r.trunkPosition) }, // surroundings { id: "stopLineDist", kind: "number", unit: "meter", precision: 0, range: () => [0, 200], category: "assist", bus: "any", @@ -116,6 +118,12 @@ { id: "leftBlindSpot", kind: "bool", unit: "none", category: "signals", bus: "any", read: (r) => flag(r.leftBlindSpot) }, { id: "rightBlindSpot", kind: "bool", unit: "none", category: "signals", bus: "any", read: (r) => flag(r.rightBlindSpot) }, { id: "anyDoorOpen", kind: "bool", unit: "none", category: "warnings", bus: "any", read: (r) => flag(r.anyDoorOpen) }, + { id: "doorOpenFL", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.doorOpenFL) }, + { id: "doorOpenFR", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.doorOpenFR) }, + { id: "doorOpenRL", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.doorOpenRL) }, + { id: "doorOpenRR", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.doorOpenRR) }, + { id: "trunkOpen", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.trunkOpen) }, + { id: "frunkOpen", kind: "bool", unit: "none", category: "warnings", bus: "vehicle", read: (r) => flag(r.frunkOpen) }, { id: "seatbeltUnbuckled", kind: "bool", unit: "none", category: "warnings", bus: "any", read: (r) => (r.buckleStatus === undefined ? undefined : Number(r.buckleStatus) === 0) }, ]; diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ble/VehicleControl.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ble/VehicleControl.kt index 1ceb2bca..5ab6c2b8 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ble/VehicleControl.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ble/VehicleControl.kt @@ -97,6 +97,13 @@ object VehicleControl { const val CMD_AC_SWING_INTENSITY: Int = 0x4D const val CMD_AC_SWING_TOGGLE: Int = 0x4E + // --- Door unlatch (VCSEC_requests 0x1F9); value = door index --- + const val CMD_DOOR_UNLATCH: Int = 0x50 + const val DOOR_FRONT_LEFT: Int = 0 + const val DOOR_FRONT_RIGHT: Int = 1 + const val DOOR_REAR_LEFT: Int = 2 + const val DOOR_REAR_RIGHT: Int = 3 + /** 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)) @@ -119,6 +126,10 @@ object VehicleControl { fun sendSportKickdownThreshold(manager: DashKitBleManager, percent: Int): Boolean = send(manager, CMD_SPORT_KICKDOWN_THRESHOLD, percent) + /** Pop one door latch (firmware refuses it in R/N/D or while moving). */ + fun sendDoorUnlatch(manager: DashKitBleManager, door: Int): Boolean = + send(manager, CMD_DOOR_UNLATCH, door) + fun sendAcSwingToggle(manager: DashKitBleManager): Boolean = send(manager, CMD_AC_SWING_TOGGLE, 0) diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/CarState.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/CarState.kt index 15c39208..1cddf754 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/CarState.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/CarState.kt @@ -57,6 +57,16 @@ data class CarState( val tpmsBattRL: Float = -1f, val tpmsBattRR: Float = -1f, + // Closures: 1 while the door/trunk latch is not closed, else 0. Trunk angle + // in degrees from the powered liftgate (0 when closed or not powered). + val doorOpenFL: Float = 0f, + val doorOpenFR: Float = 0f, + val doorOpenRL: Float = 0f, + val doorOpenRR: Float = 0f, + val trunkOpen: Float = 0f, + val trunkPosition: Float = 0f, + val frunkOpen: Float = 0f, + // Empty until fully assembled from the CAN stream by the native mapper. val vin: String = "" ) { @@ -70,7 +80,7 @@ data class CarState( ) companion object { - const val FIELD_COUNT = 51 + const val FIELD_COUNT = 58 private const val KM_TO_MILES = 0.621371f private val MILES_COUNTRIES = setOf("US", "GB", "MM", "LR") diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/DashValues.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/DashValues.kt index d74d7133..03404c7f 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/DashValues.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/datamodel/dash/DashValues.kt @@ -13,6 +13,13 @@ fun DashState.dashValues(systemDarkMode: Boolean? = null): Map { "leftBlinker" to c.leftBlinker, "rightBlinker" to c.rightBlinker, "anyDoorOpen" to c.anyDoorOpen, + "doorOpenFL" to c.doorOpenFL, + "doorOpenFR" to c.doorOpenFR, + "doorOpenRL" to c.doorOpenRL, + "doorOpenRR" to c.doorOpenRR, + "trunkOpen" to c.trunkOpen, + "trunkPosition" to c.trunkPosition, + "frunkOpen" to c.frunkOpen, "buckleStatus" to c.buckleStatus, "odometer" to c.odometer, "adasOn" to c.adasOn, diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/js/CarStateBridge.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/js/CarStateBridge.kt index 018ba450..e1e0942c 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/js/CarStateBridge.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/js/CarStateBridge.kt @@ -25,6 +25,13 @@ class CarStateBridge( @Volatile private var trafficLightColor: Float = 0f @Volatile private var buckleStatus: Float = 0f @Volatile private var anyDoorOpen: Float = 0f + @Volatile private var doorOpenFL: Float = 0f + @Volatile private var doorOpenFR: Float = 0f + @Volatile private var doorOpenRL: Float = 0f + @Volatile private var doorOpenRR: Float = 0f + @Volatile private var trunkOpen: Float = 0f + @Volatile private var trunkPosition: Float = 0f + @Volatile private var frunkOpen: Float = 0f @Volatile private var accSetSpeed: Float = 0f @Volatile private var fullPackEnergy: Float = 0f @Volatile private var nominalEnergyRemaining: Float = 0f @@ -79,6 +86,13 @@ class CarStateBridge( trafficLightColor = state.trafficLightColor buckleStatus = state.buckleStatus anyDoorOpen = state.anyDoorOpen + doorOpenFL = state.doorOpenFL + doorOpenFR = state.doorOpenFR + doorOpenRL = state.doorOpenRL + doorOpenRR = state.doorOpenRR + trunkOpen = state.trunkOpen + trunkPosition = state.trunkPosition + frunkOpen = state.frunkOpen accSetSpeed = state.accSetSpeed fullPackEnergy = state.fullPackEnergy nominalEnergyRemaining = state.nominalEnergyRemaining @@ -121,6 +135,13 @@ class CarStateBridge( @JavascriptInterface fun getTrafficLightColor(): Float = trafficLightColor @JavascriptInterface fun getBuckleStatus(): Float = buckleStatus @JavascriptInterface fun getAnyDoorOpen(): Float = anyDoorOpen + @JavascriptInterface fun getDoorOpenFL(): Float = doorOpenFL + @JavascriptInterface fun getDoorOpenFR(): Float = doorOpenFR + @JavascriptInterface fun getDoorOpenRL(): Float = doorOpenRL + @JavascriptInterface fun getDoorOpenRR(): Float = doorOpenRR + @JavascriptInterface fun getTrunkOpen(): Float = trunkOpen + @JavascriptInterface fun getTrunkPosition(): Float = trunkPosition + @JavascriptInterface fun getFrunkOpen(): Float = frunkOpen @JavascriptInterface fun getAccSetSpeed(): Float = accSetSpeed @JavascriptInterface fun getFullPackEnergy(): Float = fullPackEnergy @JavascriptInterface fun getNominalEnergyRemaining(): Float = nominalEnergyRemaining diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/controls/VehicleControls.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/controls/VehicleControls.kt index f2041129..1405d7ea 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/controls/VehicleControls.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/controls/VehicleControls.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.rounded.SouthEast import androidx.compose.material.icons.rounded.EvStation import androidx.compose.material.icons.rounded.Flip import androidx.compose.material.icons.rounded.Inbox +import androidx.compose.material.icons.rounded.MeetingRoom import androidx.compose.material.icons.rounded.PushPin import androidx.compose.material.icons.rounded.SwapHoriz import androidx.compose.material.icons.rounded.Thermostat @@ -175,6 +176,48 @@ val vehicleControls: List = listOf( manager?.let { VehicleControl.sendMirrorDipToggle(it) } } ), + // Door unlatch: a momentary latch release per door, like the Tesla app's + // "Unlatch Door". The firmware refuses it in R/N/D or while moving. + ControlAction( + id = "unlatch_front_left", + icon = Icons.Rounded.MeetingRoom, + labelRes = { R.string.control_unlatch_front_left }, + active = { false }, + gestureValue = 10, + perform = { manager -> + manager?.let { VehicleControl.sendDoorUnlatch(it, VehicleControl.DOOR_FRONT_LEFT) } + } + ), + ControlAction( + id = "unlatch_front_right", + icon = Icons.Rounded.MeetingRoom, + labelRes = { R.string.control_unlatch_front_right }, + active = { false }, + gestureValue = 11, + perform = { manager -> + manager?.let { VehicleControl.sendDoorUnlatch(it, VehicleControl.DOOR_FRONT_RIGHT) } + } + ), + ControlAction( + id = "unlatch_rear_left", + icon = Icons.Rounded.MeetingRoom, + labelRes = { R.string.control_unlatch_rear_left }, + active = { false }, + gestureValue = 12, + perform = { manager -> + manager?.let { VehicleControl.sendDoorUnlatch(it, VehicleControl.DOOR_REAR_LEFT) } + } + ), + ControlAction( + id = "unlatch_rear_right", + icon = Icons.Rounded.MeetingRoom, + labelRes = { R.string.control_unlatch_rear_right }, + active = { false }, + gestureValue = 13, + perform = { manager -> + manager?.let { VehicleControl.sendDoorUnlatch(it, VehicleControl.DOOR_REAR_RIGHT) } + } + ), // Glovebox is an electronic latch release: open only (closed by hand). ControlAction( id = "glovebox", diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/vehicle/CanFrameDecoder.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/vehicle/CanFrameDecoder.kt index d7b4f8a2..8fdb6122 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/vehicle/CanFrameDecoder.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/vehicle/CanFrameDecoder.kt @@ -78,7 +78,15 @@ class CanFrameDecoder( tpmsBattFL = values[47].toFloat(), tpmsBattFR = values[48].toFloat(), tpmsBattRL = values[49].toFloat(), - tpmsBattRR = values[50].toFloat() + tpmsBattRR = values[50].toFloat(), + // Closures after TPMS (see CarState::DOOR_OFFSET): doors FL FR RL RR, trunk open, trunk angle, frunk open. + doorOpenFL = values[51].toFloat(), + doorOpenFR = values[52].toFloat(), + doorOpenRL = values[53].toFloat(), + doorOpenRR = values[54].toFloat(), + trunkOpen = values[55].toFloat(), + trunkPosition = values[56].toFloat(), + frunkOpen = values[57].toFloat() ) } diff --git a/dashpilot-android/app/src/main/res/values-de/strings.xml b/dashpilot-android/app/src/main/res/values-de/strings.xml index 127f3a5d..49c2024f 100644 --- a/dashpilot-android/app/src/main/res/values-de/strings.xml +++ b/dashpilot-android/app/src/main/res/values-de/strings.xml @@ -247,6 +247,10 @@ Spiegelabsenkung umschalten Klima-Schwenkmodus umschalten Handschuhfach öffnen + Fahrertür entriegeln + Beifahrertür entriegeln + Tür hinten links entriegeln + Tür hinten rechts entriegeln Automatisierungen diff --git a/dashpilot-android/app/src/main/res/values-hu/strings.xml b/dashpilot-android/app/src/main/res/values-hu/strings.xml index b43e6909..b6c4ae87 100644 --- a/dashpilot-android/app/src/main/res/values-hu/strings.xml +++ b/dashpilot-android/app/src/main/res/values-hu/strings.xml @@ -246,6 +246,10 @@ Tükördöntés váltása Klíma legyező mód váltása Kesztyűtartó nyitása + Vezetőajtó kioldása + Utasajtó kioldása + Bal hátsó ajtó kioldása + Jobb hátsó ajtó kioldása Automatizációk diff --git a/dashpilot-android/app/src/main/res/values/strings.xml b/dashpilot-android/app/src/main/res/values/strings.xml index 2a4e653b..fd195f83 100644 --- a/dashpilot-android/app/src/main/res/values/strings.xml +++ b/dashpilot-android/app/src/main/res/values/strings.xml @@ -247,6 +247,10 @@ Toggle Mirror Dip Toggle AC Swing Open Glovebox + Unlatch Driver Door + Unlatch Passenger Door + Unlatch Rear Left Door + Unlatch Rear Right Door Automations diff --git a/dashpilot-android/bridge/CMakeLists.txt b/dashpilot-android/bridge/CMakeLists.txt index 271958d2..9258d09c 100644 --- a/dashpilot-android/bridge/CMakeLists.txt +++ b/dashpilot-android/bridge/CMakeLists.txt @@ -85,4 +85,13 @@ add_executable(tesla_vin_mapper_test target_include_directories(tesla_vin_mapper_test PRIVATE ${MSGQ_ROOT}) target_compile_options(tesla_vin_mapper_test PRIVATE -Wall -Wextra) add_test(NAME tesla_vin_mapper COMMAND tesla_vin_mapper_test) + +add_executable(tesla_closure_mapper_test + tests/tesla_closure_mapper_test.cpp + ${MSGQ_ROOT}/dbc/dbc.cc + ${MSGQ_ROOT}/dbc/dbcfile.cc +) +target_include_directories(tesla_closure_mapper_test PRIVATE ${MSGQ_ROOT}) +target_compile_options(tesla_closure_mapper_test PRIVATE -Wall -Wextra) +add_test(NAME tesla_closure_mapper COMMAND tesla_closure_mapper_test) endif() diff --git a/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp b/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp new file mode 100644 index 00000000..dde650c5 --- /dev/null +++ b/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp @@ -0,0 +1,209 @@ +// Host-run unit tests for the closure (door / trunk / liftgate) path of the +// Tesla DashKit CarState mapper (repo/bridge/car/cars/tesla.h). Built by +// CMakeLists.txt when not configuring for Android: +// cmake -B build-host -S . && cmake --build build-host && ctest --test-dir build-host + +#include "car/cars/tesla.h" + +#include +#include +#include + +static int failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + failures++; \ + } \ + } while (0) + +namespace { + +// The latch / liftgate signals as defined in bus_1_tesla_vehicle.dbc. +const char* kVehicleDbc = + "BO_ 258 VCLEFT_doorStatus: 8 VEH\n" + " SG_ VCLEFT_frontLatchStatus: 0|4@1+ (1,0) [0|0] \"\" X\n" + " SG_ VCLEFT_rearLatchStatus: 4|4@1+ (1,0) [0|0] \"\" X\n" + "\n" + "BO_ 259 VCRIGHT_doorStatus: 8 VEH\n" + " SG_ VCRIGHT_frontLatchStatus: 0|4@1+ (1,0) [0|0] \"\" X\n" + " SG_ VCRIGHT_rearLatchStatus: 4|4@1+ (1,0) [0|0] \"\" X\n" + " SG_ VCRIGHT_trunkLatchStatus: 56|4@1+ (1,0) [0|0] \"\" X\n" + "\n" + "BO_ 322 VCLEFT_liftgateStatus: 8 VEH\n" + " SG_ VCLEFT_liftgateStatusIndex M : 0|2@1+ (1,0) [0|2] \"\" X\n" + " SG_ VCLEFT_liftgatePosition m1 : 21|7@1- (1,46) [-5|95] \"deg\" X\n" + " SG_ VCLEFT_liftgateState m0 : 3|4@1+ (1,0) [0|12] \"\" X\n" + "\n" + "BO_ 737 VCFRONT_status: 8 VEH\n" + " SG_ VCFRONT_statusIndex M : 0|3@1+ (1,0) [0|6] \"\" X\n" + " SG_ VCFRONT_frunkLatchStatus m0 : 3|4@1+ (1,0) [0|8] \"\" X\n"; + +enum Latch { SNA = 0, OPENED = 1, CLOSED = 2, CLOSING = 3, OPENING = 4, AJAR = 5, TIMEOUT = 6, DEFAULT = 7, FAULT = 8 }; + +// Writes `value` into little-endian (Intel) bits [start, start+len) of an 8-byte frame. +void setBits(std::string& frame, int start, int len, uint64_t value) { + for (int i = 0; i < len; i++) { + int bit = start + i; + uint8_t& byte = reinterpret_cast(frame[bit / 8]); + if ((value >> i) & 1) byte |= static_cast(1u << (bit % 8)); + else byte &= static_cast(~(1u << (bit % 8))); + } +} + +std::string leftDoors(int front, int rear) { + std::string f(8, '\0'); + setBits(f, 0, 4, front); + setBits(f, 4, 4, rear); + return f; +} + +std::string rightDoors(int front, int rear, int trunk) { + std::string f(8, '\0'); + setBits(f, 0, 4, front); + setBits(f, 4, 4, rear); + setBits(f, 56, 4, trunk); + return f; +} + +std::string liftgateMux0(int state) { + std::string f(8, '\0'); + setBits(f, 0, 2, 0); + setBits(f, 3, 4, state); + return f; +} + +// Raw 7-bit two's complement; physical angle = raw + 46. +std::string liftgateMux1(int raw) { + std::string f(8, '\0'); + setBits(f, 0, 2, 1); + setBits(f, 21, 7, static_cast(raw) & 0x7F); + return f; +} + +std::string frontStatus(int mux, int frunk) { + std::string f(8, '\0'); + setBits(f, 0, 3, mux); + setBits(f, 3, 4, frunk); + return f; +} + +struct Harness { + CANParsers cp; + TeslaDashKitMapper mapper; + CarState cs; + + Harness() { + cp.addBus(1, kVehicleDbc); + cp.buildCache(); + } + + void feed(uint32_t address, const std::string& data, int bus = 1) { + cp.updateFrame(bus, address, reinterpret_cast(data.data()), data.size()); + mapper.update(cp, cs); + } +}; + +void doors_default_closed_before_any_frame() { + Harness h; + h.feed(0x405, std::string(8, '\0')); // unrelated frame + CHECK(h.cs.doorOpenFL == 0 && h.cs.doorOpenFR == 0 && h.cs.doorOpenRL == 0 && h.cs.doorOpenRR == 0); + CHECK(h.cs.trunkOpen == 0 && h.cs.trunkPosition == 0 && h.cs.frunkOpen == 0); +} + +void left_frame_maps_front_and_rear_left() { + Harness h; + h.feed(0x102, leftDoors(OPENED, CLOSED)); + CHECK(h.cs.doorOpenFL == 1 && h.cs.doorOpenRL == 0); + CHECK(h.cs.doorOpenFR == 0 && h.cs.doorOpenRR == 0); + h.feed(0x102, leftDoors(CLOSED, AJAR)); + CHECK(h.cs.doorOpenFL == 0 && h.cs.doorOpenRL == 1); +} + +void right_frame_maps_front_rear_right_and_trunk() { + Harness h; + h.feed(0x103, rightDoors(CLOSED, OPENING, CLOSED)); + CHECK(h.cs.doorOpenFR == 0 && h.cs.doorOpenRR == 1 && h.cs.trunkOpen == 0); + h.feed(0x103, rightDoors(CLOSING, CLOSED, OPENED)); + CHECK(h.cs.doorOpenFR == 1 && h.cs.doorOpenRR == 0 && h.cs.trunkOpen == 1); + CHECK(h.cs.doorOpenFL == 0 && h.cs.doorOpenRL == 0); +} + +void unknown_and_fault_latch_states_read_closed() { + const int notOpen[] = {SNA, CLOSED, TIMEOUT, DEFAULT, FAULT, 9, 15}; + for (int s : notOpen) { + Harness h; + h.feed(0x102, leftDoors(s, s)); + h.feed(0x103, rightDoors(s, s, s)); + CHECK(h.cs.doorOpenFL == 0 && h.cs.doorOpenRL == 0); + CHECK(h.cs.doorOpenFR == 0 && h.cs.doorOpenRR == 0 && h.cs.trunkOpen == 0); + } + const int open[] = {OPENED, CLOSING, OPENING, AJAR}; + for (int s : open) { + Harness h; + h.feed(0x102, leftDoors(s, CLOSED)); + CHECK(h.cs.doorOpenFL == 1); + } +} + +void liftgate_angle_only_from_mux_1_and_clamped_at_zero() { + Harness h; + h.feed(0x142, liftgateMux1(14)); // 14 + 46 = 60 deg + CHECK(h.cs.trunkPosition == 60); + h.feed(0x142, liftgateMux0(5)); // PLG_STATE_CLOSED on mux 0 keeps the last angle + CHECK(h.cs.trunkPosition == 60); + h.feed(0x142, liftgateMux1(-2)); // -2 + 46 = 44 deg (negative raw) + CHECK(h.cs.trunkPosition == 44); + h.feed(0x142, liftgateMux1(-48)); // -48 + 46 = -2 deg: closed reads slightly negative + CHECK(h.cs.trunkPosition == 0); +} + +void frunk_latch_only_from_vcfront_mux_0() { + Harness h; + h.feed(0x2E1, frontStatus(0, OPENED)); + CHECK(h.cs.frunkOpen == 1); + h.feed(0x2E1, frontStatus(1, CLOSED)); // another mux: those bits mean something else, keep the last value + CHECK(h.cs.frunkOpen == 1); + h.feed(0x2E1, frontStatus(0, CLOSED)); + CHECK(h.cs.frunkOpen == 0); +} + +void closures_round_trip_through_the_double_array() { + Harness h; + h.feed(0x102, leftDoors(OPENED, CLOSED)); + h.feed(0x103, rightDoors(CLOSED, AJAR, OPENED)); + h.feed(0x142, liftgateMux1(14)); + h.feed(0x2E1, frontStatus(0, AJAR)); + + double out[CarState::FIELD_COUNT]; + h.cs.toArray(out); + CHECK(CarState::DOOR_OFFSET == 51 && CarState::FIELD_COUNT == 58); + CHECK(out[CarState::DOOR_OFFSET + 0] == 1); // FL + CHECK(out[CarState::DOOR_OFFSET + 1] == 0); // FR + CHECK(out[CarState::DOOR_OFFSET + 2] == 0); // RL + CHECK(out[CarState::DOOR_OFFSET + 3] == 1); // RR + CHECK(out[CarState::DOOR_OFFSET + 4] == 1); // trunk open + CHECK(out[CarState::DOOR_OFFSET + 5] == 60); // trunk angle + CHECK(out[CarState::DOOR_OFFSET + 6] == 1); // frunk open +} + +} // namespace + +int main() { + doors_default_closed_before_any_frame(); + left_frame_maps_front_and_rear_left(); + right_frame_maps_front_rear_right_and_trunk(); + unknown_and_fault_latch_states_read_closed(); + liftgate_angle_only_from_mux_1_and_clamped_at_zero(); + frunk_latch_only_from_vcfront_mux_0(); + closures_round_trip_through_the_double_array(); + + if (failures) { + std::printf("%d check(s) failed\n", failures); + return 1; + } + std::printf("tesla_closure_mapper_test: all checks passed\n"); + return 0; +} diff --git a/dashpilot-ios/dashpilot/BLE/VehicleControl.swift b/dashpilot-ios/dashpilot/BLE/VehicleControl.swift index a2670965..15fbd41a 100644 --- a/dashpilot-ios/dashpilot/BLE/VehicleControl.swift +++ b/dashpilot-ios/dashpilot/BLE/VehicleControl.swift @@ -90,6 +90,15 @@ enum VehicleControl { static let cmdAcSwingIntensity = 0x4D static let cmdAcSwingToggle = 0x4E + // --- Door unlatch (VCSEC_requests 0x1F9); value = door index --- + // Momentary latch release like the Tesla app's "Unlatch Door". The + // firmware refuses it while the car is in R/N/D. + static let cmdDoorUnlatch = 0x50 + static let doorFrontLeft = 0 + static let doorFrontRight = 1 + static let doorRearLeft = 2 + static let doorRearRight = 3 + /// Bind (or clear, with actionValue 0) an N-finger tap to a control action. @discardableResult static func sendFingerAction(_ manager: DashKitBleManager, fingers: Int, actionValue: Int) -> Bool { @@ -159,6 +168,12 @@ enum VehicleControl { send(manager, opcode: cmdSportKickdownThreshold, value: percent) } + /// Pop one door latch (firmware only acts outside R/N/D). + @discardableResult + static func sendDoorUnlatch(_ manager: DashKitBleManager, door: Int) -> Bool { + send(manager, opcode: cmdDoorUnlatch, value: door) + } + @discardableResult static func sendAcSwingToggle(_ manager: DashKitBleManager) -> Bool { send(manager, opcode: cmdAcSwingToggle, value: 0) diff --git a/dashpilot-ios/dashpilot/JS/CarStateBridge.swift b/dashpilot-ios/dashpilot/JS/CarStateBridge.swift index fb423453..1b63a023 100644 --- a/dashpilot-ios/dashpilot/JS/CarStateBridge.swift +++ b/dashpilot-ios/dashpilot/JS/CarStateBridge.swift @@ -5,6 +5,6 @@ extension DashState { let cs = carState let ds = displaySettings let source = dataSourceType.map { "\"\($0.rawValue)\"" } ?? "null" - return "{\"egoSteeringAngle\":\(cs.egoSteeringAngle),\"egoSpeed\":\(cs.egoSpeed),\"leftBlinker\":\(cs.leftBlinker),\"rightBlinker\":\(cs.rightBlinker),\"gear\":\(cs.gear),\"adasOn\":\(cs.adasOn),\"leftBlindSpot\":\(cs.leftBlindSpot),\"rightBlindSpot\":\(cs.rightBlindSpot),\"fusedSpeedLimit\":\(cs.fusedSpeedLimit),\"stopLineDist\":\(cs.stopLineDist),\"trafficLightColor\":\(cs.trafficLightColor),\"anyDoorOpen\":\(cs.anyDoorOpen),\"buckleStatus\":\(cs.buckleStatus),\"accSetSpeed\":\(cs.accSetSpeed),\"fullPackEnergy\":\(cs.fullPackEnergy),\"nominalEnergyRemaining\":\(cs.nominalEnergyRemaining),\"energyBuffer\":\(cs.energyBuffer),\"maxRegenPower\":\(cs.maxRegenPower),\"maxDischargePower\":\(cs.maxDischargePower),\"packVoltage\":\(cs.packVoltage),\"packCurrent\":\(cs.packCurrent),\"powerLevel\":\(cs.powerLevel),\"packTMin\":\(cs.packTMin),\"packTMax\":\(cs.packTMax),\"odometer\":\(cs.odometer),\"madsActive\":\(cs.madsActive),\"selfdriveActive\":\(cs.selfdriveActive),\"experimentalMode\":\(cs.experimentalMode),\"changingLane\":\(cs.changingLane),\"tpmsPressureFL\":\(cs.tpmsPressureFL),\"tpmsPressureFR\":\(cs.tpmsPressureFR),\"tpmsPressureRL\":\(cs.tpmsPressureRL),\"tpmsPressureRR\":\(cs.tpmsPressureRR),\"tpmsTempFL\":\(cs.tpmsTempFL),\"tpmsTempFR\":\(cs.tpmsTempFR),\"tpmsTempRL\":\(cs.tpmsTempRL),\"tpmsTempRR\":\(cs.tpmsTempRR),\"tpmsBattFL\":\(cs.tpmsBattFL),\"tpmsBattFR\":\(cs.tpmsBattFR),\"tpmsBattRL\":\(cs.tpmsBattRL),\"tpmsBattRR\":\(cs.tpmsBattRR),\"phoneBattery\":\(phoneBattery),\"currentTime\":\(currentTime),\"dataSourceType\":\(source),\"showPhoneBattery\":\(ds.showPhoneBattery),\"showCarBattery\":\(ds.showCarBattery),\"showOdometer\":\(ds.showOdometer),\"isImperial\":\(ds.useImperial),\"darkMode\":\(ds.darkMode),\"alwaysOnBlindSpotMonitor\":\(ds.alwaysOnBlindSpotMonitor),\"renderQuality\":\(ds.renderQuality),\"darkModeBackgroundGray\":\(ds.darkModeBackgroundGray)}" + return "{\"egoSteeringAngle\":\(cs.egoSteeringAngle),\"egoSpeed\":\(cs.egoSpeed),\"leftBlinker\":\(cs.leftBlinker),\"rightBlinker\":\(cs.rightBlinker),\"gear\":\(cs.gear),\"adasOn\":\(cs.adasOn),\"leftBlindSpot\":\(cs.leftBlindSpot),\"rightBlindSpot\":\(cs.rightBlindSpot),\"fusedSpeedLimit\":\(cs.fusedSpeedLimit),\"stopLineDist\":\(cs.stopLineDist),\"trafficLightColor\":\(cs.trafficLightColor),\"anyDoorOpen\":\(cs.anyDoorOpen),\"doorOpenFL\":\(cs.doorOpenFL),\"doorOpenFR\":\(cs.doorOpenFR),\"doorOpenRL\":\(cs.doorOpenRL),\"doorOpenRR\":\(cs.doorOpenRR),\"trunkOpen\":\(cs.trunkOpen),\"trunkPosition\":\(cs.trunkPosition),\"frunkOpen\":\(cs.frunkOpen),\"buckleStatus\":\(cs.buckleStatus),\"accSetSpeed\":\(cs.accSetSpeed),\"fullPackEnergy\":\(cs.fullPackEnergy),\"nominalEnergyRemaining\":\(cs.nominalEnergyRemaining),\"energyBuffer\":\(cs.energyBuffer),\"maxRegenPower\":\(cs.maxRegenPower),\"maxDischargePower\":\(cs.maxDischargePower),\"packVoltage\":\(cs.packVoltage),\"packCurrent\":\(cs.packCurrent),\"powerLevel\":\(cs.powerLevel),\"packTMin\":\(cs.packTMin),\"packTMax\":\(cs.packTMax),\"odometer\":\(cs.odometer),\"madsActive\":\(cs.madsActive),\"selfdriveActive\":\(cs.selfdriveActive),\"experimentalMode\":\(cs.experimentalMode),\"changingLane\":\(cs.changingLane),\"tpmsPressureFL\":\(cs.tpmsPressureFL),\"tpmsPressureFR\":\(cs.tpmsPressureFR),\"tpmsPressureRL\":\(cs.tpmsPressureRL),\"tpmsPressureRR\":\(cs.tpmsPressureRR),\"tpmsTempFL\":\(cs.tpmsTempFL),\"tpmsTempFR\":\(cs.tpmsTempFR),\"tpmsTempRL\":\(cs.tpmsTempRL),\"tpmsTempRR\":\(cs.tpmsTempRR),\"tpmsBattFL\":\(cs.tpmsBattFL),\"tpmsBattFR\":\(cs.tpmsBattFR),\"tpmsBattRL\":\(cs.tpmsBattRL),\"tpmsBattRR\":\(cs.tpmsBattRR),\"phoneBattery\":\(phoneBattery),\"currentTime\":\(currentTime),\"dataSourceType\":\(source),\"showPhoneBattery\":\(ds.showPhoneBattery),\"showCarBattery\":\(ds.showCarBattery),\"showOdometer\":\(ds.showOdometer),\"isImperial\":\(ds.useImperial),\"darkMode\":\(ds.darkMode),\"alwaysOnBlindSpotMonitor\":\(ds.alwaysOnBlindSpotMonitor),\"renderQuality\":\(ds.renderQuality),\"darkModeBackgroundGray\":\(ds.darkModeBackgroundGray)}" } } diff --git a/dashpilot-ios/dashpilot/Localizable.xcstrings b/dashpilot-ios/dashpilot/Localizable.xcstrings index 4efe9b2e..3a2e7359 100644 --- a/dashpilot-ios/dashpilot/Localizable.xcstrings +++ b/dashpilot-ios/dashpilot/Localizable.xcstrings @@ -3310,6 +3310,70 @@ } } }, + "Unlatch Driver Door" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fahrertür entriegeln" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vezetőajtó kioldása" + } + } + } + }, + "Unlatch Passenger Door" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beifahrertür entriegeln" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Utasajtó kioldása" + } + } + } + }, + "Unlatch Rear Left Door" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tür hinten links entriegeln" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bal hátsó ajtó kioldása" + } + } + } + }, + "Unlatch Rear Right Door" : { + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tür hinten rechts entriegeln" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Jobb hátsó ajtó kioldása" + } + } + } + }, "Unsupported dashboard type: %@" : { "localizations" : { "de" : { diff --git a/dashpilot-ios/dashpilot/Models/CarState.swift b/dashpilot-ios/dashpilot/Models/CarState.swift index d08b8d48..0efeb187 100644 --- a/dashpilot-ios/dashpilot/Models/CarState.swift +++ b/dashpilot-ios/dashpilot/Models/CarState.swift @@ -57,6 +57,16 @@ struct CarState: Codable { var tpmsBattRL: Float = -1 var tpmsBattRR: Float = -1 + // Closures: 1 while the door/trunk latch is not closed, else 0. Trunk angle + // in degrees from the powered liftgate (0 when closed or not powered). + var doorOpenFL: Float = 0 + var doorOpenFR: Float = 0 + var doorOpenRL: Float = 0 + var doorOpenRR: Float = 0 + var trunkOpen: Float = 0 + var trunkPosition: Float = 0 + var frunkOpen: Float = 0 + init() {} init(from bridge: BridgeCarState) { @@ -108,6 +118,13 @@ struct CarState: Codable { tpmsBattFR = Float(bridge.tpmsBattFR) tpmsBattRL = Float(bridge.tpmsBattRL) tpmsBattRR = Float(bridge.tpmsBattRR) + doorOpenFL = Float(bridge.doorOpenFL) + doorOpenFR = Float(bridge.doorOpenFR) + doorOpenRL = Float(bridge.doorOpenRL) + doorOpenRR = Float(bridge.doorOpenRR) + trunkOpen = Float(bridge.trunkOpen) + trunkPosition = Float(bridge.trunkPosition) + frunkOpen = Float(bridge.frunkOpen) } private static let kmToMiles: Float = 0.621371 @@ -170,5 +187,12 @@ struct CarState: Codable { selfdriveActive = try c.decodeIfPresent(Bool.self, forKey: .selfdriveActive) ?? false experimentalMode = try c.decodeIfPresent(Bool.self, forKey: .experimentalMode) ?? false changingLane = try c.decodeIfPresent(Bool.self, forKey: .changingLane) ?? false + doorOpenFL = try c.decodeIfPresent(Float.self, forKey: .doorOpenFL) ?? 0 + doorOpenFR = try c.decodeIfPresent(Float.self, forKey: .doorOpenFR) ?? 0 + doorOpenRL = try c.decodeIfPresent(Float.self, forKey: .doorOpenRL) ?? 0 + doorOpenRR = try c.decodeIfPresent(Float.self, forKey: .doorOpenRR) ?? 0 + trunkOpen = try c.decodeIfPresent(Float.self, forKey: .trunkOpen) ?? 0 + trunkPosition = try c.decodeIfPresent(Float.self, forKey: .trunkPosition) ?? 0 + frunkOpen = try c.decodeIfPresent(Float.self, forKey: .frunkOpen) ?? 0 } } diff --git a/dashpilot-ios/dashpilot/Models/DashValues.swift b/dashpilot-ios/dashpilot/Models/DashValues.swift index becc8db5..4994274b 100644 --- a/dashpilot-ios/dashpilot/Models/DashValues.swift +++ b/dashpilot-ios/dashpilot/Models/DashValues.swift @@ -17,6 +17,13 @@ extension DashState { ("leftBlinker", .number(c.leftBlinker)), ("rightBlinker", .number(c.rightBlinker)), ("anyDoorOpen", .number(c.anyDoorOpen)), + ("doorOpenFL", .number(c.doorOpenFL)), + ("doorOpenFR", .number(c.doorOpenFR)), + ("doorOpenRL", .number(c.doorOpenRL)), + ("doorOpenRR", .number(c.doorOpenRR)), + ("trunkOpen", .number(c.trunkOpen)), + ("trunkPosition", .number(c.trunkPosition)), + ("frunkOpen", .number(c.frunkOpen)), ("buckleStatus", .number(c.buckleStatus)), ("odometer", .number(c.odometer)), ("adasOn", .bool(c.adasOn)), diff --git a/dashpilot-ios/dashpilot/Models/VehicleControls.swift b/dashpilot-ios/dashpilot/Models/VehicleControls.swift index 29517d7b..53b46895 100644 --- a/dashpilot-ios/dashpilot/Models/VehicleControls.swift +++ b/dashpilot-ios/dashpilot/Models/VehicleControls.swift @@ -143,6 +143,52 @@ let vehicleControls: [ControlAction] = [ VehicleControl.sendMirrorDipToggle(manager) } ), + // Door unlatch: a momentary latch release per door, like the Tesla app's + // "Unlatch Door". The firmware refuses it in R/N/D or while moving. + ControlAction( + id: "unlatch_front_left", + icon: "door.left.hand.open", + label: { String(localized: "Unlatch Driver Door") }, + active: { false }, + gestureValue: 10, + perform: { manager in + guard let manager else { return } + VehicleControl.sendDoorUnlatch(manager, door: VehicleControl.doorFrontLeft) + } + ), + ControlAction( + id: "unlatch_front_right", + icon: "door.right.hand.open", + label: { String(localized: "Unlatch Passenger Door") }, + active: { false }, + gestureValue: 11, + perform: { manager in + guard let manager else { return } + VehicleControl.sendDoorUnlatch(manager, door: VehicleControl.doorFrontRight) + } + ), + ControlAction( + id: "unlatch_rear_left", + icon: "door.left.hand.open", + label: { String(localized: "Unlatch Rear Left Door") }, + active: { false }, + gestureValue: 12, + perform: { manager in + guard let manager else { return } + VehicleControl.sendDoorUnlatch(manager, door: VehicleControl.doorRearLeft) + } + ), + ControlAction( + id: "unlatch_rear_right", + icon: "door.right.hand.open", + label: { String(localized: "Unlatch Rear Right Door") }, + active: { false }, + gestureValue: 13, + perform: { manager in + guard let manager else { return } + VehicleControl.sendDoorUnlatch(manager, door: VehicleControl.doorRearRight) + } + ), // Glovebox is an electronic latch release: open only (closed by hand). ControlAction( id: "glovebox",