From ef1b8119204982e46fd2482d8f95e0b99bcdf720 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Tue, 3 Nov 2026 12:22:01 -0800 Subject: [PATCH] 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. --- .../seb3thehacker/gearslip/GearslipRunner.kt | 41 ++++++++++++++++++- .../seb3thehacker/gearslip/SessionStatus.kt | 11 ++++- .../seb3thehacker/gearslip/ui/HomeScreen.kt | 5 +++ gearslip/src/main/res/values/strings.xml | 2 + 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index a1ceb0c..d2fbb87 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -58,6 +58,8 @@ class GearslipRunner( private val parser = Frames.Parser() private val assembler = Frames.Assembler() private lateinit var identityTask: FutureTask + /** 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]. */ private val writer = java.util.concurrent.Executors.newSingleThreadExecutor { @@ -130,6 +132,7 @@ class GearslipRunner( identityTask = FutureTask { val identity = identityProvider() val cert = identity.certificate + phoneCert = cert log.i("phone certificate source: ${identity.source}") SessionReport.certificate(identity.source.toString(), cert.issuerX500Principal.toString()) log.i(" subject = ${cert.subjectX500Principal}") @@ -407,8 +410,7 @@ class GearslipRunner( } -2, -23, -24 -> { val reason = when (status) { - -23 -> "The phone's certificate is not valid yet." - -24 -> "The phone's certificate has expired." + -23, -24 -> certificateDates(status) else -> "The head unit refused the phone's certificate." } SessionReport.fail(SessionReport.Category.CERTIFICATE, "AuthComplete status $status", state.name, "auth $status") @@ -449,6 +451,30 @@ class GearslipRunner( 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, * 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() } + private val startedAt = System.currentTimeMillis() + private var toldStuck = false + private fun startWatchdog() { Thread { var lastFrames = 0 @@ -1454,6 +1483,11 @@ class GearslipRunner( val idle = System.currentTimeMillis() - lastProgress 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 // "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. */ 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 const val MSG_VERSION_REQUEST = 1 const val MSG_VERSION_RESPONSE = 2 diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/SessionStatus.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/SessionStatus.kt index e62f285..c4fbe8f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/SessionStatus.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/SessionStatus.kt @@ -14,7 +14,10 @@ object SessionStatus { 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( Snapshot( @@ -30,6 +33,12 @@ object SessionStatus { 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() { flow.value = Snapshot(Phase.PROJECTING, "Projecting", "Streaming to the head unit.") } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt index 20ffd8c..90a682e 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt @@ -307,6 +307,11 @@ private fun StatusCard(status: SessionStatus.Snapshot) { ) SessionStatus.Phase.PROJECTING -> stringResource(R.string.status_projecting_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 } Text(headline, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.SemiBold) diff --git a/gearslip/src/main/res/values/strings.xml b/gearslip/src/main/res/values/strings.xml index 774dc14..8749c21 100644 --- a/gearslip/src/main/res/values/strings.xml +++ b/gearslip/src/main/res/values/strings.xml @@ -862,6 +862,8 @@ Plug into the car\'s USB port, or connect wirelessly. Gearslip opens by itself when the car answers. The phone is charging, but no car has answered. Some cables only charge; try one that carries data. The phone is charging, but no car has answered. Check that USB-C port control, under Security & privacy, allows data and isn\'t set to Charging-only. + 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. + 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. Open settings Connecting Projecting