Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bridge/bridge_ios.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ 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->ambientLightingColor = static_cast<uint32_t>(cs.ambientLightingColor);
out->ambientLightingBrightness = cs.ambientLightingBrightness;
}

static std::atomic<bool> receiveLoopRunning{false};
Expand Down
4 changes: 4 additions & 0 deletions bridge/bridge_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ typedef struct {
double trunkOpen;
double trunkPosition;
double frunkOpen;

double ambientLightingAvailable;
uint32_t ambientLightingColor;
double ambientLightingBrightness;
} BridgeCarState;

// Context
Expand Down
5 changes: 3 additions & 2 deletions bridge/car/can_parsers.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down
11 changes: 10 additions & 1 deletion bridge/car/car_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ 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 ambientLightingColor = 0; // Packed RGB (0xRRGGBB).
double ambientLightingBrightness = 0;
static constexpr size_t AMBIENT_OFFSET = DOOR_OFFSET + DOOR_COUNT;
static constexpr size_t FIELD_COUNT = AMBIENT_OFFSET + 3;

void toArray(double* out) const {
// Party bus
Expand Down Expand Up @@ -158,5 +163,9 @@ 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] = ambientLightingColor;
out[AMBIENT_OFFSET + 2] = ambientLightingBrightness;
}
};
13 changes: 12 additions & 1 deletion bridge/car/cars/tesla.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(cp.get(1, ambient, "UI_ambientLightingRed"));
const auto green = static_cast<uint32_t>(cp.get(1, ambient, "UI_ambientLightingGreen"));
const auto blue = static_cast<uint32_t>(cp.get(1, ambient, "UI_ambientLightingBlue"));
cs.ambientLightingColor = (red << 16) | (green << 8) | blue;
cs.ambientLightingBrightness = brightness;
cs.ambientLightingAvailable = 1;
}
}


Expand Down Expand Up @@ -291,4 +302,4 @@ class TeslaJuniperMapper : public CarStateMapper {
BlinkerHold leftBlinkerHold_;
BlinkerHold rightBlinkerHold_;
TeslaVinUpdater vin_;
};
};
12 changes: 12 additions & 0 deletions dash-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ 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

DashKit dashboards can bind these optional Rive properties:

| Property | Type | Value |
|---|---|---|
| `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

Web apps get one string of storage, usually JSON.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ 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 = "",
val ambientLightingAvailable: Boolean = false,
Comment thread
wpmed92 marked this conversation as resolved.
Resolved
val ambientLightingColor: Int = 0, // Packed RGB (0xRRGGBB).
val ambientLightingBrightness: Float = 0f,
) {
fun toImperial(): CarState = copy(
odometer = odometer * KM_TO_MILES,
Expand All @@ -80,12 +83,12 @@ data class CarState(
)

companion object {
const val FIELD_COUNT = 58
const val FIELD_COUNT = 61
private const val KM_TO_MILES = 0.621371f

private val MILES_COUNTRIES = setOf("US", "GB", "MM", "LR")

fun speedLimitSignsInKm(): Boolean =
Locale.getDefault().country !in MILES_COUNTRIES
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import java.time.ZoneId
fun DashState.dashValues(systemDarkMode: Boolean? = null): Map<String, Any> {
val c = carState
val d = displaySettings
return mapOf(
val values = mutableMapOf<String, Any>(
"ambientLightingAvailable" to c.ambientLightingAvailable,
"ambientLightingBrightness" to c.ambientLightingBrightness,
"egoSpeed" to c.egoSpeed,
"gear" to c.gear,
"egoSteeringAngle" to c.egoSteeringAngle,
Expand Down Expand Up @@ -69,6 +71,10 @@ fun DashState.dashValues(systemDarkMode: Boolean? = null): Map<String, Any> {
"dataSourceType" to dataSourceType,
"speedCameraDistance" to speedCameraDistance.toFloat(),
)
if (c.ambientLightingAvailable) {
values["ambientLightingColor"] = c.ambientLightingColor or 0xFF000000.toInt()
}
return values
}

private fun secondsSinceLocalMidnight(epochMillis: Long): Float =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ private fun BoundRive(
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 -> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ 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,
ambientLightingColor = values[59].toInt(),
ambientLightingBrightness = values[60].toFloat(),
)
}

Expand Down
12 changes: 12 additions & 0 deletions dashpilot-android/bridge/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,16 @@ 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)
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()
35 changes: 35 additions & 0 deletions dashpilot-android/bridge/tests/tesla_ambient_lighting_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include "common/vehicle_decoder.h"
#include <cassert>
#include <fstream>
#include <iterator>

static void check(const char* dbcPath, const char* profile) {
std::ifstream dbc(dbcPath);
assert(dbc);
const std::string content{std::istreambuf_iterator<char>(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();
};
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");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions dashpilot-ios/dashpilot/Models/CarState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct CarState: Codable {
var hvacKeepClimateOn: Float = 0
var powerLevel: Float = 0

var ambientLightingAvailable: Bool = false
Comment thread
wpmed92 marked this conversation as resolved.
Resolved
var ambientLightingColor: UInt32 = 0 // Packed RGB (0xRRGGBB).
var ambientLightingBrightness: Float = 0

// Openpilot state
var madsActive: Bool = false
var selfdriveActive: Bool = false
Expand Down Expand Up @@ -102,6 +106,9 @@ struct CarState: Codable {
hvacRecirc = Float(bridge.hvacRecirc)
hvacKeepClimateOn = Float(bridge.hvacKeepClimateOn)
powerLevel = Float(bridge.powerLevel)
ambientLightingAvailable = bridge.ambientLightingAvailable > 0
ambientLightingColor = bridge.ambientLightingColor
ambientLightingBrightness = Float(bridge.ambientLightingBrightness)
madsActive = bridge.madsActive > 0
selfdriveActive = bridge.selfdriveActive > 0
experimentalMode = bridge.experimentalMode > 0
Expand Down Expand Up @@ -183,6 +190,9 @@ 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
ambientLightingColor = try c.decodeIfPresent(UInt32.self, forKey: .ambientLightingColor) ?? 0
ambientLightingBrightness = try c.decodeIfPresent(Float.self, forKey: .ambientLightingBrightness) ?? 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
Expand Down
9 changes: 8 additions & 1 deletion dashpilot-ios/dashpilot/Models/DashValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ enum DashValue {
case number(Float)
case bool(Bool)
case string(String)
case color(UInt32)
}

extension DashState {
func dashValues(systemDarkMode: Bool? = nil) -> [(String, DashValue)] {
let c = carState
let d = displaySettings
return [
var values: [(String, DashValue)] = [
("ambientLightingAvailable", .bool(c.ambientLightingAvailable)),
("ambientLightingBrightness", .number(c.ambientLightingBrightness)),
("egoSpeed", .number(c.egoSpeed)),
("gear", .number(c.gear)),
("egoSteeringAngle", .number(c.egoSteeringAngle)),
Expand Down Expand Up @@ -72,6 +75,10 @@ extension DashState {
("renderQuality", .number(Float(d.renderQuality))),
("dataSourceType", .string(dataSourceType?.rawValue ?? "")),
]
if c.ambientLightingAvailable {
values.append(("ambientLightingColor", .color(c.ambientLightingColor)))
}
return values
}

private static func secondsSinceLocalMidnight(_ epochMillis: Int64) -> Float {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).";
Expand Down
7 changes: 7 additions & 0 deletions dashpilot-ios/dashpilot/UI/RiveDashView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = []
private var systemDarkMode = false

Expand Down Expand Up @@ -118,6 +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 rgb):
guard let p = colors[name] ?? instance.colorProperty(fromPath: name) else { missing.insert(name); continue }
colors[name] = p
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 {
Expand Down
Loading