Skip to content

Commit

Permalink
voice/audio endpoint support, improved debugging of packet decode
Browse files Browse the repository at this point in the history
  • Loading branch information
crc-32 committed Oct 18, 2024
1 parent 55cb810 commit 540fb20
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 4 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
kotlin.code.style=official

group=io.rebble.libpebblecommon
version=0.1.24
version=0.1.25
org.gradle.jvmargs=-Xms128M -Xmx1G -XX:ReservedCodeCacheSize=200M
kotlin.native.binary.memoryModel=experimental
kotlin.mpp.androidSourceSetLayoutVersion=2
Expand Down
45 changes: 45 additions & 0 deletions src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Audio.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.rebble.libpebblecommon.packets

import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
import io.rebble.libpebblecommon.structmapper.*
import io.rebble.libpebblecommon.util.Endian

/**
* Audio streaming packet. Little endian.
*/
sealed class AudioStream(command: Command, sessionId: UShort = 0u) : PebblePacket(ProtocolEndpoint.AUDIO_STREAMING) {
val command = SUByte(m, command.value)
val sessionId = SUShort(m, sessionId, endianness = Endian.Little)

class EncoderFrame : StructMappable() {
val data = SUnboundBytes(m)
}

class DataTransfer : AudioStream(AudioStream.Command.DataTransfer) {
val frameCount = SUByte(m)
val frames = SFixedList(m, 0) {
EncoderFrame()
}
init {
frames.linkWithCount(frameCount)
}
}

class StopTransfer(sessionId: UShort = 0u) : AudioStream(AudioStream.Command.StopTransfer, sessionId)

enum class Command(val value: UByte) {
DataTransfer(0x02u),
StopTransfer(0x03u)
}
}

fun audioStreamPacketsRegister() {
PacketRegistry.register(ProtocolEndpoint.AUDIO_STREAMING, AudioStream.Command.DataTransfer.value) {
AudioStream.DataTransfer()
}
PacketRegistry.register(ProtocolEndpoint.AUDIO_STREAMING, AudioStream.Command.StopTransfer.value) {
AudioStream.StopTransfer()
}
}
134 changes: 134 additions & 0 deletions src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Voice.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.rebble.libpebblecommon.packets

import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
import io.rebble.libpebblecommon.structmapper.*
import io.rebble.libpebblecommon.util.DataBuffer
import io.rebble.libpebblecommon.util.Endian


sealed class IncomingVoicePacket() : PebblePacket(ProtocolEndpoint.VOICE_CONTROL) {
/**
* Voice command. See [VoiceCommand].
*/
val command = SUByte(m)
val flags = SUInt(m, endianness = Endian.Little)
}
sealed class OutgoingVoicePacket(command: VoiceCommand) :
PebblePacket(ProtocolEndpoint.VOICE_CONTROL) {
/**
* Voice command. See [VoiceCommand].
*/
val command = SUByte(m, command.value)
val flags = SUInt(m, endianness = Endian.Little)
}

enum class VoiceCommand(val value: UByte) {
SessionSetup(0x01u),
DictationResult(0x02u),
}

class Word(confidence: UByte = 0u, data: String = "") : StructMappable() {
val confidence = SUByte(m, confidence)
val length = SUShort(m, data.length.toUShort(), endianness = Endian.Little)
val data = SFixedString(m, data.length, data)
init {
this.data.linkWithSize(length)
}
}

class Sentence(words: List<Word> = emptyList()) : StructMappable() {
val wordCount = SUShort(m, words.size.toUShort(), endianness = Endian.Little)
val words = SFixedList(m, words.size, words) { Word() }
init {
this.words.linkWithCount(wordCount)
}
}

enum class VoiceAttributeType(val value: UByte) {
SpeexEncoderInfo(0x01u),
Transcription(0x02u),
AppUuid(0x03u),
}

open class VoiceAttribute(id: UByte = 0u, content: StructMappable? = null) : StructMappable() {
val id = SUByte(m, id)
val length = SUShort(m, content?.size?.toUShort() ?: 0u, endianness = Endian.Little)
val content = SBytes(m, content?.size ?: 0, content?.toBytes() ?: ubyteArrayOf())
init {
this.content.linkWithSize(length)
}

class SpeexEncoderInfo : StructMappable() {
val version = SFixedString(m, 20)
val sampleRate = SUInt(m, endianness = Endian.Little)
val bitRate = SUShort(m, endianness = Endian.Little)
val bitstreamVersion = SUByte(m)
val frameSize = SUShort(m, endianness = Endian.Little)
}

class Transcription(
type: UByte = 0x1u,
sentences: List<Sentence> = emptyList()
) : StructMappable() {
val type = SUByte(m, type) // always 0x1? (sentence list)
val count = SUByte(m, sentences.size.toUByte())
val sentences = SFixedList(m, sentences.size, sentences) { Sentence() }
init {
this.sentences.linkWithCount(count)
}
}

class AppUuid : StructMappable() {
val uuid = SUUID(m)
}
}

/**
* Voice session setup command. Little endian.
*/
class SessionSetupCommand : IncomingVoicePacket() {
val sessionType = SUByte(m)
val sessionId = SUShort(m, endianness = Endian.Little)
val attributeCount = SUByte(m)
val attributes = SFixedList(m, 0) {
VoiceAttribute()
}
init {
attributes.linkWithCount(attributeCount)
}
}

enum class SessionType(val value: UByte) {
Dictation(0x01u),
Command(0x02u),
}

enum class Result(val value: UByte) {
Success(0x0u),
FailServiceUnavailable(0x1u),
FailTimeout(0x2u),
FailRecognizerError(0x3u),
FailInvalidRecognizerResponse(0x4u),
FailDisabled(0x5u),
FailInvalidMessage(0x6u),
}

class SessionSetupResult(sessionType: SessionType, result: Result) : OutgoingVoicePacket(VoiceCommand.SessionSetup) {
val sessionType = SUByte(m, sessionType.value)
val result = SUByte(m, result.value)
}

class DictationResult(sessionId: UShort, result: Result, attributes: List<StructMappable>) : OutgoingVoicePacket(VoiceCommand.DictationResult) {
val sessionId = SUShort(m, sessionId, endianness = Endian.Little)
val result = SUByte(m, result.value)
val attributeCount = SUByte(m, attributes.size.toUByte())
val attributes = SFixedList(m, attributes.size, attributes) { VoiceAttribute() }
}

fun voicePacketsRegister() {
PacketRegistry.register(ProtocolEndpoint.VOICE_CONTROL, VoiceCommand.SessionSetup.value) {
SessionSetupCommand()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ object PacketRegistry {
appLogPacketsRegister()
phoneControlPacketsRegister()
logDumpPacketsRegister()
voicePacketsRegister()
audioStreamPacketsRegister()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.rebble.libpebblecommon.services

import io.rebble.libpebblecommon.ProtocolHandler
import io.rebble.libpebblecommon.packets.AudioStream
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
import kotlinx.coroutines.channels.Channel

class AudioStreamService(private val protocolHandler: ProtocolHandler) : ProtocolService {
val receivedMessages = Channel<AudioStream>(Channel.BUFFERED)

init {
protocolHandler.registerReceiveCallback(ProtocolEndpoint.AUDIO_STREAMING, this::receive)
}

suspend fun send(packet: AudioStream) {
protocolHandler.send(packet)
}

fun receive(packet: PebblePacket) {
if (packet !is AudioStream) {
throw IllegalStateException("Received invalid packet type: $packet")
}

receivedMessages.trySend(packet)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.rebble.libpebblecommon.services

import io.rebble.libpebblecommon.ProtocolHandler
import io.rebble.libpebblecommon.packets.*
import io.rebble.libpebblecommon.protocolhelpers.PebblePacket
import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint
import kotlinx.coroutines.channels.Channel

class VoiceService(private val protocolHandler: ProtocolHandler) : ProtocolService {
val receivedMessages = Channel<IncomingVoicePacket>(Channel.BUFFERED)

init {
protocolHandler.registerReceiveCallback(ProtocolEndpoint.VOICE_CONTROL, this::receive)
}

suspend fun send(packet: OutgoingVoicePacket) {
protocolHandler.send(packet)
}

fun receive(packet: PebblePacket) {
if (packet !is IncomingVoicePacket) {
throw IllegalStateException("Received invalid packet type: $packet")
}

receivedMessages.trySend(packet)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.rebble.libpebblecommon.util.DataBuffer
import io.rebble.libpebblecommon.util.Endian

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

override fun toBytes(): UByteArray {
return m.toBytes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.rebble.libpebblecommon.util.Endian
/**
* Maps class properties to a struct equivalent
*/
class StructMapper(endianness: Endian = Endian.Unspecified): Mappable(endianness) {
class StructMapper(endianness: Endian = Endian.Unspecified, private val debugTag: String? = null): Mappable(endianness) {
private var struct: MutableList<Mappable> = mutableListOf()

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

}
Expand Down

0 comments on commit 540fb20

Please sign in to comment.