Clearer messages when the car's clock is wrong or a start gets stuck

A certificate the car calls not valid yet or expired, but that is fine by the
phone's clock, now says to set the car's date and time. A start that hasn't
reached the car's screen after 45 seconds says what to try, for cable and wireless.
This commit is contained in:
Seb3thehacker committed 2026-11-03 12:22:01 -08:00
1 parent fa89017521
commit ef1b811920
4 files changed
+56 -3

No files matched your search

@@ -58,6 +58,8 @@ class GearslipRunner(
private val parser = Frames.Parser() private val parser = Frames.Parser()
private val assembler = Frames.Assembler() private val assembler = Frames.Assembler()
private lateinit var identityTask: FutureTask<PhoneTls> private lateinit var identityTask: FutureTask<PhoneTls>
/** The certificate in use, to tell a wrong date on the car from a certificate that's really out of date. */
@Volatile private var phoneCert: java.security.cert.X509Certificate? = null
/** Sends every message, in order, so no other thread ever blocks on the USB link. See [send]. */ /** Sends every message, in order, so no other thread ever blocks on the USB link. See [send]. */
private val writer = java.util.concurrent.Executors.newSingleThreadExecutor { private val writer = java.util.concurrent.Executors.newSingleThreadExecutor {
@@ -130,6 +132,7 @@ class GearslipRunner(
identityTask = FutureTask { identityTask = FutureTask {
val identity = identityProvider() val identity = identityProvider()
val cert = identity.certificate val cert = identity.certificate
phoneCert = cert
log.i("phone certificate source: ${identity.source}") log.i("phone certificate source: ${identity.source}")
SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString()) SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString())
log.i(" subject = ${cert.subjectX500Principal}") log.i(" subject = ${cert.subjectX500Principal}")
@@ -407,8 +410,7 @@ class GearslipRunner(
} }
-2, -23, -24 -> { -2, -23, -24 -> {
val reason = when (status) { val reason = when (status) {
-23 -> "The phone's certificate is not valid yet." -23, -24 -> certificateDates(status)
-24 -> "The phone's certificate has expired."
else -> "The head unit refused the phone's certificate." else -> "The head unit refused the phone's certificate."
} }
SessionReport.fail(SessionReport.Category.CERTIFICATE, "AuthComplete status $status", state.name, "auth $status") SessionReport.fail(SessionReport.Category.CERTIFICATE, "AuthComplete status $status", state.name, "auth $status")
@@ -449,6 +451,30 @@ class GearslipRunner(
if (authenticationFailed) running = false if (authenticationFailed) running = false
} }
/**
* The car checks the certificate's dates against its own clock. When they're fine by the
* phone's clock, the car's clock is the one that's wrong, which a flat battery or a car
* that never got the time from GPS leaves behind.
*/
private fun certificateDates(status: Int): String {
val cert = phoneCert ?: return if (status == -23) "The phone's certificate is not valid yet." else "The phone's certificate has expired."
val now = java.util.Date()
val day = java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG)
return when {
status == -23 && now.before(cert.notBefore) ->
"The phone's certificate is not valid until ${day.format(cert.notBefore)}."
status == -24 && now.after(cert.notAfter) ->
"The phone's certificate expired on ${day.format(cert.notAfter)}. Get a new one under Certificate in Settings."
else -> {
val side = if (status == -23) "behind" else "ahead"
log.w("the certificate's dates are fine by the phone's clock, so the car's clock is $side")
"The car's clock looks wrong: the certificate is valid today, but the car thinks it " +
(if (status == -23) "isn't valid yet" else "has expired") +
". Set the car's date and time, then connect again."
}
}
}
/** /**
* What a real phone sends here: its icon at three sizes (fields 1 to 3), a label, its model, * What a real phone sends here: its icon at three sizes (fields 1 to 3), a label, its model,
* and a session ID that's new each connection (field 6). A 2017 Hyundai on protocol 1.0 * and a session ID that's new each connection (field 6). A 2017 Hyundai on protocol 1.0
@@ -1443,6 +1469,9 @@ class GearslipRunner(
SessionReport.print() SessionReport.print()
} }
private val startedAt = System.currentTimeMillis()
private var toldStuck = false
private fun startWatchdog() { private fun startWatchdog() {
Thread { Thread {
var lastFrames = 0 var lastFrames = 0
@@ -1454,6 +1483,11 @@ class GearslipRunner(
val idle = System.currentTimeMillis() - lastProgress val idle = System.currentTimeMillis() - lastProgress
if (idle > 10_000) log.w("stalled in state $state for ${idle / 1000}s") if (idle > 10_000) log.w("stalled in state $state for ${idle / 1000}s")
} }
if (!videoStarted && !toldStuck && System.currentTimeMillis() - startedAt > STUCK_MS) {
toldStuck = true
log.w("still not on the car's screen after ${STUCK_MS / 1000}s, in state $state")
SessionStatus.stillConnecting(if (wireless) SessionStatus.Link.WIRELESS else SessionStatus.Link.CABLE)
}
// Stream health. Without this a drive cannot tell "streaming fine" from // Stream health. Without this a drive cannot tell "streaming fine" from
// "stalled after three frames" - the per-frame logging is deliberately sparse. // "stalled after three frames" - the per-frame logging is deliberately sparse.
@@ -1546,6 +1580,9 @@ class GearslipRunner(
/** The stats window adaptive bitrate reads, matching the watchdog's sleep. */ /** The stats window adaptive bitrate reads, matching the watchdog's sleep. */
const val ADAPT_WINDOW_MS = 5_000L const val ADAPT_WINDOW_MS = 5_000L
/** A car shows its screen within seconds; this long without one, Home says what to try. */
const val STUCK_MS = 45_000L
// protobuf/aap_protobuf/service/control/ControlMessageType.proto:7-17 // protobuf/aap_protobuf/service/control/ControlMessageType.proto:7-17
const val MSG_VERSION_REQUEST = 1 const val MSG_VERSION_REQUEST = 1
const val MSG_VERSION_RESPONSE = 2 const val MSG_VERSION_RESPONSE = 2
@@ -14,7 +14,10 @@ object SessionStatus {
enum class Phase { IDLE, CONNECTING, PROJECTING, FAILED, DISCONNECTED } enum class Phase { IDLE, CONNECTING, PROJECTING, FAILED, DISCONNECTED }
data class Snapshot(val phase: Phase, val headline: String, val detail: String) /** [stuckOn] is set when connecting has gone on too long: the cable or Wi-Fi, for what to try. */
data class Snapshot(val phase: Phase, val headline: String, val detail: String, val stuckOn: Link? = null)
enum class Link { CABLE, WIRELESS }
private val flow = MutableStateFlow( private val flow = MutableStateFlow(
Snapshot( Snapshot(
@@ -30,6 +33,12 @@ object SessionStatus {
flow.value = Snapshot(Phase.CONNECTING, "Connecting", stage) flow.value = Snapshot(Phase.CONNECTING, "Connecting", stage)
} }
/** Still connecting after [GearslipRunner]'s patience ran out: Home says what to try. */
fun stillConnecting(link: Link) {
val now = flow.value
if (now.phase == Phase.CONNECTING) flow.value = now.copy(stuckOn = link)
}
fun projecting() { fun projecting() {
flow.value = Snapshot(Phase.PROJECTING, "Projecting", "Streaming to the head unit.") flow.value = Snapshot(Phase.PROJECTING, "Projecting", "Streaming to the head unit.")
} }
@@ -307,6 +307,11 @@ private fun StatusCard(status: SessionStatus.Snapshot) {
) )
SessionStatus.Phase.PROJECTING -> stringResource(R.string.status_projecting_detail) SessionStatus.Phase.PROJECTING -> stringResource(R.string.status_projecting_detail)
SessionStatus.Phase.DISCONNECTED -> stringResource(R.string.status_disconnected_detail) SessionStatus.Phase.DISCONNECTED -> stringResource(R.string.status_disconnected_detail)
SessionStatus.Phase.CONNECTING -> when (status.stuckOn) {
SessionStatus.Link.CABLE -> stringResource(R.string.status_stuck_cable)
SessionStatus.Link.WIRELESS -> stringResource(R.string.status_stuck_wireless)
null -> status.detail
}
else -> status.detail else -> status.detail
} }
Text(headline, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold) Text(headline, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold)
+2
View File
@@ -862,6 +862,8 @@
<string name="status_idle_detail">Plug into the car\'s USB port, or connect wirelessly. Gearslip opens by itself when the car answers.</string> <string name="status_idle_detail">Plug into the car\'s USB port, or connect wirelessly. Gearslip opens by itself when the car answers.</string>
<string name="status_no_car">The phone is charging, but no car has answered. Some cables only charge; try one that carries data.</string> <string name="status_no_car">The phone is charging, but no car has answered. Some cables only charge; try one that carries data.</string>
<string name="status_no_car_graphene">The phone is charging, but no car has answered. Check that USB-C port control, under Security &amp; privacy, allows data and isn\'t set to Charging-only.</string> <string name="status_no_car_graphene">The phone is charging, but no car has answered. Check that USB-C port control, under Security &amp; privacy, allows data and isn\'t set to Charging-only.</string>
<string name="status_stuck_cable">The car hasn\'t shown Gearslip yet. Unplug the cable, wait a few seconds, and plug it back in. If it keeps happening, try another cable or USB port, and share the log in an issue.</string>
<string name="status_stuck_wireless">The car hasn\'t shown Gearslip yet. Turn the car off and on again, or connect with a cable. If it keeps happening, share the log in an issue.</string>
<string name="status_open_settings">Open settings</string> <string name="status_open_settings">Open settings</string>
<string name="status_connecting">Connecting</string> <string name="status_connecting">Connecting</string>
<string name="status_projecting">Projecting</string> <string name="status_projecting">Projecting</string>