Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ class DeviceManager {
private var lastReadyHandledAtMs: Long = 0L
private var lastReadyHandledKey: String = ""
private var lastSystemTimeSyncConnectionKey: String = ""
private var glassesMicDegradedUntilMs: Long = 0L

private var systemMicUnavailable: Boolean
get() = DeviceStore.store.get("bluetooth", "systemMicUnavailable") as? Boolean ?: false
Expand Down Expand Up @@ -354,8 +355,19 @@ class DeviceManager {
// actually attempt recovery (was 0 before, which made the watchdog a no-op).
val timeSinceLastLc3Event = System.currentTimeMillis() - (lastLc3Event ?: 0L)
if (timeSinceLastLc3Event > 5000) {
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
sgc?.setMicEnabled(true)
if (currentMic == MicTypes.GLASSES_CUSTOM &&
preferredMic != "glasses" &&
micRanking.any { it != MicTypes.GLASSES_CUSTOM }
) {
glassesMicDegradedUntilMs = System.currentTimeMillis() + 15_000
Bridge.log(
"MAN: No audio activity in the last 5 seconds from glasses, temporarily falling back from glasses mic"
)
updateMicState()
Comment on lines +362 to +366

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-arm the glasses mic after fallback expires

When this auto-mode fallback path runs, updateMicState() skips glasses, selects the phone mic, and the cleanup loop disables the non-selected glasses mic with setMicEnabled(false). The watchdog then returns early on future ticks because glasses.micEnabled is false, and nothing calls updateMicState() when glassesMicDegradedUntilMs expires, so the intended 15-second fallback can persist indefinitely until an unrelated route or setting change occurs and the preferred auto glasses mic may never recover.

Useful? React with 👍 / 👎.

} else {
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
sgc?.setMicEnabled(true)
}
}
}

Expand Down Expand Up @@ -708,6 +720,7 @@ class DeviceManager {
*/
fun handleGlassesMicData(rawLC3Data: ByteArray, frameSize: Int = 40) {
lastLc3Event = System.currentTimeMillis()
glassesMicDegradedUntilMs = 0L

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Degrade cleared before decode

Medium Severity

glassesMicDegradedUntil is cleared at the very start of handleGlassesMicData, before LC3 decode or validation succeeds. Failed, invalid, or undecodable frames still reset the degraded state and refresh lastLc3Event, so the app can treat a broken stream as recovered and select the glasses mic again without usable audio.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d292e51. Configure here.

val pcmData: ByteArray?
synchronized(lc3Lock) {
if (lc3DecoderPtr == 0L) {
Expand Down Expand Up @@ -809,6 +822,20 @@ class DeviceManager {
}

if (micMode == MicTypes.GLASSES_CUSTOM) {
val glassesConnected =
DeviceStore.get("glasses", "connected") as? Boolean ?: false
if (!glassesConnected) {
Bridge.log("MAN: glasses mic skipped because glasses are not connected")
continue
}

if (glassesMicDegradedUntilMs > System.currentTimeMillis() &&
preferredMic != "glasses" &&
micRanking.any { it != MicTypes.GLASSES_CUSTOM }
) {
Bridge.log("MAN: glasses mic temporarily skipped due to missing LC3 frames")
continue
}
if (sgc?.hasMic == true) {
// enable the mic if it's not already on:
if (sgc?.micEnabled == false) {
Expand Down
33 changes: 30 additions & 3 deletions mobile/modules/bluetooth-sdk/ios/Source/DeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ struct ViewState {
/// Last time we received an LC3 frame from the glasses (used by the mic
/// inactivity watchdog).
private var lastLc3Event: Date?
private var glassesMicDegradedUntil: Date?
private var micReinitTimer: Timer?

/// STT:
Expand Down Expand Up @@ -394,6 +395,7 @@ struct ViewState {
*/
func handleGlassesMicData(_ lc3Data: Data, _ frameSize: Int = 20) {
lastLc3Event = Date()
glassesMicDegradedUntil = nil
guard let lc3Converter = lc3Converter else {
Bridge.log("MAN: LC3 converter not initialized")
return
Expand Down Expand Up @@ -464,6 +466,20 @@ struct ViewState {
}

if micMode == MicTypes.GLASSES_CUSTOM {
let glassesConnected = DeviceStore.shared.get("glasses", "connected") as? Bool ?? false
if !glassesConnected {
Bridge.log("MAN: glasses mic skipped because glasses are not connected")
continue
}

if let degradedUntil = glassesMicDegradedUntil,
degradedUntil > Date(),
preferredMic != "glasses",
micRanking.contains(where: { $0 != MicTypes.GLASSES_CUSTOM })
{
Bridge.log("MAN: glasses mic temporarily skipped due to missing LC3 frames")
continue
}
// Bridge.log(
// "MAN: glasses custom mic found - hasMic: \(sgc?.hasMic ?? false), micEnabled: \(sgc?.micEnabled ?? false)"
// )
Expand Down Expand Up @@ -775,10 +791,21 @@ struct ViewState {
return
}

let timeSinceLastLc3Event = Date().timeIntervalSince(lastLc3Event ?? Date())
let timeSinceLastLc3Event = lastLc3Event.map { Date().timeIntervalSince($0) } ?? .greatestFiniteMagnitude
if timeSinceLastLc3Event > 5 {
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
sgc?.setMicEnabled(true)
if currentMic == MicTypes.GLASSES_CUSTOM,
preferredMic != "glasses",
micRanking.contains(where: { $0 != MicTypes.GLASSES_CUSTOM })
{
glassesMicDegradedUntil = Date().addingTimeInterval(15)
Bridge.log(
"MAN: No audio activity in the last 5 seconds from glasses, temporarily falling back from glasses mic"
)
updateMicState()
Comment on lines +800 to +804

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-arm the glasses mic after fallback expires

When this auto-mode fallback path runs, updateMicState() skips glasses, selects the phone mic, and the cleanup loop disables the non-selected glasses mic with setMicEnabled(false). The watchdog then returns early on future ticks because glasses.micEnabled is false, and nothing calls updateMicState() when glassesMicDegradedUntil expires, so the intended 15-second fallback can persist indefinitely until an unrelated route or setting change occurs and the preferred auto glasses mic may never recover.

Useful? React with 👍 / 👎.

} else {
Bridge.log("MAN: No audio activity in the last 5 seconds from glasses, reinitializing glasses mic")
sgc?.setMicEnabled(true)
}
}
}

Expand Down
Loading