diff --git a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt index 3cdf082..06fa501 100644 --- a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt +++ b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt @@ -54,6 +54,7 @@ object AirPlayPersistence { private const val KEY_MEDIA_BUFFER_MS = "media_buffer_ms" private const val KEY_CLUSTER_MAP = "cluster_map_enabled" private const val KEY_CLUSTER_MAP_SCALE = "cluster_map_scale_percent" + private const val KEY_CLUSTER_CONTENT = "cluster_content" private const val KEY_CLUSTER_MARKER_X = "cluster_marker_horizontal_step" private const val KEY_CLUSTER_MARKER_Y = "cluster_marker_vertical_step" private const val KEY_WIDTH_PHYSICAL_MM = "display_width_physical_mm" @@ -410,6 +411,15 @@ object AirPlayPersistence { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(KEY_CLUSTER_MAP, enabled).apply() } + fun loadClusterContent(context: Context): CarPlayClusterDisplay.Content = + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_CLUSTER_CONTENT, null) + ?.let { name -> CarPlayClusterDisplay.Content.entries.firstOrNull { it.name == name } } + ?: CarPlayClusterDisplay.Content.MAP + + fun saveClusterContent(context: Context, content: CarPlayClusterDisplay.Content) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY_CLUSTER_CONTENT, content.name).apply() + } + fun loadClusterMapScalePercent(context: Context): Int = CarPlayClusterDisplay.STREAM_SCALE_PERCENT.let { default -> context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getInt(KEY_CLUSTER_MAP_SCALE, default) .takeIf { it in CarPlayClusterDisplay.scalePresets } ?: default diff --git a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt index 8b38f43..c749a71 100644 --- a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt @@ -690,8 +690,9 @@ class CarPlayHostActivity : ComponentActivity() { AirPlayPersistence.loadClusterMapScalePercent(this), AirPlayPersistence.loadClusterMarkerHorizontalStep(this), AirPlayPersistence.loadClusterMarkerVerticalStep(this), + AirPlayPersistence.loadClusterContent(this), ).also { - appendLog("Cluster map: requesting ${it.widthPixels}x${it.heightPixels} on ${size.x}x${size.y} safeArea=${it.safeArea}") + appendLog("Cluster map: requesting ${it.widthPixels}x${it.heightPixels} on ${size.x}x${size.y} safeArea=${it.safeArea} url=${it.initialUrl}") } } diff --git a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt index 8448bf1..1d87f8b 100644 --- a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt @@ -281,21 +281,32 @@ class DiPlayActivity : ComponentActivity() { } } else { val sizes = CarPlayClusterDisplay.scalePresets - choice(card, "Dashboard map size", listOf("Standard · sharpest", "Larger · default", "Largest"), + val contents = CarPlayClusterDisplay.Content.entries + val content = AirPlayPersistence.loadClusterContent(this) + choice(card, "Dashboard shows", listOf("Map · default", "Turn card", "Map with turn card"), + contents.indexOf(content)) { + AirPlayPersistence.saveClusterContent(this, contents[it]) + render() // the size and position settings below are named after what is shown + } + // The iPhone lays out the map, the car marker and the turn card inside the same safe + // area, so the position settings move whichever the dashboard shows. + val shown = if (content == CarPlayClusterDisplay.Content.TURN_CARD) "Turn card" else "Car marker" + choice(card, if (content == CarPlayClusterDisplay.Content.TURN_CARD) "Turn card size" else "Dashboard map size", + listOf("Standard · sharpest", "Larger · default", "Largest"), sizes.indexOf(AirPlayPersistence.loadClusterMapScalePercent(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMapScalePercent(this, sizes[it]) } val across = CarPlayClusterDisplay.horizontalSteps.toList() - choice(card, "Car marker · horizontal", across.map { markerStepLabel(it, "Left", "Right") }, + choice(card, "$shown · horizontal", across.map { markerStepLabel(it, "Left", "Right") }, across.indexOf(AirPlayPersistence.loadClusterMarkerHorizontalStep(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMarkerHorizontalStep(this, across[it]) } val upDown = CarPlayClusterDisplay.verticalSteps.toList() - choice(card, "Car marker · vertical", upDown.map { markerStepLabel(it, "Up", "Down") }, + choice(card, "$shown · vertical", upDown.map { markerStepLabel(it, "Up", "Down") }, upDown.indexOf(AirPlayPersistence.loadClusterMarkerVerticalStep(this)).coerceAtLeast(0)) { AirPlayPersistence.saveClusterMarkerVerticalStep(this, upDown[it]) } - card.addView(button("Reset car marker to centre", false) { + card.addView(button("Reset ${shown.lowercase()} to centre", false) { AirPlayPersistence.saveClusterMarkerHorizontalStep(this, 0) AirPlayPersistence.saveClusterMarkerVerticalStep(this, 0) render() diff --git a/docs/BYD_NAVIGATION.md b/docs/BYD_NAVIGATION.md index 0aef0fa..64b0250 100644 --- a/docs/BYD_NAVIGATION.md +++ b/docs/BYD_NAVIGATION.md @@ -22,7 +22,8 @@ Validated on DiLink5.0 / Android12, firmware `BYD-AUTO/DiLink5.0/DiLink5.0:12/SK - In the cluster's own steering-wheel menu, choose "Full screen navi" or "Small screen navi". "Turn on by navi" shows the map only during stock BYD navigation, so it keeps showing the arrow output instead. - The cluster's speed readout stays visible. "Small screen navi" crops the same picture on the cluster side; Android does not report that crop. - The car marker is placed through CarPlay's safe area: the centre of the panel (x 35–64 %, y 16–75 %), measured with a calibration grid to be clear of BYD's own readouts in both Full and Small screen navi. "Car marker · horizontal" (Left 40 % … Right 40 %) and "Car marker · vertical" (Up 30 % … Down 30 %) move it from there in 10 % steps of the panel. Near a panel edge the safe area shrinks so the marker stays at its centre. -- "Dashboard map size" sets the stream size, which the cluster scales up to the panel: Standard (100 %, sharpest), Larger (83 %, 1600x600, default) or Largest (67 %). Apple Maps ignores the reported physical size on the cluster, so resolution is the only way to change the map's scale. +- "Dashboard shows" picks which of the cluster contents the iPhone offers in `altScreenURLs` DiPlay asks for: Map (`maps:/car/instrumentcluster/map`, default), Turn card (`…/instructioncard`) or Map with turn card (`maps:/car/instrumentcluster`). On the tested car the turn card fit the Small screen navi window well and streamed only 0–5 kbit/s against 0.3–4 Mbit/s for the map. The iPhone lays out the map, the car marker and the turn card inside the same safe area, so the position settings below move whichever is shown; they are labelled "Car marker" or "Turn card" to match. +- "Dashboard map size" (or "Turn card size") sets the stream size, which the cluster scales up to the panel: Standard (100 %, sharpest), Larger (83 %, 1600x600, default) or Largest (67 %). Apple Maps ignores the reported physical size on the cluster, so resolution is the only way to change the map's scale. How it works: the iPhone lists the cluster content it offers in its `/info` request (`altScreenURLs`). DiPlay declares a second display of the cluster's size with no input devices and `initialURL=maps:/car/instrumentcluster/map`; without an initial URL the iPhone streams only a black frame. BYD exposes the cluster projection area as public presentation displays owned by `com.byd.containerservice`. The stock map's display (`fission_bg_XDJAScreenProjection`) is hidden from third-party apps, but its `shared_…_0` sibling is composited on top of it, so DiPlay shows a `Presentation` there. diff --git a/shared/src/main/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplay.kt b/shared/src/main/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplay.kt index 89b5f1b..860d89d 100644 --- a/shared/src/main/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplay.kt +++ b/shared/src/main/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplay.kt @@ -11,6 +11,17 @@ package com.shilapi.xcertplay.airplay object CarPlayClusterDisplay { const val MAP_URL = "maps:/car/instrumentcluster/map" + /** + * What the dashboard shows: one of the cluster contents the iPhone lists in `altScreenURLs`. + * On the tested car the turn card needed 0–5 kbit/s against 0.3–4 Mbit/s for the map, and + * "instrumentcluster" drew the map with the turn card on it. + */ + enum class Content(val url: String) { + MAP(MAP_URL), + TURN_CARD("maps:/car/instrumentcluster/instructioncard"), + INSTRUMENTS("maps:/car/instrumentcluster"), + } + /** * Where the car marker goes, as percent of the panel (left, top, right, bottom). Measured on a * DiLink 5.0 cluster with a calibration grid. In "Full screen navi" BYD draws a status row, @@ -51,6 +62,7 @@ object CarPlayClusterDisplay { scalePercent: Int = STREAM_SCALE_PERCENT, horizontalStep: Int = 0, verticalStep: Int = 0, + content: Content = Content.MAP, ): AirPlayDisplayConfig { // Height rounds to a multiple of 8 and width follows it, so the panel's aspect is kept // (83 % of 1920x720 gives exactly 1600x600). The cluster scales the stream to the panel. @@ -65,7 +77,7 @@ object CarPlayClusterDisplay { fps = FPS, primaryInputDevice = 0, features = 0, - initialUrl = MAP_URL, + initialUrl = content.url, safeArea = safeArea(width, height, horizontalStep, verticalStep), safeAreaDrawOutside = true, ) diff --git a/shared/src/test/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplayTest.kt b/shared/src/test/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplayTest.kt index a09a032..1f6a123 100644 --- a/shared/src/test/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplayTest.kt +++ b/shared/src/test/java/com/shilapi/xcertplay/airplay/CarPlayClusterDisplayTest.kt @@ -46,6 +46,18 @@ class CarPlayClusterDisplayTest { assertEquals(0x0A, (displays[0] as Map<*, *>)["features"]) } + @Test + fun theDashboardCanAskForTheTurnCardInstead() { + val card = CarPlayClusterDisplay.config(1920, 720, content = CarPlayClusterDisplay.Content.TURN_CARD) + + assertEquals("maps:/car/instrumentcluster/instructioncard", card.initialUrl) + // The three contents the iPhone lists in altScreenURLs. + assertEquals( + listOf("maps:/car/instrumentcluster/map", "maps:/car/instrumentcluster/instructioncard", "maps:/car/instrumentcluster"), + CarPlayClusterDisplay.Content.entries.map { it.url }, + ) + } + @Test fun scaledStreamsKeepThePanelAspect() { val sizes = (CarPlayClusterDisplay.scalePresets + 50).map {