mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 03:06:11 -04:00
Refactor packages
This commit is contained in:
+5
-3
@@ -1,11 +1,13 @@
|
||||
package eu.darken.capod.pods.core.apple.aap
|
||||
package eu.darken.capod.pods.core.apple.aap.engine
|
||||
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import java.time.Instant
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class AapAncControllerTest : BaseTest() {
|
||||
|
||||
@@ -17,7 +19,7 @@ class AapAncControllerTest : BaseTest() {
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
|
||||
private fun podStateWith(vararg settings: Pair<kotlin.reflect.KClass<out AapSetting>, AapSetting>): AapPodState =
|
||||
private fun podStateWith(vararg settings: Pair<KClass<out AapSetting>, AapSetting>): AapPodState =
|
||||
AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
settings = settings.toMap(),
|
||||
@@ -149,4 +151,4 @@ class AapAncControllerTest : BaseTest() {
|
||||
decision.podState.setting<AapSetting.AllowOffOption>()!!.enabled shouldBe false
|
||||
decision.timerActions shouldBe listOf(EngineTimerAction.Cancel(EngineTimerKey.AllowOffInference))
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
package eu.darken.capod.pods.core.apple.aap
|
||||
package eu.darken.capod.pods.core.apple.aap.engine
|
||||
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
|
||||
@@ -15,7 +15,7 @@ import testhelpers.BaseTest
|
||||
* The helper must:
|
||||
* - Work on real captured 0x1D payloads (matches production decoder on ASCII slots).
|
||||
* - Decode non-ASCII UTF-8 (emoji, non-Latin script) — the production parser drops these.
|
||||
* - Always emit segments for any payload shape, even when [DefaultAapDeviceProfile.decodeDeviceInfo]
|
||||
* - Always emit segments for any payload shape, even when [eu.darken.capod.pods.core.apple.aap.protocol.DefaultAapDeviceProfile.decodeDeviceInfo]
|
||||
* would return null (malformed / unknown-shaped packets).
|
||||
*/
|
||||
class AapDeviceInfoDiagnosticsTest : BaseTest() {
|
||||
@@ -78,8 +78,8 @@ class AapDeviceInfoDiagnosticsTest : BaseTest() {
|
||||
// so any emoji / non-Latin engraving is silently skipped. The diagnostic helper must not.
|
||||
val engraving = "My AirPods 🌈"
|
||||
val payload = hex("02 DF 00 04 00") +
|
||||
engraving.toByteArray(Charsets.UTF_8) + byteArrayOf(0x00) +
|
||||
"A3048".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00)
|
||||
engraving.toByteArray(Charsets.UTF_8) + byteArrayOf(0x00) +
|
||||
"A3048".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00)
|
||||
|
||||
val segments = AapDeviceInfoDiagnostics.describeSegments(payload)
|
||||
|
||||
@@ -94,8 +94,8 @@ class AapDeviceInfoDiagnosticsTest : BaseTest() {
|
||||
// diagnostic dump must still surface whatever segments it finds so maintainers can inspect
|
||||
// unexpected-shape packets from an engraved device.
|
||||
val payload = hex("02 DF 00 04 00") +
|
||||
"Name".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00) +
|
||||
"Model".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00)
|
||||
"Name".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00) +
|
||||
"Model".toByteArray(Charsets.UTF_8) + byteArrayOf(0x00)
|
||||
val fullMessageBytes = hex("04 00 04 00 1D 00") + payload
|
||||
val message = AapMessage.parse(fullMessageBytes)!!
|
||||
|
||||
@@ -106,4 +106,4 @@ class AapDeviceInfoDiagnosticsTest : BaseTest() {
|
||||
segments[0].utf8 shouldBe "Name"
|
||||
segments[1].utf8 shouldBe "Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
+255
-210
@@ -1,11 +1,11 @@
|
||||
package eu.darken.capod.pods.core.apple.aap
|
||||
package eu.darken.capod.pods.core.apple.aap.engine
|
||||
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceProfile
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapMessage
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.StemPressEvent
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
import io.kotest.matchers.nulls.shouldBeNull
|
||||
@@ -131,22 +131,28 @@ class AapSessionEngineTest : BaseTest() {
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = supportedModes,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
supported = supportedModes,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.TRANSPARENCY,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
engine.reset()
|
||||
@@ -243,67 +249,68 @@ class AapSessionEngineTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flush with ANC plus later setting keeps ANC recent and verifies ANC first`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val ancSetting = AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = listOf(AapSetting.AncMode.Value.ON, AapSetting.AncMode.Value.ADAPTIVE),
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
fun `flush with ANC plus later setting keeps ANC recent and verifies ANC first`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val ancSetting = AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = listOf(AapSetting.AncMode.Value.ON, AapSetting.AncMode.Value.ADAPTIVE),
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = AapSetting.EarDetection::class as KClass<out AapSetting> to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting =
|
||||
AapSetting.ConversationalAwareness::class as KClass<out AapSetting> to
|
||||
AapSetting.ConversationalAwareness(enabled = false)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
val sendRaw: suspend (AapCommand) -> Unit = { sentCommands += it }
|
||||
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), sendRaw)
|
||||
engine.send(AapCommand.SetConversationalAwareness(true), sendRaw)
|
||||
|
||||
engine.state.value.pendingSettingsCount shouldBe 2
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
nextSetting = AapSetting.EarDetection::class as KClass<out AapSetting> to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
runCurrent()
|
||||
|
||||
sentCommands.size shouldBe 2
|
||||
sentCommands[0] shouldBe AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)
|
||||
sentCommands[1] shouldBe AapCommand.SetConversationalAwareness(true)
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
nextSetting = AapSetting.AncMode::class as KClass<out AapSetting> to
|
||||
ancSetting.copy(current = AapSetting.AncMode.Value.ON)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
// This would still be ADAPTIVE if the mixed flush path lost the ANC send marker and debounced.
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ON
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
advanceTimeBy(1100L)
|
||||
|
||||
sentCommands.size shouldBe 3
|
||||
sentCommands[2] shouldBe AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = AapSetting.EarDetection::class as KClass<out AapSetting> to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting =
|
||||
AapSetting.ConversationalAwareness::class as KClass<out AapSetting> to
|
||||
AapSetting.ConversationalAwareness(enabled = false)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
val sendRaw: suspend (AapCommand) -> Unit = { sentCommands += it }
|
||||
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), sendRaw)
|
||||
engine.send(AapCommand.SetConversationalAwareness(true), sendRaw)
|
||||
|
||||
engine.state.value.pendingSettingsCount shouldBe 2
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
nextSetting = AapSetting.EarDetection::class as KClass<out AapSetting> to AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
runCurrent()
|
||||
|
||||
sentCommands.size shouldBe 2
|
||||
sentCommands[0] shouldBe AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)
|
||||
sentCommands[1] shouldBe AapCommand.SetConversationalAwareness(true)
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
nextSetting = AapSetting.AncMode::class as KClass<out AapSetting> to
|
||||
ancSetting.copy(current = AapSetting.AncMode.Value.ON)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
// This would still be ADAPTIVE if the mixed flush path lost the ANC send marker and debounced.
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ON
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
|
||||
advanceTimeBy(1100L)
|
||||
|
||||
sentCommands.size shouldBe 3
|
||||
sentCommands[2] shouldBe AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
}
|
||||
}
|
||||
|
||||
// ── Message processing — state merge ────────────────────
|
||||
@@ -402,43 +409,50 @@ class AapSessionEngineTest : BaseTest() {
|
||||
inner class InferenceTests {
|
||||
|
||||
@Test
|
||||
fun `startup OFF while no pod is in ear does not infer AllowOffOption true`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
fun `startup OFF while no pod is in ear does not infer AllowOffOption true`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.IN_CASE,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stable in-ear OFF infers AllowOffOption true after delay`() = runTest(UnconfinedTestDispatcher()) {
|
||||
@@ -455,16 +469,20 @@ class AapSessionEngineTest : BaseTest() {
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
|
||||
@@ -473,43 +491,50 @@ class AapSessionEngineTest : BaseTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `later non-OFF ANC update cancels pending AllowOffOption true inference`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
fun `later non-OFF ANC update cancels pending AllowOffOption true inference`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(500L)
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
engine.onHandshakeSent()
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
engine.processMessage(dummyMessage(commandType = 0x0002))
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.OFF,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(500L)
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(1600L)
|
||||
engine.state.value.setting<AapSetting.AllowOffOption>().shouldBeNull()
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rejected OFF command infers AllowOffOption false`() = runTest(UnconfinedTestDispatcher()) {
|
||||
@@ -525,16 +550,20 @@ class AapSessionEngineTest : BaseTest() {
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AllowOffOption(enabled = true))
|
||||
@@ -543,10 +572,12 @@ class AapSessionEngineTest : BaseTest() {
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF)) { sentCommands += it }
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
advanceTimeBy(2100L)
|
||||
@@ -561,47 +592,54 @@ class AapSessionEngineTest : BaseTest() {
|
||||
|
||||
|
||||
@Test
|
||||
fun `matching ANC echo clears pending mode without optimistic current overwrite`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
fun `matching ANC echo clears pending mode without optimistic current overwrite`() =
|
||||
runTest(UnconfinedTestDispatcher()) {
|
||||
val supportedModes = listOf(
|
||||
AapSetting.AncMode.Value.OFF,
|
||||
AapSetting.AncMode.Value.ON,
|
||||
AapSetting.AncMode.Value.ADAPTIVE,
|
||||
)
|
||||
var nextSetting: Pair<KClass<out AapSetting>, AapSetting>? = null
|
||||
val profile = mockProfile {
|
||||
every { decodeSetting(any()) } answers { nextSetting }
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)) { sentCommands += it }
|
||||
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ON
|
||||
|
||||
nextSetting = settingPair(
|
||||
AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
)
|
||||
)
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
engine.state.value.pendingAncMode.shouldBeNull()
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
sentCommands shouldBe listOf(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE))
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.startReady(this as TestScope)
|
||||
|
||||
nextSetting = settingPair(AapSetting.EarDetection(
|
||||
primaryPod = AapSetting.EarDetection.PodPlacement.IN_EAR,
|
||||
secondaryPod = AapSetting.EarDetection.PodPlacement.NOT_IN_EAR,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ON,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
val sentCommands = mutableListOf<AapCommand>()
|
||||
engine.send(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE)) { sentCommands += it }
|
||||
|
||||
engine.state.value.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ON
|
||||
|
||||
nextSetting = settingPair(AapSetting.AncMode(
|
||||
current = AapSetting.AncMode.Value.ADAPTIVE,
|
||||
supported = supportedModes,
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
engine.state.value.pendingAncMode.shouldBeNull()
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
sentCommands shouldBe listOf(AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE))
|
||||
}
|
||||
|
||||
// ── ANC Debounce ────────────────────────────────────────
|
||||
|
||||
@@ -645,7 +683,9 @@ fun `matching ANC echo clears pending mode without optimistic current overwrite`
|
||||
every { decodeBattery(any()) } returns null
|
||||
every { decodePrivateKeyResponse(any()) } returns null
|
||||
every { decodeDeviceInfo(any()) } returns null
|
||||
every { decodeSetting(any()) } returns (AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting.copy(current = AapSetting.AncMode.Value.TRANSPARENCY))
|
||||
every { decodeSetting(any()) } returns (AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting.copy(
|
||||
current = AapSetting.AncMode.Value.TRANSPARENCY
|
||||
))
|
||||
}
|
||||
val engine = AapSessionEngine(profile, timeSource)
|
||||
engine.start(this as TestScope)
|
||||
@@ -658,7 +698,9 @@ fun `matching ANC echo clears pending mode without optimistic current overwrite`
|
||||
engine.state.value.setting<AapSetting.AncMode>()!!.current shouldBe AapSetting.AncMode.Value.ON
|
||||
|
||||
// Second message: unsolicited change — should be debounced
|
||||
every { profile.decodeSetting(any()) } returns (AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting.copy(current = AapSetting.AncMode.Value.TRANSPARENCY))
|
||||
every { profile.decodeSetting(any()) } returns (AapSetting.AncMode::class as KClass<out AapSetting> to ancSetting.copy(
|
||||
current = AapSetting.AncMode.Value.TRANSPARENCY
|
||||
))
|
||||
engine.processMessage(dummyMessage())
|
||||
|
||||
// Not yet applied (debounced)
|
||||
@@ -731,7 +773,10 @@ fun `matching ANC echo clears pending mode without optimistic current overwrite`
|
||||
@Test
|
||||
fun `0x0017 does not run through decode pipeline`() = runTest(UnconfinedTestDispatcher()) {
|
||||
val profile = mockProfile {
|
||||
every { decodeStemPress(any()) } returns StemPressEvent(StemPressEvent.PressType.SINGLE, StemPressEvent.Bud.LEFT)
|
||||
every { decodeStemPress(any()) } returns StemPressEvent(
|
||||
StemPressEvent.PressType.SINGLE,
|
||||
StemPressEvent.Bud.LEFT
|
||||
)
|
||||
}
|
||||
val engine = createEngine(profile)
|
||||
engine.startReady(this as TestScope)
|
||||
@@ -750,4 +795,4 @@ fun `matching ANC echo clears pending mode without optimistic current overwrite`
|
||||
collector.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-6
@@ -1,6 +1,7 @@
|
||||
package eu.darken.capod.pods.core.apple.aap
|
||||
package eu.darken.capod.pods.core.apple.aap.engine
|
||||
|
||||
import eu.darken.capod.common.TimeSource
|
||||
import eu.darken.capod.pods.core.apple.aap.AapPodState
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
@@ -52,7 +53,8 @@ class AapSettingsCoordinatorTest : BaseTest() {
|
||||
fun `enqueue ANC returns pendingAncMode in snapshot`() {
|
||||
val coord = createCoordinator()
|
||||
|
||||
val result = coord.enqueue(emptyList(), AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), stateWithSetting())
|
||||
val result =
|
||||
coord.enqueue(emptyList(), AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), stateWithSetting())
|
||||
|
||||
result.optimisticState.shouldBeNull()
|
||||
result.snapshot.pendingAncMode shouldBe AapSetting.AncMode.Value.ADAPTIVE
|
||||
@@ -96,7 +98,8 @@ class AapSettingsCoordinatorTest : BaseTest() {
|
||||
)
|
||||
|
||||
val first = coord.enqueue(emptyList(), AapCommand.SetAdaptiveAudioNoise(70), state)
|
||||
val second = coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), state)
|
||||
val second =
|
||||
coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), state)
|
||||
|
||||
second.snapshot.count shouldBe 2
|
||||
}
|
||||
@@ -133,7 +136,8 @@ class AapSettingsCoordinatorTest : BaseTest() {
|
||||
)
|
||||
|
||||
val first = coord.enqueue(emptyList(), AapCommand.SetToneVolume(80), state)
|
||||
val second = coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), state)
|
||||
val second =
|
||||
coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.ADAPTIVE), state)
|
||||
val result = coord.flush(second.pendingCommands)
|
||||
|
||||
result.commands shouldHaveSize 2
|
||||
@@ -150,7 +154,8 @@ class AapSettingsCoordinatorTest : BaseTest() {
|
||||
)
|
||||
|
||||
val first = coord.enqueue(emptyList(), AapCommand.SetToneVolume(80), state)
|
||||
val second = coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF), state)
|
||||
val second =
|
||||
coord.enqueue(first.pendingCommands, AapCommand.SetAncMode(AapSetting.AncMode.Value.OFF), state)
|
||||
val third = coord.enqueue(second.pendingCommands, AapCommand.SetAllowOffOption(true), state)
|
||||
val result = coord.flush(third.pendingCommands)
|
||||
|
||||
@@ -296,4 +301,4 @@ class AapSettingsCoordinatorTest : BaseTest() {
|
||||
check(mismatched) shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-12
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.pods.core.apple.aap
|
||||
package eu.darken.capod.pods.core.apple.aap.engine
|
||||
|
||||
import eu.darken.capod.pods.core.apple.aap.HidTracker.HidFrameType
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -23,11 +22,11 @@ class HidTrackerTest : BaseTest() {
|
||||
fun `service directory frame from Pro 3 capture`() {
|
||||
val payload = hexToBytes(
|
||||
"FE 00 00 06 41 50 00 00 00 80 00 00 41 4F 50 00 00 80 00 00 " +
|
||||
"52 54 50 00 00 80 00 00 42 54 4D 00 00 80 00 00 " +
|
||||
"44 53 50 31 00 80 00 00 44 53 50 32 00 80 00 00"
|
||||
"52 54 50 00 00 80 00 00 42 54 4D 00 00 80 00 00 " +
|
||||
"44 53 50 31 00 80 00 00 44 53 50 32 00 80 00 00"
|
||||
)
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.ServiceDirectory>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.ServiceDirectory>()
|
||||
result.services.shouldContainExactly("AP", "AOP", "RTP", "BTM", "DSP1", "DSP2")
|
||||
}
|
||||
|
||||
@@ -36,7 +35,7 @@ class HidTrackerTest : BaseTest() {
|
||||
val fill = ByteArray(65) { 0xFF.toByte() }
|
||||
val payload = hexToBytes("00 04 00 00 44 00 01 A1 81") + fill
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.Descriptor>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Descriptor>()
|
||||
result.phase shouldBe 0x81
|
||||
result.fill shouldBe 0xFF
|
||||
}
|
||||
@@ -46,7 +45,7 @@ class HidTrackerTest : BaseTest() {
|
||||
val fill = ByteArray(65) { 0xEF.toByte() }
|
||||
val payload = hexToBytes("00 04 00 00 44 00 01 C3 02") + fill
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.Descriptor>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Descriptor>()
|
||||
result.phase shouldBe 0x02
|
||||
result.fill shouldBe 0xEF
|
||||
}
|
||||
@@ -55,7 +54,7 @@ class HidTrackerTest : BaseTest() {
|
||||
fun `terminator frame`() {
|
||||
val payload = hexToBytes("00 04 00 00 01 00 FF")
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.Terminator>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Terminator>()
|
||||
result.payloadSize shouldBe 7
|
||||
}
|
||||
|
||||
@@ -63,20 +62,20 @@ class HidTrackerTest : BaseTest() {
|
||||
fun `short frame ending in FF but not 7 bytes is Other`() {
|
||||
val payload = hexToBytes("00 04 00 FF")
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.Other>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Other>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty payload is Other`() {
|
||||
val result = HidTracker.classify(ByteArray(0))
|
||||
result.shouldBeInstanceOf<HidFrameType.Other>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Other>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random payload is Other`() {
|
||||
val payload = hexToBytes("AB CD EF 01 02 03 04 05 06 07 08")
|
||||
val result = HidTracker.classify(payload)
|
||||
result.shouldBeInstanceOf<HidFrameType.Other>()
|
||||
result.shouldBeInstanceOf<HidTracker.HidFrameType.Other>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,4 +210,4 @@ class HidTrackerTest : BaseTest() {
|
||||
logs.shouldBeEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user