diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/disk/PbwBinHeader.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/disk/PbwBinHeader.kt new file mode 100644 index 0000000..857f2cd --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/disk/PbwBinHeader.kt @@ -0,0 +1,140 @@ +package io.rebble.libpebblecommon.disk + +import io.rebble.libpebblecommon.packets.blobdb.AppMetadata +import io.rebble.libpebblecommon.structmapper.* +import io.rebble.libpebblecommon.util.DataBuffer + +/** + * Header of the + */ +class PbwBinHeader() : StructMappable() { + /** + * Major header version. + */ + val headerVersionMajor: SUByte = SUByte(m) + + /** + * Minor header version. + */ + val headerVersionMinor: SUByte = SUByte(m) + + /** + * Major sdk version. + */ + val sdkVersionMajor: SUByte = SUByte(m) + + /** + * Minor sdk version. + */ + val sdkVersionMinor: SUByte = SUByte(m) + + /** + * Major app version. + */ + val appVersionMajor: SUByte = SUByte(m) + + /** + * Minor app version. + */ + val appVersionMinor: SUByte = SUByte(m) + + /** + * Size of the app payload in bytes + */ + val appSize: SUShort = SUShort(m) + + /** + * ??? (Presumably offset where app payload starts?) + */ + val appOffset: SUInt = SUInt(m) + + /** + * CRC checksum of the app payload + */ + val crc: SUInt = SUInt(m) + + /** + * Name of the app + */ + val appName: SFixedString = SFixedString(m, 32) + + /** + * Name of the company that made the app + */ + val companyName: SFixedString = SFixedString(m, 32) + + /** + * Resource ID of the primary icon. + */ + val icon: SUInt = SUInt(m) + + /** + * ??? + */ + val symbolTableAddress: SUInt = SUInt(m) + + /** + * List of app install flags. Should be forwarded to the watch when inserting into BlobDB. + */ + val flags: SUInt = SUInt(m) + + /** + * ??? + */ + val numRelocationListEntries: SUInt = SUInt(m) + + /** + * UUID of the app + */ + val uuid: SUUID = SUUID(m) + + fun toBlobDbApp(): AppMetadata { + return AppMetadata().also { + it.uuid.set(uuid.get()) + it.flags.set(flags.get()) + it.icon.set(icon.get()) + it.appVersionMajor.set(appVersionMajor.get()) + it.appVersionMinor.set(appVersionMinor.get()) + it.sdkVersionMajor.set(sdkVersionMajor.get()) + it.sdkVersionMinor.set(sdkVersionMinor.get()) + it.appName.set(appName.get()) + } + } + + companion object { + const val SIZE: Int = 8 + 2 + 2 + 2 + 2 + 4 + 4 + 32 + 32 + 4 + 4 + 4 + 4 + 16 + + /** + * Parse existing Pbw binary payload header. You should read [SIZE] bytes from the binary + * payload and pass it into this method. + * + * @throws IllegalArgumentException if header is not valid pebble app header + */ + fun parseFileHeader(data: UByteArray): PbwBinHeader { + if (data.size != SIZE) { + throw IllegalArgumentException( + "Read data from the file should be exactly $SIZE bytes" + ) + } + + val buffer = DataBuffer(data) + + val sentinel = buffer.getBytes(8) + if (!sentinel.contentEquals(EXPECTED_SENTINEL)) { + throw IllegalArgumentException("Sentinel does not match") + } + + return PbwBinHeader().also { + it.fromBytes(buffer) + } + } + + /** + * First 8 bytes of the header, spelling the word "PBLAPP" in ASCII, + * followed by two zeros. + */ + private val EXPECTED_SENTINEL = ubyteArrayOf( + 0x50u, 0x42u, 0x4Cu, 0x41u, 0x50u, 0x50u, 0x00u, 0x00u + ) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchHardwarePlatform.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchHardwarePlatform.kt new file mode 100644 index 0000000..05aeeca --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchHardwarePlatform.kt @@ -0,0 +1,33 @@ +package io.rebble.libpebblecommon.metadata + +enum class WatchHardwarePlatform(val protocolNumber: UByte, val watchType: WatchType) { + PEBBLE_ONE_EV_1(1u, WatchType.APLITE), + PEBBLE_ONE_EV_2(2u, WatchType.APLITE), + PEBBLE_ONE_EV_2_3(3u, WatchType.APLITE), + PEBBLE_ONE_EV_2_4(4u, WatchType.APLITE), + PEBBLE_ONE_POINT_FIVE(5u, WatchType.APLITE), + PEBBLE_ONE_POINT_ZERO(6u, WatchType.APLITE), + PEBBLE_SNOWY_EVT_2(7u, WatchType.BASALT), + PEBBLE_SNOWY_DVT(8u, WatchType.BASALT), + PEBBLE_BOBBY_SMILES(10u, WatchType.BASALT), + PEBBLE_ONE_BIGBOARD_2(254u, WatchType.APLITE), + PEBBLE_ONE_BIGBOARD(255u, WatchType.APLITE), + PEBBLE_SNOWY_BIGBOARD(253u, WatchType.BASALT), + PEBBLE_SNOWY_BIGBOARD_2(252u, WatchType.BASALT), + PEBBLE_SPALDING_EVT(9u, WatchType.CHALK), + PEBBLE_SPALDING_PVT(11u, WatchType.CHALK), + PEBBLE_SPALDING_BIGBOARD(251u, WatchType.CHALK), + PEBBLE_SILK_EVT(12u, WatchType.DIORITE), + PEBBLE_SILK(14u, WatchType.DIORITE), + PEBBLE_SILK_BIGBOARD(250u, WatchType.DIORITE), + PEBBLE_SILK_BIGBOARD_2_PLUS(248u, WatchType.DIORITE), + PEBBLE_ROBERT_EVT(13u, WatchType.EMERY), + PEBBLE_ROBERT_BIGBOARD(249u, WatchType.EMERY), + PEBBLE_ROBERT_BIGBOARD_2(247u, WatchType.EMERY); + + companion object { + fun fromProtocolNumber(number: UByte): WatchHardwarePlatform? { + return values().firstOrNull { it.protocolNumber == number } + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchType.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchType.kt new file mode 100644 index 0000000..ab8d37d --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchType.kt @@ -0,0 +1,9 @@ +package io.rebble.libpebblecommon.metadata + +enum class WatchType(val codename: String) { + APLITE("aplite"), + BASALT("basalt"), + CHALK("chalk"), + DIORITE("diorite"), + EMERY("emery") +} \ No newline at end of file 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/packets/System.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/System.kt index d791ce3..5abdc9b 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/System.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/System.kt @@ -1,5 +1,6 @@ package io.rebble.libpebblecommon.packets +import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.packets.PhoneAppVersion.AppVersionRequest import io.rebble.libpebblecommon.packets.PhoneAppVersion.AppVersionResponse import io.rebble.libpebblecommon.packets.WatchVersion.WatchVersionRequest @@ -372,10 +373,15 @@ class WatchFirmwareVersion : StructMappable() { val versionTag = SFixedString(m, 32) val gitHash = SFixedString(m, 8) val isRecovery = SBoolean(m) + + /** + * See [WatchHardwarePlatform] + */ val hardwarePlatform = SUByte(m) val metadataVersion = SUByte(m) } + open class SystemMessage(message: Message) : SystemPacket(endpoint) { enum class Message(val value: UByte) { NewFirmwareAvailable(0x00u), diff --git a/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/App.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/App.kt new file mode 100644 index 0000000..8aaa913 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/App.kt @@ -0,0 +1,59 @@ +package io.rebble.libpebblecommon.packets.blobdb + +import io.rebble.libpebblecommon.structmapper.* + +/** + * Data of the APP BlobDB Entry + */ +class AppMetadata() : StructMappable() { + /** + * UUID of the app + */ + val uuid: SUUID = SUUID(m) + + /** + * App install flags. + */ + val flags: SUInt = SUInt(m) + + /** + * Resource ID of the primary icon. + */ + val icon: SUInt = SUInt(m) + + /** + * Major app version. + */ + val appVersionMajor: SUByte = SUByte(m) + + /** + * Minor app version. + */ + val appVersionMinor: SUByte = SUByte(m) + + /** + * Major sdk version. + */ + val sdkVersionMajor: SUByte = SUByte(m) + + /** + * Minor sdk version. + */ + val sdkVersionMinor: SUByte = SUByte(m) + + /** + * ??? (Always sent as 0 in the Pebble app) + */ + val appFaceBgColor: SUByte = SUByte(m, 0u) + + /** + * ??? (Always sent as 0 in the Pebble app) + */ + val appFaceTemplateId: SUByte = SUByte(m, 0u) + + /** + * Name of the app + */ + val appName: SFixedString = SFixedString(m, 96) +} + 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/protocolhelpers/PebblePacket.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt index 1f5aee3..844c6ca 100644 --- a/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt @@ -17,12 +17,13 @@ open class PebblePacket{ constructor(endpoint: ProtocolEndpoint) { //TODO: Packet-level endianness? this.endpoint = endpoint } - constructor(packet: UByteArray, endianness: Char = '>') { + + constructor(packet: UByteArray) { val meta = StructMapper() val length = SUShort(meta) val ep = SUShort(meta) meta.fromBytes(DataBuffer(packet)) - if (length.get() != (packet.size - (UShort.SIZE_BYTES*2)).toUShort()) + if (length.get() != (packet.size - (UShort.SIZE_BYTES * 2)).toUShort()) throw IllegalArgumentException("Length in packet does not match packet actual size, likely malformed") println("Importing packet: Len $length | EP $ep") 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/Crc32Calculator.kt b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/Crc32Calculator.kt new file mode 100644 index 0000000..cccf276 --- /dev/null +++ b/src/commonMain/kotlin/io/rebble/libpebblecommon/util/Crc32Calculator.kt @@ -0,0 +1,63 @@ +package io.rebble.libpebblecommon.util + +/** + * CRC32 hash Calculator that is compatible with hardware CRC32 on the STM chips. + */ +class Crc32Calculator { + private var finalized = false + private var value: UInt = 0xFFFFFFFFu + + private var leftoverBytes = UByteArray(0) + + fun addBytes(bytes: UByteArray) { + if (finalized) { + throw IllegalStateException("Cannot add more bytes to finalized CRC calculation") + } + + val mergedArray = leftoverBytes + bytes + val buffer = DataBuffer(mergedArray) + buffer.setEndian('<') + + val finalPosition = mergedArray.size - mergedArray.size % 4 + while (buffer.readPosition < finalPosition) { + addInt(buffer.getUInt()) + } + + leftoverBytes = mergedArray.copyOfRange(finalPosition, mergedArray.size) + } + + /** + * Finalizes the calculation and returns the CRC32 result + */ + fun finalize(): UInt { + if (finalized) { + return value + } + + if (leftoverBytes.isNotEmpty()) { + leftoverBytes = leftoverBytes.padZerosLeft(4 - leftoverBytes.size).reversedArray() + addInt(DataBuffer(leftoverBytes).apply { setEndian('<') }.getUInt()) + } + + finalized = true + return value + } + + private fun addInt(valueToAdd: UInt) { + this.value = this.value xor valueToAdd + + for (i in 0 until 32) { + if ((this.value and 0x80000000u) != 0u) { + this.value = (this.value shl 1) xor 0x04C11DB7u + } else { + this.value = this.value shl 1 + } + } + + this.value = this.value and 0xFFFFFFFFu + } +} + +private fun UByteArray.padZerosLeft(amount: Int): UByteArray { + return UByteArray(amount) { 0u } + this +} \ No newline at end of file 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 diff --git a/src/jvmTest/kotlin/io/rebble/libpebblecommon/util/CrcCalculatorTest.kt b/src/jvmTest/kotlin/io/rebble/libpebblecommon/util/CrcCalculatorTest.kt new file mode 100644 index 0000000..49377e2 --- /dev/null +++ b/src/jvmTest/kotlin/io/rebble/libpebblecommon/util/CrcCalculatorTest.kt @@ -0,0 +1,69 @@ +package io.rebble.libpebblecommon.util + +import kotlin.test.Test +import kotlin.test.assertEquals + +class CrcCalculatorTest { + @Test + fun assertEmpty() { + assertEquals( + 0xffffffffu, + calculateCrcOfBuffer(*ubyteArrayOf()) + ) + } + + @Test + fun assertOneByte() { + assertEquals( + 0x1d604014u, + calculateCrcOfBuffer(0xABu) + ) + } + + @Test + fun assertFourBytes() { + assertEquals( + 0x1dabe74fu, + calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u) + ) + } + + @Test + fun assertSixBytes() { + assertEquals( + 0x205dbd4fu, + calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u) + ) + } + + @Test + fun assertEightBytesAtOnce() { + assertEquals( + 0x99f9e573u, + calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u, 0x50u, 0x06u, 0x70u, 0x08u) + ) + } + + @Test + fun assertEightBytesInChunks() { + val calculator = Crc32Calculator() + calculator.addBytes(ubyteArrayOf(0x01u)) + calculator.addBytes(ubyteArrayOf(0x02u)) + calculator.addBytes(ubyteArrayOf(0x03u, 0x04u, 0x50u, 0x06u)) + calculator.addBytes(ubyteArrayOf(0x70u, 0x08u)) + + assertEquals( + 0x99f9e573u, + calculator.finalize() + ) + + } + + private fun calculateCrcOfBuffer(vararg buffer: UByte): UInt { + val calculator = Crc32Calculator() + + calculator.addBytes(buffer) + + return calculator.finalize() + } +} \ No newline at end of file