diff --git a/src/androidMain/kotlin/util/DataBuffer.kt b/src/androidMain/kotlin/util/DataBuffer.kt index 6c426fe..c4ca318 100644 --- a/src/androidMain/kotlin/util/DataBuffer.kt +++ b/src/androidMain/kotlin/util/DataBuffer.kt @@ -9,18 +9,33 @@ actual class DataBuffer { actual constructor(size: Int) { actualBuf = ByteBuffer.allocate(size) } + actual constructor(bytes: UByteArray) { actualBuf = ByteBuffer.wrap(bytes.toByteArray()) } + /** + * Total length of the buffer + */ + actual val length: Int + get() = actualBuf.capacity() + + /** + * Current position in the buffer + */ + actual val readPosition: Int + get() = actualBuf.position() + actual fun putUShort(short: UShort) { actualBuf.putShort(short.toShort()) } + actual fun getUShort(): UShort = actualBuf.short.toUShort() actual fun putShort(short: Short) { actualBuf.putShort(short) } + actual fun getShort(): Short = actualBuf.short actual fun putUByte(byte: UByte) { diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/exceptions/packet.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/exceptions/packet.kt index 346a611..f57e5d4 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/exceptions/packet.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/exceptions/packet.kt @@ -1,4 +1,4 @@ package io.rebble.libpebblecommon.exceptions -class PacketEncodeException(message: String?): Exception(message) -class PacketDecodeException(message: String?): Exception(message) \ No newline at end of file +class PacketEncodeException(message: String?) : Exception(message) +class PacketDecodeException(message: String?, cause: Exception? = null) : Exception(message, cause) \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Music.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Music.kt new file mode 100644 index 0000000..08e0364 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Music.kt @@ -0,0 +1,168 @@ +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.* + +open class MusicControl(val message: Message) : PebblePacket(ProtocolEndpoint.MUSIC_CONTROL) { + val command = SUByte(m, message.value) + + init { + type = command.get() + } + + enum class Message(val value: UByte) { + PlayPause(0x01u), + Pause(0x02u), + Play(0x03u), + NextTrack(0x04u), + PreviousTrack(0x05u), + VolumeUp(0x06u), + VolumeDown(0x07u), + GetCurrentTrack(0x08u), + UpdateCurrentTrack(0x10u), + UpdatePlayStateInfo(0x11u), + UpdateVolumeInfo(0x12u), + UpdatePlayerInfo(0x13u) + } + + class UpdateCurrentTrack( + artist: String = "", + album: String = "", + title: String = "", + trackLength: Int? = null, + trackCount: Int? = null, + currentTrack: Int? = null + ) : MusicControl(Message.UpdateCurrentTrack) { + val artist = SString(m, artist) + val album = SString(m, album) + val title = SString(m, title) + val trackLength = SOptional( + m, + SUInt(StructMapper(), trackLength?.toUInt() ?: 0u, '<'), + trackLength != null + ) + val trackCount = SOptional( + m, + SUInt(StructMapper(), trackCount?.toUInt() ?: 0u, '<'), + trackCount != null + ) + val currentTrack = SOptional( + m, + SUInt(StructMapper(), currentTrack?.toUInt() ?: 0u, '<'), + currentTrack != null + ) + } + + class UpdatePlayStateInfo( + playbackState: PlaybackState = PlaybackState.Unknown, + trackPosition: UInt = 0u, + playRate: UInt = 0u, + shuffle: ShuffleState = ShuffleState.Unknown, + repeat: RepeatState = RepeatState.Unknown + ) : MusicControl(Message.UpdatePlayStateInfo) { + val state = SUByte(m, playbackState.value) + val trackPosition = SUInt(m, trackPosition, '<') + val playRate = SUInt(m, playRate, '<') + val shuffle = SUByte(m, shuffle.value) + val repeat = SUByte(m, repeat.value) + } + + class UpdateVolumeInfo( + volumePercent: UByte = 0u, + ) : MusicControl(Message.UpdateVolumeInfo) { + val volumePercent = SUByte(m, volumePercent) + } + + class UpdatePlayerInfo( + pkg: String = "", + name: String = "" + ) : MusicControl(Message.UpdatePlayerInfo) { + val pkg = SString(m, pkg) + val name = SString(m, name) + } + + enum class PlaybackState(val value: UByte) { + Paused(0x00u), + Playing(0x01u), + Rewinding(0x02u), + FastForwarding(0x03u), + Unknown(0x04u), + } + + enum class ShuffleState(val value: UByte) { + Unknown(0x00u), + Off(0x01u), + On(0x02u), + } + + enum class RepeatState(val value: UByte) { + Unknown(0x00u), + Off(0x01u), + One(0x02u), + All(0x03u), + } +} + +fun musicPacketsRegister() { + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.PlayPause.value + ) { MusicControl(MusicControl.Message.PlayPause) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.Pause.value + ) { MusicControl(MusicControl.Message.Pause) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.Play.value + ) { MusicControl(MusicControl.Message.Play) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.NextTrack.value + ) { MusicControl(MusicControl.Message.NextTrack) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.PreviousTrack.value + ) { MusicControl(MusicControl.Message.PreviousTrack) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.VolumeUp.value + ) { MusicControl(MusicControl.Message.VolumeUp) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.VolumeDown.value + ) { MusicControl(MusicControl.Message.VolumeDown) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.GetCurrentTrack.value + ) { MusicControl(MusicControl.Message.GetCurrentTrack) } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.UpdateCurrentTrack.value + ) { MusicControl.UpdateCurrentTrack() } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.UpdatePlayStateInfo.value + ) { MusicControl.UpdatePlayStateInfo() } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.UpdateVolumeInfo.value + ) { MusicControl.UpdateVolumeInfo() } + + PacketRegistry.register( + ProtocolEndpoint.MUSIC_CONTROL, + MusicControl.Message.UpdatePlayerInfo.value + ) { MusicControl.UpdatePlayerInfo() } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt index 4a6b8fb..4a50b0e 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt @@ -1,12 +1,9 @@ package io.rebble.libpebblecommon.protocolhelpers import io.rebble.libpebblecommon.exceptions.PacketDecodeException -import io.rebble.libpebblecommon.packets.appRunStatePacketsRegister -import io.rebble.libpebblecommon.packets.appmessagePacketsRegister +import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.packets.blobdb.blobDBPacketsRegister import io.rebble.libpebblecommon.packets.blobdb.timelinePacketsRegister -import io.rebble.libpebblecommon.packets.systemPacketsRegister -import io.rebble.libpebblecommon.packets.timePacketsRegister /** * Singleton to track endpoint / type discriminators for deserialization @@ -22,6 +19,7 @@ object PacketRegistry { blobDBPacketsRegister() appmessagePacketsRegister() appRunStatePacketsRegister() + musicPacketsRegister() } /** @@ -46,7 +44,7 @@ object PacketRegistry { val typeOffset = if (typeOffsets[endpoint] != null) typeOffsets[endpoint]!! else 4 val decoder = epdecoders[packet[typeOffset]] - ?: throw PacketDecodeException("No packet class registered for endpoint $endpoint") + ?: throw PacketDecodeException("No packet class registered for type ${packet[typeOffset]} of $endpoint") return decoder(packet) } } \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt index c655cf3..1f5aee3 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt @@ -58,15 +58,19 @@ open class PebblePacket{ val length = SUShort(meta) val ep = SUShort(meta) meta.fromBytes(buf) - if (packet.size <= (2*UShort.SIZE_BYTES)) + if (packet.size <= (2 * UShort.SIZE_BYTES)) throw PacketDecodeException("Malformed packet: contents empty") - if (length.get().toInt() != (packet.size - (2*UShort.SIZE_BYTES))) + if (length.get().toInt() != (packet.size - (2 * UShort.SIZE_BYTES))) throw PacketDecodeException("Malformed packet: bad length") val ret = PacketRegistry.get( ProtocolEndpoint.getByValue(ep.get()), packet ) - ret.m.fromBytes(buf) + try { + ret.m.fromBytes(buf) + } catch (e: Exception) { + throw PacketDecodeException("Failed to decode packet $ret", e) + } return ret } } diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/services/MusicService.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/MusicService.kt new file mode 100644 index 0000000..8069ce4 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/MusicService.kt @@ -0,0 +1,28 @@ +package io.rebble.libpebblecommon.services + +import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.packets.MusicControl +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import kotlinx.coroutines.channels.Channel + +class MusicService(private val protocolHandler: ProtocolHandler) : ProtocolService { + val receivedMessages = Channel(Channel.BUFFERED) + + init { + protocolHandler.registerReceiveCallback(ProtocolEndpoint.MUSIC_CONTROL, this::receive) + } + + suspend fun send(packet: MusicControl) { + protocolHandler.send(packet) + } + + fun receive(packet: PebblePacket) { + if (packet !is MusicControl) { + throw IllegalStateException("Received invalid packet type: $packet") + } + + receivedMessages.offer(packet) + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/services/SystemService.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/SystemService.kt index ed41975..a4f6bc4 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/services/SystemService.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/SystemService.kt @@ -86,7 +86,8 @@ class SystemService(private val protocolHandler: ProtocolHandler) : ProtocolServ 2u, ProtocolCapsFlag.makeFlags( listOf( - ProtocolCapsFlag.Supports8kAppMessage + ProtocolCapsFlag.Supports8kAppMessage, + ProtocolCapsFlag.SupportsExtendedMusicProtocol ) ) diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt index 1bf2988..eb1f2e7 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt @@ -377,4 +377,41 @@ class SFixedList( override fun hashCode(): Int { return list.hashCode() } +} + +class SOptional( + mapper: StructMapper, + val value: StructElement, + var present: Boolean +) : Mappable { + init { + mapper.register(this) + } + + override fun toBytes(): UByteArray { + return if (present) value.toBytes() else UByteArray(0) + } + + override fun fromBytes(bytes: DataBuffer) { + val leftBytes = bytes.length - bytes.readPosition + if (leftBytes < value.size) { + present = false + } else { + present = true + value.fromBytes(bytes) + } + } + + fun get(): T? { + return if (present) value.get() else null + } + + fun set(value: T?) { + if (value != null) { + present = true + this.value.set(value) + } else { + present = false + } + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/util/DataBuffer.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/DataBuffer.kt index 7f127b9..8d490cc 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/util/DataBuffer.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/DataBuffer.kt @@ -34,4 +34,14 @@ expect class DataBuffer { fun array(): UByteArray fun setEndian(endian: Char) + + /** + * Total length of the buffer + */ + val length: Int + + /** + * Current position in the buffer + */ + val readPosition: Int } \ No newline at end of file diff --git a/src/iosMain/kotlin/util/DataBuffer.kt b/src/iosMain/kotlin/util/DataBuffer.kt index 607e89c..897f4a2 100644 --- a/src/iosMain/kotlin/util/DataBuffer.kt +++ b/src/iosMain/kotlin/util/DataBuffer.kt @@ -2,20 +2,36 @@ package io.rebble.libpebblecommon.util import kotlinx.cinterop.* import platform.Foundation.* -import platform.darwin.NSUInteger actual class DataBuffer { private val actualBuf: NSMutableData private var littleEndian = false + /** + * Total length of the buffer + */ + actual val length: Int + get() = actualBuf.length.toInt() + + private var _readPosition: Int = 0 + + /** + * Current position in the buffer + */ + actual val readPosition: Int + get() = _readPosition + actual constructor(size: Int) { actualBuf = NSMutableData.dataWithLength(size.toULong())!! actualBuf.setLength(size.toULong()) } + actual constructor(bytes: UByteArray) { actualBuf = NSMutableData() - actualBuf.setData(NSString.create(string = bytes.toString()) - .dataUsingEncoding(NSUTF8StringEncoding, false)!!) + actualBuf.setData( + NSString.create(string = bytes.toString()) + .dataUsingEncoding(NSUTF8StringEncoding, false)!! + ) } actual fun putUShort(short: UShort) { @@ -29,6 +45,7 @@ actual class DataBuffer { memScoped { val pShort = alloc() actualBuf.getBytes(pShort.ptr, UShort.SIZE_BYTES.toULong()) + _readPosition += UShort.SIZE_BYTES return pShort.value } } @@ -44,6 +61,7 @@ actual class DataBuffer { memScoped { val pShort = alloc() actualBuf.getBytes(pShort.ptr, Short.SIZE_BYTES.toULong()) + _readPosition += Short.SIZE_BYTES return pShort.value } } @@ -59,6 +77,7 @@ actual class DataBuffer { memScoped { val pByte = alloc() actualBuf.appendBytes(pByte.ptr, UByte.SIZE_BYTES.toULong()) + _readPosition += UByte.SIZE_BYTES return pByte.value } } @@ -74,6 +93,7 @@ actual class DataBuffer { memScoped { val pByte = alloc() actualBuf.appendBytes(pByte.ptr, Byte.SIZE_BYTES.toULong()) + _readPosition += Byte.SIZE_BYTES return pByte.value } } @@ -88,6 +108,7 @@ actual class DataBuffer { memScoped { val pBytes = allocArray(count) actualBuf.getBytes(pBytes.getPointer(this), length = count.toULong()) + _readPosition += count return pBytes.readBytes(count).toUByteArray() } } @@ -110,6 +131,7 @@ actual class DataBuffer { memScoped { val pUInt = alloc() actualBuf.getBytes(pUInt.ptr, UInt.SIZE_BYTES.toULong()) + _readPosition += UInt.SIZE_BYTES return pUInt.value } } @@ -125,6 +147,7 @@ actual class DataBuffer { memScoped { val pInt = alloc() actualBuf.getBytes(pInt.ptr, Int.SIZE_BYTES.toULong()) + _readPosition += Int.SIZE_BYTES return pInt.value } } @@ -133,13 +156,14 @@ actual class DataBuffer { memScoped { val pULong = alloc() pULong.value = ulong - actualBuf.appendBytes(pULong.ptr, UByte.SIZE_BYTES.toULong()) + actualBuf.appendBytes(pULong.ptr, ULong.SIZE_BYTES.toULong()) } } actual fun getULong(): ULong { memScoped { val pULong = alloc() actualBuf.getBytes(pULong.ptr, ULong.SIZE_BYTES.toULong()) + _readPosition += ULong.SIZE_BYTES return pULong.value } } diff --git a/src/jvmMain/kotlin/util/DataBuffer.kt b/src/jvmMain/kotlin/util/DataBuffer.kt index a49476d..254f51b 100644 --- a/src/jvmMain/kotlin/util/DataBuffer.kt +++ b/src/jvmMain/kotlin/util/DataBuffer.kt @@ -9,18 +9,34 @@ actual class DataBuffer { actual constructor(size: Int) { actualBuf = ByteBuffer.allocate(size) } + actual constructor(bytes: UByteArray) { actualBuf = ByteBuffer.wrap(bytes.toByteArray()) } + /** + * Total length of the buffer + */ + actual val length: Int + get() = actualBuf.capacity() + + /** + * Current position in the buffer + */ + actual val readPosition: Int + get() = actualBuf.position() + + actual fun putUShort(short: UShort) { actualBuf.putShort(short.toShort()) } + actual fun getUShort(): UShort = actualBuf.short.toUShort() actual fun putShort(short: Short) { actualBuf.putShort(short) } + actual fun getShort(): Short = actualBuf.short actual fun putUByte(byte: UByte) { diff --git a/src/jvmTest/kotlin/io/rebble/libpebblecommon/packets/MusicControlTest.kt b/src/jvmTest/kotlin/io/rebble/libpebblecommon/packets/MusicControlTest.kt new file mode 100644 index 0000000..06f3d58 --- /dev/null +++ b/src/jvmTest/kotlin/io/rebble/libpebblecommon/packets/MusicControlTest.kt @@ -0,0 +1,105 @@ +package io.rebble.libpebblecommon.packets + +import assertUByteArrayEquals +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import kotlin.test.Test +import kotlin.test.assertEquals + + +class MusicControlTest { + @Test + fun `serialize UpdateCurrentTrack with optional parameters`() { + val packet = MusicControl.UpdateCurrentTrack( + "A", + "B", + "C", + 10, + 20, + 30 + ) + + val expectedData = ubyteArrayOf( + 0u, 19u, + 0u, 32u, + 16u, + 1u, 65u, + 1u, 66u, + 1u, 67u, + 10u, 0u, 0u, 0u, + 20u, 0u, 0u, 0u, + 30u, 0u, 0u, 0u + ) + + val actualData = packet.serialize() + + assertUByteArrayEquals(expectedData, actualData) + } + + @Test + fun `serialize UpdateCurrentTrack without optional parameters`() { + val packet = MusicControl.UpdateCurrentTrack( + "A", + "B", + "C" + ) + + val expectedData = ubyteArrayOf( + 0u, 7u, + 0u, 32u, + 16u, + 1u, 65u, + 1u, 66u, + 1u, 67u + ) + + val actualData = packet.serialize() + + assertUByteArrayEquals(expectedData, actualData) + } + + @Test + fun `deserialize UpdateCurrentTrack with optional parameters`() { + val data = ubyteArrayOf( + 0u, 19u, + 0u, 32u, + 16u, + 1u, 65u, + 1u, 66u, + 1u, 67u, + 10u, 0u, 0u, 0u, + 20u, 0u, 0u, 0u, + 30u, 0u, 0u, 0u + ) + + val packet = PebblePacket.deserialize(data) as MusicControl.UpdateCurrentTrack + + assertEquals("A", packet.artist.get()) + assertEquals("B", packet.album.get()) + assertEquals("C", packet.title.get()) + assertEquals(10u, packet.trackLength.get()) + assertEquals(20u, packet.trackCount.get()) + assertEquals(30u, packet.currentTrack.get()) + } + + @Test + fun `deserialize UpdateCurrentTrack without optional parameters`() { + val data = ubyteArrayOf( + 0u, 7u, + 0u, 32u, + 16u, + 1u, 65u, + 1u, 66u, + 1u, 67u + ) + + val packet = PebblePacket.deserialize(data) as MusicControl.UpdateCurrentTrack + + assertEquals("A", packet.artist.get()) + assertEquals("B", packet.album.get()) + assertEquals("C", packet.title.get()) + assertEquals(null, packet.trackLength.get()) + assertEquals(null, packet.trackCount.get()) + assertEquals(null, packet.currentTrack.get()) + } + +} \ No newline at end of file