mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
fix(aap): Fix flush ANC debounce, HANDSHAKING→READY regression, and flush ordering
Track lastAncSentAt separately so non-ANC commands during flush don't break the ANC echo debounce window. Prioritize ANC for post-flush verification. Move HANDSHAKING→READY transition to top of processMessage so decoded battery, stem press, and device info messages also trigger it. Sort flush: AllowOffOption before AncMode before others, preventing device rejection when enabling OFF mode.
This commit is contained in:
@@ -38,8 +38,7 @@ internal class AapConnection(
|
||||
private val device: BluetoothDevice,
|
||||
private val profile: AapDeviceProfile,
|
||||
private val socketFactory: L2capSocketFactory,
|
||||
private val timeSource: TimeSource,
|
||||
private val psm: Int = 0x1001,
|
||||
timeSource: TimeSource,
|
||||
) {
|
||||
|
||||
private val engine = AapSessionEngine(profile, timeSource)
|
||||
@@ -65,7 +64,7 @@ internal class AapConnection(
|
||||
engine.start(scope)
|
||||
|
||||
try {
|
||||
val sock = socketFactory.createSocket(device, psm)
|
||||
val sock = socketFactory.createSocket(device, PSM)
|
||||
sock.connect()
|
||||
socket = sock
|
||||
log(TAG, INFO) { "Connected to ${device.address}" }
|
||||
@@ -168,6 +167,7 @@ internal class AapConnection(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PSM = 0x1001
|
||||
private val TAG = logTag("AAP", "Connection")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ internal class AapSessionEngine(
|
||||
private var ancDebounceJob: Job? = null
|
||||
private var lastSentCommand: AapCommand? = null
|
||||
private var lastSentAt: Long = 0L
|
||||
/** Separate tracking for ANC sends — not overwritten by non-ANC commands during flush. */
|
||||
private var lastAncSentAt: Long = 0L
|
||||
private var handshakeResponseReceived: Boolean = false
|
||||
|
||||
/** Stored reference to the socket write callback — set on each [send] / flush call. */
|
||||
@@ -82,6 +84,7 @@ internal class AapSessionEngine(
|
||||
scope = null
|
||||
lastSentCommand = null
|
||||
lastSentAt = 0L
|
||||
lastAncSentAt = 0L
|
||||
activeSendRaw = null
|
||||
handshakeResponseReceived = false
|
||||
_state.value = AapPodState(connectionState = AapPodState.ConnectionState.DISCONNECTED)
|
||||
@@ -148,7 +151,9 @@ internal class AapSessionEngine(
|
||||
private suspend fun wrappedSend(sendRaw: suspend (AapCommand) -> Unit, command: AapCommand) {
|
||||
sendRaw(command)
|
||||
lastSentCommand = command
|
||||
lastSentAt = timeSource.currentTimeMillis()
|
||||
val now = timeSource.currentTimeMillis()
|
||||
lastSentAt = now
|
||||
if (command is AapCommand.SetAncMode) lastAncSentAt = now
|
||||
}
|
||||
|
||||
private fun onVerificationOutcome(outcome: AapSettingsCoordinator.VerificationOutcome) {
|
||||
@@ -173,6 +178,16 @@ internal class AapSessionEngine(
|
||||
val hex = message.raw.joinToString(" ") { "%02X".format(it) }
|
||||
log(TAG, VERBOSE) { "MSG cmd=0x${"%04X".format(message.commandType)} len=${message.raw.size} raw=$hex" }
|
||||
|
||||
// HANDSHAKING → READY: first non-settings-echo message means the device is talking.
|
||||
// Must happen before any early returns so decoded messages (battery, stem, etc.) also trigger it.
|
||||
if (!handshakeResponseReceived && message.commandType != 0x0009
|
||||
&& _state.value.connectionState == AapPodState.ConnectionState.HANDSHAKING
|
||||
) {
|
||||
handshakeResponseReceived = true
|
||||
_state.value = _state.value.copy(connectionState = AapPodState.ConnectionState.READY)
|
||||
log(TAG) { "Connection READY" }
|
||||
}
|
||||
|
||||
// Issue #173 diagnostic dump
|
||||
if (message.commandType == 0x001D) {
|
||||
val segments = describeDeviceInfoSegments(message.payload)
|
||||
@@ -241,8 +256,7 @@ internal class AapSessionEngine(
|
||||
// ANC debounce
|
||||
if (value is AapSetting.AncMode) {
|
||||
val isFirstAncMode = _state.value.setting<AapSetting.AncMode>() == null
|
||||
val recentAncSend =
|
||||
lastSentCommand is AapCommand.SetAncMode && (timeSource.currentTimeMillis() - lastSentAt) <= 3000L
|
||||
val recentAncSend = lastAncSentAt > 0L && (timeSource.currentTimeMillis() - lastAncSentAt) <= 3000L
|
||||
if (isFirstAncMode || recentAncSend) {
|
||||
ancDebounceJob?.cancel()
|
||||
_state.value = _state.value.withSetting(key, value).copy(lastMessageAt = timeSource.now())
|
||||
@@ -309,7 +323,10 @@ internal class AapSessionEngine(
|
||||
break
|
||||
}
|
||||
}
|
||||
commands.lastOrNull()?.let {
|
||||
// Verify ANC mode if it was in the batch (most important for divergence detection).
|
||||
// Fall back to verifying the last command if no ANC was flushed.
|
||||
val toVerify = commands.firstOrNull { it is AapCommand.SetAncMode } ?: commands.lastOrNull()
|
||||
toVerify?.let {
|
||||
coordinator.startVerification(
|
||||
it,
|
||||
this,
|
||||
@@ -322,22 +339,9 @@ internal class AapSessionEngine(
|
||||
}
|
||||
}
|
||||
|
||||
// HANDSHAKING → READY transition
|
||||
if (!handshakeResponseReceived && _state.value.connectionState == AapPodState.ConnectionState.HANDSHAKING) {
|
||||
handshakeResponseReceived = true
|
||||
_state.value = _state.value.copy(connectionState = AapPodState.ConnectionState.READY)
|
||||
log(TAG) { "Connection READY" }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Track handshake response for non-setting messages too
|
||||
if (!handshakeResponseReceived && message.commandType != 0x0009 && _state.value.connectionState == AapPodState.ConnectionState.HANDSHAKING) {
|
||||
handshakeResponseReceived = true
|
||||
_state.value = _state.value.copy(connectionState = AapPodState.ConnectionState.READY)
|
||||
log(TAG) { "Connection READY" }
|
||||
}
|
||||
|
||||
val payloadHex = message.payload.joinToString(" ") { "%02X".format(it) }
|
||||
val sinceSend = if (lastSentAt == 0L) -1L else timeSource.currentTimeMillis() - lastSentAt
|
||||
val lastSend = lastSentCommand?.let { it::class.simpleName } ?: "none"
|
||||
|
||||
@@ -73,7 +73,16 @@ internal class AapSettingsCoordinator(
|
||||
commands = pendingCommands.values.toList()
|
||||
pendingCommands.clear()
|
||||
}
|
||||
val sorted = commands.sortedBy { if (it is AapCommand.SetAncMode) 0 else 1 }
|
||||
// Dependency-aware ordering:
|
||||
// AllowOffOption must precede AncMode (device rejects OFF without it)
|
||||
// AncMode must precede mode-dependent settings (e.g. AdaptiveAudioNoise)
|
||||
val sorted = commands.sortedBy {
|
||||
when (it) {
|
||||
is AapCommand.SetAllowOffOption -> 0
|
||||
is AapCommand.SetAncMode -> 1
|
||||
else -> 2
|
||||
}
|
||||
}
|
||||
return sorted to snapshot()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user