From cb6c361ef258f63e8880c421f4d38fb1498099ab Mon Sep 17 00:00:00 2001 From: progphil1337 Date: Fri, 6 Nov 2026 20:18:46 +0100 Subject: [PATCH 1/2] [android][ios] Expose Tesla ambient lighting to Rive dashboards --- bridge/bridge_ios.cpp | 7 ++ bridge/bridge_ios.h | 8 ++ bridge/car/car_state.h | 19 ++++- bridge/car/tesla_ambient_lighting.h | 19 +++++ bridge/common/vehicle_decoder.h | 4 + dash-apps/README.md | 40 +++++++++ .../dashpilot/datamodel/dash/CarState.kt | 15 +++- .../dashpilot/datamodel/dash/DashValues.kt | 20 ++++- .../dashpilot/ui/RiveDashView.kt | 32 +++++-- .../dashpilot/vehicle/CanFrameDecoder.kt | 11 ++- .../ui/AmbientLightingBindingTest.kt | 84 +++++++++++++++++++ dashpilot-android/bridge/CMakeLists.txt | 9 ++ .../tests/tesla_ambient_lighting_test.cpp | 62 ++++++++++++++ .../tests/tesla_closure_mapper_test.cpp | 2 +- .../DataSource/DashKitDataSource.swift | 6 +- dashpilot-ios/dashpilot/Models/CarState.swift | 29 +++++++ .../dashpilot/Models/DashValues.swift | 16 +++- dashpilot-ios/dashpilot/UI/RiveDashView.swift | 6 ++ 18 files changed, 372 insertions(+), 17 deletions(-) create mode 100644 bridge/car/tesla_ambient_lighting.h create mode 100644 dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt create mode 100644 dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp diff --git a/bridge/bridge_ios.cpp b/bridge/bridge_ios.cpp index 5ef116b5..edb8dc05 100644 --- a/bridge/bridge_ios.cpp +++ b/bridge/bridge_ios.cpp @@ -62,6 +62,13 @@ static void carStateToBridge(const CarState& cs, BridgeCarState* out) { out->trunkOpen = cs.trunkOpen; out->trunkPosition = cs.trunkPosition; out->frunkOpen = cs.frunkOpen; + out->ambientLightingAvailable = cs.ambientLightingAvailable; + out->ambientLightingRed = cs.ambientLightingRed; + out->ambientLightingGreen = cs.ambientLightingGreen; + out->ambientLightingBlue = cs.ambientLightingBlue; + out->ambientLightingBrightness = cs.ambientLightingBrightness; + out->ambientLightingEnableEffectRaw = cs.ambientLightingEnableEffectRaw; + out->ambientLightingSectionMaskRaw = cs.ambientLightingSectionMaskRaw; } static std::atomic receiveLoopRunning{false}; diff --git a/bridge/bridge_ios.h b/bridge/bridge_ios.h index c12882a2..ef4a44c2 100644 --- a/bridge/bridge_ios.h +++ b/bridge/bridge_ios.h @@ -73,6 +73,14 @@ typedef struct { double trunkOpen; double trunkPosition; double frunkOpen; + + double ambientLightingAvailable; + double ambientLightingRed; + double ambientLightingGreen; + double ambientLightingBlue; + double ambientLightingBrightness; + double ambientLightingEnableEffectRaw; + double ambientLightingSectionMaskRaw; } BridgeCarState; // Context diff --git a/bridge/car/car_state.h b/bridge/car/car_state.h index 32f0ce5f..b3e44c96 100644 --- a/bridge/car/car_state.h +++ b/bridge/car/car_state.h @@ -84,7 +84,16 @@ struct CarState { static constexpr size_t TPMS_COUNT = 12; 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; + // Append fields after closures to preserve all existing array indices. + double ambientLightingAvailable = 0; + double ambientLightingRed = 0; + double ambientLightingGreen = 0; + double ambientLightingBlue = 0; + double ambientLightingBrightness = 0; + double ambientLightingEnableEffectRaw = 0; + double ambientLightingSectionMaskRaw = 0; + static constexpr size_t AMBIENT_OFFSET = DOOR_OFFSET + DOOR_COUNT; + static constexpr size_t FIELD_COUNT = AMBIENT_OFFSET + 7; void toArray(double* out) const { // Party bus @@ -158,5 +167,13 @@ struct CarState { out[DOOR_OFFSET + 4] = trunkOpen; out[DOOR_OFFSET + 5] = trunkPosition; out[DOOR_OFFSET + 6] = frunkOpen; + + out[AMBIENT_OFFSET + 0] = ambientLightingAvailable; + out[AMBIENT_OFFSET + 1] = ambientLightingRed; + out[AMBIENT_OFFSET + 2] = ambientLightingGreen; + out[AMBIENT_OFFSET + 3] = ambientLightingBlue; + out[AMBIENT_OFFSET + 4] = ambientLightingBrightness; + out[AMBIENT_OFFSET + 5] = ambientLightingEnableEffectRaw; + out[AMBIENT_OFFSET + 6] = ambientLightingSectionMaskRaw; } }; diff --git a/bridge/car/tesla_ambient_lighting.h b/bridge/car/tesla_ambient_lighting.h new file mode 100644 index 00000000..1b42fd08 --- /dev/null +++ b/bridge/car/tesla_ambient_lighting.h @@ -0,0 +1,19 @@ +#pragma once + +#include "car_state.h" +#include + +// Candidate UI_ambientLightingCtrls layout; pending vehicle validation. +// Latest complete update wins for the dashboard accent, regardless of section. +// Byte 0 and the section mask deliberately remain uninterpreted. +inline void updateTeslaAmbientLighting(CarState& state, int bus, uint32_t address, + const uint8_t* data, size_t len) { + if (bus != 1 || address != 0x679 || len < 7 || !data) return; + state.ambientLightingRed = data[1]; + state.ambientLightingGreen = data[2]; + state.ambientLightingBlue = data[3]; + state.ambientLightingBrightness = data[4] & 0x7F; + state.ambientLightingEnableEffectRaw = data[0]; + state.ambientLightingSectionMaskRaw = ((data[5] >> 3) | (data[6] << 5)) & 0x3F; + state.ambientLightingAvailable = 1; +} diff --git a/bridge/common/vehicle_decoder.h b/bridge/common/vehicle_decoder.h index 2a2bf74a..62e600b4 100644 --- a/bridge/common/vehicle_decoder.h +++ b/bridge/common/vehicle_decoder.h @@ -3,6 +3,7 @@ #include "car/can_parsers.h" #include "car/car_state.h" #include "car/car_state_mapper.h" +#include "car/tesla_ambient_lighting.h" #include "car/cars/tesla.h" #include "msgq/ipc.h" #include @@ -30,9 +31,11 @@ class VehicleDecoder { } parsers_.buildCache(); mapper_ = createMapper(vehicleType); + decodeAmbientLighting_ = vehicleType == "dashkit" || vehicleType == "tesla_juniper"; } void updateFrame(int bus, uint32_t address, const uint8_t* data, size_t len) { + if (decodeAmbientLighting_) updateTeslaAmbientLighting(state_, bus, address, data, len); parsers_.updateFrame(bus, address, data, len); } @@ -46,4 +49,5 @@ class VehicleDecoder { CANParsers parsers_; std::unique_ptr mapper_; CarState state_; + bool decodeAmbientLighting_ = false; }; diff --git a/dash-apps/README.md b/dash-apps/README.md index 367d908b..f59b8879 100644 --- a/dash-apps/README.md +++ b/dash-apps/README.md @@ -121,6 +121,46 @@ The app plays the file's default state machine and resizes the artboard to the s To try your own `.riv` on a phone, go to Settings → Developer → **Load Rive File**. +### Tesla interior ambient lighting (candidate) + +DashKit and Juniper profiles receive standard CAN ID `0x679` +(`UI_ambientLightingCtrls`) on Vehicle CAN, bus 1. This candidate layout is +pending vehicle validation. It requires at least seven payload bytes and adds +no vehicle writes, so it also works on read-only BLE connections. + +Declare these optional properties on the default `DashViewModel` instance: + +| Rive property | Rive type | meaning | +|---|---|---| +| `ambientLightingColor` | Color | Opaque RGB color, independent of brightness | +| `ambientLightingHex` | String | Uppercase, zero-padded `#RRGGBB`; also retained in app CarState | +| `ambientLightingRed`, `ambientLightingGreen`, `ambientLightingBlue` | Number | Unsigned byte values, 0–255 | +| `ambientLightingBrightness` | Number | Byte 4 masked with `0x7F`; documented range 0–100 percent | +| `ambientLightingEnableEffectRaw` | Number | Uninterpreted byte 0, 0–255; not an on/off flag | +| `ambientLightingSectionMaskRaw` | Number | Uninterpreted bits 43–48, 0–63; no section labels are assigned | +| `ambientLightingAvailable` | Boolean | A complete ambient frame has been received in this session | + +Bind `ambientLightingColor` to your accent fill/stroke color. Bind brightness +separately if desired (for example, divide by 100 in a Rive converter for +opacity); the host never scales RGB or alpha by brightness. These properties +are added to the Rive interface; Android's web JavaScript interface is unchanged. + +There is no existing dashboard section-priority convention. The global accent +therefore uses the **latest complete frame**, including section-specific frames; +the raw mask and RGB/brightness belong to that same update. Dashboards can use +the mask to implement their own selection policy once vehicle validation +establishes its meaning. + +Before the first frame, availability is false, hex is empty, numeric values +are zero, and the Color property is left at its authored value. Use availability +to select your fallback accent. Like other CAN telemetry, the latest value is +held without a timeout until the session is reset; truncated or unrelated +frames cannot clear it. Mounted dashboards receive updates and new dashboard +subscriptions replay the session's latest state. + +For example, `A5 12 34 56 E4 AD FF 5A` gives `#123456`, RGB (18, 52, 86), +brightness 100, raw enable/effect 165, and raw section mask 53. + ## Saving settings Web apps get one string of storage, usually JSON. 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 1cddf754..41ae8411 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 @@ -68,7 +68,16 @@ data class CarState( val frunkOpen: Float = 0f, // Empty until fully assembled from the CAN stream by the native mapper. - val vin: String = "" + val vin: String = "", + // Candidate Tesla ambient lighting; unknown until a complete frame arrives. + val ambientLightingAvailable: Boolean = false, + val ambientLightingHex: String = "", + val ambientLightingRed: Float = 0f, + val ambientLightingGreen: Float = 0f, + val ambientLightingBlue: Float = 0f, + val ambientLightingBrightness: Float = 0f, + val ambientLightingEnableEffectRaw: Float = 0f, + val ambientLightingSectionMaskRaw: Float = 0f, ) { fun toImperial(): CarState = copy( odometer = odometer * KM_TO_MILES, @@ -80,7 +89,7 @@ data class CarState( ) companion object { - const val FIELD_COUNT = 58 + const val FIELD_COUNT = 65 private const val KM_TO_MILES = 0.621371f private val MILES_COUNTRIES = setOf("US", "GB", "MM", "LR") @@ -88,4 +97,4 @@ data class CarState( fun speedLimitSignsInKm(): Boolean = Locale.getDefault().country !in MILES_COUNTRIES } -} \ No newline at end of file +} 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 03404c7f..055d7f82 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 @@ -3,10 +3,23 @@ package com.softwiredtech.dashpilot.datamodel.dash import java.time.Instant import java.time.ZoneId +// Opaque ARGB for Rive's typed Color API; brightness is a separate property. +data class DashColor(val red: Int, val green: Int, val blue: Int) { + val argb: Int get() = (0xFF shl 24) or (red shl 16) or (green shl 8) or blue +} + fun DashState.dashValues(systemDarkMode: Boolean? = null): Map { val c = carState val d = displaySettings - return mapOf( + val values = mutableMapOf( + "ambientLightingAvailable" to c.ambientLightingAvailable, + "ambientLightingHex" to c.ambientLightingHex, + "ambientLightingRed" to c.ambientLightingRed, + "ambientLightingGreen" to c.ambientLightingGreen, + "ambientLightingBlue" to c.ambientLightingBlue, + "ambientLightingBrightness" to c.ambientLightingBrightness, + "ambientLightingEnableEffectRaw" to c.ambientLightingEnableEffectRaw, + "ambientLightingSectionMaskRaw" to c.ambientLightingSectionMaskRaw, "egoSpeed" to c.egoSpeed, "gear" to c.gear, "egoSteeringAngle" to c.egoSteeringAngle, @@ -69,6 +82,11 @@ fun DashState.dashValues(systemDarkMode: Boolean? = null): Map { "dataSourceType" to dataSourceType, "speedCameraDistance" to speedCameraDistance.toFloat(), ) + if (c.ambientLightingAvailable) { + values["ambientLightingColor"] = DashColor(c.ambientLightingRed.toInt(), + c.ambientLightingGreen.toInt(), c.ambientLightingBlue.toInt()) + } + return values } private fun secondsSinceLocalMidnight(epochMillis: Long): Float = diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt index 0012d451..8a17d408 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt @@ -27,6 +27,7 @@ import app.rive.rememberRiveWorkerOrNull import app.rive.rememberViewModelInstance import app.rive.runtime.kotlin.core.ViewModel.PropertyDataType import com.softwiredtech.dashpilot.datamodel.dash.DashState +import com.softwiredtech.dashpilot.datamodel.dash.DashColor import com.softwiredtech.dashpilot.datamodel.dash.dashValues import kotlinx.coroutines.flow.Flow @@ -122,14 +123,29 @@ private fun BoundRive( vmi.setBoolean("darkMode", systemDarkMode) } dashStateFlow.collect { state -> - for ((name, value) in state.dashValues(systemDarkMode)) { - when (properties[name]) { - PropertyDataType.NUMBER -> (value as? Float)?.let { vmi.setNumber(name, it) } - PropertyDataType.BOOLEAN -> (value as? Boolean)?.let { vmi.setBoolean(name, it) } - PropertyDataType.STRING -> vmi.setString(name, value.toString()) - else -> {} - } - } + applyRiveDashValues(properties, state.dashValues(systemDarkMode), + vmi::setNumber, vmi::setBoolean, vmi::setString, vmi::setColor) + } + } +} + +// Keep typed dispatch independent of the native Rive worker so it can be +// checked with successive telemetry snapshots as well as newly mounted files. +internal fun applyRiveDashValues( + properties: Map, + values: Map, + setNumber: (String, Float) -> Unit, + setBoolean: (String, Boolean) -> Unit, + setString: (String, String) -> Unit, + setColor: (String, Int) -> Unit, +) { + for ((name, value) in values) { + when (properties[name]) { + PropertyDataType.NUMBER -> (value as? Float)?.let { setNumber(name, it) } + PropertyDataType.BOOLEAN -> (value as? Boolean)?.let { setBoolean(name, it) } + PropertyDataType.STRING -> setString(name, value.toString()) + PropertyDataType.COLOR -> (value as? DashColor)?.let { setColor(name, it.argb) } + else -> {} } } } 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 8fdb6122..edcdc673 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 @@ -86,7 +86,16 @@ class CanFrameDecoder( doorOpenRR = values[54].toFloat(), trunkOpen = values[55].toFloat(), trunkPosition = values[56].toFloat(), - frunkOpen = values[57].toFloat() + frunkOpen = values[57].toFloat(), + ambientLightingAvailable = values[58] > 0, + ambientLightingHex = if (values[58] > 0) + "#%02X%02X%02X".format(values[59].toInt(), values[60].toInt(), values[61].toInt()) else "", + ambientLightingRed = values[59].toFloat(), + ambientLightingGreen = values[60].toFloat(), + ambientLightingBlue = values[61].toFloat(), + ambientLightingBrightness = values[62].toFloat(), + ambientLightingEnableEffectRaw = values[63].toFloat(), + ambientLightingSectionMaskRaw = values[64].toFloat(), ) } diff --git a/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt b/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt new file mode 100644 index 00000000..6d4bf42d --- /dev/null +++ b/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt @@ -0,0 +1,84 @@ +package com.softwiredtech.dashpilot.ui + +import app.rive.runtime.kotlin.core.ViewModel.PropertyDataType +import com.softwiredtech.dashpilot.datamodel.dash.CarState +import com.softwiredtech.dashpilot.datamodel.dash.DashColor +import com.softwiredtech.dashpilot.datamodel.dash.DashState +import com.softwiredtech.dashpilot.datamodel.dash.dashValues +import com.softwiredtech.dashpilot.vehicle.CanFrameDecoder +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Test + +class AmbientLightingBindingTest { + private fun snapshot(red: Int, green: Int, blue: Int, brightness: Int = 100, mask: Int = 53): DashState { + val values = DoubleArray(CarState.FIELD_COUNT) + values[39] = 2.5 // Existing TPMS slots must stay independent of ambient lighting. + values[46] = 31.0 + values[58] = 1.0 + values[59] = red.toDouble() + values[60] = green.toDouble() + values[61] = blue.toDouble() + values[62] = brightness.toDouble() + values[63] = 165.0 + values[64] = mask.toDouble() + return DashState(carState = CanFrameDecoder.arrayToCarState(values)) + } + + @Test fun candidate_state_and_hex_edge_cases() { + val state = snapshot(18, 52, 86).carState + assertEquals("#123456", state.ambientLightingHex) + assertEquals(100f, state.ambientLightingBrightness) + assertEquals(165f, state.ambientLightingEnableEffectRaw) + assertEquals(53f, state.ambientLightingSectionMaskRaw) + assertEquals(2.5f, state.tpmsPressureFL) + assertEquals(31f, state.tpmsTempRR) + assertEquals(state.ambientLightingHex, state.toImperial().ambientLightingHex) + assertEquals("#01020A", snapshot(1, 2, 10).carState.ambientLightingHex) + assertEquals("#000000", snapshot(0, 0, 0).carState.ambientLightingHex) + assertEquals("#FFFFFF", snapshot(255, 255, 255).carState.ambientLightingHex) + assertEquals(DashColor(255, 255, 255), snapshot(255, 255, 255, 0).dashValues()["ambientLightingColor"]) + val unknown = CanFrameDecoder.arrayToCarState(DoubleArray(CarState.FIELD_COUNT)) + assertFalse(unknown.ambientLightingAvailable) + assertEquals("", unknown.ambientLightingHex) + assertFalse(DashState(unknown).dashValues().containsKey("ambientLightingColor")) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test fun live_updates_and_mount_replay_dispatch_typed_opaque_colors() = runTest { + // DashKit's telemetry stream replays the latest snapshot to each mount. + val stream = MutableSharedFlow(replay = 1) + val properties = mapOf( + "ambientLightingColor" to PropertyDataType.COLOR, + "ambientLightingHex" to PropertyDataType.STRING, + "ambientLightingBrightness" to PropertyDataType.NUMBER, + "ambientLightingSectionMaskRaw" to PropertyDataType.NUMBER, + "ambientLightingAvailable" to PropertyDataType.BOOLEAN, + ) + val writes = mutableMapOf() + stream.emit(snapshot(18, 52, 86)) + fun mount(target: MutableMap) = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + stream.collect { state -> + applyRiveDashValues(properties, state.dashValues(), + { k, v -> target[k] = v }, { k, v -> target[k] = v }, + { k, v -> target[k] = v }, { k, v -> target[k] = v }) + } + } + val firstMount = mount(writes) + assertEquals(0xFF123456.toInt(), writes["ambientLightingColor"]) + assertEquals(100f, writes["ambientLightingBrightness"]) + stream.emit(snapshot(255, 255, 255, brightness = 0, mask = 1)) + assertEquals(0xFFFFFFFF.toInt(), writes["ambientLightingColor"]) + assertEquals(0f, writes["ambientLightingBrightness"]) + assertEquals(1f, writes["ambientLightingSectionMaskRaw"]) + firstMount.cancel() + val remounted = mutableMapOf() + mount(remounted) + assertEquals(writes, remounted) + assertFalse(writes.containsKey("egoSpeed")) // undeclared bindings skipped + } +} diff --git a/dashpilot-android/bridge/CMakeLists.txt b/dashpilot-android/bridge/CMakeLists.txt index 9258d09c..58ddf3fc 100644 --- a/dashpilot-android/bridge/CMakeLists.txt +++ b/dashpilot-android/bridge/CMakeLists.txt @@ -94,4 +94,13 @@ add_executable(tesla_closure_mapper_test 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) + +add_executable(tesla_ambient_lighting_test + tests/tesla_ambient_lighting_test.cpp + ${MSGQ_ROOT}/dbc/dbc.cc + ${MSGQ_ROOT}/dbc/dbcfile.cc +) +target_include_directories(tesla_ambient_lighting_test PRIVATE ${MSGQ_ROOT}) +target_compile_options(tesla_ambient_lighting_test PRIVATE -Wall -Wextra) +add_test(NAME tesla_ambient_lighting COMMAND tesla_ambient_lighting_test) endif() diff --git a/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp b/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp new file mode 100644 index 00000000..6c3c0584 --- /dev/null +++ b/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp @@ -0,0 +1,62 @@ +#include "common/vehicle_decoder.h" +#include +#include + +int main() { + const uint8_t candidate[] = {0xA5, 0x12, 0x34, 0x56, 0xE4, 0xAD, 0xFF, 0x5A}; + for (const auto* type : {"dashkit", "tesla_juniper"}) { + VehicleDecoder decoder({}, {}, type); + auto& state = decoder.state(); + assert(!state.ambientLightingAvailable); + for (size_t len = 0; len < 7; ++len) { + decoder.updateFrame(1, 0x679, candidate, len); + decoder.updateMapper(); + assert(!state.ambientLightingAvailable); + } + for (int bus : {0, 2, 5}) { + decoder.updateFrame(bus, 0x679, candidate, 8); + assert(!state.ambientLightingAvailable); + } + decoder.updateFrame(1, 0x678, candidate, 8); + decoder.updateFrame(1, 0x80000679, candidate, 8); + assert(!state.ambientLightingAvailable); + decoder.updateFrame(1, 0x679, candidate, 8); + decoder.updateMapper(); + assert(state.ambientLightingAvailable); + assert(state.ambientLightingRed == 18 && state.ambientLightingGreen == 52); + assert(state.ambientLightingBlue == 86 && state.ambientLightingBrightness == 100); + assert(state.ambientLightingEnableEffectRaw == 165 && state.ambientLightingSectionMaskRaw == 53); + std::strcpy(state.vin, "5YJ3E7EB1MF123456"); + state.tpmsPressureFL = 2.5; + state.tpmsTempRR = 31; + double values[CarState::FIELD_COUNT] = {}; + state.toArray(values); + assert(values[39] == 2.5 && values[46] == 31); + assert(values[58] == 1 && values[59] == 18 && values[60] == 52 && values[61] == 86); + assert(values[62] == 100 && values[63] == 165 && values[64] == 53); + assert(std::memcmp(values + CarState::VIN_OFFSET, state.vin, 17) == 0); + // Truncation must preserve the entire previous update, including mask. + decoder.updateFrame(1, 0x679, candidate, 6); + decoder.updateMapper(); + assert(state.ambientLightingSectionMaskRaw == 53 && state.ambientLightingRed == 18); + // Latest section-specific update supplies the global accent; no labels. + const uint8_t white[] = {0, 255, 255, 255, 0, 8, 0}; + decoder.updateFrame(1, 0x679, white, 7); + decoder.updateMapper(); + assert(state.ambientLightingRed == 255 && state.ambientLightingBrightness == 0); + assert(state.ambientLightingSectionMaskRaw == 1 && state.ambientLightingEnableEffectRaw == 0); + const uint8_t black[] = {255, 0, 0, 0, 255, 0, 0}; + decoder.updateFrame(1, 0x679, black, 7); + assert(state.ambientLightingAvailable && state.ambientLightingBlue == 0); + assert(state.ambientLightingBrightness == 127); // retain masked raw value, don't clamp + for (int mask = 0; mask < 64; ++mask) { + uint8_t data[] = {0, 1, 2, 3, 0, static_cast(mask << 3), + static_cast(mask >> 5)}; + decoder.updateFrame(1, 0x679, data, 7); + assert(state.ambientLightingSectionMaskRaw == mask); + } + } + VehicleDecoder other({}, {}, "tesla_party"); + other.updateFrame(1, 0x679, candidate, 8); + assert(!other.state().ambientLightingAvailable); +} diff --git a/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp b/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp index dde650c5..343eb364 100644 --- a/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp +++ b/dashpilot-android/bridge/tests/tesla_closure_mapper_test.cpp @@ -179,7 +179,7 @@ void closures_round_trip_through_the_double_array() { double out[CarState::FIELD_COUNT]; h.cs.toArray(out); - CHECK(CarState::DOOR_OFFSET == 51 && CarState::FIELD_COUNT == 58); + CHECK(CarState::DOOR_OFFSET == 51 && CarState::DOOR_COUNT == 7); CHECK(out[CarState::DOOR_OFFSET + 0] == 1); // FL CHECK(out[CarState::DOOR_OFFSET + 1] == 0); // FR CHECK(out[CarState::DOOR_OFFSET + 2] == 0); // RL diff --git a/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift b/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift index 4614f9ef..b4dbaef4 100644 --- a/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift +++ b/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift @@ -89,6 +89,7 @@ final class DashKitDataSource: IDataSource { var offset = 1 var state = BridgeCarState() var decodedAny = false + var decodedAmbientLighting = false for _ in 0..= 7) bytes[offset..<(offset + len)].withUnsafeBufferPointer { buf in bridge_decode_can_frame(decoder, Int32(bus), addr, buf.baseAddress, Int32(len), &state) } @@ -115,7 +117,9 @@ final class DashKitDataSource: IDataSource { guard decodedAny else { return } let now = ProcessInfo.processInfo.systemUptime - guard now - lastEmit >= Self.emitInterval else { return } + // Ambient updates may be infrequent. Deliver even a final update that + // arrives inside the throttle window, without waiting for another frame. + guard decodedAmbientLighting || now - lastEmit >= Self.emitInterval else { return } lastEmit = now continuation?.yield(CarState(from: state)) } diff --git a/dashpilot-ios/dashpilot/Models/CarState.swift b/dashpilot-ios/dashpilot/Models/CarState.swift index 0efeb187..752b71ca 100644 --- a/dashpilot-ios/dashpilot/Models/CarState.swift +++ b/dashpilot-ios/dashpilot/Models/CarState.swift @@ -37,6 +37,16 @@ struct CarState: Codable { var hvacKeepClimateOn: Float = 0 var powerLevel: Float = 0 + // Candidate Tesla ambient lighting; unknown until a complete frame arrives. + var ambientLightingAvailable: Bool = false + var ambientLightingHex: String = "" + var ambientLightingRed: Float = 0 + var ambientLightingGreen: Float = 0 + var ambientLightingBlue: Float = 0 + var ambientLightingBrightness: Float = 0 + var ambientLightingEnableEffectRaw: Float = 0 + var ambientLightingSectionMaskRaw: Float = 0 + // Openpilot state var madsActive: Bool = false var selfdriveActive: Bool = false @@ -102,6 +112,17 @@ struct CarState: Codable { hvacRecirc = Float(bridge.hvacRecirc) hvacKeepClimateOn = Float(bridge.hvacKeepClimateOn) powerLevel = Float(bridge.powerLevel) + ambientLightingAvailable = bridge.ambientLightingAvailable > 0 + ambientLightingRed = Float(bridge.ambientLightingRed) + ambientLightingGreen = Float(bridge.ambientLightingGreen) + ambientLightingBlue = Float(bridge.ambientLightingBlue) + ambientLightingBrightness = Float(bridge.ambientLightingBrightness) + ambientLightingEnableEffectRaw = Float(bridge.ambientLightingEnableEffectRaw) + ambientLightingSectionMaskRaw = Float(bridge.ambientLightingSectionMaskRaw) + if ambientLightingAvailable { + ambientLightingHex = String(format: "#%02X%02X%02X", + Int(ambientLightingRed), Int(ambientLightingGreen), Int(ambientLightingBlue)) + } madsActive = bridge.madsActive > 0 selfdriveActive = bridge.selfdriveActive > 0 experimentalMode = bridge.experimentalMode > 0 @@ -183,6 +204,14 @@ struct CarState: Codable { hvacRecirc = try c.decodeIfPresent(Float.self, forKey: .hvacRecirc) ?? 0 hvacKeepClimateOn = try c.decodeIfPresent(Float.self, forKey: .hvacKeepClimateOn) ?? 0 powerLevel = try c.decodeIfPresent(Float.self, forKey: .powerLevel) ?? 0 + ambientLightingAvailable = try c.decodeIfPresent(Bool.self, forKey: .ambientLightingAvailable) ?? false + ambientLightingHex = try c.decodeIfPresent(String.self, forKey: .ambientLightingHex) ?? "" + ambientLightingRed = try c.decodeIfPresent(Float.self, forKey: .ambientLightingRed) ?? 0 + ambientLightingGreen = try c.decodeIfPresent(Float.self, forKey: .ambientLightingGreen) ?? 0 + ambientLightingBlue = try c.decodeIfPresent(Float.self, forKey: .ambientLightingBlue) ?? 0 + ambientLightingBrightness = try c.decodeIfPresent(Float.self, forKey: .ambientLightingBrightness) ?? 0 + ambientLightingEnableEffectRaw = try c.decodeIfPresent(Float.self, forKey: .ambientLightingEnableEffectRaw) ?? 0 + ambientLightingSectionMaskRaw = try c.decodeIfPresent(Float.self, forKey: .ambientLightingSectionMaskRaw) ?? 0 madsActive = try c.decodeIfPresent(Bool.self, forKey: .madsActive) ?? false selfdriveActive = try c.decodeIfPresent(Bool.self, forKey: .selfdriveActive) ?? false experimentalMode = try c.decodeIfPresent(Bool.self, forKey: .experimentalMode) ?? false diff --git a/dashpilot-ios/dashpilot/Models/DashValues.swift b/dashpilot-ios/dashpilot/Models/DashValues.swift index 4994274b..69eceaf5 100644 --- a/dashpilot-ios/dashpilot/Models/DashValues.swift +++ b/dashpilot-ios/dashpilot/Models/DashValues.swift @@ -4,13 +4,22 @@ enum DashValue { case number(Float) case bool(Bool) case string(String) + case color(red: Float, green: Float, blue: Float) } extension DashState { func dashValues(systemDarkMode: Bool? = nil) -> [(String, DashValue)] { let c = carState let d = displaySettings - return [ + var values: [(String, DashValue)] = [ + ("ambientLightingAvailable", .bool(c.ambientLightingAvailable)), + ("ambientLightingHex", .string(c.ambientLightingHex)), + ("ambientLightingRed", .number(c.ambientLightingRed)), + ("ambientLightingGreen", .number(c.ambientLightingGreen)), + ("ambientLightingBlue", .number(c.ambientLightingBlue)), + ("ambientLightingBrightness", .number(c.ambientLightingBrightness)), + ("ambientLightingEnableEffectRaw", .number(c.ambientLightingEnableEffectRaw)), + ("ambientLightingSectionMaskRaw", .number(c.ambientLightingSectionMaskRaw)), ("egoSpeed", .number(c.egoSpeed)), ("gear", .number(c.gear)), ("egoSteeringAngle", .number(c.egoSteeringAngle)), @@ -72,6 +81,11 @@ extension DashState { ("renderQuality", .number(Float(d.renderQuality))), ("dataSourceType", .string(dataSourceType?.rawValue ?? "")), ] + if c.ambientLightingAvailable { + values.append(("ambientLightingColor", .color(red: c.ambientLightingRed, + green: c.ambientLightingGreen, blue: c.ambientLightingBlue))) + } + return values } private static func secondsSinceLocalMidnight(_ epochMillis: Int64) -> Float { diff --git a/dashpilot-ios/dashpilot/UI/RiveDashView.swift b/dashpilot-ios/dashpilot/UI/RiveDashView.swift index df10aecd..1d5248c9 100644 --- a/dashpilot-ios/dashpilot/UI/RiveDashView.swift +++ b/dashpilot-ios/dashpilot/UI/RiveDashView.swift @@ -65,6 +65,7 @@ final class RiveDashBinder { private var numbers: [String: RiveDataBindingViewModel.Instance.NumberProperty] = [:] private var bools: [String: RiveDataBindingViewModel.Instance.BooleanProperty] = [:] private var strings: [String: RiveDataBindingViewModel.Instance.StringProperty] = [:] + private var colors: [String: RiveDataBindingViewModel.Instance.ColorProperty] = [:] private var missing: Set = [] private var systemDarkMode = false @@ -118,6 +119,11 @@ final class RiveDashBinder { guard let p = strings[name] ?? instance.stringProperty(fromPath: name) else { missing.insert(name); continue } strings[name] = p p.value = v + case .color(let red, let green, let blue): + guard let p = colors[name] ?? instance.colorProperty(fromPath: name) else { missing.insert(name); continue } + colors[name] = p + p.value = UIColor(red: CGFloat(red) / 255, green: CGFloat(green) / 255, + blue: CGFloat(blue) / 255, alpha: 1) } } if !riveViewModel.isPlaying { From 89b588dfacc48124e8e9638b7ad990d9f9fe4cef Mon Sep 17 00:00:00 2001 From: progphil1337 Date: Fri, 13 Nov 2026 10:32:39 +0100 Subject: [PATCH 2/2] Address ambient lighting review feedback Decode through the Tesla DBC mappers and expose color, brightness, and availability. Restore the normal iOS telemetry throttle and simplify the docs and tests. Ambient lighting behavior confirmed in-car by the author. --- bridge/bridge_ios.cpp | 6 +- bridge/bridge_ios.h | 6 +- bridge/car/can_parsers.h | 5 +- bridge/car/car_state.h | 16 +--- bridge/car/cars/tesla.h | 13 ++- bridge/car/tesla_ambient_lighting.h | 19 ---- bridge/common/vehicle_decoder.h | 4 - dash-apps/README.md | 44 ++-------- .../vehicles/tesla/bus_1_tesla_vehicle.dbc | 6 ++ .../assets/vehicles/tesla/new_gen_juniper.dbc | 6 ++ .../dashpilot/datamodel/dash/CarState.kt | 10 +-- .../dashpilot/datamodel/dash/DashValues.kt | 14 +-- .../dashpilot/ui/RiveDashView.kt | 33 ++----- .../dashpilot/vehicle/CanFrameDecoder.kt | 10 +-- .../ui/AmbientLightingBindingTest.kt | 84 ------------------ dashpilot-android/bridge/CMakeLists.txt | 5 +- .../tests/tesla_ambient_lighting_test.cpp | 87 +++++++------------ .../DataSource/DashKitDataSource.swift | 6 +- dashpilot-ios/dashpilot/Models/CarState.swift | 25 +----- .../dashpilot/Models/DashValues.swift | 11 +-- .../vehicles/tesla/bus_1_tesla_vehicle.dbc | 6 ++ .../vehicles/tesla/new_gen_juniper.dbc | 6 ++ dashpilot-ios/dashpilot/UI/RiveDashView.swift | 7 +- 23 files changed, 111 insertions(+), 318 deletions(-) delete mode 100644 bridge/car/tesla_ambient_lighting.h delete mode 100644 dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt diff --git a/bridge/bridge_ios.cpp b/bridge/bridge_ios.cpp index edb8dc05..3427fce0 100644 --- a/bridge/bridge_ios.cpp +++ b/bridge/bridge_ios.cpp @@ -63,12 +63,8 @@ static void carStateToBridge(const CarState& cs, BridgeCarState* out) { out->trunkPosition = cs.trunkPosition; out->frunkOpen = cs.frunkOpen; out->ambientLightingAvailable = cs.ambientLightingAvailable; - out->ambientLightingRed = cs.ambientLightingRed; - out->ambientLightingGreen = cs.ambientLightingGreen; - out->ambientLightingBlue = cs.ambientLightingBlue; + out->ambientLightingColor = static_cast(cs.ambientLightingColor); out->ambientLightingBrightness = cs.ambientLightingBrightness; - out->ambientLightingEnableEffectRaw = cs.ambientLightingEnableEffectRaw; - out->ambientLightingSectionMaskRaw = cs.ambientLightingSectionMaskRaw; } static std::atomic receiveLoopRunning{false}; diff --git a/bridge/bridge_ios.h b/bridge/bridge_ios.h index ef4a44c2..4fba705b 100644 --- a/bridge/bridge_ios.h +++ b/bridge/bridge_ios.h @@ -75,12 +75,8 @@ typedef struct { double frunkOpen; double ambientLightingAvailable; - double ambientLightingRed; - double ambientLightingGreen; - double ambientLightingBlue; + uint32_t ambientLightingColor; double ambientLightingBrightness; - double ambientLightingEnableEffectRaw; - double ambientLightingSectionMaskRaw; } BridgeCarState; // Context diff --git a/bridge/car/can_parsers.h b/bridge/car/can_parsers.h index 60dfc2b0..0bf96d6e 100644 --- a/bridge/car/can_parsers.h +++ b/bridge/car/can_parsers.h @@ -60,7 +60,8 @@ class CANParsers { // Unscaled signal bits; false if the signal is unknown, no frame has been // seen, or the frame's multiplexor doesn't select this signal. - bool getRaw(int bus, const std::string& msgName, const std::string& sigName, uint64_t* out) const { + bool getRaw(int bus, const std::string& msgName, const std::string& sigName, uint64_t* out, + size_t minLength = 0) const { int maskedBus = bus & 3; auto busIt = busSignalCache_.find(maskedBus); if (busIt == busSignalCache_.end()) return false; @@ -70,7 +71,7 @@ class CANParsers { const auto& cached = it->second; auto frameIt = latestFrames_.find(frameKey(maskedBus, cached.address)); - if (frameIt == latestFrames_.end()) return false; + if (frameIt == latestFrames_.end() || frameIt->second.size() < minLength) return false; return cached.signal->getRawBits(frameIt->second.data(), frameIt->second.size(), out); } diff --git a/bridge/car/car_state.h b/bridge/car/car_state.h index b3e44c96..e6714568 100644 --- a/bridge/car/car_state.h +++ b/bridge/car/car_state.h @@ -86,14 +86,10 @@ struct CarState { static constexpr size_t DOOR_COUNT = 7; // Append fields after closures to preserve all existing array indices. double ambientLightingAvailable = 0; - double ambientLightingRed = 0; - double ambientLightingGreen = 0; - double ambientLightingBlue = 0; + double ambientLightingColor = 0; // Packed RGB (0xRRGGBB). double ambientLightingBrightness = 0; - double ambientLightingEnableEffectRaw = 0; - double ambientLightingSectionMaskRaw = 0; static constexpr size_t AMBIENT_OFFSET = DOOR_OFFSET + DOOR_COUNT; - static constexpr size_t FIELD_COUNT = AMBIENT_OFFSET + 7; + static constexpr size_t FIELD_COUNT = AMBIENT_OFFSET + 3; void toArray(double* out) const { // Party bus @@ -169,11 +165,7 @@ struct CarState { out[DOOR_OFFSET + 6] = frunkOpen; out[AMBIENT_OFFSET + 0] = ambientLightingAvailable; - out[AMBIENT_OFFSET + 1] = ambientLightingRed; - out[AMBIENT_OFFSET + 2] = ambientLightingGreen; - out[AMBIENT_OFFSET + 3] = ambientLightingBlue; - out[AMBIENT_OFFSET + 4] = ambientLightingBrightness; - out[AMBIENT_OFFSET + 5] = ambientLightingEnableEffectRaw; - out[AMBIENT_OFFSET + 6] = ambientLightingSectionMaskRaw; + out[AMBIENT_OFFSET + 1] = ambientLightingColor; + out[AMBIENT_OFFSET + 2] = ambientLightingBrightness; } }; diff --git a/bridge/car/cars/tesla.h b/bridge/car/cars/tesla.h index 6b8fc596..0444516c 100644 --- a/bridge/car/cars/tesla.h +++ b/bridge/car/cars/tesla.h @@ -138,6 +138,17 @@ static inline void updateVehicleBus(const CANParsers& cp, CarState& cs) { cs.packTMax = cp.get(1, "BMS_bmbMinMax", "BMS_thermistorTMax"); } cs.odometer = cp.get(1, "DI_odometerStatus", "DI_odometer"); + + constexpr auto ambient = "UI_ambientLightingCtrls"; + uint64_t brightness = 0; + if (cp.getRaw(1, ambient, "UI_ambientLightingBrightness", &brightness, 7)) { + const auto red = static_cast(cp.get(1, ambient, "UI_ambientLightingRed")); + const auto green = static_cast(cp.get(1, ambient, "UI_ambientLightingGreen")); + const auto blue = static_cast(cp.get(1, ambient, "UI_ambientLightingBlue")); + cs.ambientLightingColor = (red << 16) | (green << 8) | blue; + cs.ambientLightingBrightness = brightness; + cs.ambientLightingAvailable = 1; + } } @@ -291,4 +302,4 @@ class TeslaJuniperMapper : public CarStateMapper { BlinkerHold leftBlinkerHold_; BlinkerHold rightBlinkerHold_; TeslaVinUpdater vin_; -}; \ No newline at end of file +}; diff --git a/bridge/car/tesla_ambient_lighting.h b/bridge/car/tesla_ambient_lighting.h deleted file mode 100644 index 1b42fd08..00000000 --- a/bridge/car/tesla_ambient_lighting.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "car_state.h" -#include - -// Candidate UI_ambientLightingCtrls layout; pending vehicle validation. -// Latest complete update wins for the dashboard accent, regardless of section. -// Byte 0 and the section mask deliberately remain uninterpreted. -inline void updateTeslaAmbientLighting(CarState& state, int bus, uint32_t address, - const uint8_t* data, size_t len) { - if (bus != 1 || address != 0x679 || len < 7 || !data) return; - state.ambientLightingRed = data[1]; - state.ambientLightingGreen = data[2]; - state.ambientLightingBlue = data[3]; - state.ambientLightingBrightness = data[4] & 0x7F; - state.ambientLightingEnableEffectRaw = data[0]; - state.ambientLightingSectionMaskRaw = ((data[5] >> 3) | (data[6] << 5)) & 0x3F; - state.ambientLightingAvailable = 1; -} diff --git a/bridge/common/vehicle_decoder.h b/bridge/common/vehicle_decoder.h index 62e600b4..2a2bf74a 100644 --- a/bridge/common/vehicle_decoder.h +++ b/bridge/common/vehicle_decoder.h @@ -3,7 +3,6 @@ #include "car/can_parsers.h" #include "car/car_state.h" #include "car/car_state_mapper.h" -#include "car/tesla_ambient_lighting.h" #include "car/cars/tesla.h" #include "msgq/ipc.h" #include @@ -31,11 +30,9 @@ class VehicleDecoder { } parsers_.buildCache(); mapper_ = createMapper(vehicleType); - decodeAmbientLighting_ = vehicleType == "dashkit" || vehicleType == "tesla_juniper"; } void updateFrame(int bus, uint32_t address, const uint8_t* data, size_t len) { - if (decodeAmbientLighting_) updateTeslaAmbientLighting(state_, bus, address, data, len); parsers_.updateFrame(bus, address, data, len); } @@ -49,5 +46,4 @@ class VehicleDecoder { CANParsers parsers_; std::unique_ptr mapper_; CarState state_; - bool decodeAmbientLighting_ = false; }; diff --git a/dash-apps/README.md b/dash-apps/README.md index f59b8879..c89cb2cb 100644 --- a/dash-apps/README.md +++ b/dash-apps/README.md @@ -121,45 +121,17 @@ The app plays the file's default state machine and resizes the artboard to the s To try your own `.riv` on a phone, go to Settings → Developer → **Load Rive File**. -### Tesla interior ambient lighting (candidate) +### Tesla interior ambient lighting -DashKit and Juniper profiles receive standard CAN ID `0x679` -(`UI_ambientLightingCtrls`) on Vehicle CAN, bus 1. This candidate layout is -pending vehicle validation. It requires at least seven payload bytes and adds -no vehicle writes, so it also works on read-only BLE connections. +DashKit dashboards can bind these optional Rive properties: -Declare these optional properties on the default `DashViewModel` instance: - -| Rive property | Rive type | meaning | +| Property | Type | Value | |---|---|---| -| `ambientLightingColor` | Color | Opaque RGB color, independent of brightness | -| `ambientLightingHex` | String | Uppercase, zero-padded `#RRGGBB`; also retained in app CarState | -| `ambientLightingRed`, `ambientLightingGreen`, `ambientLightingBlue` | Number | Unsigned byte values, 0–255 | -| `ambientLightingBrightness` | Number | Byte 4 masked with `0x7F`; documented range 0–100 percent | -| `ambientLightingEnableEffectRaw` | Number | Uninterpreted byte 0, 0–255; not an on/off flag | -| `ambientLightingSectionMaskRaw` | Number | Uninterpreted bits 43–48, 0–63; no section labels are assigned | -| `ambientLightingAvailable` | Boolean | A complete ambient frame has been received in this session | - -Bind `ambientLightingColor` to your accent fill/stroke color. Bind brightness -separately if desired (for example, divide by 100 in a Rive converter for -opacity); the host never scales RGB or alpha by brightness. These properties -are added to the Rive interface; Android's web JavaScript interface is unchanged. - -There is no existing dashboard section-priority convention. The global accent -therefore uses the **latest complete frame**, including section-specific frames; -the raw mask and RGB/brightness belong to that same update. Dashboards can use -the mask to implement their own selection policy once vehicle validation -establishes its meaning. - -Before the first frame, availability is false, hex is empty, numeric values -are zero, and the Color property is left at its authored value. Use availability -to select your fallback accent. Like other CAN telemetry, the latest value is -held without a timeout until the session is reset; truncated or unrelated -frames cannot clear it. Mounted dashboards receive updates and new dashboard -subscriptions replay the session's latest state. - -For example, `A5 12 34 56 E4 AD FF 5A` gives `#123456`, RGB (18, 52, 86), -brightness 100, raw enable/effect 165, and raw section mask 53. +| `ambientLightingColor` | Color | Opaque interior accent color | +| `ambientLightingBrightness` | Number | Brightness percent, independently of color | +| `ambientLightingAvailable` | Boolean | Whether the app has received a valid lighting frame | + +Use availability to select a fallback accent. Color and brightness come from the latest valid frame. ## Saving settings diff --git a/dashpilot-android/app/src/main/assets/vehicles/tesla/bus_1_tesla_vehicle.dbc b/dashpilot-android/app/src/main/assets/vehicles/tesla/bus_1_tesla_vehicle.dbc index 958cb5ef..ab7be151 100644 --- a/dashpilot-android/app/src/main/assets/vehicles/tesla/bus_1_tesla_vehicle.dbc +++ b/dashpilot-android/app/src/main/assets/vehicles/tesla/bus_1_tesla_vehicle.dbc @@ -9141,6 +9141,12 @@ BO_ 505 VCSEC_requests: 1 VEH SG_ VCSEC_chargePortRequest: 0|2@1+ (1,0) [0|0] "" X SG_ VCSEC_driveAttemptedWithoutAuth: 2|1@1+ (1,0) [0|0] "" X +BO_ 1657 UI_ambientLightingCtrls: 8 VEH + SG_ UI_ambientLightingRed : 8|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingGreen : 16|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBlue : 24|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBrightness : 32|7@1+ (1,0) [0|100] "%" X + CM_ "Generated by Tesla Firmware Decode @ version 923e442d6230241a259eeb3f415525373d40325c"; BA_DEF_ BO_ "GenMsgSendType" ENUM "Cyclic"; diff --git a/dashpilot-android/app/src/main/assets/vehicles/tesla/new_gen_juniper.dbc b/dashpilot-android/app/src/main/assets/vehicles/tesla/new_gen_juniper.dbc index 8ce08557..07ba6fd4 100644 --- a/dashpilot-android/app/src/main/assets/vehicles/tesla/new_gen_juniper.dbc +++ b/dashpilot-android/app/src/main/assets/vehicles/tesla/new_gen_juniper.dbc @@ -521,6 +521,12 @@ BO_ 1029 VIN_info: 8 VEH SG_ VIN_C405 m18 : 8|56@1+ (1,0) [0|7.2057594038E+016] "" X +BO_ 1657 UI_ambientLightingCtrls: 8 VEH + SG_ UI_ambientLightingRed : 8|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingGreen : 16|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBlue : 24|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBrightness : 32|7@1+ (1,0) [0|100] "%" X + CM_ "New-generation Model Y (Juniper), Tesla FW 2026.x. Reverse-engineered from comma route 077b771458fd542f/000000a9--5f9f91a356 (2026-09-22 16:49-17:05) against an annotated action timeline; names and enums cross-checked with the Tesla FW 2026.2 signal dataset. Covers the messages listed in dbc/used_messages.txt plus DI_state (for DashPilot). Node VEH = seen on the vehicle bus (comma bus 1, DashKit bus 1); node CH = seen ONLY on the chassis bus (comma bus 0/2, DashKit bus 0). Each message comment lists the buses it was observed on. Bytesum checksum = (id_lo + id_hi + sum of data bytes except the checksum byte) & 0xFF."; CM_ BO_ 585 "CHANGED. Vehicle bus only, 20 Hz. DLC 3 -> 4. Byte 0 is no longer the Tesla bytesum (0 of 19597 frames match) and no CRC-8 matched: Tesla calls it SCCM_leftStalkCrc and the firmware cannot synthesize this frame until the algorithm is known. Bits 20-31 are a new 12-bit stalk angle. SCCM_washWipeButtonStatus and SCCM_highBeamStalkStatus (bits 12-15) never left 0 in the whole log, including the wiper button press at 16:49:47.9: the wiper button now reports through VCLEFT_switchStatus.VCLEFT_swcWiperButtonState."; CM_ SG_ 585 SCCM_turnIndicatorStalkStatus "Enum changed: right tap (16:53:31) = 2 UP_1, left tap (16:53:34, 16:53:35) = 6 DOWN_1 (legacy DOWN_1 was 7)."; 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 41ae8411..ba5d8c49 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 @@ -69,15 +69,9 @@ data class CarState( // Empty until fully assembled from the CAN stream by the native mapper. val vin: String = "", - // Candidate Tesla ambient lighting; unknown until a complete frame arrives. val ambientLightingAvailable: Boolean = false, - val ambientLightingHex: String = "", - val ambientLightingRed: Float = 0f, - val ambientLightingGreen: Float = 0f, - val ambientLightingBlue: Float = 0f, + val ambientLightingColor: Int = 0, // Packed RGB (0xRRGGBB). val ambientLightingBrightness: Float = 0f, - val ambientLightingEnableEffectRaw: Float = 0f, - val ambientLightingSectionMaskRaw: Float = 0f, ) { fun toImperial(): CarState = copy( odometer = odometer * KM_TO_MILES, @@ -89,7 +83,7 @@ data class CarState( ) companion object { - const val FIELD_COUNT = 65 + const val FIELD_COUNT = 61 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 055d7f82..657810f1 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 @@ -3,23 +3,12 @@ package com.softwiredtech.dashpilot.datamodel.dash import java.time.Instant import java.time.ZoneId -// Opaque ARGB for Rive's typed Color API; brightness is a separate property. -data class DashColor(val red: Int, val green: Int, val blue: Int) { - val argb: Int get() = (0xFF shl 24) or (red shl 16) or (green shl 8) or blue -} - fun DashState.dashValues(systemDarkMode: Boolean? = null): Map { val c = carState val d = displaySettings val values = mutableMapOf( "ambientLightingAvailable" to c.ambientLightingAvailable, - "ambientLightingHex" to c.ambientLightingHex, - "ambientLightingRed" to c.ambientLightingRed, - "ambientLightingGreen" to c.ambientLightingGreen, - "ambientLightingBlue" to c.ambientLightingBlue, "ambientLightingBrightness" to c.ambientLightingBrightness, - "ambientLightingEnableEffectRaw" to c.ambientLightingEnableEffectRaw, - "ambientLightingSectionMaskRaw" to c.ambientLightingSectionMaskRaw, "egoSpeed" to c.egoSpeed, "gear" to c.gear, "egoSteeringAngle" to c.egoSteeringAngle, @@ -83,8 +72,7 @@ fun DashState.dashValues(systemDarkMode: Boolean? = null): Map { "speedCameraDistance" to speedCameraDistance.toFloat(), ) if (c.ambientLightingAvailable) { - values["ambientLightingColor"] = DashColor(c.ambientLightingRed.toInt(), - c.ambientLightingGreen.toInt(), c.ambientLightingBlue.toInt()) + values["ambientLightingColor"] = c.ambientLightingColor or 0xFF000000.toInt() } return values } diff --git a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt index 8a17d408..505d53ed 100644 --- a/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt +++ b/dashpilot-android/app/src/main/java/com/softwiredtech/dashpilot/ui/RiveDashView.kt @@ -27,7 +27,6 @@ import app.rive.rememberRiveWorkerOrNull import app.rive.rememberViewModelInstance import app.rive.runtime.kotlin.core.ViewModel.PropertyDataType import com.softwiredtech.dashpilot.datamodel.dash.DashState -import com.softwiredtech.dashpilot.datamodel.dash.DashColor import com.softwiredtech.dashpilot.datamodel.dash.dashValues import kotlinx.coroutines.flow.Flow @@ -123,29 +122,15 @@ private fun BoundRive( vmi.setBoolean("darkMode", systemDarkMode) } dashStateFlow.collect { state -> - applyRiveDashValues(properties, state.dashValues(systemDarkMode), - vmi::setNumber, vmi::setBoolean, vmi::setString, vmi::setColor) - } - } -} - -// Keep typed dispatch independent of the native Rive worker so it can be -// checked with successive telemetry snapshots as well as newly mounted files. -internal fun applyRiveDashValues( - properties: Map, - values: Map, - setNumber: (String, Float) -> Unit, - setBoolean: (String, Boolean) -> Unit, - setString: (String, String) -> Unit, - setColor: (String, Int) -> Unit, -) { - for ((name, value) in values) { - when (properties[name]) { - PropertyDataType.NUMBER -> (value as? Float)?.let { setNumber(name, it) } - PropertyDataType.BOOLEAN -> (value as? Boolean)?.let { setBoolean(name, it) } - PropertyDataType.STRING -> setString(name, value.toString()) - PropertyDataType.COLOR -> (value as? DashColor)?.let { setColor(name, it.argb) } - else -> {} + for ((name, value) in state.dashValues(systemDarkMode)) { + when (properties[name]) { + PropertyDataType.NUMBER -> (value as? Float)?.let { vmi.setNumber(name, it) } + PropertyDataType.BOOLEAN -> (value as? Boolean)?.let { vmi.setBoolean(name, it) } + PropertyDataType.STRING -> vmi.setString(name, value.toString()) + PropertyDataType.COLOR -> (value as? Int)?.let { vmi.setColor(name, it) } + else -> {} + } + } } } } 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 edcdc673..87daf9bd 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 @@ -88,14 +88,8 @@ class CanFrameDecoder( trunkPosition = values[56].toFloat(), frunkOpen = values[57].toFloat(), ambientLightingAvailable = values[58] > 0, - ambientLightingHex = if (values[58] > 0) - "#%02X%02X%02X".format(values[59].toInt(), values[60].toInt(), values[61].toInt()) else "", - ambientLightingRed = values[59].toFloat(), - ambientLightingGreen = values[60].toFloat(), - ambientLightingBlue = values[61].toFloat(), - ambientLightingBrightness = values[62].toFloat(), - ambientLightingEnableEffectRaw = values[63].toFloat(), - ambientLightingSectionMaskRaw = values[64].toFloat(), + ambientLightingColor = values[59].toInt(), + ambientLightingBrightness = values[60].toFloat(), ) } diff --git a/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt b/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt deleted file mode 100644 index 6d4bf42d..00000000 --- a/dashpilot-android/app/src/test/java/com/softwiredtech/dashpilot/ui/AmbientLightingBindingTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.softwiredtech.dashpilot.ui - -import app.rive.runtime.kotlin.core.ViewModel.PropertyDataType -import com.softwiredtech.dashpilot.datamodel.dash.CarState -import com.softwiredtech.dashpilot.datamodel.dash.DashColor -import com.softwiredtech.dashpilot.datamodel.dash.DashState -import com.softwiredtech.dashpilot.datamodel.dash.dashValues -import com.softwiredtech.dashpilot.vehicle.CanFrameDecoder -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.junit.Assert.* -import org.junit.Test - -class AmbientLightingBindingTest { - private fun snapshot(red: Int, green: Int, blue: Int, brightness: Int = 100, mask: Int = 53): DashState { - val values = DoubleArray(CarState.FIELD_COUNT) - values[39] = 2.5 // Existing TPMS slots must stay independent of ambient lighting. - values[46] = 31.0 - values[58] = 1.0 - values[59] = red.toDouble() - values[60] = green.toDouble() - values[61] = blue.toDouble() - values[62] = brightness.toDouble() - values[63] = 165.0 - values[64] = mask.toDouble() - return DashState(carState = CanFrameDecoder.arrayToCarState(values)) - } - - @Test fun candidate_state_and_hex_edge_cases() { - val state = snapshot(18, 52, 86).carState - assertEquals("#123456", state.ambientLightingHex) - assertEquals(100f, state.ambientLightingBrightness) - assertEquals(165f, state.ambientLightingEnableEffectRaw) - assertEquals(53f, state.ambientLightingSectionMaskRaw) - assertEquals(2.5f, state.tpmsPressureFL) - assertEquals(31f, state.tpmsTempRR) - assertEquals(state.ambientLightingHex, state.toImperial().ambientLightingHex) - assertEquals("#01020A", snapshot(1, 2, 10).carState.ambientLightingHex) - assertEquals("#000000", snapshot(0, 0, 0).carState.ambientLightingHex) - assertEquals("#FFFFFF", snapshot(255, 255, 255).carState.ambientLightingHex) - assertEquals(DashColor(255, 255, 255), snapshot(255, 255, 255, 0).dashValues()["ambientLightingColor"]) - val unknown = CanFrameDecoder.arrayToCarState(DoubleArray(CarState.FIELD_COUNT)) - assertFalse(unknown.ambientLightingAvailable) - assertEquals("", unknown.ambientLightingHex) - assertFalse(DashState(unknown).dashValues().containsKey("ambientLightingColor")) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test fun live_updates_and_mount_replay_dispatch_typed_opaque_colors() = runTest { - // DashKit's telemetry stream replays the latest snapshot to each mount. - val stream = MutableSharedFlow(replay = 1) - val properties = mapOf( - "ambientLightingColor" to PropertyDataType.COLOR, - "ambientLightingHex" to PropertyDataType.STRING, - "ambientLightingBrightness" to PropertyDataType.NUMBER, - "ambientLightingSectionMaskRaw" to PropertyDataType.NUMBER, - "ambientLightingAvailable" to PropertyDataType.BOOLEAN, - ) - val writes = mutableMapOf() - stream.emit(snapshot(18, 52, 86)) - fun mount(target: MutableMap) = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { - stream.collect { state -> - applyRiveDashValues(properties, state.dashValues(), - { k, v -> target[k] = v }, { k, v -> target[k] = v }, - { k, v -> target[k] = v }, { k, v -> target[k] = v }) - } - } - val firstMount = mount(writes) - assertEquals(0xFF123456.toInt(), writes["ambientLightingColor"]) - assertEquals(100f, writes["ambientLightingBrightness"]) - stream.emit(snapshot(255, 255, 255, brightness = 0, mask = 1)) - assertEquals(0xFFFFFFFF.toInt(), writes["ambientLightingColor"]) - assertEquals(0f, writes["ambientLightingBrightness"]) - assertEquals(1f, writes["ambientLightingSectionMaskRaw"]) - firstMount.cancel() - val remounted = mutableMapOf() - mount(remounted) - assertEquals(writes, remounted) - assertFalse(writes.containsKey("egoSpeed")) // undeclared bindings skipped - } -} diff --git a/dashpilot-android/bridge/CMakeLists.txt b/dashpilot-android/bridge/CMakeLists.txt index 58ddf3fc..baf75aac 100644 --- a/dashpilot-android/bridge/CMakeLists.txt +++ b/dashpilot-android/bridge/CMakeLists.txt @@ -102,5 +102,8 @@ add_executable(tesla_ambient_lighting_test ) target_include_directories(tesla_ambient_lighting_test PRIVATE ${MSGQ_ROOT}) target_compile_options(tesla_ambient_lighting_test PRIVATE -Wall -Wextra) -add_test(NAME tesla_ambient_lighting COMMAND tesla_ambient_lighting_test) +set(TESLA_DBC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../app/src/main/assets/vehicles/tesla") +add_test(NAME tesla_ambient_lighting COMMAND tesla_ambient_lighting_test + "${TESLA_DBC_DIR}/bus_1_tesla_vehicle.dbc" + "${TESLA_DBC_DIR}/new_gen_juniper.dbc") endif() diff --git a/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp b/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp index 6c3c0584..218aa7d8 100644 --- a/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp +++ b/dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp @@ -1,62 +1,35 @@ #include "common/vehicle_decoder.h" #include -#include +#include +#include -int main() { - const uint8_t candidate[] = {0xA5, 0x12, 0x34, 0x56, 0xE4, 0xAD, 0xFF, 0x5A}; - for (const auto* type : {"dashkit", "tesla_juniper"}) { - VehicleDecoder decoder({}, {}, type); - auto& state = decoder.state(); - assert(!state.ambientLightingAvailable); - for (size_t len = 0; len < 7; ++len) { - decoder.updateFrame(1, 0x679, candidate, len); - decoder.updateMapper(); - assert(!state.ambientLightingAvailable); - } - for (int bus : {0, 2, 5}) { - decoder.updateFrame(bus, 0x679, candidate, 8); - assert(!state.ambientLightingAvailable); - } - decoder.updateFrame(1, 0x678, candidate, 8); - decoder.updateFrame(1, 0x80000679, candidate, 8); - assert(!state.ambientLightingAvailable); - decoder.updateFrame(1, 0x679, candidate, 8); +static void check(const char* dbcPath, const char* profile) { + std::ifstream dbc(dbcPath); + assert(dbc); + const std::string content{std::istreambuf_iterator(dbc), {}}; + VehicleDecoder decoder({content, content}, {0, 1}, profile); + const auto& state = decoder.state(); + const uint8_t payload[] = {0xA5, 0x12, 0x34, 0x56, 0xE4, 0xAD, 0xFF, 0x5A}; + const auto feed = [&](int bus, uint32_t id, const uint8_t* data, size_t length) { + decoder.updateFrame(bus, id, data, length); decoder.updateMapper(); - assert(state.ambientLightingAvailable); - assert(state.ambientLightingRed == 18 && state.ambientLightingGreen == 52); - assert(state.ambientLightingBlue == 86 && state.ambientLightingBrightness == 100); - assert(state.ambientLightingEnableEffectRaw == 165 && state.ambientLightingSectionMaskRaw == 53); - std::strcpy(state.vin, "5YJ3E7EB1MF123456"); - state.tpmsPressureFL = 2.5; - state.tpmsTempRR = 31; - double values[CarState::FIELD_COUNT] = {}; - state.toArray(values); - assert(values[39] == 2.5 && values[46] == 31); - assert(values[58] == 1 && values[59] == 18 && values[60] == 52 && values[61] == 86); - assert(values[62] == 100 && values[63] == 165 && values[64] == 53); - assert(std::memcmp(values + CarState::VIN_OFFSET, state.vin, 17) == 0); - // Truncation must preserve the entire previous update, including mask. - decoder.updateFrame(1, 0x679, candidate, 6); - decoder.updateMapper(); - assert(state.ambientLightingSectionMaskRaw == 53 && state.ambientLightingRed == 18); - // Latest section-specific update supplies the global accent; no labels. - const uint8_t white[] = {0, 255, 255, 255, 0, 8, 0}; - decoder.updateFrame(1, 0x679, white, 7); - decoder.updateMapper(); - assert(state.ambientLightingRed == 255 && state.ambientLightingBrightness == 0); - assert(state.ambientLightingSectionMaskRaw == 1 && state.ambientLightingEnableEffectRaw == 0); - const uint8_t black[] = {255, 0, 0, 0, 255, 0, 0}; - decoder.updateFrame(1, 0x679, black, 7); - assert(state.ambientLightingAvailable && state.ambientLightingBlue == 0); - assert(state.ambientLightingBrightness == 127); // retain masked raw value, don't clamp - for (int mask = 0; mask < 64; ++mask) { - uint8_t data[] = {0, 1, 2, 3, 0, static_cast(mask << 3), - static_cast(mask >> 5)}; - decoder.updateFrame(1, 0x679, data, 7); - assert(state.ambientLightingSectionMaskRaw == mask); - } - } - VehicleDecoder other({}, {}, "tesla_party"); - other.updateFrame(1, 0x679, candidate, 8); - assert(!other.state().ambientLightingAvailable); + }; + feed(0, 0x679, payload, 8); + feed(1, 0x678, payload, 8); + feed(1, 0x679, payload, 6); + assert(!state.ambientLightingAvailable); + feed(1, 0x679, payload, 7); + assert(state.ambientLightingAvailable && state.ambientLightingColor == 0x123456); + assert(state.ambientLightingBrightness == 100); + const uint8_t black[8] = {}; + feed(1, 0x679, black, 6); + assert(state.ambientLightingColor == 0x123456 && state.ambientLightingBrightness == 100); + feed(1, 0x679, black, 8); + assert(state.ambientLightingAvailable && state.ambientLightingColor == 0 && state.ambientLightingBrightness == 0); +} + +int main(int argc, char** argv) { + if (argc != 3) return 1; + check(argv[1], "dashkit"); + check(argv[2], "tesla_juniper"); } diff --git a/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift b/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift index b4dbaef4..4614f9ef 100644 --- a/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift +++ b/dashpilot-ios/dashpilot/DataSource/DashKitDataSource.swift @@ -89,7 +89,6 @@ final class DashKitDataSource: IDataSource { var offset = 1 var state = BridgeCarState() var decodedAny = false - var decodedAmbientLighting = false for _ in 0..= 7) bytes[offset..<(offset + len)].withUnsafeBufferPointer { buf in bridge_decode_can_frame(decoder, Int32(bus), addr, buf.baseAddress, Int32(len), &state) } @@ -117,9 +115,7 @@ final class DashKitDataSource: IDataSource { guard decodedAny else { return } let now = ProcessInfo.processInfo.systemUptime - // Ambient updates may be infrequent. Deliver even a final update that - // arrives inside the throttle window, without waiting for another frame. - guard decodedAmbientLighting || now - lastEmit >= Self.emitInterval else { return } + guard now - lastEmit >= Self.emitInterval else { return } lastEmit = now continuation?.yield(CarState(from: state)) } diff --git a/dashpilot-ios/dashpilot/Models/CarState.swift b/dashpilot-ios/dashpilot/Models/CarState.swift index 752b71ca..e090b5eb 100644 --- a/dashpilot-ios/dashpilot/Models/CarState.swift +++ b/dashpilot-ios/dashpilot/Models/CarState.swift @@ -37,15 +37,9 @@ struct CarState: Codable { var hvacKeepClimateOn: Float = 0 var powerLevel: Float = 0 - // Candidate Tesla ambient lighting; unknown until a complete frame arrives. var ambientLightingAvailable: Bool = false - var ambientLightingHex: String = "" - var ambientLightingRed: Float = 0 - var ambientLightingGreen: Float = 0 - var ambientLightingBlue: Float = 0 + var ambientLightingColor: UInt32 = 0 // Packed RGB (0xRRGGBB). var ambientLightingBrightness: Float = 0 - var ambientLightingEnableEffectRaw: Float = 0 - var ambientLightingSectionMaskRaw: Float = 0 // Openpilot state var madsActive: Bool = false @@ -113,16 +107,8 @@ struct CarState: Codable { hvacKeepClimateOn = Float(bridge.hvacKeepClimateOn) powerLevel = Float(bridge.powerLevel) ambientLightingAvailable = bridge.ambientLightingAvailable > 0 - ambientLightingRed = Float(bridge.ambientLightingRed) - ambientLightingGreen = Float(bridge.ambientLightingGreen) - ambientLightingBlue = Float(bridge.ambientLightingBlue) + ambientLightingColor = bridge.ambientLightingColor ambientLightingBrightness = Float(bridge.ambientLightingBrightness) - ambientLightingEnableEffectRaw = Float(bridge.ambientLightingEnableEffectRaw) - ambientLightingSectionMaskRaw = Float(bridge.ambientLightingSectionMaskRaw) - if ambientLightingAvailable { - ambientLightingHex = String(format: "#%02X%02X%02X", - Int(ambientLightingRed), Int(ambientLightingGreen), Int(ambientLightingBlue)) - } madsActive = bridge.madsActive > 0 selfdriveActive = bridge.selfdriveActive > 0 experimentalMode = bridge.experimentalMode > 0 @@ -205,13 +191,8 @@ struct CarState: Codable { hvacKeepClimateOn = try c.decodeIfPresent(Float.self, forKey: .hvacKeepClimateOn) ?? 0 powerLevel = try c.decodeIfPresent(Float.self, forKey: .powerLevel) ?? 0 ambientLightingAvailable = try c.decodeIfPresent(Bool.self, forKey: .ambientLightingAvailable) ?? false - ambientLightingHex = try c.decodeIfPresent(String.self, forKey: .ambientLightingHex) ?? "" - ambientLightingRed = try c.decodeIfPresent(Float.self, forKey: .ambientLightingRed) ?? 0 - ambientLightingGreen = try c.decodeIfPresent(Float.self, forKey: .ambientLightingGreen) ?? 0 - ambientLightingBlue = try c.decodeIfPresent(Float.self, forKey: .ambientLightingBlue) ?? 0 + ambientLightingColor = try c.decodeIfPresent(UInt32.self, forKey: .ambientLightingColor) ?? 0 ambientLightingBrightness = try c.decodeIfPresent(Float.self, forKey: .ambientLightingBrightness) ?? 0 - ambientLightingEnableEffectRaw = try c.decodeIfPresent(Float.self, forKey: .ambientLightingEnableEffectRaw) ?? 0 - ambientLightingSectionMaskRaw = try c.decodeIfPresent(Float.self, forKey: .ambientLightingSectionMaskRaw) ?? 0 madsActive = try c.decodeIfPresent(Bool.self, forKey: .madsActive) ?? false selfdriveActive = try c.decodeIfPresent(Bool.self, forKey: .selfdriveActive) ?? false experimentalMode = try c.decodeIfPresent(Bool.self, forKey: .experimentalMode) ?? false diff --git a/dashpilot-ios/dashpilot/Models/DashValues.swift b/dashpilot-ios/dashpilot/Models/DashValues.swift index 69eceaf5..4369f400 100644 --- a/dashpilot-ios/dashpilot/Models/DashValues.swift +++ b/dashpilot-ios/dashpilot/Models/DashValues.swift @@ -4,7 +4,7 @@ enum DashValue { case number(Float) case bool(Bool) case string(String) - case color(red: Float, green: Float, blue: Float) + case color(UInt32) } extension DashState { @@ -13,13 +13,7 @@ extension DashState { let d = displaySettings var values: [(String, DashValue)] = [ ("ambientLightingAvailable", .bool(c.ambientLightingAvailable)), - ("ambientLightingHex", .string(c.ambientLightingHex)), - ("ambientLightingRed", .number(c.ambientLightingRed)), - ("ambientLightingGreen", .number(c.ambientLightingGreen)), - ("ambientLightingBlue", .number(c.ambientLightingBlue)), ("ambientLightingBrightness", .number(c.ambientLightingBrightness)), - ("ambientLightingEnableEffectRaw", .number(c.ambientLightingEnableEffectRaw)), - ("ambientLightingSectionMaskRaw", .number(c.ambientLightingSectionMaskRaw)), ("egoSpeed", .number(c.egoSpeed)), ("gear", .number(c.gear)), ("egoSteeringAngle", .number(c.egoSteeringAngle)), @@ -82,8 +76,7 @@ extension DashState { ("dataSourceType", .string(dataSourceType?.rawValue ?? "")), ] if c.ambientLightingAvailable { - values.append(("ambientLightingColor", .color(red: c.ambientLightingRed, - green: c.ambientLightingGreen, blue: c.ambientLightingBlue))) + values.append(("ambientLightingColor", .color(c.ambientLightingColor))) } return values } diff --git a/dashpilot-ios/dashpilot/Resources/vehicles/tesla/bus_1_tesla_vehicle.dbc b/dashpilot-ios/dashpilot/Resources/vehicles/tesla/bus_1_tesla_vehicle.dbc index 958cb5ef..ab7be151 100644 --- a/dashpilot-ios/dashpilot/Resources/vehicles/tesla/bus_1_tesla_vehicle.dbc +++ b/dashpilot-ios/dashpilot/Resources/vehicles/tesla/bus_1_tesla_vehicle.dbc @@ -9141,6 +9141,12 @@ BO_ 505 VCSEC_requests: 1 VEH SG_ VCSEC_chargePortRequest: 0|2@1+ (1,0) [0|0] "" X SG_ VCSEC_driveAttemptedWithoutAuth: 2|1@1+ (1,0) [0|0] "" X +BO_ 1657 UI_ambientLightingCtrls: 8 VEH + SG_ UI_ambientLightingRed : 8|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingGreen : 16|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBlue : 24|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBrightness : 32|7@1+ (1,0) [0|100] "%" X + CM_ "Generated by Tesla Firmware Decode @ version 923e442d6230241a259eeb3f415525373d40325c"; BA_DEF_ BO_ "GenMsgSendType" ENUM "Cyclic"; diff --git a/dashpilot-ios/dashpilot/Resources/vehicles/tesla/new_gen_juniper.dbc b/dashpilot-ios/dashpilot/Resources/vehicles/tesla/new_gen_juniper.dbc index 8ce08557..07ba6fd4 100644 --- a/dashpilot-ios/dashpilot/Resources/vehicles/tesla/new_gen_juniper.dbc +++ b/dashpilot-ios/dashpilot/Resources/vehicles/tesla/new_gen_juniper.dbc @@ -521,6 +521,12 @@ BO_ 1029 VIN_info: 8 VEH SG_ VIN_C405 m18 : 8|56@1+ (1,0) [0|7.2057594038E+016] "" X +BO_ 1657 UI_ambientLightingCtrls: 8 VEH + SG_ UI_ambientLightingRed : 8|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingGreen : 16|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBlue : 24|8@1+ (1,0) [0|255] "" X + SG_ UI_ambientLightingBrightness : 32|7@1+ (1,0) [0|100] "%" X + CM_ "New-generation Model Y (Juniper), Tesla FW 2026.x. Reverse-engineered from comma route 077b771458fd542f/000000a9--5f9f91a356 (2026-09-22 16:49-17:05) against an annotated action timeline; names and enums cross-checked with the Tesla FW 2026.2 signal dataset. Covers the messages listed in dbc/used_messages.txt plus DI_state (for DashPilot). Node VEH = seen on the vehicle bus (comma bus 1, DashKit bus 1); node CH = seen ONLY on the chassis bus (comma bus 0/2, DashKit bus 0). Each message comment lists the buses it was observed on. Bytesum checksum = (id_lo + id_hi + sum of data bytes except the checksum byte) & 0xFF."; CM_ BO_ 585 "CHANGED. Vehicle bus only, 20 Hz. DLC 3 -> 4. Byte 0 is no longer the Tesla bytesum (0 of 19597 frames match) and no CRC-8 matched: Tesla calls it SCCM_leftStalkCrc and the firmware cannot synthesize this frame until the algorithm is known. Bits 20-31 are a new 12-bit stalk angle. SCCM_washWipeButtonStatus and SCCM_highBeamStalkStatus (bits 12-15) never left 0 in the whole log, including the wiper button press at 16:49:47.9: the wiper button now reports through VCLEFT_switchStatus.VCLEFT_swcWiperButtonState."; CM_ SG_ 585 SCCM_turnIndicatorStalkStatus "Enum changed: right tap (16:53:31) = 2 UP_1, left tap (16:53:34, 16:53:35) = 6 DOWN_1 (legacy DOWN_1 was 7)."; diff --git a/dashpilot-ios/dashpilot/UI/RiveDashView.swift b/dashpilot-ios/dashpilot/UI/RiveDashView.swift index 1d5248c9..f2213e6f 100644 --- a/dashpilot-ios/dashpilot/UI/RiveDashView.swift +++ b/dashpilot-ios/dashpilot/UI/RiveDashView.swift @@ -119,11 +119,12 @@ final class RiveDashBinder { guard let p = strings[name] ?? instance.stringProperty(fromPath: name) else { missing.insert(name); continue } strings[name] = p p.value = v - case .color(let red, let green, let blue): + case .color(let rgb): guard let p = colors[name] ?? instance.colorProperty(fromPath: name) else { missing.insert(name); continue } colors[name] = p - p.value = UIColor(red: CGFloat(red) / 255, green: CGFloat(green) / 255, - blue: CGFloat(blue) / 255, alpha: 1) + p.value = UIColor(red: CGFloat((rgb >> 16) & 255) / 255, + green: CGFloat((rgb >> 8) & 255) / 255, + blue: CGFloat(rgb & 255) / 255, alpha: 1) } } if !riveViewModel.isPlaying {