From 39bbc546bc58d3ad9ad8a95a01bced3813ecd529 Mon Sep 17 00:00:00 2001 From: Matej Drobnic Date: Wed, 17 Mar 2021 15:59:33 +0100 Subject: [PATCH] add app install packets --- .../libpebblecommon/packets/AppFetch.kt | 89 ++++++++++++ .../libpebblecommon/packets/PutBytes.kt | 137 ++++++++++++++++++ .../protocolhelpers/PacketRegistry.kt | 2 + .../services/AppFetchService.kt | 29 ++++ .../services/PutBytesService.kt | 29 ++++ .../libpebblecommon/structmapper/types.kt | 20 ++- .../rebble/libpebblecommon/util/PacketSize.kt | 29 ++++ 7 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppFetch.kt create mode 100644 src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PutBytes.kt create mode 100644 src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppFetchService.kt create mode 100644 src/commonMain/kotlin/io/rebble/libpebblecommon/services/PutBytesService.kt create mode 100644 src/commonMain/kotlin/io/rebble/libpebblecommon/util/PacketSize.kt diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppFetch.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppFetch.kt new file mode 100644 index 0000000..b29cb73 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppFetch.kt @@ -0,0 +1,89 @@ +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.SUByte +import io.rebble.libpebblecommon.structmapper.SUInt +import io.rebble.libpebblecommon.structmapper.SUUID + +sealed class AppFetchIncomingPacket() : PebblePacket(ProtocolEndpoint.APP_FETCH) { + /** + * Request command. See [AppFetchRequestCommand]. + */ + val command = SUByte(m) + +} + +sealed class AppFetchOutgoingPacket(command: AppFetchRequestCommand) : + PebblePacket(ProtocolEndpoint.APP_FETCH) { + /** + * Request command. See [AppFetchRequestCommand]. + */ + val command = SUByte(m, command.value) + +} + + +/** + * Packet sent from the watch when user opens an app that is not in the watch storage. + */ +class AppFetchRequest : AppFetchIncomingPacket() { + + /** + * UUID of the app to request + */ + val uuid = SUUID(m) + + /** + * ID of the app bank. Use in the [PutBytesAppInit] packet to identify this app install. + */ + val appId = SUInt(m, endianness = '<') +} + +/** + * Packet sent from the watch when user opens an app that is not in the watch storage. + */ +class AppFetchResponse( + status: AppFetchResponseStatus +) : AppFetchOutgoingPacket(AppFetchRequestCommand.FETCH_APP) { + /** + * Response status + */ + val status = SUByte(m, status.value) + +} + +enum class AppFetchRequestCommand(val value: UByte) { + FETCH_APP(0x01u) +} + +enum class AppFetchResponseStatus(val value: UByte) { + /** + * Sent right before starting to send PutBytes data + */ + START(0x01u), + + /** + * Sent when phone PutBytes is already busy sending something else + */ + BUSY(0x02u), + + /** + * Sent when UUID that watch sent is not in the locker + */ + INVALID_UUID(0x03u), + + /** + * Sent when there is generic data sending error (such as failure to read the local pbw file) + */ + NO_DATA(0x01u), +} + + +fun appFetchIncomingPacketsRegister() { + PacketRegistry.register( + ProtocolEndpoint.APP_FETCH, + AppFetchRequestCommand.FETCH_APP.value + ) { AppFetchRequest() } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PutBytes.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PutBytes.kt new file mode 100644 index 0000000..24cc3a9 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PutBytes.kt @@ -0,0 +1,137 @@ +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.SBytes +import io.rebble.libpebblecommon.structmapper.SNullTerminatedString +import io.rebble.libpebblecommon.structmapper.SUByte +import io.rebble.libpebblecommon.structmapper.SUInt + +sealed class PutBytesOutgoingPacket(command: PutBytesCommand) : + PebblePacket(ProtocolEndpoint.PUT_BYTES) { + /** + * Request command. See [PutBytesCommand]. + */ + val command = SUByte(m, command.value) + +} + +class PutBytesResponse : PebblePacket(ProtocolEndpoint.PUT_BYTES) { + + /** + * See [PutBytesResult] + */ + val result = SUByte(m) + + /** + * Cookie to send to all other put bytes requests + */ + val cookie = SUInt(m) +} + +/** + * Send to init non-app related file transfer + */ +class PutBytesInit( + objectSize: UInt, + objectType: ObjectType, + bank: UByte, + filename: String +) : PutBytesOutgoingPacket(PutBytesCommand.INIT) { + val objectSize = SUInt(m, objectSize) + val objectType = SUByte(m, objectType.value) + val bank = SUByte(m, bank) + val filename = SNullTerminatedString(m, filename) +} + +/** + * Send to init app-specific file transfer. + */ +class PutBytesAppInit( + objectSize: UInt, + objectType: ObjectType, + appId: UInt +) : PutBytesOutgoingPacket(PutBytesCommand.INIT) { + val objectSize = SUInt(m, objectSize) + + // Object type in app init packet must have 8th bit set (?) + val objectType = SUByte(m, objectType.value or (1u shl 7).toUByte()) + val appId = SUInt(m, appId) +} + +/** + * Send file data to the watch. After every put you have to wait for response from the watch. + */ +class PutBytesPut( + cookie: UInt, + payload: UByteArray +) : PutBytesOutgoingPacket(PutBytesCommand.PUT) { + val cookie = SUInt(m, cookie) + val payloadSize = SUInt(m, payload.size.toUInt()) + val payload = SBytes(m, payload.size, payload) +} + +/** + * Sent when current file transfer is complete. [objectCrc] is the CRC32 hash of the sent payload. + */ +class PutBytesCommit( + cookie: UInt, + objectCrc: UInt +) : PutBytesOutgoingPacket(PutBytesCommand.COMMIT) { + val cookie = SUInt(m, cookie) + val objectCrc = SUInt(m, objectCrc) +} + +/** + * Send when there was an error during transfer and transfer cannot complete. + */ +class PutBytesAbort( + cookie: UInt +) : PutBytesOutgoingPacket(PutBytesCommand.ABORT) { + val cookie = SUInt(m, cookie) +} + +/** + * Send after app-related file was commited to complete install sequence + */ +class PutBytesInstall( + cookie: UInt +) : PutBytesOutgoingPacket(PutBytesCommand.INSTALL) { + val cookie = SUInt(m, cookie) +} + +enum class PutBytesCommand(val value: UByte) { + INIT(0x01u), + PUT(0x02u), + COMMIT(0x03u), + ABORT(0x04u), + INSTALL(0x05u) +} + +enum class PutBytesResult(val value: UByte) { + ACK(0x01u), + NACK(0x02u) +} + +enum class ObjectType(val value: UByte) { + FIRMWARE(0x01u), + RECOVERY(0x02u), + SYSTEM_RESOURCE(0x03u), + APP_RESOURCE(0x04u), + APP_EXECUTABLE(0x05u), + FILE(0x06u), + WORKER(0x07u) +} + +fun putBytesIncomingPacketsRegister() { + PacketRegistry.register( + ProtocolEndpoint.PUT_BYTES, + PutBytesResult.ACK.value, + ) { PutBytesResponse() } + + PacketRegistry.register( + ProtocolEndpoint.PUT_BYTES, + PutBytesResult.NACK.value, + ) { PutBytesResponse() } +} \ 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 4a50b0e..b844b70 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt @@ -20,6 +20,8 @@ object PacketRegistry { appmessagePacketsRegister() appRunStatePacketsRegister() musicPacketsRegister() + appFetchIncomingPacketsRegister() + putBytesIncomingPacketsRegister() } /** diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppFetchService.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppFetchService.kt new file mode 100644 index 0000000..403030d --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppFetchService.kt @@ -0,0 +1,29 @@ +package io.rebble.libpebblecommon.services + +import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.packets.AppFetchIncomingPacket +import io.rebble.libpebblecommon.packets.AppFetchOutgoingPacket +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import kotlinx.coroutines.channels.Channel + +class AppFetchService(private val protocolHandler: ProtocolHandler) : ProtocolService { + val receivedMessages = Channel(Channel.BUFFERED) + + init { + protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_FETCH, this::receive) + } + + suspend fun send(packet: AppFetchOutgoingPacket) { + protocolHandler.send(packet) + } + + fun receive(packet: PebblePacket) { + if (packet !is AppFetchIncomingPacket) { + 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/PutBytesService.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/PutBytesService.kt new file mode 100644 index 0000000..488e5d5 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/services/PutBytesService.kt @@ -0,0 +1,29 @@ +package io.rebble.libpebblecommon.services + +import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.packets.PutBytesOutgoingPacket +import io.rebble.libpebblecommon.packets.PutBytesResponse +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import kotlinx.coroutines.channels.Channel + +class PutBytesService(private val protocolHandler: ProtocolHandler) : ProtocolService { + val receivedMessages = Channel(Channel.BUFFERED) + + init { + protocolHandler.registerReceiveCallback(ProtocolEndpoint.PUT_BYTES, this::receive) + } + + suspend fun send(packet: PutBytesOutgoingPacket) { + protocolHandler.send(packet) + } + + fun receive(packet: PebblePacket) { + if (packet !is PutBytesResponse) { + 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/structmapper/types.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt index a1988ca..6bddb72 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/types.kt @@ -269,6 +269,24 @@ class SFixedString(mapper: StructMapper, size: Int, default: String = "") : }, mapper, size, default ) +/** + * Upload-only type that writes String as unbound null-terminated byte array. + */ +class SNullTerminatedString(mapper: StructMapper, default: String = "") : + StructElement( + { buf, el -> + val bytes = el.get().encodeToByteArray() + + buf.putBytes( + bytes.toUByteArray() + ) + buf.putUByte(0u) + }, + { buf, el -> + throw UnsupportedOperationException("SNullTerminatedString is upload-only") + }, mapper, 0, default + ) + /** * Represents arbitrary bytes in a struct * @param length the number of bytes, when serializing this is used to pad/truncate the provided value to ensure it's 'length' bytes long (-1 to disable this) @@ -358,7 +376,7 @@ class SFixedList( } override val size: Int - get() = list.fold(0, {t,el -> t+el.size}) + get() = list.fold(0, { t, el -> t + el.size }) /** * Link the count of this element to the value of another struct element. Count will diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/util/PacketSize.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/PacketSize.kt new file mode 100644 index 0000000..ff01525 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/PacketSize.kt @@ -0,0 +1,29 @@ +package io.rebble.libpebblecommon.util + +import io.rebble.libpebblecommon.packets.ProtocolCapsFlag +import io.rebble.libpebblecommon.packets.WatchVersion +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint + +fun getMaxPebblePacketPayloadSize( + endpoint: ProtocolEndpoint, + watchVersion: WatchVersion.WatchVersionResponse? +): Int { + if (endpoint != ProtocolEndpoint.APP_MESSAGE) { + return STANDARD_MAX_PEBBLE_PACKET_SIZE + } + + val capabilities = watchVersion?.capabilities?.let { ProtocolCapsFlag.fromFlags(it.get()) } + + return if (capabilities?.contains(ProtocolCapsFlag.Supports8kAppMessage) == true) { + 8222 + } else { + STANDARD_MAX_PEBBLE_PACKET_SIZE + } +} + +fun getPutBytesMaximumDataSize(watchVersion: WatchVersion.WatchVersionResponse?): Int { + // 4 bytes get used for the cookie + return getMaxPebblePacketPayloadSize(ProtocolEndpoint.PUT_BYTES, watchVersion) - 4 +} + +val STANDARD_MAX_PEBBLE_PACKET_SIZE = 2048