Skip to content

Commit 540fb20

Browse files
committed
voice/audio endpoint support, improved debugging of packet decode
1 parent 55cb810 commit 540fb20

File tree

8 files changed

+239
-4
lines changed

8 files changed

+239
-4
lines changed

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
kotlin.code.style=official
22

33
group=io.rebble.libpebblecommon
4-
version=0.1.24
4+
version=0.1.25
55
org.gradle.jvmargs=-Xms128M -Xmx1G -XX:ReservedCodeCacheSize=200M
66
kotlin.native.binary.memoryModel=experimental
77
kotlin.mpp.androidSourceSetLayoutVersion=2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.rebble.libpebblecommon.packets
2+
3+
import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry
4+
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
5+
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
6+
import io.rebble.libpebblecommon.structmapper.*
7+
import io.rebble.libpebblecommon.util.Endian
8+
9+
/**
10+
* Audio streaming packet. Little endian.
11+
*/
12+
sealed class AudioStream(command: Command, sessionId: UShort = 0u) : PebblePacket(ProtocolEndpoint.AUDIO_STREAMING) {
13+
val command = SUByte(m, command.value)
14+
val sessionId = SUShort(m, sessionId, endianness = Endian.Little)
15+
16+
class EncoderFrame : StructMappable() {
17+
val data = SUnboundBytes(m)
18+
}
19+
20+
class DataTransfer : AudioStream(AudioStream.Command.DataTransfer) {
21+
val frameCount = SUByte(m)
22+
val frames = SFixedList(m, 0) {
23+
EncoderFrame()
24+
}
25+
init {
26+
frames.linkWithCount(frameCount)
27+
}
28+
}
29+
30+
class StopTransfer(sessionId: UShort = 0u) : AudioStream(AudioStream.Command.StopTransfer, sessionId)
31+
32+
enum class Command(val value: UByte) {
33+
DataTransfer(0x02u),
34+
StopTransfer(0x03u)
35+
}
36+
}
37+
38+
fun audioStreamPacketsRegister() {
39+
PacketRegistry.register(ProtocolEndpoint.AUDIO_STREAMING, AudioStream.Command.DataTransfer.value) {
40+
AudioStream.DataTransfer()
41+
}
42+
PacketRegistry.register(ProtocolEndpoint.AUDIO_STREAMING, AudioStream.Command.StopTransfer.value) {
43+
AudioStream.StopTransfer()
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.rebble.libpebblecommon.packets
2+
3+
import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry
4+
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
5+
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
6+
import io.rebble.libpebblecommon.structmapper.*
7+
import io.rebble.libpebblecommon.util.DataBuffer
8+
import io.rebble.libpebblecommon.util.Endian
9+
10+
11+
sealed class IncomingVoicePacket() : PebblePacket(ProtocolEndpoint.VOICE_CONTROL) {
12+
/**
13+
* Voice command. See [VoiceCommand].
14+
*/
15+
val command = SUByte(m)
16+
val flags = SUInt(m, endianness = Endian.Little)
17+
}
18+
sealed class OutgoingVoicePacket(command: VoiceCommand) :
19+
PebblePacket(ProtocolEndpoint.VOICE_CONTROL) {
20+
/**
21+
* Voice command. See [VoiceCommand].
22+
*/
23+
val command = SUByte(m, command.value)
24+
val flags = SUInt(m, endianness = Endian.Little)
25+
}
26+
27+
enum class VoiceCommand(val value: UByte) {
28+
SessionSetup(0x01u),
29+
DictationResult(0x02u),
30+
}
31+
32+
class Word(confidence: UByte = 0u, data: String = "") : StructMappable() {
33+
val confidence = SUByte(m, confidence)
34+
val length = SUShort(m, data.length.toUShort(), endianness = Endian.Little)
35+
val data = SFixedString(m, data.length, data)
36+
init {
37+
this.data.linkWithSize(length)
38+
}
39+
}
40+
41+
class Sentence(words: List<Word> = emptyList()) : StructMappable() {
42+
val wordCount = SUShort(m, words.size.toUShort(), endianness = Endian.Little)
43+
val words = SFixedList(m, words.size, words) { Word() }
44+
init {
45+
this.words.linkWithCount(wordCount)
46+
}
47+
}
48+
49+
enum class VoiceAttributeType(val value: UByte) {
50+
SpeexEncoderInfo(0x01u),
51+
Transcription(0x02u),
52+
AppUuid(0x03u),
53+
}
54+
55+
open class VoiceAttribute(id: UByte = 0u, content: StructMappable? = null) : StructMappable() {
56+
val id = SUByte(m, id)
57+
val length = SUShort(m, content?.size?.toUShort() ?: 0u, endianness = Endian.Little)
58+
val content = SBytes(m, content?.size ?: 0, content?.toBytes() ?: ubyteArrayOf())
59+
init {
60+
this.content.linkWithSize(length)
61+
}
62+
63+
class SpeexEncoderInfo : StructMappable() {
64+
val version = SFixedString(m, 20)
65+
val sampleRate = SUInt(m, endianness = Endian.Little)
66+
val bitRate = SUShort(m, endianness = Endian.Little)
67+
val bitstreamVersion = SUByte(m)
68+
val frameSize = SUShort(m, endianness = Endian.Little)
69+
}
70+
71+
class Transcription(
72+
type: UByte = 0x1u,
73+
sentences: List<Sentence> = emptyList()
74+
) : StructMappable() {
75+
val type = SUByte(m, type) // always 0x1? (sentence list)
76+
val count = SUByte(m, sentences.size.toUByte())
77+
val sentences = SFixedList(m, sentences.size, sentences) { Sentence() }
78+
init {
79+
this.sentences.linkWithCount(count)
80+
}
81+
}
82+
83+
class AppUuid : StructMappable() {
84+
val uuid = SUUID(m)
85+
}
86+
}
87+
88+
/**
89+
* Voice session setup command. Little endian.
90+
*/
91+
class SessionSetupCommand : IncomingVoicePacket() {
92+
val sessionType = SUByte(m)
93+
val sessionId = SUShort(m, endianness = Endian.Little)
94+
val attributeCount = SUByte(m)
95+
val attributes = SFixedList(m, 0) {
96+
VoiceAttribute()
97+
}
98+
init {
99+
attributes.linkWithCount(attributeCount)
100+
}
101+
}
102+
103+
enum class SessionType(val value: UByte) {
104+
Dictation(0x01u),
105+
Command(0x02u),
106+
}
107+
108+
enum class Result(val value: UByte) {
109+
Success(0x0u),
110+
FailServiceUnavailable(0x1u),
111+
FailTimeout(0x2u),
112+
FailRecognizerError(0x3u),
113+
FailInvalidRecognizerResponse(0x4u),
114+
FailDisabled(0x5u),
115+
FailInvalidMessage(0x6u),
116+
}
117+
118+
class SessionSetupResult(sessionType: SessionType, result: Result) : OutgoingVoicePacket(VoiceCommand.SessionSetup) {
119+
val sessionType = SUByte(m, sessionType.value)
120+
val result = SUByte(m, result.value)
121+
}
122+
123+
class DictationResult(sessionId: UShort, result: Result, attributes: List<StructMappable>) : OutgoingVoicePacket(VoiceCommand.DictationResult) {
124+
val sessionId = SUShort(m, sessionId, endianness = Endian.Little)
125+
val result = SUByte(m, result.value)
126+
val attributeCount = SUByte(m, attributes.size.toUByte())
127+
val attributes = SFixedList(m, attributes.size, attributes) { VoiceAttribute() }
128+
}
129+
130+
fun voicePacketsRegister() {
131+
PacketRegistry.register(ProtocolEndpoint.VOICE_CONTROL, VoiceCommand.SessionSetup.value) {
132+
SessionSetupCommand()
133+
}
134+
}

src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ object PacketRegistry {
3030
appLogPacketsRegister()
3131
phoneControlPacketsRegister()
3232
logDumpPacketsRegister()
33+
voicePacketsRegister()
34+
audioStreamPacketsRegister()
3335
}
3436

3537
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.rebble.libpebblecommon.services
2+
3+
import io.rebble.libpebblecommon.ProtocolHandler
4+
import io.rebble.libpebblecommon.packets.AudioStream
5+
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
6+
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
7+
import kotlinx.coroutines.channels.Channel
8+
9+
class AudioStreamService(private val protocolHandler: ProtocolHandler) : ProtocolService {
10+
val receivedMessages = Channel<AudioStream>(Channel.BUFFERED)
11+
12+
init {
13+
protocolHandler.registerReceiveCallback(ProtocolEndpoint.AUDIO_STREAMING, this::receive)
14+
}
15+
16+
suspend fun send(packet: AudioStream) {
17+
protocolHandler.send(packet)
18+
}
19+
20+
fun receive(packet: PebblePacket) {
21+
if (packet !is AudioStream) {
22+
throw IllegalStateException("Received invalid packet type: $packet")
23+
}
24+
25+
receivedMessages.trySend(packet)
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.rebble.libpebblecommon.services
2+
3+
import io.rebble.libpebblecommon.ProtocolHandler
4+
import io.rebble.libpebblecommon.packets.*
5+
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
6+
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
7+
import kotlinx.coroutines.channels.Channel
8+
9+
class VoiceService(private val protocolHandler: ProtocolHandler) : ProtocolService {
10+
val receivedMessages = Channel<IncomingVoicePacket>(Channel.BUFFERED)
11+
12+
init {
13+
protocolHandler.registerReceiveCallback(ProtocolEndpoint.VOICE_CONTROL, this::receive)
14+
}
15+
16+
suspend fun send(packet: OutgoingVoicePacket) {
17+
protocolHandler.send(packet)
18+
}
19+
20+
fun receive(packet: PebblePacket) {
21+
if (packet !is IncomingVoicePacket) {
22+
throw IllegalStateException("Received invalid packet type: $packet")
23+
}
24+
25+
receivedMessages.trySend(packet)
26+
}
27+
}

src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/StructMappable.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import io.rebble.libpebblecommon.util.DataBuffer
44
import io.rebble.libpebblecommon.util.Endian
55

66
abstract class StructMappable(endianness: Endian = Endian.Unspecified) : Mappable(endianness) {
7-
val m = StructMapper(endianness = endianness)
7+
val m = StructMapper(endianness = endianness, debugTag = this::class.simpleName)
88

99
override fun toBytes(): UByteArray {
1010
return m.toBytes()

src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/StructMapper.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import io.rebble.libpebblecommon.util.Endian
77
/**
88
* Maps class properties to a struct equivalent
99
*/
10-
class StructMapper(endianness: Endian = Endian.Unspecified): Mappable(endianness) {
10+
class StructMapper(endianness: Endian = Endian.Unspecified, private val debugTag: String? = null): Mappable(endianness) {
1111
private var struct: MutableList<Mappable> = mutableListOf()
1212

1313
/**
@@ -40,7 +40,7 @@ class StructMapper(endianness: Endian = Endian.Unspecified): Mappable(endianness
4040
try {
4141
mappable.fromBytes(bytes)
4242
}catch (e: Exception) {
43-
throw PacketDecodeException("Unable to deserialize mappable ${mappable::class.simpleName} at index $i (${mappable})", e)
43+
throw PacketDecodeException("Unable to deserialize mappable ${mappable::class.simpleName} at index $i (${mappable}) ($debugTag)\n${bytes.array().toHexString()}", e)
4444
}
4545

4646
}

0 commit comments

Comments
 (0)