mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
The cockpit turn card stays clear of the car's own dials and bars
This commit is contained in:
1 parent
31d8232978
commit
99407dfcb2
2 files changed
+63
-11
No files matched your search
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import app.seb3thehacker.gearslip.car.ClusterScreen
|
import app.seb3thehacker.gearslip.car.ClusterScreen
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gearslip's picture on the car's instrument-cluster screen: a second video sink, for a car that
|
* Gearslip's picture on the car's instrument-cluster screen: a second video sink, for a car that
|
||||||
@@ -50,6 +51,8 @@ internal class ClusterVideo(
|
|||||||
@Volatile private var maxUnacked = 1
|
@Volatile private var maxUnacked = 1
|
||||||
@Volatile private var lastAckAt = 0L
|
@Volatile private var lastAckAt = 0L
|
||||||
private var frames = 0
|
private var frames = 0
|
||||||
|
/** The part of the picture the car's own gauges and bars leave clear, as the car last said. */
|
||||||
|
private val clearArea = MutableStateFlow<Insets?>(null)
|
||||||
|
|
||||||
fun onMessage(messageId: Int, body: ByteArray) {
|
fun onMessage(messageId: Int, body: ByteArray) {
|
||||||
when (messageId) {
|
when (messageId) {
|
||||||
@@ -92,6 +95,13 @@ internal class ClusterVideo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUiConfigRequest { AdditionalVideoConfig { display_insets = 1 } }: an Audi's
|
||||||
|
// virtual cockpit sends one as the picture starts, and draws its dials over the frame.
|
||||||
|
GearslipRunner.MSG_VIDEO_UI_CONFIG -> {
|
||||||
|
log.i("<- UpdateUiConfigRequest (${body.size} bytes)\n${Protobuf.describe(body)}")
|
||||||
|
clearArea.value = insetsFrom(body)
|
||||||
|
}
|
||||||
|
|
||||||
else -> log.i("<- unhandled message id=$messageId (${body.size} bytes)")
|
else -> log.i("<- unhandled message id=$messageId (${body.size} bytes)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +168,7 @@ internal class ClusterVideo(
|
|||||||
projector = ScreenProjector(context).also {
|
projector = ScreenProjector(context).also {
|
||||||
it.start(surface, width, height, density, name = "gearslip-cluster") {
|
it.start(surface, width, height, density, name = "gearslip-cluster") {
|
||||||
// The margins the car crops off are kept clear, as on the main screen.
|
// The margins the car crops off are kept clear, as on the main screen.
|
||||||
ClusterScreen(config.widthMargin, config.heightMargin)
|
ClusterScreen(config.widthMargin, config.heightMargin, clearArea)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +195,15 @@ internal class ClusterVideo(
|
|||||||
if (++frames <= 2) log.i("-> Data frame #$frames (${data.size} bytes)")
|
if (++frames <= 2) log.i("-> Data frame #$frames (${data.size} bytes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** VideoInsets { top = 1; bottom = 2; left = 3; right = 4 }, or null when the car sent none. */
|
||||||
|
private fun insetsFrom(body: ByteArray): Insets? {
|
||||||
|
val config = Wire.bytes(Wire.fields(body), 1) ?: return null
|
||||||
|
val insets = Wire.fields(Wire.bytes(Wire.fields(config), 1) ?: return null)
|
||||||
|
fun side(n: Int) = Wire.varint(insets, n)?.toInt() ?: 0
|
||||||
|
return Insets(top = side(1), bottom = side(2), left = side(3), right = side(4))
|
||||||
|
.also { log.i("the car keeps clear: $it") }
|
||||||
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
running = false
|
running = false
|
||||||
main.removeCallbacks(watchRoute)
|
main.removeCallbacks(watchRoute)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -24,11 +25,14 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.seb3thehacker.gearslip.Insets
|
||||||
import app.seb3thehacker.gearslip.host.clockTime
|
import app.seb3thehacker.gearslip.host.clockTime
|
||||||
import app.seb3thehacker.gearslip.host.display
|
import app.seb3thehacker.gearslip.host.display
|
||||||
import app.seb3thehacker.gearslip.host.remainingTime
|
import app.seb3thehacker.gearslip.host.remainingTime
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The instrument-cluster screen: the next turn, big enough to read in a glance past the wheel.
|
* The instrument-cluster screen: the next turn, big enough to read in a glance past the wheel.
|
||||||
@@ -36,10 +40,13 @@ import kotlinx.coroutines.delay
|
|||||||
* along the bottom. With no route it shows what's playing, quietly, or nothing.
|
* along the bottom. With no route it shows what's playing, quietly, or nothing.
|
||||||
*
|
*
|
||||||
* Always dark, like the dials around it. [marginX] and [marginY] are the pixels the car crops off
|
* Always dark, like the dials around it. [marginX] and [marginY] are the pixels the car crops off
|
||||||
* the frame, kept clear.
|
* the frame, kept clear. Many cars also draw their own dials and bars over what's left, so the
|
||||||
|
* card keeps to [clearArea] when the car says where that is, and otherwise to the middle of a
|
||||||
|
* wide strip: on an Audi's virtual cockpit it ran under the rev counter and the bottom bar (#56).
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ClusterScreen(marginX: Int, marginY: Int) {
|
internal fun ClusterScreen(marginX: Int, marginY: Int, clearArea: StateFlow<Insets?>) {
|
||||||
|
val carInsets by clearArea.collectAsState()
|
||||||
// The map app's turn changes a few times a minute: read it once a second.
|
// The map app's turn changes a few times a minute: read it once a second.
|
||||||
val turn by produceState<ClusterTurn?>(null) {
|
val turn by produceState<ClusterTurn?>(null) {
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -52,18 +59,47 @@ internal fun ClusterScreen(marginX: Int, marginY: Int) {
|
|||||||
CompositionLocalProvider(LocalContentColor provides Color.White) {
|
CompositionLocalProvider(LocalContentColor provides Color.White) {
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
Modifier.fillMaxSize().background(Color.Black).padding(
|
Modifier.fillMaxSize().background(Color.Black).padding(
|
||||||
horizontal = with(density) { (marginX / 2).toDp() } + 24.dp,
|
horizontal = with(density) { (marginX / 2).toDp() },
|
||||||
vertical = with(density) { (marginY / 2).toDp() } + 16.dp,
|
vertical = with(density) { (marginY / 2).toDp() },
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.CenterStart,
|
|
||||||
) {
|
) {
|
||||||
// Sized off the height: clusters run from short strips to near squares.
|
val clear = clearPadding(maxWidth, maxHeight, carInsets?.let { with(density) { toDp(it) } })
|
||||||
val unit = maxHeight / 12
|
BoxWithConstraints(
|
||||||
|
Modifier.fillMaxSize().padding(start = clear[0], top = clear[1], end = clear[2], bottom = clear[3]),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
// The card is about 8.5 units tall with two lines of directions, and 14 wide: fit both ways.
|
||||||
|
val unit = minOf(maxHeight / 8.5f, maxWidth / 14)
|
||||||
val t = turn
|
val t = turn
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
NowPlayingLine(Modifier.align(Alignment.Center))
|
NowPlayingLine(Modifier.align(Alignment.Center))
|
||||||
return@BoxWithConstraints
|
return@BoxWithConstraints
|
||||||
}
|
}
|
||||||
|
TurnCard(t, unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start, top, end and bottom padding around the card inside the visible picture. The car's own
|
||||||
|
* insets win when they leave a usable area; a wide strip, where dials sit at both ends and a bar
|
||||||
|
* along the bottom, keeps to the upper middle; anything squarer just keeps off the edges.
|
||||||
|
*/
|
||||||
|
private fun clearPadding(width: Dp, height: Dp, car: List<Dp>?): List<Dp> {
|
||||||
|
if (car != null && width - car[0] - car[2] >= width * 0.3f && height - car[1] - car[3] >= height * 0.3f) return car
|
||||||
|
if (width >= height * 2) return listOf(width * 0.22f, height * 0.08f, width * 0.22f, height * 0.33f)
|
||||||
|
return listOf(24.dp, 16.dp, 24.dp, 16.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insets in frame pixels as start, top, end and bottom padding. */
|
||||||
|
private fun androidx.compose.ui.unit.Density.toDp(insets: Insets): List<Dp> =
|
||||||
|
listOf(insets.left.toDp(), insets.top.toDp(), insets.right.toDp(), insets.bottom.toDp())
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TurnCard(t: ClusterTurn, unit: Dp) {
|
||||||
|
val density = LocalDensity.current
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(unit * 0.4f)) {
|
Column(verticalArrangement = Arrangement.spacedBy(unit * 0.4f)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
CarGlyph(t.icon, Modifier.size(unit * 3.4f))
|
CarGlyph(t.icon, Modifier.size(unit * 3.4f))
|
||||||
@@ -94,9 +130,6 @@ internal fun ClusterScreen(marginX: Int, marginY: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NowPlayingLine(modifier: Modifier) {
|
private fun NowPlayingLine(modifier: Modifier) {
|
||||||
|
|||||||
Reference in new issue
Block a user