├── .github └── workflows │ ├── build.yml │ ├── pull.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Package.swift.template ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libpebblecommon.iosMain.iml ├── libpebblecommon.iosTest.iml ├── settings.gradle.kts └── src ├── androidMain └── kotlin │ ├── main.kt │ └── util │ ├── Bitmap.kt │ ├── DataBuffer.kt │ └── UtilFunctionsDroid.kt ├── commonMain └── kotlin │ └── io │ └── rebble │ └── libpebblecommon │ ├── Logging.kt │ ├── PacketPriority.kt │ ├── ProtocolHandler.kt │ ├── ProtocolHandlerImpl.kt │ ├── ble │ ├── GATTPacket.kt │ └── LEConstants.kt │ ├── disk │ └── PbwBinHeader.kt │ ├── exceptions │ └── packet.kt │ ├── main.kt │ ├── metadata │ ├── StringOrBoolean.kt │ ├── WatchHardwarePlatform.kt │ ├── WatchType.kt │ ├── pbw │ │ ├── appinfo │ │ │ ├── Media.kt │ │ │ ├── PbwAppInfo.kt │ │ │ ├── Resources.kt │ │ │ └── Watchapp.kt │ │ └── manifest │ │ │ ├── Debug.kt │ │ │ ├── PbwBlob.kt │ │ │ ├── PbwManifest.kt │ │ │ └── SdkVersion.kt │ └── pbz │ │ └── manifest │ │ ├── JsTooling.kt │ │ ├── PbzFirmware.kt │ │ ├── PbzManifest.kt │ │ └── SystemResources.kt │ ├── packets │ ├── AppCustomization.kt │ ├── AppFetch.kt │ ├── AppLog.kt │ ├── AppMessage.kt │ ├── AppReorder.kt │ ├── AppRunState.kt │ ├── Audio.kt │ ├── Emulator.kt │ ├── LogDump.kt │ ├── Music.kt │ ├── PhoneControl.kt │ ├── PutBytes.kt │ ├── Screenshot.kt │ ├── System.kt │ ├── Voice.kt │ └── blobdb │ │ ├── App.kt │ │ ├── BlobDB.kt │ │ ├── Notification.kt │ │ ├── Timeline.kt │ │ └── TimelineIcon.kt │ ├── protocolhelpers │ ├── PacketRegistry.kt │ ├── PebblePacket.kt │ └── ProtocolEndpoint.kt │ ├── services │ ├── AppFetchService.kt │ ├── AppLogService.kt │ ├── AppReorderService.kt │ ├── AudioStreamService.kt │ ├── LogDumpService.kt │ ├── MusicService.kt │ ├── PhoneControlService.kt │ ├── ProtocolService.kt │ ├── PutBytesService.kt │ ├── ScreenshotService.kt │ ├── SystemService.kt │ ├── VoiceService.kt │ ├── app │ │ └── AppRunStateService.kt │ ├── appmessage │ │ └── AppMessageService.kt │ ├── blobdb │ │ ├── BlobDBService.kt │ │ └── TimelineService.kt │ └── notification │ │ └── NotificationService.kt │ ├── structmapper │ ├── StructMappable.kt │ ├── StructMapper.kt │ └── types.kt │ └── util │ ├── Bitmap.kt │ ├── Crc32Calculator.kt │ ├── DataBuffer.kt │ ├── Endian.kt │ ├── LazyLock.kt │ ├── PacketSize.kt │ ├── PebbleColor.kt │ ├── SerializationUtil.kt │ ├── TimelineAttributeFactory.kt │ └── UtilFunctions.kt ├── commonTest └── kotlin │ ├── TestProtocolHandler.kt │ ├── TestUtils.kt │ ├── Tests.kt │ └── io │ └── rebble │ └── libpebblecommon │ ├── metadata │ └── pbw │ │ ├── appinfo │ │ └── TestAppInfo.kt │ │ └── manifest │ │ └── TestManifest.kt │ └── packets │ ├── AppMessageTest.kt │ ├── AppRunStateMessageTest.kt │ ├── SystemMessageTest.kt │ └── blobdb │ └── AppTest.kt ├── iosMain └── kotlin │ ├── io │ └── rebble │ │ └── libpebblecommon │ │ └── util │ │ └── Bitmap.kt │ ├── main.kt │ └── util │ ├── DataBuffer.kt │ └── UtilFunctions.kt ├── jvmMain ├── jvmMain.iml └── kotlin │ ├── io │ └── rebble │ │ └── libpebblecommon │ │ └── util │ │ └── Bitmap.kt │ ├── main.kt │ └── util │ ├── DataBuffer.kt │ └── UtilFunctionsJVM.kt └── jvmTest ├── jvmTest.iml └── kotlin ├── DeviceTest.kt ├── JvmTestUtils.kt └── io └── rebble └── libpebblecommon ├── packets └── MusicControlTest.kt ├── services └── notification │ └── NotificationServiceTest.kt └── util └── CrcCalculatorTest.kt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | 9 | steps: 10 | - name: Setup JDK 11 | uses: actions/setup-java@v1.4.4 12 | with: 13 | java-version: 17 14 | - name: Checkout source 15 | uses: actions/checkout@v4 16 | - name: Cache build deps 17 | uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.gradle/wrapper 21 | ~/.konan/cache 22 | ~/.konan/dependencies 23 | key: build-deps-${{ runner.os }}-${{ hashFiles('gradle/**', 'gradlew*', 'gradle.properties', '*.gradle*') }} 24 | - name: Build 25 | run: ./gradlew build 26 | -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: Build PR 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: macos-latest 8 | 9 | steps: 10 | - name: Setup JDK 11 | uses: actions/setup-java@v1.4.4 12 | with: 13 | java-version: 17 14 | - name: Checkout source 15 | uses: actions/checkout@v4 16 | - name: Cache build deps 17 | uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.gradle/wrapper 21 | ~/.konan/cache 22 | ~/.konan/dependencies 23 | key: build-deps-${{ runner.os }}-${{ hashFiles('gradle/**', 'gradlew*', 'gradle.properties', '*.gradle*') }} 24 | - name: Build 25 | run: ./gradlew build 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-swift: 9 | runs-on: macos-latest 10 | steps: 11 | - name: Setup JDK 12 | uses: actions/setup-java@v1.4.4 13 | with: 14 | java-version: 17 15 | - name: Checkout source 16 | uses: actions/checkout@v4 17 | - name: Cache build deps 18 | uses: actions/cache@v4 19 | with: 20 | path: | 21 | ~/.gradle/wrapper 22 | ~/.konan/cache 23 | ~/.konan/dependencies 24 | key: build-deps-${{ runner.os }}-${{ hashFiles('gradle/**', 'gradlew*', 'gradle.properties', '*.gradle*') }} 25 | 26 | - name: Build XCFrameworks 27 | run: ./gradlew assembleXCFramework 28 | 29 | - name: Zip XCFramework 30 | uses: vimtor/action-zip@v1 31 | with: 32 | files: build/xcframework/ 33 | dest: build/xcframework.zip 34 | 35 | - name: Add release artifacts 36 | id: release_artifacts 37 | uses: softprops/action-gh-release@15d2aaca23625e5b2744248f7b68fc1e6bbff48e 38 | with: 39 | tag_name: ${{ github.event.release.tag_name }} 40 | files: | 41 | build/xcframework.zip 42 | 43 | - name: Calculate checksums for XCFrameworks 44 | run: | 45 | cp Package.swift.template Package.swift 46 | echo RELEASE_CHECKSUM=$(swift package compute-checksum build/xcframework.zip) >> $GITHUB_ENV 47 | 48 | - name: Checkout master 49 | uses: actions/checkout@v2 50 | with: 51 | ref: 'master' 52 | 53 | - name: Update swift package 54 | run: | 55 | sed -e 's|RELEASE-URL|${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}/xcframework.zip|;w Package.swift.tmp' Package.swift.template 56 | sed -e 's/RELEASE-CHECKSUM/${{ env.RELEASE_CHECKSUM }}/;w Package.swift' Package.swift.tmp 57 | 58 | - name: Commit swift package 59 | run: | 60 | git config user.name "github-actions[bot]" 61 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 62 | git add Package.swift 63 | git commit -m "[CI] Update swift package" 64 | git tag swiftpm-${{ github.event.release.tag_name }} 65 | git push origin swiftpm-${{ github.event.release.tag_name }} 66 | 67 | publish-maven: 68 | runs-on: macos-latest 69 | 70 | steps: 71 | - name: Setup JDK 72 | uses: actions/setup-java@v1.4.4 73 | with: 74 | java-version: 17 75 | - name: Cache build deps 76 | uses: actions/cache@v2 77 | with: 78 | path: | 79 | ~/.gradle/wrapper 80 | ~/.konan/cache 81 | ~/.konan/dependencies 82 | key: build-deps-${{ hashFiles('~/.gradle/**') }}-${{ hashFiles('~/.konan/**') }} 83 | 84 | - name: Checkout source 85 | uses: actions/checkout@v2 86 | 87 | - name: Publish artifact 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | 91 | # The GITHUB_REF tag comes in the format 'refs/tags/xxx'. 92 | # If we split on '/' and take the 3rd value, 93 | # we can get the release name. 94 | run: | 95 | NEW_VERSION=$(echo "${GITHUB_REF}" | cut -d "/" -f3) 96 | echo "New version: ${NEW_VERSION}" 97 | echo "Github username: ${GITHUB_ACTOR}" 98 | ./gradlew -Pversion=${NEW_VERSION} publish 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /local.properties 2 | /build 3 | /.idea 4 | /.gradle 5 | **.DS_Store 6 | /.kotlin 7 | -------------------------------------------------------------------------------- /Package.swift.template: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | let package = Package( 4 | name: "libpebblecommon", 5 | platforms: [ 6 | .iOS(.v9) 7 | ], 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "libpebblecommon", 12 | targets: ["libpebblecommon"] 13 | ) 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | ], 18 | targets: [ 19 | .binaryTarget( 20 | name: "libpebblecommon", 21 | url: "RELEASE-URL", 22 | checksum: "RELEASE-CHECKSUM" 23 | ) 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libpebblecommon 2 | A port/rewrite of [libpebble2](https://github.com/pebble/libpebble2/) to Kotlin Multiplatform with additional features useful for watch companion apps 3 | 4 | ![Build](https://github.com/crc-32/libpebblecommon/workflows/Build/badge.svg) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | group=io.rebble.libpebblecommon 4 | version=0.1.27 5 | org.gradle.jvmargs=-Xms128M -Xmx1G -XX:ReservedCodeCacheSize=200M 6 | kotlin.native.binary.memoryModel=experimental 7 | kotlin.mpp.androidSourceSetLayoutVersion=2 8 | kotlinVersion=2.0.0 9 | agpVersion=8.1.4 -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.1.4" 3 | kotlin = "2.0.0" 4 | klock = "2.4.13" 5 | ktor = "1.6.7" 6 | coroutine = "1.8.0" 7 | uuid = "0.4.1" 8 | serialization = "1.5.0" 9 | kermit = "2.0.0-RC4" 10 | 11 | [plugins] 12 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 13 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 14 | android-library = { id = "com.android.library", version.ref = "agp" } 15 | 16 | [libraries] 17 | uuid = { module = "com.benasher44:uuid", version.ref = "uuid" } 18 | klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" } 19 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } 20 | serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } 21 | kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } 22 | ktor-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" } 23 | ktor-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } 24 | ktor-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 25 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 26 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 27 | 28 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pebble-dev/libpebblecommon/ae55f4d6b52bb1e21030c3b6d7ae11114cc11c05/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /libpebblecommon.iosMain.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | libpebblecommon:commonMain 7 | 8 | libpebblecommon.commonMain~1 9 | 10 | COMPILATION_AND_SOURCE_SET_HOLDER 11 | 12 | 13 | 18 | 21 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /libpebblecommon.iosTest.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | libpebblecommon:commonTest 7 | 8 | libpebblecommon.commonTest~1 9 | libpebblecommon.iosMain 10 | libpebblecommon.commonMain~1 11 | 12 | COMPILATION_AND_SOURCE_SET_HOLDER 13 | 14 | 15 | 20 | 23 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven { url = uri("https://jitpack.io") } 7 | } 8 | } 9 | 10 | rootProject.name = "libpebblecommon" -------------------------------------------------------------------------------- /src/androidMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import io.rebble.libpebblecommon.packets.PhoneAppVersion 4 | 5 | actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Android -------------------------------------------------------------------------------- /src/androidMain/kotlin/util/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | /** 4 | * Convert android bitmap into common multiplatform bitmap. 5 | * 6 | * Only supported for [android.graphics.Bitmap.Config.ARGB_8888] formats 7 | */ 8 | actual class Bitmap(private val androidBitmap: android.graphics.Bitmap) { 9 | init { 10 | if (androidBitmap.config != android.graphics.Bitmap.Config.ARGB_8888) { 11 | throw IllegalArgumentException("Only ARGB_8888 bitmaps are supported") 12 | } 13 | } 14 | 15 | actual val width: Int 16 | get() = androidBitmap.width 17 | actual val height: Int 18 | get() = androidBitmap.height 19 | 20 | /** 21 | * Return pixel at the specified position at in AARRGGBB format. 22 | */ 23 | actual fun getPixel(x: Int, y: Int): Int { 24 | return androidBitmap.getPixel(x, y) 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/androidMain/kotlin/util/DataBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.ByteOrder 5 | 6 | actual class DataBuffer { 7 | private val actualBuf: ByteBuffer 8 | 9 | actual constructor(size: Int) { 10 | actualBuf = ByteBuffer.allocate(size) 11 | } 12 | 13 | actual constructor(bytes: UByteArray) { 14 | actualBuf = ByteBuffer.wrap(bytes.toByteArray()) 15 | } 16 | 17 | /** 18 | * Total length of the buffer 19 | */ 20 | actual val length: Int 21 | get() = actualBuf.capacity() 22 | 23 | /** 24 | * Current position in the buffer 25 | */ 26 | actual val readPosition: Int 27 | get() = actualBuf.position() 28 | 29 | actual val remaining: Int 30 | get() = actualBuf.remaining() 31 | 32 | actual fun putUShort(short: UShort) { 33 | actualBuf.putShort(short.toShort()) 34 | } 35 | 36 | actual fun getUShort(): UShort = actualBuf.short.toUShort() 37 | 38 | actual fun putShort(short: Short) { 39 | actualBuf.putShort(short) 40 | } 41 | 42 | actual fun getShort(): Short = actualBuf.short 43 | 44 | actual fun putUByte(byte: UByte) { 45 | actualBuf.put(byte.toByte()) 46 | } 47 | actual fun getUByte(): UByte = actualBuf.get().toUByte() 48 | 49 | actual fun putByte(byte: Byte) { 50 | actualBuf.put(byte) 51 | } 52 | actual fun getByte(): Byte = actualBuf.get() 53 | 54 | actual fun putBytes(bytes: UByteArray) { 55 | actualBuf.put(bytes.toByteArray()) 56 | } 57 | actual fun getBytes(count: Int): UByteArray { 58 | val tBuf = ByteArray(count) 59 | actualBuf.get(tBuf) 60 | return tBuf.toUByteArray() 61 | } 62 | 63 | actual fun array(): UByteArray = actualBuf.array().toUByteArray() 64 | 65 | actual fun setEndian(endian: Endian) { 66 | when (endian) { 67 | Endian.Big -> actualBuf.order(ByteOrder.BIG_ENDIAN) 68 | Endian.Little -> actualBuf.order(ByteOrder.LITTLE_ENDIAN) 69 | else -> {} 70 | } 71 | } 72 | 73 | actual fun putInt(int: Int) { 74 | actualBuf.putInt(int) 75 | } 76 | actual fun getInt(): Int = actualBuf.int 77 | 78 | actual fun putUInt(uint: UInt) { 79 | actualBuf.putInt(uint.toInt()) 80 | } 81 | actual fun getUInt(): UInt = actualBuf.int.toUInt() 82 | 83 | actual fun putULong(ulong: ULong) { 84 | actualBuf.putLong(ulong.toLong()) 85 | } 86 | actual fun getULong(): ULong = actualBuf.long.toULong() 87 | 88 | actual fun rewind() { 89 | actualBuf.rewind() 90 | } 91 | } -------------------------------------------------------------------------------- /src/androidMain/kotlin/util/UtilFunctionsDroid.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | actual fun runBlocking(block: suspend () -> Unit) = kotlinx.coroutines.runBlocking{block()} -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/Logging.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import co.touchlab.kermit.Logger 4 | import co.touchlab.kermit.Severity 5 | 6 | object Logging { 7 | fun setMinSeverity(severity: LPKSeverity) = Logger.setMinSeverity( 8 | when (severity) { 9 | LPKSeverity.Verbose -> Severity.Verbose 10 | LPKSeverity.Debug -> Severity.Debug 11 | LPKSeverity.Info -> Severity.Info 12 | LPKSeverity.Warn -> Severity.Warn 13 | LPKSeverity.Error -> Severity.Error 14 | LPKSeverity.Assert -> Severity.Assert 15 | } 16 | ) 17 | } 18 | 19 | enum class LPKSeverity { 20 | Verbose, 21 | Debug, 22 | Info, 23 | Warn, 24 | Error, 25 | Assert 26 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/PacketPriority.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | enum class PacketPriority { 4 | NORMAL, 5 | LOW 6 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/ProtocolHandler.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 4 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 5 | import kotlinx.coroutines.CompletableDeferred 6 | 7 | interface ProtocolHandler { 8 | /** 9 | * Send data to the watch. 10 | * 11 | * @param priority Priority of the packet. Higher priority items will be sent before 12 | * low priority ones. Use low priority for background messages like sync and higher priority 13 | * for user-initiated actions that should be transmitted faster 14 | * 15 | * @return *true* if sending was successful, *false* if packet sending failed due to 16 | * unrecoverable circumstances (such as watch disconnecting completely). 17 | */ 18 | suspend fun send( 19 | packet: PebblePacket, 20 | priority: PacketPriority = PacketPriority.NORMAL 21 | ): Boolean 22 | 23 | /** 24 | * Send raw data to the watch. 25 | * 26 | * @param priority Priority of the packet. Higher priority items will be sent before 27 | * low priority ones. Use low priority for background messages like sync and higher priority 28 | * for user-initiated actions that should be transmitted faster 29 | * 30 | * @return *true* if sending was successful, *false* if packet sending failed due to 31 | * unrecoverable circumstances (such as watch disconnecting completely). 32 | */ 33 | suspend fun send( 34 | packetData: UByteArray, 35 | priority: PacketPriority = PacketPriority.NORMAL 36 | ): Boolean 37 | 38 | suspend fun startPacketSendingLoop(rawSend: suspend (UByteArray) -> Boolean) 39 | 40 | fun registerReceiveCallback( 41 | endpoint: ProtocolEndpoint, 42 | callback: suspend (PebblePacket) -> Unit 43 | ) 44 | 45 | suspend fun receivePacket(bytes: UByteArray): Boolean 46 | 47 | suspend fun openProtocol() 48 | suspend fun closeProtocol() 49 | 50 | /** 51 | * Wait for the next packet in the sending queue. After you are done handling this packet, you must call 52 | * [PendingPacket.notifyPacketStatus]. 53 | * 54 | * This is a lower level method. Before getting any packets, you MUST call [openProtocol] and 55 | * after you are done, you MUST call [closeProtocol] to trigger any cleanup operations. 56 | */ 57 | suspend fun waitForNextPacket(): PendingPacket 58 | 59 | /** 60 | * Get the next packet in the sending queue or *null* if there are no waiting packets. 61 | * After you are done handling this packet, you must call [PendingPacket.notifyPacketStatus]. 62 | * 63 | * This is a lower level method. Before getting any packets, you MUST call [openProtocol] and 64 | * after you are done, you MUST call [closeProtocol] to trigger any cleanup operations. 65 | */ 66 | suspend fun getNextPacketOrNull(): PendingPacket? 67 | 68 | class PendingPacket( 69 | val data: UByteArray, 70 | private val callback: CompletableDeferred 71 | ) { 72 | /** 73 | * @param success *true* if sending was successful or 74 | * *false* if packet sending failed due to unrecoverable circumstances 75 | * (such as watch disconnecting completely) 76 | */ 77 | fun notifyPacketStatus(success: Boolean) { 78 | callback.complete(success) 79 | } 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/ProtocolHandlerImpl.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import co.touchlab.kermit.Logger 4 | import io.rebble.libpebblecommon.exceptions.PacketDecodeException 5 | import io.rebble.libpebblecommon.packets.PingPong 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import kotlinx.coroutines.* 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.selects.select 11 | import kotlin.coroutines.coroutineContext 12 | import kotlin.jvm.Volatile 13 | 14 | /** 15 | * Default pebble protocol handler 16 | */ 17 | class ProtocolHandlerImpl() : ProtocolHandler { 18 | private val receiveRegistry = HashMap Unit>() 19 | 20 | private val normalPriorityPackets = Channel(Channel.BUFFERED) 21 | private val lowPriorityPackets = Channel(Channel.BUFFERED) 22 | 23 | @Volatile 24 | private var idlePacketLoop: Job? = null 25 | 26 | companion object { 27 | const val TAG = "ProtocolHandlerImpl" 28 | } 29 | 30 | init { 31 | startIdlePacketLoop() 32 | } 33 | 34 | override suspend fun send(packetData: UByteArray, priority: PacketPriority): Boolean { 35 | val targetChannel = when (priority) { 36 | PacketPriority.NORMAL -> normalPriorityPackets 37 | PacketPriority.LOW -> lowPriorityPackets 38 | } 39 | 40 | val callback = CompletableDeferred() 41 | targetChannel.send(ProtocolHandler.PendingPacket(packetData, callback)) 42 | 43 | return callback.await() 44 | } 45 | 46 | override suspend fun send(packet: PebblePacket, priority: PacketPriority): Boolean { 47 | return send(packet.serialize(), priority) 48 | } 49 | 50 | override suspend fun openProtocol() { 51 | idlePacketLoop?.cancelAndJoin() 52 | } 53 | 54 | override suspend fun closeProtocol() { 55 | startIdlePacketLoop() 56 | } 57 | 58 | /** 59 | * Start a loop that will wait for any packets to be sent through [send] method and then 60 | * call provided lambda with byte array to send to the watch. 61 | * 62 | * Lambda should return *true* if sending was successful or 63 | * *false* if packet sending failed due to unrecoverable circumstances 64 | * (such as watch disconnecting completely) 65 | * 66 | * When lambda returns false, this method terminates. 67 | * 68 | * This method automatically calls [openProtocol] and [closeProtocol] for you. 69 | */ 70 | override suspend fun startPacketSendingLoop(rawSend: suspend (UByteArray) -> Boolean) { 71 | openProtocol() 72 | 73 | try { 74 | while (coroutineContext.isActive) { 75 | val packet = waitForNextPacket() 76 | val success = rawSend(packet.data) 77 | 78 | if (success) { 79 | packet.notifyPacketStatus(true) 80 | } else { 81 | packet.notifyPacketStatus(false) 82 | break 83 | } 84 | } 85 | } finally { 86 | closeProtocol() 87 | } 88 | } 89 | 90 | 91 | override suspend fun waitForNextPacket(): ProtocolHandler.PendingPacket { 92 | // Receive packet first from normalPriorityPackets or from 93 | // lowPriorityPackets if there is no normal packet 94 | 95 | return select { 96 | normalPriorityPackets.onReceive { it } 97 | lowPriorityPackets.onReceive { it } 98 | } 99 | } 100 | 101 | override suspend fun getNextPacketOrNull(): ProtocolHandler.PendingPacket? { 102 | return normalPriorityPackets.tryReceive().getOrNull() ?: lowPriorityPackets.tryReceive().getOrNull() 103 | } 104 | 105 | /** 106 | * Start idle loop when there is no packet sending loop active. This loop will just 107 | * reject all packets with false 108 | */ 109 | private fun startIdlePacketLoop() { 110 | idlePacketLoop = GlobalScope.launch { 111 | while (isActive) { 112 | val packet = waitForNextPacket() 113 | 114 | packet.notifyPacketStatus(false) 115 | } 116 | } 117 | } 118 | 119 | 120 | override fun registerReceiveCallback( 121 | endpoint: ProtocolEndpoint, 122 | callback: suspend (PebblePacket) -> Unit 123 | ) { 124 | val existingCallback = receiveRegistry.put(endpoint, callback) 125 | if (existingCallback != null) { 126 | throw IllegalStateException( 127 | "Duplicate callback registered for $endpoint: $callback, $existingCallback" 128 | ) 129 | } 130 | } 131 | 132 | /** 133 | * Handle a raw pebble packet 134 | * @param bytes the raw pebble packet (including framing) 135 | * @return true if packet was handled, otherwise false 136 | */ 137 | override suspend fun receivePacket(bytes: UByteArray): Boolean { 138 | try { 139 | val packet = PebblePacket.deserialize(bytes) 140 | 141 | when (packet) { 142 | //TODO move this to separate service (PingPong service?) 143 | is PingPong.Ping -> send(PingPong.Pong(packet.cookie.get())) 144 | is PingPong.Pong -> Logger.d(tag = TAG) { "Pong! ${packet.cookie.get()}" } 145 | } 146 | 147 | val receiveCallback = receiveRegistry[packet.endpoint] 148 | if (receiveCallback == null) { 149 | //TODO better logging 150 | Logger.w(tag = TAG) { "${packet.endpoint} does not have receive callback" } 151 | } else { 152 | receiveCallback.invoke(packet) 153 | } 154 | 155 | } catch (e: PacketDecodeException) { 156 | Logger.w(throwable = e, tag = TAG) { "Failed to decode a packet" } 157 | return false 158 | } 159 | return true 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/ble/GATTPacket.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.ble 2 | 3 | import io.rebble.libpebblecommon.util.DataBuffer 4 | import io.rebble.libpebblecommon.util.shr 5 | import kotlin.experimental.and 6 | import kotlin.experimental.or 7 | 8 | 9 | /** 10 | * Describes a GATT packet, which is NOT a pebble packet, it is simply a discrete chunk of data sent to the watch with a header (the data contents is chunks of the pebble packet currently being sent, size depending on MTU) 11 | */ 12 | class GATTPacket { 13 | 14 | enum class PacketType(val value: Byte) { 15 | DATA(0), 16 | ACK(1), 17 | RESET(2), 18 | RESET_ACK(3); 19 | 20 | companion object { 21 | fun fromHeader(value: Byte): PacketType { 22 | val valueMasked = value and typeMask 23 | return PacketType.values().first { it.value == valueMasked } 24 | } 25 | } 26 | } 27 | 28 | enum class PPoGConnectionVersion(val value: Byte, val supportsWindowNegotiation: Boolean, val supportsCoalescedAcking: Boolean) { 29 | ZERO(0, false, false), 30 | ONE(1, true, true); 31 | 32 | companion object { 33 | fun fromByte(value: Byte): PPoGConnectionVersion { 34 | return PPoGConnectionVersion.values().first { it.value == value } 35 | } 36 | } 37 | 38 | override fun toString(): String { 39 | return "< value = $value, supportsWindowNegotiation = $supportsWindowNegotiation, supportsCoalescedAcking = $supportsCoalescedAcking >" 40 | } 41 | } 42 | 43 | val data: ByteArray 44 | val type: PacketType 45 | val sequence: Int 46 | 47 | companion object { 48 | private const val typeMask: Byte = 0b111 49 | private const val sequenceMask: Byte = 0b11111000.toByte() 50 | } 51 | 52 | constructor(data: ByteArray) { 53 | //Timber.d("${data.toHexString()} -> ${ubyteArrayOf((data[0] and sequenceMask).toUByte()).toHexString()} -> ${ubyteArrayOf((data[0] and sequenceMask).toUByte() shr 3).toHexString()}") 54 | this.data = data 55 | sequence = ((data[0] and sequenceMask).toUByte() shr 3).toInt() 56 | if (sequence < 0 || sequence > 31) throw IllegalArgumentException("Sequence must be between 0 and 31 inclusive") 57 | type = PacketType.fromHeader(data[0]) 58 | } 59 | 60 | constructor(type: PacketType, sequence: Int, data: ByteArray? = null) { 61 | this.sequence = sequence 62 | if (sequence < 0 || sequence > 31) throw IllegalArgumentException("Sequence must be between 0 and 31 inclusive") 63 | this.type = type 64 | val len = if (data != null) data.size + 1 else 1 65 | 66 | val dataBuf = DataBuffer(len) 67 | 68 | dataBuf.putByte((type.value or (((sequence shl 3) and sequenceMask.toInt()).toByte()))) 69 | if (data != null) { 70 | dataBuf.putBytes(data.asUByteArray()) 71 | } 72 | dataBuf.rewind() 73 | this.data = dataBuf.getBytes(len).asByteArray() 74 | } 75 | 76 | fun toByteArray(): ByteArray { 77 | return data 78 | } 79 | 80 | fun getPPoGConnectionVersion(): PPoGConnectionVersion { 81 | if (type != PacketType.RESET) throw IllegalStateException("Function does not apply to packet type") 82 | return PPoGConnectionVersion.fromByte(data[1]) 83 | } 84 | 85 | fun hasWindowSizes(): Boolean { 86 | if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") 87 | return data.size >= 3 88 | } 89 | 90 | fun getMaxTXWindow(): Byte { 91 | if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") 92 | return data[2] 93 | } 94 | 95 | fun getMaxRXWindow(): Byte { 96 | if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") 97 | return data[1] 98 | } 99 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/ble/LEConstants.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.ble 2 | 3 | object LEConstants { 4 | object UUIDs { 5 | val CHARACTERISTIC_CONFIGURATION_DESCRIPTOR = "00002902-0000-1000-8000-00805f9b34fb" 6 | 7 | val PAIRING_SERVICE_UUID = "0000fed9-0000-1000-8000-00805f9b34fb" 8 | val APPLAUNCH_SERVICE_UUID = "20000000-328E-0FBB-C642-1AA6699BDADA" 9 | 10 | val CONNECTIVITY_CHARACTERISTIC = "00000001-328E-0FBB-C642-1AA6699BDADA" 11 | val PAIRING_TRIGGER_CHARACTERISTIC = "00000002-328E-0FBB-C642-1AA6699BDADA" 12 | val META_CHARACTERISTIC_SERVER = "10000002-328E-0FBB-C642-1AA6699BDADA" 13 | val APPLAUNCH_CHARACTERISTIC = "20000001-328E-0FBB-C642-1AA6699BDADA" 14 | 15 | val PPOGATT_DEVICE_SERVICE_UUID_CLIENT = "30000003-328E-0FBB-C642-1AA6699BDADA" 16 | val PPOGATT_DEVICE_SERVICE_UUID_SERVER = "10000000-328E-0FBB-C642-1AA6699BDADA" 17 | val PPOGATT_DEVICE_CHARACTERISTIC_READ = "30000004-328E-0FBB-C642-1AA6699BDADA" 18 | val PPOGATT_DEVICE_CHARACTERISTIC_WRITE = "30000006-328e-0fbb-c642-1aa6699bdada" 19 | val PPOGATT_DEVICE_CHARACTERISTIC_SERVER = "10000001-328E-0FBB-C642-1AA6699BDADA" 20 | 21 | val CONNECTION_PARAMETERS_CHARACTERISTIC = "00000005-328E-0FBB-C642-1AA6699BDADA" 22 | 23 | val FAKE_SERVICE_UUID = "BADBADBA-DBAD-BADB-ADBA-BADBADBADBAD" 24 | } 25 | 26 | val CHARACTERISTIC_SUBSCRIBE_VALUE = byteArrayOf(1, 0) 27 | val DEFAULT_MTU = 23 28 | val TARGET_MTU = 339 29 | val MAX_RX_WINDOW: Byte = 25 30 | val MAX_TX_WINDOW: Byte = 25 31 | 32 | // PPoGConnectionVersion.minSupportedVersion(), PPoGConnectionVersion.maxSupportedVersion(), ??? (magic numbers in stock app too) 33 | val SERVER_META_RESPONSE = byteArrayOf(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/disk/PbwBinHeader.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.disk 2 | 3 | import io.rebble.libpebblecommon.packets.blobdb.AppMetadata 4 | import io.rebble.libpebblecommon.structmapper.* 5 | import io.rebble.libpebblecommon.util.DataBuffer 6 | 7 | /** 8 | * Header of the 9 | */ 10 | class PbwBinHeader() : StructMappable() { 11 | /** 12 | * Major header version. 13 | */ 14 | val headerVersionMajor: SUByte = SUByte(m) 15 | 16 | /** 17 | * Minor header version. 18 | */ 19 | val headerVersionMinor: SUByte = SUByte(m) 20 | 21 | /** 22 | * Major sdk version. 23 | */ 24 | val sdkVersionMajor: SUByte = SUByte(m) 25 | 26 | /** 27 | * Minor sdk version. 28 | */ 29 | val sdkVersionMinor: SUByte = SUByte(m) 30 | 31 | /** 32 | * Major app version. 33 | */ 34 | val appVersionMajor: SUByte = SUByte(m) 35 | 36 | /** 37 | * Minor app version. 38 | */ 39 | val appVersionMinor: SUByte = SUByte(m) 40 | 41 | /** 42 | * Size of the app payload in bytes 43 | */ 44 | val appSize: SUShort = SUShort(m) 45 | 46 | /** 47 | * ??? (Presumably offset where app payload starts?) 48 | */ 49 | val appOffset: SUInt = SUInt(m) 50 | 51 | /** 52 | * CRC checksum of the app payload 53 | */ 54 | val crc: SUInt = SUInt(m) 55 | 56 | /** 57 | * Name of the app 58 | */ 59 | val appName: SFixedString = SFixedString(m, 32) 60 | 61 | /** 62 | * Name of the company that made the app 63 | */ 64 | val companyName: SFixedString = SFixedString(m, 32) 65 | 66 | /** 67 | * Resource ID of the primary icon. 68 | */ 69 | val icon: SUInt = SUInt(m) 70 | 71 | /** 72 | * ??? 73 | */ 74 | val symbolTableAddress: SUInt = SUInt(m) 75 | 76 | /** 77 | * List of app install flags. Should be forwarded to the watch when inserting into BlobDB. 78 | */ 79 | val flags: SUInt = SUInt(m) 80 | 81 | /** 82 | * ??? 83 | */ 84 | val numRelocationListEntries: SUInt = SUInt(m) 85 | 86 | /** 87 | * UUID of the app 88 | */ 89 | val uuid: SUUID = SUUID(m) 90 | 91 | fun toBlobDbApp(): AppMetadata { 92 | return AppMetadata().also { 93 | it.uuid.set(uuid.get()) 94 | it.flags.set(flags.get()) 95 | it.icon.set(icon.get()) 96 | it.appVersionMajor.set(appVersionMajor.get()) 97 | it.appVersionMinor.set(appVersionMinor.get()) 98 | it.sdkVersionMajor.set(sdkVersionMajor.get()) 99 | it.sdkVersionMinor.set(sdkVersionMinor.get()) 100 | it.appName.set(appName.get()) 101 | } 102 | } 103 | 104 | companion object { 105 | const val SIZE: Int = 8 + 2 + 2 + 2 + 2 + 4 + 4 + 32 + 32 + 4 + 4 + 4 + 4 + 16 106 | 107 | /** 108 | * Parse existing Pbw binary payload header. You should read [SIZE] bytes from the binary 109 | * payload and pass it into this method. 110 | * 111 | * @throws IllegalArgumentException if header is not valid pebble app header 112 | */ 113 | fun parseFileHeader(data: UByteArray): PbwBinHeader { 114 | if (data.size != SIZE) { 115 | throw IllegalArgumentException( 116 | "Read data from the file should be exactly $SIZE bytes" 117 | ) 118 | } 119 | 120 | val buffer = DataBuffer(data) 121 | 122 | val sentinel = buffer.getBytes(8) 123 | if (!sentinel.contentEquals(EXPECTED_SENTINEL)) { 124 | throw IllegalArgumentException("Sentinel does not match") 125 | } 126 | 127 | return PbwBinHeader().also { 128 | it.fromBytes(buffer) 129 | } 130 | } 131 | 132 | /** 133 | * First 8 bytes of the header, spelling the word "PBLAPP" in ASCII, 134 | * followed by two zeros. 135 | */ 136 | private val EXPECTED_SENTINEL = ubyteArrayOf( 137 | 0x50u, 0x42u, 0x4Cu, 0x41u, 0x50u, 0x50u, 0x00u, 0x00u 138 | ) 139 | } 140 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/exceptions/packet.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.exceptions 2 | 3 | class PacketEncodeException(message: String?) : Exception(message) 4 | class PacketDecodeException(message: String?, cause: Exception? = null) : Exception(message, cause) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/main.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import io.rebble.libpebblecommon.packets.PhoneAppVersion 4 | 5 | expect fun getPlatform(): PhoneAppVersion.OSType -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/StringOrBoolean.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata 2 | import kotlinx.serialization.* 3 | import kotlinx.serialization.encoding.Decoder 4 | import kotlinx.serialization.encoding.Encoder 5 | import kotlinx.serialization.json.JsonContentPolymorphicSerializer 6 | import kotlinx.serialization.json.JsonDecoder 7 | import kotlinx.serialization.json.JsonElement 8 | import kotlinx.serialization.json.jsonPrimitive 9 | 10 | @Serializable(StringOrBoolean.Companion::class) 11 | data class StringOrBoolean(val value: Boolean) { 12 | @Serializer(forClass = StringOrBoolean::class) 13 | companion object : KSerializer { 14 | override fun serialize(encoder: Encoder, value: StringOrBoolean) { 15 | encoder.encodeString(if (value.value) "true" else "false") 16 | } 17 | 18 | override fun deserialize(decoder: Decoder): StringOrBoolean { 19 | require(decoder is JsonDecoder) 20 | val element = decoder.decodeJsonElement() 21 | if (element.jsonPrimitive.content != "true" && element.jsonPrimitive.content != "false") { 22 | throw SerializationException("StringOrBoolean value is not a boolean keyword") 23 | } 24 | return StringOrBoolean(element.jsonPrimitive.content == "true") 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchHardwarePlatform.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata 2 | 3 | import kotlinx.serialization.SerializationException 4 | import kotlinx.serialization.Serializer 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | import kotlin.reflect.KClass 11 | 12 | enum class WatchHardwarePlatform(val protocolNumber: UByte, val watchType: WatchType, val revision: String) { 13 | PEBBLE_ONE_EV_1(1u, WatchType.APLITE, "ev1"), 14 | PEBBLE_ONE_EV_2(2u, WatchType.APLITE, "ev2"), 15 | PEBBLE_ONE_EV_2_3(3u, WatchType.APLITE, "ev2_3"), 16 | PEBBLE_ONE_EV_2_4(4u, WatchType.APLITE, "ev2_4"), 17 | PEBBLE_ONE_POINT_FIVE(5u, WatchType.APLITE, "v1_5"), 18 | PEBBLE_TWO_POINT_ZERO(6u, WatchType.APLITE, "v2_0"), 19 | PEBBLE_SNOWY_EVT_2(7u, WatchType.BASALT, "snowy_evt2"), 20 | PEBBLE_SNOWY_DVT(8u, WatchType.BASALT, "snowy_dvt"), 21 | PEBBLE_BOBBY_SMILES(10u, WatchType.BASALT, "snowy_s3"), 22 | PEBBLE_ONE_BIGBOARD_2(254u, WatchType.APLITE, "unk"), 23 | PEBBLE_ONE_BIGBOARD(255u, WatchType.APLITE, "unk"), 24 | PEBBLE_SNOWY_BIGBOARD(253u, WatchType.BASALT, "unk"), 25 | PEBBLE_SNOWY_BIGBOARD_2(252u, WatchType.BASALT, "unk"), 26 | PEBBLE_SPALDING_EVT(9u, WatchType.CHALK, "spalding_evt"), 27 | PEBBLE_SPALDING_PVT(11u, WatchType.CHALK, "spalding"), 28 | PEBBLE_SPALDING_BIGBOARD(251u, WatchType.CHALK, "unk"), 29 | PEBBLE_SILK_EVT(12u, WatchType.DIORITE, "silk_evt"), 30 | PEBBLE_SILK(14u, WatchType.DIORITE, "silk"), 31 | PEBBLE_SILK_BIGBOARD(250u, WatchType.DIORITE, "unk"), 32 | PEBBLE_SILK_BIGBOARD_2_PLUS(248u, WatchType.DIORITE, "unk"), 33 | PEBBLE_ROBERT_EVT(13u, WatchType.EMERY, "robert_evt"), 34 | PEBBLE_ROBERT_BIGBOARD(249u, WatchType.EMERY, "unk"), 35 | PEBBLE_ROBERT_BIGBOARD_2(247u, WatchType.EMERY, "unk"); 36 | 37 | companion object { 38 | fun fromProtocolNumber(number: UByte): WatchHardwarePlatform? { 39 | return values().firstOrNull { it.protocolNumber == number } 40 | } 41 | 42 | fun fromHWRevision(revision: String): WatchHardwarePlatform? { 43 | if (revision == "unk") return null 44 | return values().firstOrNull() { it.revision == revision } 45 | } 46 | } 47 | } 48 | 49 | @Serializer(WatchHardwarePlatform::class) 50 | class WatchHardwarePlatformSerializer { 51 | override val descriptor: SerialDescriptor 52 | get() = PrimitiveSerialDescriptor("WatchHardwarePlatform", PrimitiveKind.STRING) 53 | 54 | override fun deserialize(decoder: Decoder): WatchHardwarePlatform { 55 | val revision = decoder.decodeString() 56 | return WatchHardwarePlatform.fromHWRevision(revision) ?: throw SerializationException("Unknown hardware revision $revision") 57 | } 58 | 59 | override fun serialize(encoder: Encoder, value: WatchHardwarePlatform) { 60 | val revision = value.revision 61 | encoder.encodeString(revision) 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/WatchType.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata 2 | 3 | enum class WatchType(val codename: String) { 4 | APLITE("aplite"), 5 | BASALT("basalt"), 6 | CHALK("chalk"), 7 | DIORITE("diorite"), 8 | EMERY("emery"); 9 | 10 | fun getCompatibleAppVariants(): List { 11 | return when (this) { 12 | APLITE -> listOf(APLITE) 13 | BASALT -> listOf(BASALT, APLITE) 14 | CHALK -> listOf(CHALK) 15 | DIORITE -> listOf(DIORITE, APLITE) 16 | EMERY -> listOf( 17 | EMERY, 18 | BASALT, 19 | DIORITE, 20 | APLITE 21 | ) 22 | } 23 | } 24 | 25 | /** 26 | * Get the most compatible variant for this WatchType 27 | * @param availableAppVariants List of variants, from [io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo.targetPlatforms] 28 | */ 29 | fun getBestVariant(availableAppVariants: List): WatchType? { 30 | val compatibleVariants = getCompatibleAppVariants() 31 | 32 | return compatibleVariants.firstOrNull() { variant -> 33 | availableAppVariants.contains(variant.codename) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/appinfo/Media.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.appinfo 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonNames 6 | import io.rebble.libpebblecommon.metadata.StringOrBoolean 7 | 8 | @Serializable 9 | data class Media( 10 | @SerialName("file") 11 | val resourceFile: String, 12 | val menuIcon: StringOrBoolean = StringOrBoolean(false), 13 | val name: String, 14 | val type: String, 15 | val targetPlatforms: List? = null, 16 | val characterRegex: String? = null 17 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/appinfo/PbwAppInfo.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.appinfo 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PbwAppInfo( 7 | val uuid: String, 8 | val shortName: String, 9 | val longName: String = "", 10 | val companyName: String = "", 11 | val versionCode: Long = -1, 12 | val versionLabel: String, 13 | val appKeys: Map = emptyMap(), 14 | val capabilities: List = emptyList(), 15 | val resources: Resources, 16 | val sdkVersion: String = "3", 17 | // If list of target platforms is not present, pbw is legacy applite app 18 | val targetPlatforms: List = listOf("aplite"), 19 | val watchapp: Watchapp = Watchapp() 20 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/appinfo/Resources.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.appinfo 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Resources( 7 | val media: List 8 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/appinfo/Watchapp.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.appinfo 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Watchapp( 7 | val watchface: Boolean = false, 8 | val hiddenApp: Boolean = false, 9 | val onlyShownOnCommunication: Boolean = false 10 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/manifest/Debug.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.manifest 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Debug( 7 | val empty: String? = null 8 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/manifest/PbwBlob.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.manifest 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | @Serializable 8 | data class PbwBlob( 9 | val crc: Long?, 10 | val name: String, 11 | @SerialName("sdk_version") 12 | val sdkVersion: SdkVersion? = null, 13 | val size: Int, 14 | val timestamp: Int? 15 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/manifest/PbwManifest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.manifest 2 | 3 | 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class PbwManifest( 8 | val application: PbwBlob, 9 | val resources: PbwBlob?, 10 | val worker: PbwBlob? = null, 11 | val debug: Debug?, 12 | val generatedAt: Int?, 13 | val generatedBy: String?, 14 | val manifestVersion: Int?, 15 | val type: String? 16 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbw/manifest/SdkVersion.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.manifest 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SdkVersion( 7 | val major: Int?, 8 | val minor: Int? 9 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbz/manifest/JsTooling.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbz.manifest 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class JsTooling( 8 | @SerialName("bytecode_version") 9 | val bytecodeVersion: Int 10 | ) 11 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbz/manifest/PbzFirmware.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbz.manifest 2 | 3 | import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform 4 | import io.rebble.libpebblecommon.metadata.WatchHardwarePlatformSerializer 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class PbzFirmware( 10 | val name: String, 11 | val type: String, 12 | val timestamp: Long, 13 | val commit: String, 14 | @Serializable(with = WatchHardwarePlatformSerializer::class) 15 | @SerialName("hwrev") 16 | val hwRev: WatchHardwarePlatform, 17 | val size: Long, 18 | val crc: Long, 19 | val versionTag: String? = null 20 | ) 21 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbz/manifest/PbzManifest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbz.manifest 2 | 3 | import io.rebble.libpebblecommon.metadata.pbw.manifest.Debug 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class PbzManifest( 9 | val manifestVersion: Int, 10 | val generatedAt: Long, 11 | val generatedBy: String? = null, 12 | val debug: Debug? = null, 13 | val firmware: PbzFirmware, 14 | val resources: SystemResources? = null, 15 | @SerialName("js_tooling") 16 | val jsTooling: JsTooling? = null, 17 | val type: String 18 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/metadata/pbz/manifest/SystemResources.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbz.manifest 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SystemResources( 7 | val name: String, 8 | val timestamp: Long, 9 | val size: Long, 10 | val crc: Long 11 | ) 12 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppCustomization.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 4 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 5 | import io.rebble.libpebblecommon.structmapper.SBytes 6 | import io.rebble.libpebblecommon.structmapper.SFixedString 7 | import io.rebble.libpebblecommon.structmapper.SUByte 8 | import io.rebble.libpebblecommon.structmapper.SUShort 9 | import io.rebble.libpebblecommon.util.Bitmap 10 | import io.rebble.libpebblecommon.util.DataBuffer 11 | import io.rebble.libpebblecommon.util.Endian 12 | 13 | class AppCustomizationSetStockAppTitleMessage( 14 | appType: AppType, 15 | newName: String 16 | ) : PebblePacket(ProtocolEndpoint.APP_CUSTOMIZE) { 17 | val appType = SUByte(m, appType.value) 18 | val name = SFixedString(m, 30, newName) 19 | } 20 | 21 | class AppCustomizationSetStockAppIconMessage( 22 | appType: AppType, 23 | icon: Bitmap 24 | ) : PebblePacket(ProtocolEndpoint.APP_CUSTOMIZE) { 25 | // First bit being set signifies that this is icon packet instead of name packet 26 | val appType = SUByte(m, appType.value or 0b10000000u) 27 | val bytesPerLine = SUShort(m, endianness = Endian.Little) 28 | 29 | /** 30 | * No idea what flags are possible. Stock app always sends 4096 here. 31 | */ 32 | val flags = SUShort(m, 4096u, endianness = Endian.Little) 33 | 34 | /** 35 | * Offset is not supported by app. Always 0. 36 | */ 37 | val originY = SUShort(m, 0u, endianness = Endian.Little) 38 | val originX = SUShort(m, 0u, endianness = Endian.Little) 39 | 40 | val width = SUShort(m, endianness = Endian.Little) 41 | val height = SUShort(m, endianness = Endian.Little) 42 | 43 | val imageData = SBytes(m) 44 | 45 | init { 46 | val width = icon.width 47 | val height = icon.height 48 | 49 | this.width.set(width.toUShort()) 50 | this.height.set(height.toUShort()) 51 | 52 | val bytesPerLine = ((width + 31) / 32) * 4 53 | val totalBytes = bytesPerLine * height 54 | 55 | val dataBuffer = DataBuffer(totalBytes) 56 | 57 | for (y in 0 until height) { 58 | for (lineIntIndex in 0 until bytesPerLine / 4) { 59 | var currentInt = 0 60 | val startX = lineIntIndex * 32 61 | val pixelsToTraverse = 32.coerceAtMost(width - startX) 62 | 63 | for (innerX in 0 until pixelsToTraverse) { 64 | val x = startX + innerX 65 | 66 | val pixelValue = icon.getPixel(x, y) 67 | val valueWithoutAlpha = pixelValue and 0x00FFFFFF 68 | 69 | val pixelBit = if (valueWithoutAlpha > 0) { 70 | 1 71 | } else { 72 | 0 73 | } 74 | 75 | currentInt = currentInt or (pixelBit shl innerX) 76 | } 77 | 78 | dataBuffer.putInt(currentInt) 79 | } 80 | } 81 | 82 | val imageBytes = dataBuffer.array() 83 | imageData.set(imageBytes, imageBytes.size) 84 | 85 | this.bytesPerLine.set(bytesPerLine.toUShort()) 86 | } 87 | } 88 | 89 | enum class AppType(val value: UByte) { 90 | SPORTS(0x00u), 91 | GOLF(0x01u), 92 | } 93 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppFetch.kt: -------------------------------------------------------------------------------- 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.SUByte 7 | import io.rebble.libpebblecommon.structmapper.SUInt 8 | import io.rebble.libpebblecommon.structmapper.SUUID 9 | import io.rebble.libpebblecommon.util.Endian 10 | 11 | sealed class AppFetchIncomingPacket() : PebblePacket(ProtocolEndpoint.APP_FETCH) { 12 | /** 13 | * Request command. See [AppFetchRequestCommand]. 14 | */ 15 | val command = SUByte(m) 16 | 17 | } 18 | 19 | sealed class AppFetchOutgoingPacket(command: AppFetchRequestCommand) : 20 | PebblePacket(ProtocolEndpoint.APP_FETCH) { 21 | /** 22 | * Request command. See [AppFetchRequestCommand]. 23 | */ 24 | val command = SUByte(m, command.value) 25 | 26 | } 27 | 28 | 29 | /** 30 | * Packet sent from the watch when user opens an app that is not in the watch storage. 31 | */ 32 | class AppFetchRequest : AppFetchIncomingPacket() { 33 | 34 | /** 35 | * UUID of the app to request 36 | */ 37 | val uuid = SUUID(m) 38 | 39 | /** 40 | * ID of the app bank. Use in the [PutBytesAppInit] packet to identify this app install. 41 | */ 42 | val appId = SUInt(m, endianness = Endian.Little) 43 | } 44 | 45 | /** 46 | * Packet sent from the watch when user opens an app that is not in the watch storage. 47 | */ 48 | class AppFetchResponse( 49 | status: AppFetchResponseStatus 50 | ) : AppFetchOutgoingPacket(AppFetchRequestCommand.FETCH_APP) { 51 | /** 52 | * Response status 53 | */ 54 | val status = SUByte(m, status.value) 55 | 56 | } 57 | 58 | enum class AppFetchRequestCommand(val value: UByte) { 59 | FETCH_APP(0x01u) 60 | } 61 | 62 | enum class AppFetchResponseStatus(val value: UByte) { 63 | /** 64 | * Sent right before starting to send PutBytes data 65 | */ 66 | START(0x01u), 67 | 68 | /** 69 | * Sent when phone PutBytes is already busy sending something else 70 | */ 71 | BUSY(0x02u), 72 | 73 | /** 74 | * Sent when UUID that watch sent is not in the locker 75 | */ 76 | INVALID_UUID(0x03u), 77 | 78 | /** 79 | * Sent when there is generic data sending error (such as failure to read the local pbw file) 80 | */ 81 | NO_DATA(0x01u), 82 | } 83 | 84 | 85 | fun appFetchIncomingPacketsRegister() { 86 | PacketRegistry.register( 87 | ProtocolEndpoint.APP_FETCH, 88 | AppFetchRequestCommand.FETCH_APP.value 89 | ) { AppFetchRequest() } 90 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppLog.kt: -------------------------------------------------------------------------------- 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 | 8 | class AppLogShippingControlMessage(enable: Boolean) : PebblePacket(ProtocolEndpoint.APP_LOGS) { 9 | private val enable = SBoolean(m, enable) 10 | } 11 | 12 | class AppLogReceivedMessage() : PebblePacket(ProtocolEndpoint.APP_LOGS) { 13 | val uuid = SUUID(m) 14 | val timestamp = SUInt(m) 15 | val level = SUByte(m) 16 | val messageLength = SUByte(m) 17 | val lineNumber = SUShort(m) 18 | val filename = SFixedString(m, 16) 19 | val message = SFixedString(m, 0) 20 | 21 | init { 22 | message.linkWithSize(messageLength) 23 | } 24 | } 25 | 26 | fun appLogPacketsRegister() { 27 | PacketRegistry.register(ProtocolEndpoint.APP_LOGS) { AppLogReceivedMessage() } 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppReorder.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import com.benasher44.uuid.Uuid 4 | import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 7 | import io.rebble.libpebblecommon.structmapper.* 8 | 9 | class AppReorderResult() : 10 | PebblePacket(ProtocolEndpoint.APP_REORDER) { 11 | /** 12 | * Result code. See [AppOrderResultCode]. 13 | */ 14 | val status = SUByte(m) 15 | 16 | } 17 | 18 | sealed class AppReorderOutgoingPacket(type: AppReorderType) : 19 | PebblePacket(ProtocolEndpoint.APP_REORDER) { 20 | 21 | /** 22 | * Packet type. See [AppReorderType]. 23 | */ 24 | val command = SUByte(m, type.value) 25 | } 26 | 27 | enum class AppOrderResultCode(val value: UByte) { 28 | SUCCESS(0x01u), 29 | FAILED(0x02u), 30 | INVALID(0x03u), 31 | RETRY(0x04u); 32 | 33 | companion object { 34 | fun fromByte(value: UByte): AppOrderResultCode { 35 | return values().firstOrNull { it.value == value } ?: error("Unknown result: $value") 36 | } 37 | } 38 | } 39 | 40 | 41 | /** 42 | * Packet sent from the watch when user opens an app that is not in the watch storage. 43 | */ 44 | class AppReorderRequest( 45 | appList: List 46 | ) : AppReorderOutgoingPacket(AppReorderType.REORDER_APPS) { 47 | val appCount = SByte(m, appList.size.toByte()) 48 | 49 | val appList = SFixedList( 50 | mapper = m, 51 | count = appList.size, 52 | default = appList.map { SUUID(StructMapper(), it) }, 53 | itemFactory = { 54 | SUUID( 55 | StructMapper() 56 | ) 57 | }) 58 | } 59 | 60 | enum class AppReorderType(val value: UByte) { 61 | REORDER_APPS(0x01u) 62 | } 63 | 64 | 65 | fun appReorderIncomingRegister() { 66 | PacketRegistry.register( 67 | ProtocolEndpoint.APP_REORDER, 68 | ) { AppReorderResult() } 69 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/AppRunState.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import com.benasher44.uuid.Uuid 4 | import io.rebble.libpebblecommon.protocolhelpers.PacketRegistry 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 7 | import io.rebble.libpebblecommon.structmapper.SUByte 8 | import io.rebble.libpebblecommon.structmapper.SUUID 9 | 10 | 11 | sealed class AppRunStateMessage(message: Message) : PebblePacket(endpoint) { 12 | val command = SUByte(m, message.value) 13 | 14 | init { 15 | type = command.get() 16 | } 17 | 18 | enum class Message(val value: UByte) { 19 | AppRunStateStart(0x01u), 20 | AppRunStateStop(0x02u), 21 | AppRunStateRequest(0x03u) 22 | } 23 | 24 | class AppRunStateStart( 25 | uuid: Uuid = Uuid(0L, 0L) 26 | ) : 27 | AppRunStateMessage(Message.AppRunStateStart) { 28 | val uuid = SUUID(m, uuid) 29 | override fun equals(other: Any?): Boolean { 30 | if (this === other) return true 31 | if (other !is AppRunStateStart) return false 32 | if (!super.equals(other)) return false 33 | 34 | if (uuid != other.uuid) return false 35 | 36 | return true 37 | } 38 | 39 | override fun hashCode(): Int { 40 | var result = super.hashCode() 41 | result = 31 * result + uuid.hashCode() 42 | return result 43 | } 44 | 45 | override fun toString(): String { 46 | return "AppRunStateStart(uuid=$uuid)" 47 | } 48 | } 49 | 50 | class AppRunStateStop( 51 | uuid: Uuid = Uuid(0L, 0L) 52 | ) : 53 | AppRunStateMessage(Message.AppRunStateStop) { 54 | val uuid = SUUID(m, uuid) 55 | override fun equals(other: Any?): Boolean { 56 | if (this === other) return true 57 | if (other !is AppRunStateStop) return false 58 | if (!super.equals(other)) return false 59 | 60 | if (uuid != other.uuid) return false 61 | 62 | return true 63 | } 64 | 65 | override fun hashCode(): Int { 66 | var result = super.hashCode() 67 | result = 31 * result + uuid.hashCode() 68 | return result 69 | } 70 | 71 | override fun toString(): String { 72 | return "AppRunStateStop(uuid=$uuid)" 73 | } 74 | } 75 | 76 | class AppRunStateRequest : AppRunStateMessage(Message.AppRunStateRequest) 77 | 78 | companion object { 79 | val endpoint = ProtocolEndpoint.APP_RUN_STATE 80 | } 81 | 82 | override fun equals(other: Any?): Boolean { 83 | if (this === other) return true 84 | if (other !is AppRunStateMessage) return false 85 | 86 | if (command != other.command) return false 87 | 88 | return true 89 | } 90 | 91 | override fun hashCode(): Int { 92 | return command.hashCode() 93 | } 94 | } 95 | 96 | fun appRunStatePacketsRegister() { 97 | PacketRegistry.register( 98 | AppRunStateMessage.endpoint, 99 | AppRunStateMessage.Message.AppRunStateStart.value 100 | ) { AppRunStateMessage.AppRunStateStart() } 101 | 102 | PacketRegistry.register( 103 | AppRunStateMessage.endpoint, 104 | AppRunStateMessage.Message.AppRunStateStop.value 105 | ) { AppRunStateMessage.AppRunStateStop() } 106 | 107 | PacketRegistry.register( 108 | AppRunStateMessage.endpoint, 109 | AppRunStateMessage.Message.AppRunStateRequest.value 110 | ) { AppRunStateMessage.AppRunStateRequest() } 111 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Audio.kt: -------------------------------------------------------------------------------- 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 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Emulator.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import co.touchlab.kermit.Logger 4 | import io.rebble.libpebblecommon.structmapper.SBytes 5 | import io.rebble.libpebblecommon.structmapper.SUShort 6 | import io.rebble.libpebblecommon.structmapper.StructMapper 7 | import io.rebble.libpebblecommon.util.DataBuffer 8 | 9 | const val HEADER_SIGNATURE = 0xFEEDU 10 | const val FOOTER_SIGNATURE = 0xBEEFU 11 | 12 | open class QemuPacket(protocol: Protocol) { 13 | val m = StructMapper() 14 | val signature = SUShort(m, HEADER_SIGNATURE.toUShort()) 15 | val protocol = SUShort(m, protocol.value) 16 | val length = SUShort(m) 17 | 18 | enum class Protocol(val value: UShort) { 19 | SPP(1U), 20 | Tap(2U), 21 | BluetoothConnection(3U), 22 | Compass(4U), 23 | Battery(5U), 24 | Accel(6U), 25 | Vibration(7U), 26 | Button(8U), 27 | TimeFormat(9U), 28 | TimelinePeek(10U), 29 | ContentSize(11U), 30 | RebbleTest(100U), 31 | Invalid(UShort.MAX_VALUE) 32 | } 33 | 34 | class QemuSPP(data: UByteArray? = null): QemuPacket(Protocol.SPP) { 35 | val payload = SBytes(m, data?.size?:-1, data?: ubyteArrayOf()) 36 | val footer = SUShort(m, FOOTER_SIGNATURE.toUShort()) 37 | 38 | init { 39 | if (data == null) payload.linkWithSize(length) 40 | } 41 | } 42 | 43 | companion object { 44 | fun deserialize(packet: UByteArray): QemuPacket { 45 | val buf = DataBuffer(packet) 46 | val meta = StructMapper() 47 | val header = SUShort(meta) 48 | val protocol = SUShort(meta) 49 | meta.fromBytes(buf) 50 | buf.rewind() 51 | return when (protocol.get()) { 52 | Protocol.SPP.value -> QemuSPP().also { it.m.fromBytes(buf) } 53 | else -> { 54 | Logger.w(tag = "Emulator") { "QEMU packet left generic" } 55 | QemuPacket(Protocol.Invalid).also { it.m.fromBytes(buf) } 56 | } 57 | } 58 | } 59 | } 60 | 61 | fun serialize(): UByteArray { 62 | length.set((m.size-(4*UShort.SIZE_BYTES)).toUShort()) //total size - header+footer = payload length 63 | return m.toBytes() 64 | } 65 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/LogDump.kt: -------------------------------------------------------------------------------- 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 | 8 | open class LogDump(val message: Message): PebblePacket(ProtocolEndpoint.LOG_DUMP) { 9 | val command = SUByte(m, message.value) 10 | 11 | init { 12 | type = command.get() 13 | } 14 | 15 | enum class Message(val value: UByte) { 16 | RequestLogDump(0x10u), 17 | LogLine(0x80u), 18 | Done(0x81u), 19 | NoLogs(0x82u) 20 | } 21 | 22 | class RequestLogDump(logGeneration: UByte, cookie: UInt): LogDump(Message.RequestLogDump) { 23 | val generation = SUByte(m, logGeneration) 24 | val cookie = SUInt(m, cookie) 25 | } 26 | 27 | open class ReceivedLogDumpMessage(message: Message): LogDump(message) { 28 | val cookie = SUInt(m) 29 | } 30 | 31 | class LogLine: ReceivedLogDumpMessage(Message.LogLine) { 32 | val timestamp = SUInt(m) 33 | val level = SUByte(m) 34 | val length = SUByte(m) 35 | val line = SUShort(m) 36 | val filename = SFixedString(m, 16) 37 | val messageText = SFixedString(m, 0) 38 | 39 | init { 40 | messageText.linkWithSize(length) 41 | } 42 | } 43 | 44 | class Done: ReceivedLogDumpMessage(Message.Done) 45 | 46 | class NoLogs: ReceivedLogDumpMessage(Message.NoLogs) 47 | } 48 | 49 | fun logDumpPacketsRegister() { 50 | PacketRegistry.register(ProtocolEndpoint.LOG_DUMP, LogDump.Message.NoLogs.value) { 51 | LogDump.NoLogs() 52 | } 53 | 54 | PacketRegistry.register(ProtocolEndpoint.LOG_DUMP, LogDump.Message.Done.value) { 55 | LogDump.Done() 56 | } 57 | 58 | PacketRegistry.register(ProtocolEndpoint.LOG_DUMP, LogDump.Message.LogLine.value) { 59 | LogDump.LogLine() 60 | } 61 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Music.kt: -------------------------------------------------------------------------------- 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 | open class MusicControl(val message: Message) : PebblePacket(ProtocolEndpoint.MUSIC_CONTROL) { 10 | val command = SUByte(m, message.value) 11 | 12 | init { 13 | type = command.get() 14 | } 15 | 16 | enum class Message(val value: UByte) { 17 | PlayPause(0x01u), 18 | Pause(0x02u), 19 | Play(0x03u), 20 | NextTrack(0x04u), 21 | PreviousTrack(0x05u), 22 | VolumeUp(0x06u), 23 | VolumeDown(0x07u), 24 | GetCurrentTrack(0x08u), 25 | UpdateCurrentTrack(0x10u), 26 | UpdatePlayStateInfo(0x11u), 27 | UpdateVolumeInfo(0x12u), 28 | UpdatePlayerInfo(0x13u) 29 | } 30 | 31 | class UpdateCurrentTrack( 32 | artist: String = "", 33 | album: String = "", 34 | title: String = "", 35 | /** 36 | * Length of current track in milliseconds 37 | */ 38 | trackLength: Int? = null, 39 | trackCount: Int? = null, 40 | currentTrack: Int? = null 41 | ) : MusicControl(Message.UpdateCurrentTrack) { 42 | val artist = SString(m, artist) 43 | val album = SString(m, album) 44 | val title = SString(m, title) 45 | val trackLength = SOptional( 46 | m, 47 | SUInt(StructMapper(), trackLength?.toUInt() ?: 0u, Endian.Little), 48 | trackLength != null 49 | ) 50 | val trackCount = SOptional( 51 | m, 52 | SUInt(StructMapper(), trackCount?.toUInt() ?: 0u, Endian.Little), 53 | trackCount != null 54 | ) 55 | val currentTrack = SOptional( 56 | m, 57 | SUInt(StructMapper(), currentTrack?.toUInt() ?: 0u, Endian.Little), 58 | currentTrack != null 59 | ) 60 | } 61 | 62 | class UpdatePlayStateInfo( 63 | playbackState: PlaybackState = PlaybackState.Unknown, 64 | /** 65 | * Current playback position in milliseconds 66 | */ 67 | trackPosition: UInt = 0u, 68 | /** 69 | * Play rate in percentage (100 = normal speed) 70 | */ 71 | playRate: UInt = 0u, 72 | shuffle: ShuffleState = ShuffleState.Unknown, 73 | repeat: RepeatState = RepeatState.Unknown 74 | ) : MusicControl(Message.UpdatePlayStateInfo) { 75 | val state = SUByte(m, playbackState.value) 76 | val trackPosition = SUInt(m, trackPosition, Endian.Little) 77 | val playRate = SUInt(m, playRate, Endian.Little) 78 | val shuffle = SUByte(m, shuffle.value) 79 | val repeat = SUByte(m, repeat.value) 80 | } 81 | 82 | class UpdateVolumeInfo( 83 | volumePercent: UByte = 0u, 84 | ) : MusicControl(Message.UpdateVolumeInfo) { 85 | val volumePercent = SUByte(m, volumePercent) 86 | } 87 | 88 | class UpdatePlayerInfo( 89 | pkg: String = "", 90 | name: String = "" 91 | ) : MusicControl(Message.UpdatePlayerInfo) { 92 | val pkg = SString(m, pkg) 93 | val name = SString(m, name) 94 | } 95 | 96 | enum class PlaybackState(val value: UByte) { 97 | Paused(0x00u), 98 | Playing(0x01u), 99 | Rewinding(0x02u), 100 | FastForwarding(0x03u), 101 | Unknown(0x04u), 102 | } 103 | 104 | enum class ShuffleState(val value: UByte) { 105 | Unknown(0x00u), 106 | Off(0x01u), 107 | On(0x02u), 108 | } 109 | 110 | enum class RepeatState(val value: UByte) { 111 | Unknown(0x00u), 112 | Off(0x01u), 113 | One(0x02u), 114 | All(0x03u), 115 | } 116 | } 117 | 118 | fun musicPacketsRegister() { 119 | PacketRegistry.register( 120 | ProtocolEndpoint.MUSIC_CONTROL, 121 | MusicControl.Message.PlayPause.value 122 | ) { MusicControl(MusicControl.Message.PlayPause) } 123 | 124 | PacketRegistry.register( 125 | ProtocolEndpoint.MUSIC_CONTROL, 126 | MusicControl.Message.Pause.value 127 | ) { MusicControl(MusicControl.Message.Pause) } 128 | 129 | PacketRegistry.register( 130 | ProtocolEndpoint.MUSIC_CONTROL, 131 | MusicControl.Message.Play.value 132 | ) { MusicControl(MusicControl.Message.Play) } 133 | 134 | PacketRegistry.register( 135 | ProtocolEndpoint.MUSIC_CONTROL, 136 | MusicControl.Message.NextTrack.value 137 | ) { MusicControl(MusicControl.Message.NextTrack) } 138 | 139 | PacketRegistry.register( 140 | ProtocolEndpoint.MUSIC_CONTROL, 141 | MusicControl.Message.PreviousTrack.value 142 | ) { MusicControl(MusicControl.Message.PreviousTrack) } 143 | 144 | PacketRegistry.register( 145 | ProtocolEndpoint.MUSIC_CONTROL, 146 | MusicControl.Message.VolumeUp.value 147 | ) { MusicControl(MusicControl.Message.VolumeUp) } 148 | 149 | PacketRegistry.register( 150 | ProtocolEndpoint.MUSIC_CONTROL, 151 | MusicControl.Message.VolumeDown.value 152 | ) { MusicControl(MusicControl.Message.VolumeDown) } 153 | 154 | PacketRegistry.register( 155 | ProtocolEndpoint.MUSIC_CONTROL, 156 | MusicControl.Message.GetCurrentTrack.value 157 | ) { MusicControl(MusicControl.Message.GetCurrentTrack) } 158 | 159 | PacketRegistry.register( 160 | ProtocolEndpoint.MUSIC_CONTROL, 161 | MusicControl.Message.UpdateCurrentTrack.value 162 | ) { MusicControl.UpdateCurrentTrack() } 163 | 164 | PacketRegistry.register( 165 | ProtocolEndpoint.MUSIC_CONTROL, 166 | MusicControl.Message.UpdatePlayStateInfo.value 167 | ) { MusicControl.UpdatePlayStateInfo() } 168 | 169 | PacketRegistry.register( 170 | ProtocolEndpoint.MUSIC_CONTROL, 171 | MusicControl.Message.UpdateVolumeInfo.value 172 | ) { MusicControl.UpdateVolumeInfo() } 173 | 174 | PacketRegistry.register( 175 | ProtocolEndpoint.MUSIC_CONTROL, 176 | MusicControl.Message.UpdatePlayerInfo.value 177 | ) { MusicControl.UpdatePlayerInfo() } 178 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PhoneControl.kt: -------------------------------------------------------------------------------- 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 | 8 | 9 | sealed class PhoneControl(message: Message, cookie: UInt) : PebblePacket(endpoint) { 10 | enum class Message(val value: UByte) { 11 | Unknown(0u), 12 | Answer(0x01u), 13 | Hangup(0x02u), 14 | GetState(0x03u), 15 | IncomingCall(0x04u), 16 | OutgoingCall(0x05u), 17 | MissedCall(0x06u), 18 | Ring(0x07u), 19 | Start(0x08u), 20 | End(0x09u) 21 | } 22 | companion object { 23 | val endpoint = ProtocolEndpoint.PHONE_CONTROL 24 | } 25 | 26 | val command = SUByte(m, message.value) 27 | val cookie = SUInt(m, cookie) 28 | 29 | class IncomingCall(cookie: UInt = 0u, callerNumber: String = "", callerName: String = "") : PhoneControl(Message.IncomingCall, cookie) { 30 | val callerNumber = SString(m, callerNumber) 31 | val callerName = SString(m, callerName) 32 | } 33 | class MissedCall(cookie: UInt = 0u, callerNumber: String = "", callerName: String = "") : PhoneControl(Message.MissedCall, cookie) { 34 | val callerNumber = SString(m, callerNumber) 35 | val callerName = SString(m, callerName) 36 | } 37 | class Ring(cookie: UInt = 0u) : PhoneControl(Message.Ring, cookie) 38 | class Start(cookie: UInt = 0u) : PhoneControl(Message.Start, cookie) 39 | class End(cookie: UInt = 0u) : PhoneControl(Message.End, cookie) 40 | 41 | class Answer(cookie: UInt = 0u) : PhoneControl(Message.Answer, cookie) 42 | class Hangup(cookie: UInt = 0u) : PhoneControl(Message.Hangup, cookie) 43 | } 44 | 45 | fun phoneControlPacketsRegister() { 46 | PacketRegistry.register( 47 | PhoneControl.endpoint, 48 | PhoneControl.Message.Answer.value, 49 | ) { PhoneControl.Answer() } 50 | PacketRegistry.register( 51 | PhoneControl.endpoint, 52 | PhoneControl.Message.Hangup.value, 53 | ) { PhoneControl.Hangup() } 54 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/PutBytes.kt: -------------------------------------------------------------------------------- 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 | 8 | sealed class PutBytesOutgoingPacket(command: PutBytesCommand) : 9 | PebblePacket(ProtocolEndpoint.PUT_BYTES) { 10 | /** 11 | * Request command. See [PutBytesCommand]. 12 | */ 13 | val command = SUByte(m, command.value) 14 | 15 | } 16 | 17 | class PutBytesResponse : PebblePacket(ProtocolEndpoint.PUT_BYTES) { 18 | 19 | /** 20 | * See [PutBytesResult] 21 | */ 22 | val result = SUByte(m) 23 | 24 | /** 25 | * Cookie to send to all other put bytes requests 26 | */ 27 | val cookie = SUInt(m) 28 | } 29 | 30 | /** 31 | * Send to init non-app related file transfer 32 | */ 33 | class PutBytesInit( 34 | objectSize: UInt, 35 | objectType: ObjectType, 36 | bank: UByte, 37 | filename: String 38 | ) : PutBytesOutgoingPacket(PutBytesCommand.INIT) { 39 | val objectSize = SUInt(m, objectSize) 40 | val objectType = SUByte(m, objectType.value) 41 | val bank = SUByte(m, bank) 42 | val filename = SOptional(m, SNullTerminatedString(StructMapper(), filename), filename.isNotEmpty()) 43 | } 44 | 45 | /** 46 | * Send to init app-specific file transfer. 47 | */ 48 | class PutBytesAppInit( 49 | objectSize: UInt, 50 | objectType: ObjectType, 51 | appId: UInt 52 | ) : PutBytesOutgoingPacket(PutBytesCommand.INIT) { 53 | val objectSize = SUInt(m, objectSize) 54 | 55 | // Object type in app init packet must have 8th bit set (?) 56 | val objectType = SUByte(m, objectType.value or (1u shl 7).toUByte()) 57 | val appId = SUInt(m, appId) 58 | } 59 | 60 | /** 61 | * Send file data to the watch. After every put you have to wait for response from the watch. 62 | */ 63 | class PutBytesPut( 64 | cookie: UInt, 65 | payload: UByteArray 66 | ) : PutBytesOutgoingPacket(PutBytesCommand.PUT) { 67 | val cookie = SUInt(m, cookie) 68 | val payloadSize = SUInt(m, payload.size.toUInt()) 69 | val payload = SBytes(m, payload.size, payload) 70 | } 71 | 72 | /** 73 | * Sent when current file transfer is complete. [objectCrc] is the CRC32 hash of the sent payload. 74 | */ 75 | class PutBytesCommit( 76 | cookie: UInt, 77 | objectCrc: UInt 78 | ) : PutBytesOutgoingPacket(PutBytesCommand.COMMIT) { 79 | val cookie = SUInt(m, cookie) 80 | val objectCrc = SUInt(m, objectCrc) 81 | } 82 | 83 | /** 84 | * Send when there was an error during transfer and transfer cannot complete. 85 | */ 86 | class PutBytesAbort( 87 | cookie: UInt 88 | ) : PutBytesOutgoingPacket(PutBytesCommand.ABORT) { 89 | val cookie = SUInt(m, cookie) 90 | } 91 | 92 | /** 93 | * Send after app-related file was commited to complete install sequence 94 | */ 95 | class PutBytesInstall( 96 | cookie: UInt 97 | ) : PutBytesOutgoingPacket(PutBytesCommand.INSTALL) { 98 | val cookie = SUInt(m, cookie) 99 | } 100 | 101 | enum class PutBytesCommand(val value: UByte) { 102 | INIT(0x01u), 103 | PUT(0x02u), 104 | COMMIT(0x03u), 105 | ABORT(0x04u), 106 | INSTALL(0x05u) 107 | } 108 | 109 | enum class PutBytesResult(val value: UByte) { 110 | ACK(0x01u), 111 | NACK(0x02u) 112 | } 113 | 114 | enum class ObjectType(val value: UByte) { 115 | FIRMWARE(0x01u), 116 | RECOVERY(0x02u), 117 | SYSTEM_RESOURCE(0x03u), 118 | APP_RESOURCE(0x04u), 119 | APP_EXECUTABLE(0x05u), 120 | FILE(0x06u), 121 | WORKER(0x07u) 122 | } 123 | 124 | fun putBytesIncomingPacketsRegister() { 125 | PacketRegistry.register( 126 | ProtocolEndpoint.PUT_BYTES, 127 | PutBytesResult.ACK.value, 128 | ) { PutBytesResponse() } 129 | 130 | PacketRegistry.register( 131 | ProtocolEndpoint.PUT_BYTES, 132 | PutBytesResult.NACK.value, 133 | ) { PutBytesResponse() } 134 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Screenshot.kt: -------------------------------------------------------------------------------- 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.SUByte 7 | import io.rebble.libpebblecommon.structmapper.SUInt 8 | import io.rebble.libpebblecommon.structmapper.SUnboundBytes 9 | import io.rebble.libpebblecommon.structmapper.StructMappable 10 | 11 | class ScreenshotRequest : PebblePacket(ProtocolEndpoint.SCREENSHOT) { 12 | /** 13 | * Command. Only one command (take screenshot, value 0) is supported for now 14 | */ 15 | val command = SUByte(m, 0u) 16 | } 17 | 18 | class ScreenshotResponse : PebblePacket(ProtocolEndpoint.SCREENSHOT) { 19 | val data = SUnboundBytes(m) 20 | } 21 | 22 | class ScreenshotHeader : StructMappable() { 23 | /** 24 | * @see ScreenshotResponseCode 25 | */ 26 | val responseCode = SUByte(m) 27 | 28 | /** 29 | * @see ScreenshotVersion 30 | */ 31 | val version = SUInt(m) 32 | 33 | val width = SUInt(m) 34 | val height = SUInt(m) 35 | val data = SUnboundBytes(m) 36 | } 37 | 38 | enum class ScreenshotResponseCode(val rawCode: UByte) { 39 | OK(0u), 40 | MalformedCommand(0u), 41 | OutOfMemory(0u), 42 | AlreadyInProgress(0u); 43 | 44 | companion object { 45 | fun fromRawCode(rawCode: UByte): ScreenshotResponseCode { 46 | return values().firstOrNull { it.rawCode == rawCode } 47 | ?: error("Unknown screenshot response code: $rawCode") 48 | } 49 | } 50 | } 51 | 52 | enum class ScreenshotVersion(val rawCode: UInt) { 53 | BLACK_WHITE_1_BIT(1u), 54 | COLOR_8_BIT(2u); 55 | 56 | companion object { 57 | fun fromRawCode(rawCode: UInt): ScreenshotVersion { 58 | return values().firstOrNull { it.rawCode == rawCode } 59 | ?: error("Unknown screenshots version: $rawCode") 60 | } 61 | } 62 | } 63 | 64 | fun screenshotPacketsRegister() { 65 | PacketRegistry.register(ProtocolEndpoint.SCREENSHOT) { 66 | ScreenshotResponse() 67 | } 68 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/Voice.kt: -------------------------------------------------------------------------------- 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 = 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 = 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) : 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/packets/blobdb/App.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets.blobdb 2 | 3 | import io.rebble.libpebblecommon.structmapper.* 4 | import io.rebble.libpebblecommon.util.Endian 5 | 6 | /** 7 | * Data of the APP BlobDB Entry 8 | */ 9 | class AppMetadata() : StructMappable() { 10 | /** 11 | * UUID of the app 12 | */ 13 | val uuid: SUUID = SUUID(m) 14 | 15 | /** 16 | * App install flags. 17 | */ 18 | val flags: SUInt = SUInt(m, endianness = Endian.Little) 19 | 20 | /** 21 | * Resource ID of the primary icon. 22 | */ 23 | val icon: SUInt = SUInt(m, endianness = Endian.Little) 24 | 25 | /** 26 | * Major app version. 27 | */ 28 | val appVersionMajor: SUByte = SUByte(m) 29 | 30 | /** 31 | * Minor app version. 32 | */ 33 | val appVersionMinor: SUByte = SUByte(m) 34 | 35 | /** 36 | * Major sdk version. 37 | */ 38 | val sdkVersionMajor: SUByte = SUByte(m) 39 | 40 | /** 41 | * Minor sdk version. 42 | */ 43 | val sdkVersionMinor: SUByte = SUByte(m) 44 | 45 | /** 46 | * ??? (Always sent as 0 in the Pebble app) 47 | */ 48 | val appFaceBgColor: SUByte = SUByte(m, 0u) 49 | 50 | /** 51 | * ??? (Always sent as 0 in the Pebble app) 52 | */ 53 | val appFaceTemplateId: SUByte = SUByte(m, 0u) 54 | 55 | /** 56 | * Name of the app 57 | */ 58 | val appName: SFixedString = SFixedString(m, 96) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/BlobDB.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets.blobdb 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.SBytes 7 | import io.rebble.libpebblecommon.structmapper.SUByte 8 | import io.rebble.libpebblecommon.structmapper.SUShort 9 | import io.rebble.libpebblecommon.util.Endian 10 | 11 | open class BlobCommand constructor(message: Message, token: UShort, database: BlobDatabase) : 12 | PebblePacket( 13 | endpoint 14 | ) { 15 | enum class Message(val value: UByte) { 16 | Insert(0x01u), 17 | Delete(0x04u), 18 | Clear(0x05u) 19 | } 20 | 21 | enum class BlobDatabase(val id: UByte) { 22 | Test(0u), 23 | Pin(1u), 24 | App(2u), 25 | Reminder(3u), 26 | Notification(4u), 27 | Weather(5u), 28 | CannedResponses(6u), 29 | HealthParams(7u), 30 | Contacts(8u), 31 | AppConfigs(9u), 32 | HealthStats(10u), 33 | AppGlance(11u) 34 | } 35 | 36 | val command = SUByte(m, message.value) 37 | val token = SUShort(m, token) 38 | val database = SUByte(m, database.id) 39 | 40 | open class InsertCommand( 41 | token: UShort, 42 | database: BlobDatabase, 43 | key: UByteArray, 44 | value: UByteArray 45 | ) : BlobCommand( 46 | Message.Insert, token, database 47 | ) { 48 | val keySize = SUByte(m, key.size.toUByte()) 49 | val targetKey = SBytes(m, key.size, key) 50 | val valSize = SUShort(m, value.size.toUShort(), endianness = Endian.Little) 51 | val targetValue = SBytes(m, value.size, value) 52 | } 53 | 54 | class DeleteCommand(token: UShort, database: BlobDatabase, key: UByteArray) : BlobCommand( 55 | Message.Delete, token, database 56 | ) { 57 | val keySize = SUByte(m, key.size.toUByte()) 58 | val targetKey = SBytes(m, key.size, key) 59 | } 60 | 61 | class ClearCommand(token: UShort, database: BlobDatabase) : BlobCommand( 62 | Message.Clear, token, database 63 | ) 64 | 65 | companion object { 66 | val endpoint = ProtocolEndpoint.BLOBDB_V1 67 | } 68 | } 69 | 70 | open class BlobResponse(response: BlobStatus = BlobStatus.GeneralFailure) : PebblePacket(endpoint) { 71 | enum class BlobStatus(val value: UByte) { 72 | Success(0x01u), 73 | GeneralFailure(0x02u), 74 | InvalidOperation(0x03u), 75 | InvalidDatabaseID(0x04u), 76 | InvalidData(0x05u), 77 | KeyDoesNotExist(0x06u), 78 | DatabaseFull(0x07u), 79 | DataStale(0x08u), 80 | NotSupported(0x09u), 81 | Locked(0xAu), 82 | TryLater(0xBu), 83 | 84 | // Added status by libpebblecommon to expose watch connection error 85 | WatchDisconnected(0xFFu), 86 | } 87 | 88 | class Success : BlobResponse(BlobStatus.Success) 89 | class GeneralFailure : BlobResponse(BlobStatus.GeneralFailure) 90 | class InvalidOperation : BlobResponse(BlobStatus.InvalidOperation) 91 | class InvalidDatabaseID : BlobResponse(BlobStatus.InvalidDatabaseID) 92 | class InvalidData : BlobResponse(BlobStatus.InvalidData) 93 | class KeyDoesNotExist : BlobResponse(BlobStatus.KeyDoesNotExist) 94 | class DatabaseFull : BlobResponse(BlobStatus.DatabaseFull) 95 | class DataStale : BlobResponse(BlobStatus.DataStale) 96 | class NotSupported : BlobResponse(BlobStatus.NotSupported) 97 | class Locked : BlobResponse(BlobStatus.Locked) 98 | class TryLater : BlobResponse(BlobStatus.TryLater) 99 | 100 | val token = SUShort(m) 101 | val response = SUByte(m, response.value) 102 | 103 | val responseValue 104 | get() = BlobStatus.values().firstOrNull { it.value == response.get() } 105 | ?: error("Unknown blobdb response ${response.get()}") 106 | 107 | companion object { 108 | val endpoint = ProtocolEndpoint.BLOBDB_V1 109 | } 110 | } 111 | 112 | fun blobDBPacketsRegister() { 113 | PacketRegistry.registerCustomTypeOffset(BlobResponse.endpoint, 4 + UShort.SIZE_BYTES) 114 | PacketRegistry.register( 115 | BlobResponse.endpoint, 116 | BlobResponse.BlobStatus.Success.value 117 | ) { BlobResponse.Success() } 118 | PacketRegistry.register( 119 | BlobResponse.endpoint, 120 | BlobResponse.BlobStatus.GeneralFailure.value 121 | ) { BlobResponse.GeneralFailure() } 122 | PacketRegistry.register( 123 | BlobResponse.endpoint, 124 | BlobResponse.BlobStatus.InvalidOperation.value 125 | ) { BlobResponse.InvalidOperation() } 126 | PacketRegistry.register( 127 | BlobResponse.endpoint, 128 | BlobResponse.BlobStatus.InvalidDatabaseID.value 129 | ) { BlobResponse.InvalidDatabaseID() } 130 | PacketRegistry.register( 131 | BlobResponse.endpoint, 132 | BlobResponse.BlobStatus.InvalidData.value 133 | ) { BlobResponse.InvalidData() } 134 | PacketRegistry.register( 135 | BlobResponse.endpoint, 136 | BlobResponse.BlobStatus.KeyDoesNotExist.value 137 | ) { BlobResponse.KeyDoesNotExist() } 138 | PacketRegistry.register( 139 | BlobResponse.endpoint, 140 | BlobResponse.BlobStatus.DatabaseFull.value 141 | ) { BlobResponse.DatabaseFull() } 142 | PacketRegistry.register( 143 | BlobResponse.endpoint, 144 | BlobResponse.BlobStatus.DataStale.value 145 | ) { BlobResponse.DataStale() } 146 | PacketRegistry.register( 147 | BlobResponse.endpoint, 148 | BlobResponse.BlobStatus.NotSupported.value 149 | ) { BlobResponse.NotSupported() } 150 | PacketRegistry.register( 151 | BlobResponse.endpoint, 152 | BlobResponse.BlobStatus.Locked.value 153 | ) { BlobResponse.Locked() } 154 | PacketRegistry.register( 155 | BlobResponse.endpoint, 156 | BlobResponse.BlobStatus.TryLater.value 157 | ) { BlobResponse.TryLater() } 158 | } 159 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/Notification.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets.blobdb 2 | 3 | import com.benasher44.uuid.uuid4 4 | import com.benasher44.uuid.uuidFrom 5 | import com.benasher44.uuid.uuidOf 6 | import com.soywiz.klock.DateTime 7 | import io.rebble.libpebblecommon.structmapper.SUInt 8 | import io.rebble.libpebblecommon.structmapper.StructMapper 9 | import io.rebble.libpebblecommon.util.TimelineAttributeFactory 10 | import kotlin.random.Random 11 | 12 | private val notifsUUID = uuidFrom("B2CAE818-10F8-46DF-AD2B-98AD2254A3C1") 13 | 14 | enum class NotificationSource(val id: UInt) { //TODO: There's likely more... (probably fw >3) 15 | Generic(1u), 16 | Twitter(6u), 17 | Facebook(11u), 18 | Email(19u), 19 | SMS(45u), 20 | } 21 | 22 | /** 23 | * Helper class to generate a BlobDB command that inserts a notification 24 | */ 25 | open class PushNotification(subject: String, sender: String? = null, message: String? = null, source: NotificationSource = NotificationSource.Generic, backgroundColor: UByte? = null): BlobCommand.InsertCommand(Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), 26 | BlobDatabase.Notification, ubyteArrayOf(), ubyteArrayOf()) { 27 | init { 28 | val itemID = uuid4() 29 | 30 | //TODO: Replies, open on phone, detect dismiss 31 | val attributes = mutableListOf( 32 | TimelineAttributeFactory.sender(sender ?: ""), 33 | TimelineAttributeFactory.icon(TimelineIcon.fromId(source.id)) 34 | ) 35 | if (message != null) attributes += TimelineAttributeFactory.body(message) 36 | attributes += TimelineAttributeFactory.subtitle(subject) 37 | 38 | if (backgroundColor != null) { 39 | // XXX: https://youtrack.jetbrains.com/issue/KT-49366 40 | val bgColTemp = backgroundColor.toUByte() 41 | attributes += TimelineAttributeFactory.primaryColor(bgColTemp) 42 | } 43 | 44 | val actions = mutableListOf( 45 | TimelineItem.Action( 46 | 0u, TimelineItem.Action.Type.Dismiss, mutableListOf( 47 | TimelineItem.Attribute( 48 | 0x01u, 49 | "Dismiss".encodeToByteArray().toUByteArray() 50 | ) 51 | ) 52 | ) 53 | ) 54 | 55 | val timestamp = DateTime.nowUnixLong() / 1000 56 | 57 | val notification = TimelineItem( 58 | itemID, 59 | uuidOf(ByteArray(2 * Long.SIZE_BYTES) { 0 }), 60 | timestamp.toUInt(), 61 | 0u, 62 | TimelineItem.Type.Notification, 63 | 0u, 64 | TimelineItem.Layout.GenericPin, 65 | attributes, 66 | actions 67 | ) 68 | super.targetKey.set(notification.itemId.toBytes(), notification.itemId.size) 69 | super.keySize.set(super.targetKey.size.toUByte()) 70 | val nbytes = notification.toBytes() 71 | super.targetValue.set(nbytes, nbytes.size) 72 | super.valSize.set(super.targetValue.size.toUShort()) 73 | } 74 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/packets/blobdb/TimelineIcon.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets.blobdb 2 | 3 | enum class TimelineIcon(val id: UInt) { 4 | NotificationReminder(3u), 5 | HockeyGame(30u), 6 | PayBill(38u), 7 | NotificationLinkedIn(115u), 8 | NotificationGoogleInbox(61u), 9 | GenericQuestion(63u), 10 | NotificationFlag(4u), 11 | GenericSms(45u), 12 | WatchDisconnected(48u), 13 | TvShow(73u), 14 | Basketball(74u), 15 | GenericWarning(28u), 16 | LightRain(32u), 17 | NotificationFacebook(11u), 18 | IncomingPhoneCall(78u), 19 | NotificationGoogleMessenger(76u), 20 | NotificationTelegram(7u), 21 | NotificationFacetime(110u), 22 | ArrowDown(99u), 23 | NotificationOutlook(64u), 24 | NoEvents(57u), 25 | AudioCassette(12u), 26 | Sunset(85u), 27 | NotificationTwitter(6u), 28 | Sunrise(84u), 29 | HeavyRain(52u), 30 | NotificationMailbox(60u), 31 | AmericanFootball(20u), 32 | CarRental(24u), 33 | CricketGame(26u), 34 | NotificationWeChat(71u), 35 | NotificationGeneric(1u), 36 | NotificationSkype(68u), 37 | CloudyDay(25u), 38 | DuringPhoneCallCentered(95u), 39 | NotificationLine(67u), 40 | HotelReservation(31u), 41 | NotificationFacebookMessenger(10u), 42 | NotificationLighthouse(81u), 43 | TimelineEmptyCalendar(96u), 44 | NotificationIosPhotos(114u), 45 | ResultDeleted(43u), 46 | NotificationGmail(9u), 47 | TimelineMissedCall(2u), 48 | Sleep(101u), 49 | ResultMute(46u), 50 | NotificationAmazon(111u), 51 | ThumbsUp(97u), 52 | ScheduledFlight(54u), 53 | Settings(83u), 54 | PartlyCloudy(37u), 55 | StocksEvent(42u), 56 | NotificationGoogleMaps(112u), 57 | RewardGood(103u), 58 | NotificationYahooMail(72u), 59 | BirthdayEvent(23u), 60 | GenericEmail(19u), 61 | ResultDismissed(51u), 62 | NotificationGooglePhotos(113u), 63 | TideIsHigh(50u), 64 | NotificationViber(70u), 65 | LightSnow(33u), 66 | NewsEvent(36u), 67 | GenericConfirmation(55u), 68 | TimelineSports(17u), 69 | NotificationSlack(116u), 70 | CheckInternetConnection(44u), 71 | Activity(100u), 72 | NotificationHipChat(77u), 73 | NotificationInstagram(59u), 74 | TimelineBaseball(22u), 75 | RewardBad(102u), 76 | ReachedFitnessGoal(66u), 77 | DaySeparator(56u), 78 | TimelineCalendar(21u), 79 | RainingAndSnowing(65u), 80 | RadioShow(39u), 81 | DismissedPhoneCall(75u), 82 | ArrowUp(98u), 83 | RewardAverage(104u), 84 | MusicEvent(35u), 85 | NotificationSnapchat(69u), 86 | NotificationBlackberryMessenger(58u), 87 | NotificationWhatsapp(5u), 88 | Location(82u), 89 | SoccerGame(41u), 90 | ResultFailed(62u), 91 | ResultUnmute(86u), 92 | ScheduledEvent(40u), 93 | TimelineWeather(14u), 94 | TimelineSun(16u), 95 | NotificationGoogleHangouts(8u), 96 | DuringPhoneCall(49u), 97 | NotificationKik(80u), 98 | ResultUnmuteAlt(94u), 99 | MovieEvent(34u), 100 | GlucoseMonitor(29u), 101 | ResultSent(47u), 102 | AlarmClock(13u), 103 | HeavySnow(53u), 104 | DinnerReservation(27u), 105 | NotificationKakaoTalk(79u); 106 | 107 | companion object { 108 | fun fromId(id: UInt): TimelineIcon { 109 | return entries.firstOrNull { it.id == id } 110 | ?: error("Unknown timeline icon id: $id") 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PacketRegistry.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.protocolhelpers 2 | 3 | import io.rebble.libpebblecommon.exceptions.PacketDecodeException 4 | import io.rebble.libpebblecommon.packets.* 5 | import io.rebble.libpebblecommon.packets.blobdb.blobDBPacketsRegister 6 | import io.rebble.libpebblecommon.packets.blobdb.timelinePacketsRegister 7 | 8 | /** 9 | * Singleton to track endpoint / type discriminators for deserialization 10 | */ 11 | object PacketRegistry { 12 | private var typeOffsets: MutableMap = mutableMapOf() 13 | private var typedDecoders: MutableMap PebblePacket>> = 14 | mutableMapOf() 15 | private var universalDecoders: MutableMap PebblePacket> = 16 | mutableMapOf() 17 | 18 | init { 19 | systemPacketsRegister() 20 | timePacketsRegister() 21 | timelinePacketsRegister() 22 | blobDBPacketsRegister() 23 | appmessagePacketsRegister() 24 | appRunStatePacketsRegister() 25 | musicPacketsRegister() 26 | appFetchIncomingPacketsRegister() 27 | putBytesIncomingPacketsRegister() 28 | appReorderIncomingRegister() 29 | screenshotPacketsRegister() 30 | appLogPacketsRegister() 31 | phoneControlPacketsRegister() 32 | logDumpPacketsRegister() 33 | voicePacketsRegister() 34 | audioStreamPacketsRegister() 35 | } 36 | 37 | /** 38 | * Register a custom offset for the type discriminator (e.g. if the first byte after frame is not the command) 39 | * @param endpoint the endpoint to register the new offset to 40 | * @param offset the new offset, including frame offset (4) 41 | */ 42 | fun registerCustomTypeOffset(endpoint: ProtocolEndpoint, offset: Int) { 43 | typeOffsets[endpoint] = offset 44 | } 45 | 46 | fun register(endpoint: ProtocolEndpoint, decoder: (UByteArray) -> PebblePacket) { 47 | universalDecoders[endpoint] = decoder 48 | } 49 | 50 | fun register(endpoint: ProtocolEndpoint, type: UByte, decoder: (UByteArray) -> PebblePacket) { 51 | if (typedDecoders[endpoint] == null) { 52 | typedDecoders[endpoint] = mutableMapOf() 53 | } 54 | typedDecoders[endpoint]!![type] = decoder 55 | } 56 | 57 | fun get(endpoint: ProtocolEndpoint, packet: UByteArray): PebblePacket { 58 | universalDecoders[endpoint]?.let { return it(packet) } 59 | 60 | val epdecoders = typedDecoders[endpoint] 61 | ?: throw PacketDecodeException("No packet class registered for endpoint $endpoint") 62 | 63 | val typeOffset = if (typeOffsets[endpoint] != null) typeOffsets[endpoint]!! else 4 64 | val decoder = epdecoders[packet[typeOffset]] 65 | ?: throw PacketDecodeException("No packet class registered for type ${packet[typeOffset]} of $endpoint") 66 | return decoder(packet) 67 | } 68 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/PebblePacket.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.protocolhelpers 2 | 3 | import co.touchlab.kermit.Logger 4 | import io.rebble.libpebblecommon.exceptions.PacketDecodeException 5 | import io.rebble.libpebblecommon.exceptions.PacketEncodeException 6 | import io.rebble.libpebblecommon.structmapper.SUShort 7 | import io.rebble.libpebblecommon.structmapper.StructMapper 8 | import io.rebble.libpebblecommon.util.DataBuffer 9 | 10 | /** 11 | * Represents a pebble protocol packet 12 | */ 13 | open class PebblePacket{ 14 | val endpoint: ProtocolEndpoint 15 | val m = StructMapper() 16 | var type: UByte? = null 17 | val importedLength: UShort? 18 | 19 | constructor(endpoint: ProtocolEndpoint) { //TODO: Packet-level endianness? 20 | this.endpoint = endpoint 21 | importedLength = null 22 | } 23 | 24 | constructor(packet: UByteArray) { 25 | val meta = StructMapper() 26 | val length = SUShort(meta) 27 | val ep = SUShort(meta) 28 | meta.fromBytes(DataBuffer(packet)) 29 | if (length.get() != (packet.size - (UShort.SIZE_BYTES * 2)).toUShort()) 30 | throw IllegalArgumentException("Length in packet does not match packet actual size, likely malformed") 31 | 32 | Logger.v { "Importing packet: Len $length | EP $ep" } 33 | importedLength = length.get() 34 | 35 | this.endpoint = 36 | ProtocolEndpoint.getByValue(ep.get()) 37 | } 38 | 39 | /** 40 | * Serializes a framed pebble protocol packet into a byte array 41 | * @return The serialized packet 42 | */ 43 | fun serialize(): UByteArray { 44 | val content = m.toBytes() 45 | if (content.isEmpty()) throw PacketEncodeException("Malformed packet: contents empty") 46 | val meta = StructMapper() 47 | val length = SUShort(meta, content.size.toUShort()) 48 | val ep = SUShort(meta, endpoint.value) 49 | 50 | return meta.toBytes() + content // Whole packet (meta + content) 51 | } 52 | 53 | companion object { 54 | /** 55 | * Deserializes a framed pebble protocol packet into a PebblePacket class 56 | * @param packet the packet to deserialize 57 | * @return The deserialized packet 58 | */ 59 | fun deserialize(packet: UByteArray): PebblePacket { 60 | val buf = DataBuffer(packet) 61 | 62 | val meta = StructMapper() 63 | val length = SUShort(meta) 64 | val ep = SUShort(meta) 65 | meta.fromBytes(buf) 66 | if (packet.size <= (2 * UShort.SIZE_BYTES)) 67 | throw PacketDecodeException("Malformed packet: contents empty") 68 | val lengthVal = length.get().toInt() 69 | if (lengthVal != (packet.size - (2 * UShort.SIZE_BYTES))) 70 | throw PacketDecodeException("Malformed packet: bad length (expected ${length.get()}, got ${packet.size - (2 * UShort.SIZE_BYTES)})") 71 | val ret = PacketRegistry.get( 72 | ProtocolEndpoint.getByValue(ep.get()), 73 | packet 74 | ) 75 | try { 76 | ret.m.fromBytes(buf) 77 | } catch (e: Exception) { 78 | throw PacketDecodeException("Failed to decode packet $ret", e) 79 | } 80 | return ret 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/protocolhelpers/ProtocolEndpoint.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.protocolhelpers 2 | 3 | import co.touchlab.kermit.Logger 4 | 5 | enum class ProtocolEndpoint(val value: UShort) { 6 | RECOVERY(0u), 7 | TIME(11u), 8 | WATCH_VERSION(16u), 9 | PHONE_VERSION(17u), 10 | SYSTEM_MESSAGE(18u), 11 | MUSIC_CONTROL(32u), 12 | PHONE_CONTROL(33u), 13 | APP_MESSAGE(48u), 14 | LEGACY_APP_LAUNCH(49u), 15 | APP_CUSTOMIZE(50u), 16 | BLE_CONTROL(51u), 17 | APP_RUN_STATE(52u), 18 | LOGS(2000u), 19 | PING(2001u), 20 | LOG_DUMP(2002u), 21 | RESET(2003u), 22 | APP_LOGS(2006u), 23 | SYS_REG(5000u), 24 | FCT_REG(5001u), 25 | APP_FETCH(6001u), 26 | PUT_BYTES(48879u /* 0xbeef */), 27 | DATA_LOG(6778u), 28 | SCREENSHOT(8000u), 29 | FILE_INSTALL_MANAGER(8181u), 30 | GET_BYTES(9000u), 31 | AUDIO_STREAMING(10000u), 32 | APP_REORDER(43981u /* 0xabcd */), 33 | BLOBDB_V1(45531u /* 0xb1db */), 34 | BLOBDB_V2(45787u /* 0xb2db */), 35 | TIMELINE_ACTIONS(11440u), 36 | VOICE_CONTROL(11000u), 37 | HEALTH_SYNC(911u), 38 | INVALID_ENDPOINT(0xffffu); 39 | 40 | companion object { 41 | private val values = entries.toTypedArray() 42 | fun getByValue(value: UShort) = values.firstOrNull { it.value == value } 43 | ?: INVALID_ENDPOINT.also { 44 | Logger.e { 45 | "Received unknown packet endpoint: 0x${value.toInt().toString(16)}" 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppFetchService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.AppFetchIncomingPacket 5 | import io.rebble.libpebblecommon.packets.AppFetchOutgoingPacket 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import kotlinx.coroutines.channels.Channel 9 | 10 | class AppFetchService(private val protocolHandler: ProtocolHandler) : ProtocolService { 11 | val receivedMessages = Channel(Channel.BUFFERED) 12 | 13 | init { 14 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_FETCH, this::receive) 15 | } 16 | 17 | suspend fun send(packet: AppFetchOutgoingPacket) { 18 | protocolHandler.send(packet) 19 | } 20 | 21 | fun receive(packet: PebblePacket) { 22 | if (packet !is AppFetchIncomingPacket) { 23 | throw IllegalStateException("Received invalid packet type: $packet") 24 | } 25 | 26 | receivedMessages.trySend(packet) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppLogService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.AppLogReceivedMessage 5 | import io.rebble.libpebblecommon.packets.AppLogShippingControlMessage 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import kotlinx.coroutines.channels.Channel 9 | 10 | class AppLogService(private val protocolHandler: ProtocolHandler) : ProtocolService { 11 | val receivedMessages = Channel(Channel.BUFFERED) 12 | 13 | init { 14 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_LOGS, this::receive) 15 | } 16 | 17 | suspend fun send(packet: AppLogShippingControlMessage) { 18 | protocolHandler.send(packet) 19 | } 20 | 21 | fun receive(packet: PebblePacket) { 22 | if (packet !is AppLogReceivedMessage) { 23 | throw IllegalStateException("Received invalid packet type: $packet") 24 | } 25 | 26 | receivedMessages.trySend(packet) 27 | } 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/AppReorderService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.AppOrderResultCode 5 | import io.rebble.libpebblecommon.packets.AppReorderOutgoingPacket 6 | import io.rebble.libpebblecommon.packets.AppReorderResult 7 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 8 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 9 | import kotlinx.coroutines.channels.Channel 10 | 11 | class AppReorderService(private val protocolHandler: ProtocolHandler) : ProtocolService { 12 | val receivedMessages = Channel(Channel.BUFFERED) 13 | private var lastOrderPacket: AppReorderOutgoingPacket? = null 14 | 15 | init { 16 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_REORDER, this::receive) 17 | } 18 | 19 | suspend fun send(packet: AppReorderOutgoingPacket) { 20 | protocolHandler.send(packet) 21 | } 22 | 23 | suspend fun receive(packet: PebblePacket) { 24 | if (packet !is AppReorderResult) { 25 | throw IllegalStateException("Received invalid packet type: $packet") 26 | } 27 | 28 | if (packet.status.get() == AppOrderResultCode.RETRY.value) { 29 | lastOrderPacket?.let { send(it) } 30 | return 31 | } 32 | 33 | lastOrderPacket = null 34 | receivedMessages.trySend(packet) 35 | } 36 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/AudioStreamService.kt: -------------------------------------------------------------------------------- 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(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 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/LogDumpService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.LogDump 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 7 | import kotlinx.coroutines.channels.Channel 8 | 9 | class LogDumpService(private val protocolHandler: ProtocolHandler) : ProtocolService { 10 | val receivedMessages = Channel(Channel.BUFFERED) 11 | 12 | init { 13 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.LOG_DUMP, this::receive) 14 | } 15 | 16 | suspend fun send(packet: LogDump) { 17 | protocolHandler.send(packet) 18 | } 19 | 20 | fun receive(packet: PebblePacket) { 21 | if (packet !is LogDump) { 22 | throw IllegalStateException("Received invalid packet type: $packet") 23 | } 24 | 25 | receivedMessages.trySend(packet) 26 | } 27 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/MusicService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.MusicControl 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 7 | import kotlinx.coroutines.channels.Channel 8 | 9 | class MusicService(private val protocolHandler: ProtocolHandler) : ProtocolService { 10 | val receivedMessages = Channel(Channel.BUFFERED) 11 | 12 | init { 13 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.MUSIC_CONTROL, this::receive) 14 | } 15 | 16 | suspend fun send(packet: MusicControl) { 17 | protocolHandler.send(packet) 18 | } 19 | 20 | fun receive(packet: PebblePacket) { 21 | if (packet !is MusicControl) { 22 | throw IllegalStateException("Received invalid packet type: $packet") 23 | } 24 | 25 | receivedMessages.trySend(packet) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/PhoneControlService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.PhoneControl 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 7 | import kotlinx.coroutines.channels.Channel 8 | 9 | class PhoneControlService(private val protocolHandler: ProtocolHandler) : ProtocolService { 10 | val receivedMessages = Channel(Channel.BUFFERED) 11 | 12 | init { 13 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.PHONE_CONTROL, this::receive) 14 | } 15 | 16 | suspend fun send(packet: PhoneControl) { 17 | protocolHandler.send(packet) 18 | } 19 | 20 | fun receive(packet: PebblePacket) { 21 | if (packet !is PhoneControl) { 22 | throw IllegalStateException("Received invalid packet type: $packet") 23 | } 24 | 25 | receivedMessages.trySend(packet) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/ProtocolService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | interface ProtocolService -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/PutBytesService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import co.touchlab.kermit.Logger 4 | import io.rebble.libpebblecommon.ProtocolHandler 5 | import io.rebble.libpebblecommon.metadata.WatchType 6 | import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwBlob 7 | import io.rebble.libpebblecommon.packets.* 8 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 9 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 10 | import io.rebble.libpebblecommon.util.Crc32Calculator 11 | import io.rebble.libpebblecommon.util.DataBuffer 12 | import io.rebble.libpebblecommon.util.getPutBytesMaximumDataSize 13 | import kotlinx.coroutines.channels.Channel 14 | import kotlinx.coroutines.withTimeout 15 | import kotlin.math.log 16 | 17 | class PutBytesService(private val protocolHandler: ProtocolHandler) : ProtocolService { 18 | val receivedMessages = Channel(Channel.BUFFERED) 19 | val progressUpdates = Channel(Channel.BUFFERED) 20 | 21 | private val logger = Logger.withTag("PutBytesService") 22 | 23 | data class PutBytesProgress( 24 | val count: Int, 25 | val total: Int, 26 | val delta: Int, 27 | val cookie: UInt 28 | ) 29 | 30 | init { 31 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.PUT_BYTES, this::receive) 32 | } 33 | 34 | suspend fun send(packet: PutBytesOutgoingPacket) { 35 | if (packet is PutBytesAbort) { 36 | lastCookie = null 37 | } 38 | 39 | protocolHandler.send(packet) 40 | } 41 | 42 | fun receive(packet: PebblePacket) { 43 | if (packet !is PutBytesResponse) { 44 | throw IllegalStateException("Received invalid packet type: $packet") 45 | } 46 | 47 | receivedMessages.trySend(packet) 48 | } 49 | 50 | var lastCookie: UInt? = null 51 | 52 | class PutBytesException(val cookie: UInt?, message: String, cause: Throwable? = null) : Error(message, cause); 53 | 54 | /** 55 | * Inits a PutBytes session on the device and sends an app, leaves aborting to the caller 56 | */ 57 | @Throws(PutBytesException::class, IllegalStateException::class) 58 | suspend fun sendAppPart( 59 | appId: UInt, 60 | blob: ByteArray, 61 | watchType: WatchType, 62 | watchVersion: WatchVersion.WatchVersionResponse, 63 | manifestEntry: PbwBlob, 64 | type: ObjectType 65 | ) { 66 | logger.i { "Send app part $watchType $appId $manifestEntry $type ${type.value}" } 67 | send( 68 | PutBytesAppInit(manifestEntry.size.toUInt(), type, appId) 69 | ) 70 | 71 | val cookie = awaitCookieAndPutByteArray( 72 | blob, 73 | manifestEntry.crc, 74 | watchVersion 75 | ) 76 | 77 | logger.d { "Sending install" } 78 | 79 | send( 80 | PutBytesInstall(cookie) 81 | ) 82 | awaitAck() 83 | 84 | logger.i { "Install complete" } 85 | } 86 | 87 | suspend fun sendFirmwarePart( 88 | blob: ByteArray, 89 | watchVersion: WatchVersion.WatchVersionResponse, 90 | crc: Long, 91 | size: UInt, 92 | bank: UByte, 93 | type: ObjectType 94 | ) { 95 | logger.i { "Send FW part $type ${type.value}" } 96 | send( 97 | PutBytesInit(size, type, bank, "") 98 | ) 99 | 100 | logger.d { "Putting byte array" } 101 | val cookie = awaitCookieAndPutByteArray( 102 | blob, 103 | crc, 104 | watchVersion 105 | ) 106 | 107 | logger.d { "Sending install" } 108 | 109 | send( 110 | PutBytesInstall(cookie) 111 | ) 112 | awaitAck() 113 | 114 | logger.i { "Install complete" } 115 | } 116 | 117 | suspend fun awaitCookieAndPutByteArray( 118 | byteArray: ByteArray, 119 | expectedCrc: Long?, 120 | watchVersion: WatchVersion.WatchVersionResponse 121 | ): UInt { 122 | try { 123 | val totalToPut = byteArray.size 124 | val cookie = awaitAck().cookie.get() 125 | lastCookie = cookie 126 | progressUpdates.trySend( 127 | PutBytesProgress(0, totalToPut, 0, cookie) 128 | ) 129 | 130 | val maxDataSize = if (watchVersion.running.isRecovery.get()) 2000 else getPutBytesMaximumDataSize(watchVersion) 131 | val buffer = DataBuffer(byteArray.asUByteArray()) 132 | val crcCalculator = Crc32Calculator() 133 | 134 | var totalBytes = 0 135 | while (true) { 136 | val dataToRead = maxDataSize.coerceAtMost(buffer.remaining) 137 | if (dataToRead <= 0) { 138 | break 139 | } 140 | val payload = buffer.getBytes(dataToRead) 141 | 142 | crcCalculator.addBytes(payload) 143 | 144 | send(PutBytesPut(cookie, payload)) 145 | awaitAck() 146 | totalBytes += dataToRead 147 | progressUpdates.trySend( 148 | PutBytesProgress(totalBytes, totalToPut, dataToRead, cookie) 149 | ) 150 | } 151 | val calculatedCrc = crcCalculator.finalize() 152 | if (expectedCrc != null && calculatedCrc != expectedCrc.toUInt()) { 153 | throw IllegalStateException( 154 | "Sending fail: Crc mismatch ($calculatedCrc != $expectedCrc)" 155 | ) 156 | } 157 | 158 | logger.d { "Sending commit" } 159 | send( 160 | PutBytesCommit(cookie, calculatedCrc) 161 | ) 162 | awaitAck() 163 | return cookie 164 | } catch (e: Error) { 165 | throw PutBytesException(lastCookie, "awaitCookieAndPutByteArray failed: ${e.message}", e) 166 | } 167 | } 168 | 169 | private suspend fun getResponse(): PutBytesResponse { 170 | return withTimeout(20_000) { 171 | val iterator = receivedMessages.iterator() 172 | if (!iterator.hasNext()) { 173 | throw IllegalStateException("Received messages channel is closed") 174 | } 175 | 176 | iterator.next() 177 | } 178 | } 179 | 180 | private suspend fun awaitAck(): PutBytesResponse { 181 | val response = getResponse() 182 | 183 | val result = response.result.get() 184 | if (result != PutBytesResult.ACK.value) { 185 | throw PutBytesException(lastCookie, "Watch responded with NACK ($result). Aborting transfer") 186 | } 187 | 188 | return response 189 | } 190 | 191 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/ScreenshotService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.ScreenshotRequest 5 | import io.rebble.libpebblecommon.packets.ScreenshotResponse 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import kotlinx.coroutines.channels.Channel 9 | 10 | class ScreenshotService(private val protocolHandler: ProtocolHandler) : ProtocolService { 11 | val receivedMessages = Channel(Channel.BUFFERED) 12 | 13 | init { 14 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.SCREENSHOT, this::receive) 15 | } 16 | 17 | suspend fun send(packet: ScreenshotRequest) { 18 | protocolHandler.send(packet) 19 | } 20 | 21 | fun receive(packet: PebblePacket) { 22 | if (packet !is ScreenshotResponse) { 23 | throw IllegalStateException("Received invalid packet type: $packet") 24 | } 25 | 26 | receivedMessages.trySend(packet) 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/SystemService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services 2 | 3 | import co.touchlab.kermit.Logger 4 | import io.rebble.libpebblecommon.PacketPriority 5 | import io.rebble.libpebblecommon.ProtocolHandler 6 | import io.rebble.libpebblecommon.packets.* 7 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 8 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 9 | import io.rebble.libpebblecommon.structmapper.SInt 10 | import io.rebble.libpebblecommon.structmapper.StructMapper 11 | import io.rebble.libpebblecommon.util.DataBuffer 12 | import kotlinx.coroutines.CompletableDeferred 13 | import kotlinx.coroutines.channels.Channel 14 | 15 | /** 16 | * Singleton to handle sending notifications cleanly, as well as TODO: receiving/acting on action events 17 | */ 18 | class SystemService(private val protocolHandler: ProtocolHandler) : ProtocolService { 19 | val receivedMessages = Channel(Channel.BUFFERED) 20 | public final var appVersionRequestHandler: (suspend () -> PhoneAppVersion.AppVersionResponse)? = null 21 | 22 | private var watchVersionCallback: CompletableDeferred? = null 23 | private var watchModelCallback: CompletableDeferred? = null 24 | private var firmwareUpdateStartResponseCallback: CompletableDeferred? = null 25 | 26 | init { 27 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.PHONE_VERSION, this::receive) 28 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.WATCH_VERSION, this::receive) 29 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.FCT_REG, this::receive) 30 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.SYSTEM_MESSAGE, this::receive) 31 | } 32 | 33 | /** 34 | * Send an AppMessage 35 | */ 36 | suspend fun send(packet: SystemPacket, priority: PacketPriority = PacketPriority.NORMAL) { 37 | protocolHandler.send(packet, priority) 38 | } 39 | 40 | suspend fun requestWatchVersion(): WatchVersion.WatchVersionResponse { 41 | val callback = CompletableDeferred() 42 | watchVersionCallback = callback 43 | 44 | send(WatchVersion.WatchVersionRequest()) 45 | 46 | return callback.await() 47 | } 48 | 49 | suspend fun requestWatchModel(): Int { 50 | val callback = CompletableDeferred() 51 | watchModelCallback = callback 52 | 53 | send(WatchFactoryData.WatchFactoryDataRequest("mfg_color")) 54 | 55 | val modelBytes = callback.await() 56 | 57 | return SInt(StructMapper()).also { it.fromBytes(DataBuffer(modelBytes)) }.get() 58 | } 59 | 60 | suspend fun firmwareUpdateStart(bytesAlreadyTransferred: UInt, bytesToSend: UInt): UByte { 61 | val callback = CompletableDeferred() 62 | firmwareUpdateStartResponseCallback = callback 63 | send(SystemMessage.FirmwareUpdateStart(bytesAlreadyTransferred, bytesToSend)) 64 | val response = callback.await() 65 | return response.response.get() 66 | } 67 | 68 | suspend fun receive(packet: PebblePacket) { 69 | if (packet !is SystemPacket) { 70 | throw IllegalStateException("Received invalid packet type: $packet") 71 | } 72 | 73 | when (packet) { 74 | is WatchVersion.WatchVersionResponse -> { 75 | watchVersionCallback?.complete(packet) 76 | watchVersionCallback = null 77 | } 78 | is WatchFactoryData.WatchFactoryDataResponse -> { 79 | watchModelCallback?.complete(packet.model.get()) 80 | watchModelCallback = null 81 | } 82 | is WatchFactoryData.WatchFactoryDataError -> { 83 | watchModelCallback?.completeExceptionally(Exception("Failed to fetch watch model")) 84 | watchModelCallback = null 85 | } 86 | is PhoneAppVersion.AppVersionRequest -> { 87 | val res = appVersionRequestHandler?.invoke() 88 | if (res != null) { 89 | send(res) // Cannot be low priority 90 | } 91 | } 92 | is SystemMessage.FirmwareUpdateStartResponse -> { 93 | firmwareUpdateStartResponseCallback?.complete(packet) 94 | firmwareUpdateStartResponseCallback = null 95 | } 96 | else -> receivedMessages.trySend(packet) 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/VoiceService.kt: -------------------------------------------------------------------------------- 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(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/services/app/AppRunStateService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.app 2 | 3 | import com.benasher44.uuid.Uuid 4 | import io.rebble.libpebblecommon.ProtocolHandler 5 | import io.rebble.libpebblecommon.packets.AppRunStateMessage 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import io.rebble.libpebblecommon.services.ProtocolService 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.flow.receiveAsFlow 12 | 13 | class AppRunStateService(private val protocolHandler: ProtocolHandler) : ProtocolService { 14 | val receivedMessages = Channel(Channel.BUFFERED) 15 | 16 | init { 17 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_RUN_STATE, this::receive) 18 | } 19 | 20 | suspend fun send(packet: AppRunStateMessage) { 21 | protocolHandler.send(packet) 22 | } 23 | 24 | fun receive(packet: PebblePacket) { 25 | if (packet !is AppRunStateMessage) { 26 | throw IllegalStateException("Received invalid packet type: $packet") 27 | } 28 | 29 | receivedMessages.trySend(packet) 30 | } 31 | 32 | suspend fun startApp(uuid: Uuid) { 33 | send(AppRunStateMessage.AppRunStateStart(uuid)) 34 | } 35 | 36 | suspend fun stopApp(uuid: Uuid) { 37 | send(AppRunStateMessage.AppRunStateStop(uuid)) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/appmessage/AppMessageService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.appmessage 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppIconMessage 5 | import io.rebble.libpebblecommon.packets.AppCustomizationSetStockAppTitleMessage 6 | import io.rebble.libpebblecommon.packets.AppMessage 7 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 8 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 9 | import io.rebble.libpebblecommon.services.ProtocolService 10 | import kotlinx.coroutines.channels.Channel 11 | 12 | class AppMessageService(private val protocolHandler: ProtocolHandler) : ProtocolService { 13 | val receivedMessages = Channel(Channel.BUFFERED) 14 | 15 | init { 16 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.APP_MESSAGE, this::receive) 17 | } 18 | 19 | /** 20 | * Send an AppMessage 21 | */ 22 | suspend fun send(packet: AppMessage) { 23 | protocolHandler.send(packet) 24 | } 25 | 26 | suspend fun send(packet: AppCustomizationSetStockAppIconMessage) { 27 | protocolHandler.send(packet) 28 | } 29 | 30 | suspend fun send(packet: AppCustomizationSetStockAppTitleMessage) { 31 | protocolHandler.send(packet) 32 | } 33 | 34 | fun receive(packet: PebblePacket) { 35 | if (packet !is AppMessage) { 36 | throw IllegalStateException("Received invalid packet type: $packet") 37 | } 38 | 39 | receivedMessages.trySend(packet) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/blobdb/BlobDBService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.blobdb 2 | 3 | import io.rebble.libpebblecommon.PacketPriority 4 | import io.rebble.libpebblecommon.ProtocolHandler 5 | import io.rebble.libpebblecommon.packets.blobdb.BlobCommand 6 | import io.rebble.libpebblecommon.packets.blobdb.BlobResponse 7 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 8 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 9 | import io.rebble.libpebblecommon.services.ProtocolService 10 | import kotlinx.coroutines.CompletableDeferred 11 | 12 | /** 13 | * Singleton to handle sending BlobDB commands cleanly, by allowing registered callbacks to be triggered when the sending packet receives a BlobResponse 14 | * @see BlobResponse 15 | */ 16 | class BlobDBService(private val protocolHandler: ProtocolHandler) : ProtocolService { 17 | private val pending: MutableMap> = mutableMapOf() 18 | 19 | init { 20 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.BLOBDB_V1, this::receive) 21 | } 22 | 23 | /** 24 | * Send a BlobCommand, with an optional callback to be triggered when a matching BlobResponse is received 25 | * @see BlobCommand 26 | * @see BlobResponse 27 | * @param packet the packet to send 28 | * 29 | * @return [BlobResponse] from the watch or *null* if the sending failed 30 | */ 31 | suspend fun send( 32 | packet: BlobCommand, 33 | priority: PacketPriority = PacketPriority.NORMAL 34 | ): BlobResponse { 35 | val result = CompletableDeferred() 36 | pending[packet.token.get()] = result 37 | 38 | val sendingResult = protocolHandler.send(packet, priority) 39 | if (!sendingResult) { 40 | return BlobResponse(BlobResponse.BlobStatus.WatchDisconnected) 41 | } 42 | 43 | return result.await() 44 | } 45 | 46 | /** 47 | * Intended to be called via a protocol handler, handles BlobResponse packets 48 | * @return true if the packet was handled, false if it wasn't (e.g. not sent via send()) 49 | * @see send 50 | * @see BlobResponse 51 | */ 52 | fun receive(packet: PebblePacket) { 53 | if (packet !is BlobResponse) { 54 | throw IllegalStateException("Received invalid packet type: $packet") 55 | } 56 | 57 | pending.remove(packet.token.get())?.complete(packet) 58 | } 59 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/blobdb/TimelineService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.blobdb 2 | 3 | import io.rebble.libpebblecommon.ProtocolHandler 4 | import io.rebble.libpebblecommon.packets.blobdb.TimelineAction 5 | import io.rebble.libpebblecommon.packets.blobdb.TimelineItem 6 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 7 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 8 | import io.rebble.libpebblecommon.services.ProtocolService 9 | 10 | /** 11 | * Singleton that handles receiving of timeline actions. 12 | * 13 | * Consumer must set [actionHandler] that will then be called whenever user triggers timeline pin 14 | * action. 15 | */ 16 | class TimelineService(private val protocolHandler: ProtocolHandler) : ProtocolService { 17 | var actionHandler: (suspend (TimelineAction.InvokeAction) -> ActionResponse)? = null 18 | 19 | init { 20 | protocolHandler.registerReceiveCallback(ProtocolEndpoint.TIMELINE_ACTIONS, this::receive) 21 | } 22 | 23 | 24 | suspend fun receive(packet: PebblePacket) { 25 | if (packet !is TimelineAction.InvokeAction) { 26 | throw IllegalStateException("Received invalid packet type: $packet") 27 | } 28 | 29 | val result = actionHandler?.invoke(packet) ?: return 30 | 31 | val returnPacket = TimelineAction.ActionResponse().apply { 32 | itemID.set(packet.itemID.get()) 33 | response.set(if (result.success) 0u else 1u) 34 | numAttributes.set(result.attributes.size.toUByte()) 35 | attributes.list = result.attributes 36 | } 37 | 38 | protocolHandler.send(returnPacket) 39 | } 40 | 41 | class ActionResponse( 42 | val success: Boolean, 43 | val attributes: List = emptyList() 44 | ) 45 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/services/notification/NotificationService.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.notification 2 | 3 | import io.rebble.libpebblecommon.packets.blobdb.BlobResponse 4 | import io.rebble.libpebblecommon.packets.blobdb.PushNotification 5 | import io.rebble.libpebblecommon.services.ProtocolService 6 | import io.rebble.libpebblecommon.services.blobdb.BlobDBService 7 | import kotlinx.coroutines.delay 8 | import kotlin.random.Random 9 | 10 | /** 11 | * Singleton to handle sending notifications cleanly, as well as TODO: receiving/acting on action events 12 | */ 13 | class NotificationService(private val blobDbService: BlobDBService) : ProtocolService { 14 | 15 | /** 16 | * Send a PushNotification command 17 | * @param notif the notification to send 18 | * @see PushNotification 19 | * 20 | * @return notification [BlobResponse] from the watch or *null* if sending failed 21 | */ 22 | suspend fun send(notif: PushNotification): BlobResponse? { 23 | while (true) { 24 | val res = blobDbService.send(notif) 25 | if (res is BlobResponse.TryLater) { 26 | // Device pushed it back, let's change the token and do an async delayed send 27 | notif.token.set(Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort()) 28 | delay(100) 29 | } else { 30 | return res 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/StructMappable.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.structmapper 2 | 3 | import io.rebble.libpebblecommon.util.DataBuffer 4 | import io.rebble.libpebblecommon.util.Endian 5 | 6 | abstract class StructMappable(endianness: Endian = Endian.Unspecified) : Mappable(endianness) { 7 | val m = StructMapper(endianness = endianness, debugTag = this::class.simpleName) 8 | 9 | override fun toBytes(): UByteArray { 10 | return m.toBytes() 11 | } 12 | 13 | override fun fromBytes(bytes: DataBuffer) { 14 | m.fromBytes(bytes) 15 | } 16 | 17 | override val size get() = m.size 18 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/structmapper/StructMapper.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.structmapper 2 | 3 | import io.rebble.libpebblecommon.exceptions.PacketDecodeException 4 | import io.rebble.libpebblecommon.util.DataBuffer 5 | import io.rebble.libpebblecommon.util.Endian 6 | 7 | /** 8 | * Maps class properties to a struct equivalent 9 | */ 10 | class StructMapper(endianness: Endian = Endian.Unspecified, private val debugTag: String? = null): Mappable(endianness) { 11 | private var struct: MutableList = mutableListOf() 12 | 13 | /** 14 | * Register a mappable object with the StructMapper (tracks index + auto serialization based on declaration order) 15 | * Ideally only use this within mappable type definitions 16 | */ 17 | fun register(type: Mappable): Int { 18 | struct.add(type) 19 | return struct.size - 1 20 | } 21 | 22 | /** 23 | * Get the struct as a list of Mappables 24 | * @return The list of mappables 25 | */ 26 | fun getStruct(): List { 27 | return struct.toList() 28 | } 29 | 30 | override fun toBytes(): UByteArray { 31 | var bytes = ubyteArrayOf() 32 | getStruct().forEach { 33 | bytes += it.toBytes() 34 | } 35 | return bytes 36 | } 37 | 38 | override fun fromBytes(bytes: DataBuffer) { 39 | getStruct().forEachIndexed { i: Int, mappable: Mappable -> 40 | try { 41 | mappable.fromBytes(bytes) 42 | }catch (e: Exception) { 43 | throw PacketDecodeException("Unable to deserialize mappable ${mappable::class.simpleName} at index $i (${mappable}) ($debugTag)\n${bytes.array().toHexString()}", e) 44 | } 45 | 46 | } 47 | } 48 | 49 | override val size: Int 50 | get() = getStruct().fold(0, {t,el -> t+el.size}) 51 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | expect class Bitmap { 4 | val width: Int 5 | val height: Int 6 | 7 | /** 8 | * Return pixel at the specified position at in AARRGGBB format. 9 | */ 10 | fun getPixel(x: Int, y: Int): Int 11 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/Crc32Calculator.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | /** 4 | * CRC32 hash Calculator that is compatible with hardware CRC32 on the STM chips. 5 | */ 6 | class Crc32Calculator { 7 | private var finalized = false 8 | private var value: UInt = 0xFFFFFFFFu 9 | 10 | private var leftoverBytes = UByteArray(0) 11 | 12 | fun addBytes(bytes: UByteArray) { 13 | if (finalized) { 14 | throw IllegalStateException("Cannot add more bytes to finalized CRC calculation") 15 | } 16 | 17 | val mergedArray = leftoverBytes + bytes 18 | val buffer = DataBuffer(mergedArray) 19 | buffer.setEndian(Endian.Little) 20 | 21 | val finalPosition = mergedArray.size - mergedArray.size % 4 22 | while (buffer.readPosition < finalPosition) { 23 | addInt(buffer.getUInt()) 24 | } 25 | 26 | leftoverBytes = mergedArray.copyOfRange(finalPosition, mergedArray.size) 27 | } 28 | 29 | /** 30 | * Finalizes the calculation and returns the CRC32 result 31 | */ 32 | fun finalize(): UInt { 33 | if (finalized) { 34 | return value 35 | } 36 | 37 | if (leftoverBytes.isNotEmpty()) { 38 | leftoverBytes = leftoverBytes.padZerosLeft(4 - leftoverBytes.size).reversedArray() 39 | addInt(DataBuffer(leftoverBytes).apply { setEndian(Endian.Little) }.getUInt()) 40 | } 41 | 42 | finalized = true 43 | return value 44 | } 45 | 46 | private fun addInt(valueToAdd: UInt) { 47 | this.value = this.value xor valueToAdd 48 | 49 | for (i in 0 until 32) { 50 | if ((this.value and 0x80000000u) != 0u) { 51 | this.value = (this.value shl 1) xor 0x04C11DB7u 52 | } else { 53 | this.value = this.value shl 1 54 | } 55 | } 56 | 57 | this.value = this.value and 0xFFFFFFFFu 58 | } 59 | } 60 | 61 | private fun UByteArray.padZerosLeft(amount: Int): UByteArray { 62 | return UByteArray(amount) { 0u } + this 63 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/DataBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | /** 4 | * Common DataBuffer with bindings for each platform 5 | */ 6 | expect class DataBuffer { 7 | constructor(size: Int) 8 | constructor(bytes: UByteArray) 9 | 10 | fun putUShort(short: UShort) 11 | fun getUShort(): UShort 12 | 13 | fun putShort(short: Short) 14 | fun getShort(): Short 15 | 16 | fun putUByte(byte: UByte) 17 | fun getUByte(): UByte 18 | 19 | fun putByte(byte: Byte) 20 | fun getByte(): Byte 21 | 22 | fun putBytes(bytes: UByteArray) 23 | fun getBytes(count: Int): UByteArray 24 | 25 | fun putUInt(uint: UInt) 26 | fun getUInt(): UInt 27 | 28 | fun putInt(int: Int) 29 | fun getInt(): Int 30 | 31 | fun putULong(ulong: ULong) 32 | fun getULong(): ULong 33 | 34 | fun array(): UByteArray 35 | 36 | fun setEndian(endian: Endian) 37 | 38 | fun rewind() 39 | 40 | /** 41 | * Total length of the buffer 42 | */ 43 | val length: Int 44 | 45 | /** 46 | * Current position in the buffer 47 | */ 48 | val readPosition: Int 49 | 50 | /** 51 | * Remaining bytes in the buffer 52 | */ 53 | val remaining: Int 54 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/Endian.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | enum class Endian { 4 | Big, 5 | Little, 6 | Unspecified 7 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/LazyLock.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import kotlinx.coroutines.delay 4 | 5 | /** 6 | * Lock specific functions until related async function is complete 7 | * Use sparingly, intended for asynchronously retrying but blocking completely new attempts while we do so 8 | */ 9 | class LazyLock { 10 | var isLocked = false 11 | 12 | /** 13 | * Causes syncLock to block the thread / coroutine it is called on until unlock() used 14 | * @see syncLock 15 | * @see unlock 16 | */ 17 | fun lock() {isLocked = true} 18 | 19 | /** 20 | * Releases any currently blocking syncLocks of this object 21 | * @see syncLock 22 | */ 23 | fun unlock() {isLocked = false} 24 | 25 | /** 26 | * If locked, blocks the thread / coroutine until unlock is called 27 | * @see lock 28 | * @see unlock 29 | */ 30 | fun syncLock(timeout: Long) { 31 | var timer: Long = 0 32 | while (isLocked && timer*10 < timeout) {timer++; runBlocking { delay(10)}} 33 | } 34 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/PacketSize.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import io.rebble.libpebblecommon.packets.ProtocolCapsFlag 4 | import io.rebble.libpebblecommon.packets.WatchVersion 5 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 6 | 7 | fun getMaxPebblePacketPayloadSize( 8 | endpoint: ProtocolEndpoint, 9 | watchVersion: WatchVersion.WatchVersionResponse? 10 | ): Int { 11 | if (endpoint != ProtocolEndpoint.APP_MESSAGE) { 12 | return STANDARD_MAX_PEBBLE_PACKET_SIZE 13 | } 14 | 15 | val capabilities = watchVersion?.capabilities?.let { ProtocolCapsFlag.fromFlags(it.get()) } 16 | 17 | return if (capabilities?.contains(ProtocolCapsFlag.Supports8kAppMessage) == true) { 18 | 8222 19 | } else { 20 | STANDARD_MAX_PEBBLE_PACKET_SIZE 21 | } 22 | } 23 | 24 | fun getPutBytesMaximumDataSize(watchVersion: WatchVersion.WatchVersionResponse?): Int { 25 | // 4 bytes get used for the cookie 26 | return getMaxPebblePacketPayloadSize(ProtocolEndpoint.PUT_BYTES, watchVersion) - 4 27 | } 28 | 29 | val STANDARD_MAX_PEBBLE_PACKET_SIZE = 2048 30 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/PebbleColor.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | /** 4 | * Represents an ARGB8888 color, which is converted to an ARGB2222 color for the Pebble 5 | */ 6 | data class PebbleColor( 7 | val alpha: UByte, 8 | val red: UByte, 9 | val green: UByte, 10 | val blue: UByte 11 | ) 12 | 13 | fun PebbleColor.toProtocolNumber() = 14 | (((alpha / 85u) shl 6) or 15 | ((red / 85u) shl 4) or 16 | ((green / 85u) shl 2) or 17 | (blue / 85u)).toUByte() 18 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/SerializationUtil.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo 4 | import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest 5 | import kotlinx.serialization.SerializationException 6 | import kotlinx.serialization.decodeFromString 7 | import kotlinx.serialization.encodeToString 8 | import kotlinx.serialization.json.Json 9 | 10 | object SerializationUtil { 11 | private val json = Json { ignoreUnknownKeys = true } 12 | @Throws(SerializationException::class) 13 | fun serializeAppInfo(appInfo: PbwAppInfo): String = json.encodeToString(appInfo) 14 | @Throws(SerializationException::class) 15 | fun deserializeAppInfo(jsonString: String): PbwAppInfo = json.decodeFromString(jsonString) 16 | 17 | @Throws(SerializationException::class) 18 | fun serializeManifest(manifest: PbwManifest): String = json.encodeToString(manifest) 19 | @Throws(SerializationException::class) 20 | fun deserializeManifest(jsonString: String): PbwManifest = json.decodeFromString(jsonString) 21 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/rebble/libpebblecommon/util/UtilFunctions.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | expect fun runBlocking(block: suspend () -> Unit) 4 | 5 | object KUtil { 6 | fun byteArrayAsUByteArray(arr: ByteArray): UByteArray = arr.asUByteArray() 7 | fun uByteArrayAsByteArray(arr: UByteArray): ByteArray = arr.asByteArray() 8 | } 9 | 10 | infix fun Byte.ushr(bitCount: Int): Byte = ((this.toInt()) ushr bitCount).toByte() 11 | infix fun Byte.shl(bitCount: Int): Byte = ((this.toInt()) shl bitCount).toByte() 12 | infix fun UByte.shr(bitCount: Int): UByte = ((this.toUInt()) shr bitCount).toUByte() 13 | infix fun UByte.shl(bitCount: Int): UByte = ((this.toUInt()) shl bitCount).toUByte() 14 | 15 | infix fun Short.shl(bitCount: Int): Short = ((this.toInt()) shl bitCount).toShort() 16 | infix fun UShort.shl(bitCount: Int): UShort = ((this.toUInt()) shl bitCount).toUShort() 17 | 18 | fun String.encodeToByteArrayTrimmed(maxBytes: Int): ByteArray { 19 | check(maxBytes >= 2) { 20 | "maxBytes must be at least 2 to fit ellipsis character. Got $maxBytes instead" 21 | } 22 | 23 | val encodedOriginal = encodeToByteArray() 24 | if (encodedOriginal.size <= maxBytes) { 25 | return encodedOriginal 26 | } 27 | 28 | var trimmedString = take(maxBytes - 1) 29 | var encoded = "$trimmedString…".encodeToByteArray() 30 | 31 | while (encoded.size > maxBytes) { 32 | trimmedString = trimmedString.take(trimmedString.length - 1) 33 | encoded = "$trimmedString…".encodeToByteArray() 34 | } 35 | 36 | return encoded 37 | } 38 | 39 | fun String.trimWithEllipsis(maxLength: Int): String { 40 | if (length <= maxLength) { 41 | return this 42 | } 43 | return take(maxLength - 1) + "…" 44 | } 45 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/TestProtocolHandler.kt: -------------------------------------------------------------------------------- 1 | import io.rebble.libpebblecommon.PacketPriority 2 | import io.rebble.libpebblecommon.ProtocolHandler 3 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 4 | import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint 5 | 6 | /** 7 | * Test double of the protocol handler. 8 | * 9 | * If you specify [sender] parameter, it will be called whenever packet is supposed to be sent. 10 | * Otherwise outgoing packets collect in the [sentPackets] list. 11 | * 12 | * You can manually receive packets by calling [receivePacket]. 13 | */ 14 | class TestProtocolHandler(private val sender: (suspend TestProtocolHandler.(PebblePacket) -> Unit)? = null) : 15 | ProtocolHandler { 16 | val sentPackets = ArrayList() 17 | 18 | private val receiveRegistry = HashMap Unit>() 19 | 20 | 21 | override suspend fun send(packet: PebblePacket, priority: PacketPriority): Boolean { 22 | if (sender != null) { 23 | sender.invoke(this, packet) 24 | } else { 25 | sentPackets += packet 26 | } 27 | 28 | return true 29 | } 30 | 31 | override suspend fun send(packetData: UByteArray, priority: PacketPriority): Boolean { 32 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 33 | } 34 | 35 | override suspend fun startPacketSendingLoop(rawSend: suspend (UByteArray) -> Boolean) { 36 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 37 | } 38 | 39 | override suspend fun openProtocol() { 40 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 41 | } 42 | 43 | override suspend fun closeProtocol() { 44 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 45 | } 46 | 47 | override suspend fun waitForNextPacket(): ProtocolHandler.PendingPacket { 48 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 49 | } 50 | 51 | override suspend fun getNextPacketOrNull(): ProtocolHandler.PendingPacket? { 52 | throw UnsupportedOperationException("Not supported for TestProtocolHandler") 53 | } 54 | 55 | override suspend fun receivePacket(bytes: UByteArray): Boolean { 56 | return receivePacket(PebblePacket.deserialize(bytes)) 57 | } 58 | 59 | 60 | override fun registerReceiveCallback( 61 | endpoint: ProtocolEndpoint, 62 | callback: suspend (PebblePacket) -> Unit 63 | ) { 64 | val existingCallback = receiveRegistry.put(endpoint, callback) 65 | if (existingCallback != null) { 66 | throw IllegalStateException( 67 | "Duplicate callback registered for $endpoint: $callback, $existingCallback" 68 | ) 69 | } 70 | } 71 | 72 | /** 73 | * Handle pebble packet 74 | */ 75 | suspend fun receivePacket(packet: PebblePacket): Boolean { 76 | val receiveCallback = receiveRegistry[packet.endpoint] 77 | if (receiveCallback == null) { 78 | throw IllegalStateException("${packet.endpoint} does not have receive callback") 79 | } else { 80 | receiveCallback.invoke(packet) 81 | } 82 | 83 | return true 84 | } 85 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/TestUtils.kt: -------------------------------------------------------------------------------- 1 | import kotlin.contracts.ExperimentalContracts 2 | import kotlin.contracts.contract 3 | import kotlin.test.assertEquals 4 | import kotlin.test.assertTrue 5 | 6 | fun assertUByteArrayEquals(expected: UByteArray, actual: UByteArray) { 7 | try { 8 | assertTrue( 9 | expected.contentEquals(actual) 10 | ) 11 | } catch (e: AssertionError) { 12 | // Print prettier error message 13 | assertEquals( 14 | expected.contentToString(), actual.contentToString() 15 | ) 16 | 17 | // rethrow original error just in case strings somehow match 18 | throw e 19 | } 20 | } 21 | 22 | @OptIn(ExperimentalContracts::class) 23 | inline fun assertIs(obj: Any) { 24 | contract { 25 | returns() implies (obj is T) 26 | } 27 | assertTrue( 28 | obj is T, 29 | "$obj should be ${T::class.simpleName}" 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/Tests.kt: -------------------------------------------------------------------------------- 1 | import io.rebble.libpebblecommon.packets.PingPong 2 | import io.rebble.libpebblecommon.packets.QemuPacket 3 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class Tests { 8 | private fun bytesToHex(bytes: UByteArray): String { 9 | val hexArray = "0123456789ABCDEF".toCharArray() 10 | val hexChars = CharArray(bytes.size * 2) 11 | for (j in bytes.indices) { 12 | val v = (bytes[j] and 0xFFu).toInt() 13 | 14 | hexChars[j * 2] = hexArray[v ushr 4] 15 | hexChars[j * 2 + 1] = hexArray[v and 0x0F] 16 | } 17 | return hexChars.concatToString() 18 | } 19 | 20 | @Test 21 | fun serializeSimplePacket() { 22 | val packet: PebblePacket = PingPong.Ping(51966u) 23 | 24 | assertEquals(bytesToHex(ubyteArrayOf(0x00u,0x05u,0x07u,0xD1u,0x00u,0x00u,0x00u,0xCAu,0xFEu)), bytesToHex(packet.serialize()), 25 | "Serialized big-endian packet invalid") // [short1,short2],[short1,short2],[byte],[uint1,uint2,uint3,uint4] 26 | } 27 | 28 | @Test 29 | fun deserializeSimplePacket() { 30 | val expect = ubyteArrayOf(0x00u,0x05u,0x07u,0xD1u,0x00u,0x00u,0x00u,0xCAu,0xFEu) 31 | 32 | val bytes = byteArrayOf(0x00,0x05,0x07, 0xD1.toByte(),0x00,0x00,0x00, 0xCA.toByte(), 0xFE.toByte()) 33 | val packet = PebblePacket.deserialize(bytes.toUByteArray()) 34 | 35 | assertEquals(bytesToHex(expect), bytesToHex(packet.serialize())) 36 | } 37 | 38 | @Test 39 | fun serializeQemuPacket() { 40 | val packet = QemuPacket.QemuSPP(ubyteArrayOf(0xCAu,0xFEu,0x10u)) 41 | assertUByteArrayEquals(ubyteArrayOf(0xFEu, 0xEDu, 0x00u, 0x01u, 0x00u, 0x03u, 0xCAu, 0xFEu, 0x10u, 0xBEu, 0xEFu), packet.serialize()) 42 | } 43 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/metadata/pbw/appinfo/TestAppInfo.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.appinfo 2 | 3 | import kotlinx.serialization.decodeFromString 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.Json 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import io.rebble.libpebblecommon.metadata.StringOrBoolean 9 | 10 | class TestAppInfo { 11 | companion object { 12 | const val APP_INFO_JSON_SIMPLICITY = "{\"versionLabel\": \"3.2\", \"companyName\": \"Pebble Technology\", \"targetPlatforms\": [\"aplite\", \"basalt\", \"chalk\"], \"resources\": {\"media\": [{\"name\": \"IMAGE_MENU_ICON\", \"type\": \"png\", \"file\": \"images/menu_icon_simplicity.png\", \"menuIcon\": true}]}, \"sdkVersion\": \"3\", \"longName\": \"Simplicity\", \"watchapp\": {\"watchface\": true}, \"projectType\": \"native\", \"capabilities\": [\"\"], \"uuid\": \"54eada19-67fd-4947-a7c5-256f24e3a7d7\", \"shortName\": \"Simplicity\", \"appKeys\": {}}" 13 | const val APP_INFO_JSON_DIALER_FOR_PEBBLE = "{\"targetPlatforms\":[\"aplite\",\"basalt\",\"chalk\"],\"projectType\":\"native\",\"messageKeys\":{},\"companyName\":\"matejdro\",\"enableMultiJS\":false,\"watchapp\":{\"onlyShownOnCommunication\":false,\"hiddenApp\":false,\"watchface\":false},\"versionLabel\":\"3.3\",\"longName\":\"Dialer\",\"shortName\":\"Dialer\",\"name\":\"Dialer\",\"sdkVersion\":\"3\",\"displayName\":\"Dialer\",\"uuid\":\"158a074d-85ce-43d2-ab7d-14416ddc1058\",\"appKeys\":{},\"capabilities\":[],\"resources\":{\"media\":[{\"menuIcon\":\"true\",\"type\":\"bitmap\",\"name\":\"ICON\",\"file\":\"icon.png\"},{\"type\":\"bitmap\",\"name\":\"ANSWER\",\"file\":\"answer.png\"},{\"type\":\"bitmap\",\"name\":\"ENDCALL\",\"file\":\"endcall.png\"},{\"type\":\"bitmap\",\"name\":\"MIC_OFF\",\"file\":\"mic_off.png\"},{\"type\":\"bitmap\",\"name\":\"MIC_ON\",\"file\":\"micon.png\"},{\"type\":\"bitmap\",\"name\":\"SPEAKER_ON\",\"file\":\"speakeron.png\"},{\"type\":\"bitmap\",\"name\":\"SPEAKER_OFF\",\"file\":\"speakeroff.png\"},{\"type\":\"bitmap\",\"name\":\"VOLUME_UP\",\"file\":\"volumeup.png\"},{\"type\":\"bitmap\",\"name\":\"VOLUME_DOWN\",\"file\":\"volumedown.png\"},{\"type\":\"bitmap\",\"name\":\"CALL_HISTORY\",\"file\":\"callhistory.png\"},{\"type\":\"bitmap\",\"name\":\"CONTACTS\",\"file\":\"contacts.png\"},{\"type\":\"bitmap\",\"name\":\"CONTACT_GROUP\",\"file\":\"contactgroup.png\"},{\"type\":\"bitmap\",\"name\":\"INCOMING_CALL\",\"file\":\"incomingcall.png\"},{\"type\":\"bitmap\",\"name\":\"OUTGOING_CALL\",\"file\":\"outgoingcall.png\"},{\"type\":\"bitmap\",\"name\":\"MISSED_CALL\",\"file\":\"missedcall.png\"},{\"type\":\"bitmap\",\"name\":\"MESSAGE\",\"file\":\"message.png\"},{\"type\":\"bitmap\",\"name\":\"CALL\",\"file\":\"call.png\"}]}}" 14 | 15 | val APP_INFO_OBJ_SIMPLICITY = PbwAppInfo( 16 | uuid = "54eada19-67fd-4947-a7c5-256f24e3a7d7", 17 | shortName = "Simplicity", 18 | longName = "Simplicity", 19 | companyName = "Pebble Technology", 20 | versionLabel = "3.2", 21 | capabilities = listOf(""), 22 | resources = Resources( 23 | media = listOf( 24 | Media( 25 | resourceFile = "images/menu_icon_simplicity.png", 26 | menuIcon = StringOrBoolean(true), 27 | name = "IMAGE_MENU_ICON", 28 | type = "png" 29 | ) 30 | ) 31 | ), 32 | sdkVersion = "3", 33 | targetPlatforms = listOf("aplite", "basalt", "chalk"), 34 | watchapp = Watchapp( 35 | watchface = true 36 | ) 37 | ) 38 | 39 | val APP_INFO_OBJ_DIALER_FOR_PEBBLE = PbwAppInfo( 40 | uuid = "158a074d-85ce-43d2-ab7d-14416ddc1058", 41 | shortName = "Dialer", 42 | longName = "Dialer", 43 | companyName = "matejdro", 44 | versionLabel = "3.3", 45 | resources = Resources( 46 | media = listOf( 47 | Media( 48 | resourceFile = "icon.png", 49 | menuIcon = StringOrBoolean(true), 50 | name = "ICON", 51 | type = "bitmap" 52 | ), 53 | Media( 54 | resourceFile = "answer.png", 55 | name = "ANSWER", 56 | type = "bitmap" 57 | ), 58 | Media( 59 | resourceFile = "endcall.png", 60 | name = "ENDCALL", 61 | type = "bitmap" 62 | ), 63 | Media( 64 | resourceFile = "mic_off.png", 65 | name = "MIC_OFF", 66 | type = "bitmap" 67 | ), 68 | Media( 69 | resourceFile = "micon.png", 70 | name = "MIC_ON", 71 | type = "bitmap" 72 | ), 73 | Media( 74 | resourceFile = "speakeron.png", 75 | name = "SPEAKER_ON", 76 | type = "bitmap" 77 | ), 78 | Media( 79 | resourceFile = "speakeroff.png", 80 | name = "SPEAKER_OFF", 81 | type = "bitmap" 82 | ), 83 | Media( 84 | resourceFile = "volumeup.png", 85 | name = "VOLUME_UP", 86 | type = "bitmap" 87 | ), 88 | Media( 89 | resourceFile = "volumedown.png", 90 | name = "VOLUME_DOWN", 91 | type = "bitmap" 92 | ), 93 | Media( 94 | resourceFile = "callhistory.png", 95 | name = "CALL_HISTORY", 96 | type = "bitmap" 97 | ), 98 | Media( 99 | resourceFile = "contacts.png", 100 | name = "CONTACTS", 101 | type = "bitmap" 102 | ), 103 | Media( 104 | resourceFile = "contactgroup.png", 105 | name = "CONTACT_GROUP", 106 | type = "bitmap" 107 | ), 108 | Media( 109 | resourceFile = "incomingcall.png", 110 | name = "INCOMING_CALL", 111 | type = "bitmap" 112 | ), 113 | Media( 114 | resourceFile = "outgoingcall.png", 115 | name = "OUTGOING_CALL", 116 | type = "bitmap" 117 | ), 118 | Media( 119 | resourceFile = "missedcall.png", 120 | name = "MISSED_CALL", 121 | type = "bitmap" 122 | ), 123 | Media( 124 | resourceFile = "message.png", 125 | name = "MESSAGE", 126 | type = "bitmap" 127 | ), 128 | Media( 129 | resourceFile = "call.png", 130 | name = "CALL", 131 | type = "bitmap" 132 | ) 133 | ) 134 | ), 135 | sdkVersion = "3", 136 | targetPlatforms = listOf("aplite", "basalt", "chalk"), 137 | watchapp = Watchapp() 138 | ) 139 | } 140 | @Test 141 | fun deserialization() { 142 | val json = Json{ ignoreUnknownKeys = true } 143 | val simplicity: PbwAppInfo = json.decodeFromString(APP_INFO_JSON_SIMPLICITY) 144 | assertEquals(APP_INFO_OBJ_SIMPLICITY, simplicity) 145 | 146 | val dialerForPebble: PbwAppInfo = json.decodeFromString(APP_INFO_JSON_DIALER_FOR_PEBBLE) 147 | assertEquals(APP_INFO_OBJ_DIALER_FOR_PEBBLE, dialerForPebble) 148 | } 149 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/metadata/pbw/manifest/TestManifest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.metadata.pbw.manifest 2 | 3 | import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo 4 | import io.rebble.libpebblecommon.metadata.pbw.appinfo.TestAppInfo 5 | import kotlinx.serialization.decodeFromString 6 | import kotlinx.serialization.json.Json 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class TestManifest { 11 | companion object { 12 | const val MANIFEST_JSON_SIMPLICITY_V1 = "{\"manifestVersion\": 1, \"generatedBy\": \"46e8a544-50dc-4610-aa27-d86115f76f11\", \"generatedAt\": 1449884654, \"application\": {\"timestamp\": 1449884653, \"sdk_version\": {\"major\": 5, \"minor\": 19}, \"crc\": 893549736, \"name\": \"pebble-app.bin\", \"size\": 1339}, \"debug\": {}, \"type\": \"application\", \"resources\": {\"timestamp\": 1449884653, \"crc\": 3436670150, \"name\": \"app_resources.pbpack\", \"size\": 4232}}" 13 | const val MANIFEST_JSON_SIMPLICITY = "{\"manifestVersion\": 2, \"generatedBy\": \"46e8a544-50dc-4610-aa27-d86115f76f11\", \"generatedAt\": 1449884654, \"application\": {\"timestamp\": 1449884653, \"sdk_version\": {\"major\": 5, \"minor\": 72}, \"crc\": 266802728, \"name\": \"pebble-app.bin\", \"size\": 1363}, \"debug\": {}, \"app_layouts\": \"layouts.json\", \"type\": \"application\", \"resources\": {\"timestamp\": 1449884653, \"crc\": 3168848230, \"name\": \"app_resources.pbpack\", \"size\": 4218}}" 14 | 15 | val MANIFEST_OBJ_SIMPLICITY_V1 = PbwManifest( 16 | application = PbwBlob( 17 | crc = 893549736, 18 | name = "pebble-app.bin", 19 | sdkVersion = SdkVersion( 20 | major = 5, 21 | minor = 19 22 | ), 23 | size = 1339, 24 | timestamp = 1449884653 25 | ), 26 | resources = PbwBlob( 27 | crc = 3436670150, 28 | name = "app_resources.pbpack", 29 | size = 4232, 30 | timestamp = 1449884653 31 | ), 32 | debug = Debug(), 33 | generatedAt = 1449884654, 34 | generatedBy = "46e8a544-50dc-4610-aa27-d86115f76f11", 35 | manifestVersion = 1, 36 | type = "application" 37 | ) 38 | } 39 | 40 | @Test 41 | fun deserialization() { 42 | val json = Json{ ignoreUnknownKeys = true } 43 | val simplicityv1: PbwManifest = json.decodeFromString(MANIFEST_JSON_SIMPLICITY_V1) 44 | assertEquals(MANIFEST_OBJ_SIMPLICITY_V1, simplicityv1) 45 | 46 | //TODO: v2 manifest 47 | } 48 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/packets/AppMessageTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import assertIs 4 | import assertUByteArrayEquals 5 | import com.benasher44.uuid.Uuid 6 | import com.benasher44.uuid.uuidFrom 7 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | internal class AppMessageTest { 12 | @Test 13 | fun serializeDeserializePushMessage() { 14 | val testPushMessage = AppMessage.AppMessagePush( 15 | 5u, 16 | uuidFrom("30880933-cead-49f6-ba94-3a6f8cd3218a"), 17 | listOf( 18 | AppMessageTuple.createUByteArray(77u, ubyteArrayOf(1u, 170u, 245u)), 19 | AppMessageTuple.createString(6710u, "Hello World"), 20 | AppMessageTuple.createString(7710u, "Emoji: \uD83D\uDC7D."), 21 | AppMessageTuple.createByte(38485u, -7), 22 | AppMessageTuple.createUByte(2130680u, 177u.toUByte()), 23 | AppMessageTuple.createShort(2845647u, -20), 24 | AppMessageTuple.createUShort(2845648u, 49885u.toUShort()), 25 | AppMessageTuple.createInt(2845649u, -707573), 26 | AppMessageTuple.createUInt(2845650u, 2448461u) 27 | ) 28 | ) 29 | 30 | val bytes = testPushMessage.serialize() 31 | val newMessage = PebblePacket.deserialize(bytes) 32 | assertIs(newMessage) 33 | 34 | val list = newMessage.dictionary.list 35 | 36 | assertEquals(testPushMessage.dictionary.list.size, list.size) 37 | 38 | assertUByteArrayEquals(ubyteArrayOf(1u, 170u, 245u), list[0].dataAsBytes) 39 | assertEquals("Hello World", list[1].dataAsString) 40 | assertEquals("Emoji: \uD83D\uDC7D.", list[2].dataAsString) 41 | assertEquals(-7, list[3].dataAsSignedNumber) 42 | assertEquals(177, list[4].dataAsUnsignedNumber) 43 | assertEquals(-20, list[5].dataAsSignedNumber) 44 | assertEquals(49885, list[6].dataAsUnsignedNumber) 45 | assertEquals(-707573, list[7].dataAsSignedNumber) 46 | assertEquals(2448461, list[8].dataAsUnsignedNumber) 47 | 48 | assertEquals(testPushMessage, newMessage) 49 | } 50 | 51 | @Test 52 | fun serializeDeserializeAckMessage() { 53 | val testPushMessage = AppMessage.AppMessageACK( 54 | 74u 55 | ) 56 | 57 | val bytes = testPushMessage.serialize() 58 | val newMessage = PebblePacket.deserialize(bytes) 59 | assertIs(newMessage) 60 | 61 | assertEquals(74u, newMessage.transactionId.get()) 62 | } 63 | 64 | @Test 65 | fun serializeDeserializeNackMessage() { 66 | val testPushMessage = AppMessage.AppMessageNACK( 67 | 244u 68 | ) 69 | 70 | val bytes = testPushMessage.serialize() 71 | val newMessage = PebblePacket.deserialize(bytes) 72 | assertIs(newMessage) 73 | 74 | assertEquals(244u, newMessage.transactionId.get()) 75 | } 76 | 77 | @Test 78 | fun appMessageShortShouldBeLittleEndian() { 79 | val testPushMessage = AppMessage.AppMessagePush( 80 | 0u, 81 | Uuid(0L, 0L), 82 | listOf( 83 | AppMessageTuple.createShort(0u, -50), 84 | ) 85 | ) 86 | 87 | val expectedMessage = ubyteArrayOf( 88 | 1u, 0u, 0u, 0u, 89 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 90 | 0u, 0u, 0u, 0u, 91 | 0u, 1u, 92 | 0u, 0u, 0u, 0u, 93 | 3u, 94 | 2u, 0u, 95 | 206u, 255u 96 | ) 97 | 98 | assertUByteArrayEquals( 99 | expectedMessage, 100 | testPushMessage.m.toBytes() 101 | ) 102 | } 103 | 104 | @Test 105 | fun appMessageUShortShouldBeLittleEndian() { 106 | val testPushMessage = AppMessage.AppMessagePush( 107 | 0u, 108 | Uuid(0L, 0L), 109 | listOf( 110 | AppMessageTuple.createUShort(0u, 4876u), 111 | ) 112 | ) 113 | 114 | val expectedMessage = ubyteArrayOf( 115 | 1u, 0u, 0u, 0u, 116 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 117 | 0u, 0u, 0u, 0u, 118 | 0u, 1u, 119 | 0u, 0u, 0u, 0u, 120 | 2u, 121 | 2u, 0u, 122 | 12u, 19u 123 | ) 124 | 125 | assertUByteArrayEquals( 126 | expectedMessage, 127 | testPushMessage.m.toBytes() 128 | ) 129 | } 130 | 131 | @Test 132 | fun appMessageIntShouldBeLittleEndian() { 133 | val testPushMessage = AppMessage.AppMessagePush( 134 | 0u, 135 | Uuid(0L, 0L), 136 | listOf( 137 | AppMessageTuple.createInt(0u, -90000), 138 | ) 139 | ) 140 | 141 | val expectedMessage = ubyteArrayOf( 142 | 1u, 0u, 0u, 0u, 143 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 144 | 0u, 0u, 0u, 0u, 145 | 0u, 1u, 146 | 0u, 0u, 0u, 0u, 147 | 3u, 148 | 4u, 0u, 149 | 112u, 160u, 254u, 255u 150 | ) 151 | 152 | assertUByteArrayEquals( 153 | expectedMessage, 154 | testPushMessage.m.toBytes() 155 | ) 156 | } 157 | 158 | @Test 159 | fun appMessageUIntShouldBeLittleEndian() { 160 | val testPushMessage = AppMessage.AppMessagePush( 161 | 0u, 162 | Uuid(0L, 0L), 163 | listOf( 164 | AppMessageTuple.createUInt(0u, 900000u), 165 | ) 166 | ) 167 | 168 | val expectedMessage = ubyteArrayOf( 169 | 1u, 0u, 0u, 0u, 170 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 171 | 0u, 0u, 0u, 0u, 172 | 0u, 1u, 173 | 0u, 0u, 0u, 0u, 174 | 2u, 175 | 4u, 0u, 176 | 160u, 187u, 13u, 0u 177 | ) 178 | 179 | assertUByteArrayEquals( 180 | expectedMessage, 181 | testPushMessage.m.toBytes() 182 | ) 183 | } 184 | 185 | @Test 186 | fun appMessageStringShouldBeBigEndianAndTerminatedWithZero() { 187 | val testPushMessage = AppMessage.AppMessagePush( 188 | 0u, 189 | Uuid(0L, 0L), 190 | listOf( 191 | AppMessageTuple.createString(0u, "Hello"), 192 | ) 193 | ) 194 | 195 | val expectedMessage = ubyteArrayOf( 196 | 1u, 0u, 0u, 0u, 197 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 198 | 0u, 0u, 0u, 0u, 199 | 0u, 1u, 200 | 0u, 0u, 0u, 0u, 201 | 1u, 202 | 6u, 0u, 203 | 'H'.code.toUByte(), 204 | 'e'.code.toUByte(), 205 | 'l'.code.toUByte(), 206 | 'l'.code.toUByte(), 207 | 'o'.code.toUByte(), 208 | 0u 209 | ) 210 | 211 | assertUByteArrayEquals( 212 | expectedMessage, 213 | testPushMessage.m.toBytes() 214 | ) 215 | } 216 | 217 | @Test 218 | fun appMessageBytesShouldBeBigEndian() { 219 | val testPushMessage = AppMessage.AppMessagePush( 220 | 0u, 221 | Uuid(0L, 0L), 222 | listOf( 223 | AppMessageTuple.createUByteArray(0u, ubyteArrayOf(1u, 2u, 3u)), 224 | ) 225 | ) 226 | 227 | val expectedMessage = ubyteArrayOf( 228 | 1u, 0u, 0u, 0u, 229 | 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 230 | 0u, 0u, 0u, 0u, 231 | 0u, 1u, 232 | 0u, 0u, 0u, 0u, 233 | 0u, 234 | 3u, 0u, 235 | 1u, 2u, 3u 236 | ) 237 | 238 | assertUByteArrayEquals( 239 | expectedMessage, 240 | testPushMessage.m.toBytes() 241 | ) 242 | } 243 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/packets/AppRunStateMessageTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import assertIs 4 | import com.benasher44.uuid.uuidFrom 5 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | 10 | internal class AppRunStateMessageTest { 11 | @Test 12 | fun serializeDeserializeStartMessage() { 13 | val originalMessage = AppRunStateMessage.AppRunStateStart( 14 | uuidFrom("30880933-cead-49f6-ba94-3a6f8cd3218a") 15 | ) 16 | 17 | val bytes = originalMessage.serialize() 18 | val newMessage = PebblePacket.deserialize(bytes) 19 | 20 | assertIs(newMessage) 21 | assertEquals(uuidFrom("30880933-cead-49f6-ba94-3a6f8cd3218a"), newMessage.uuid.get()) 22 | } 23 | 24 | @Test 25 | fun serializeDeserializeStopMessage() { 26 | val originalMessage = AppRunStateMessage.AppRunStateStop( 27 | uuidFrom("30880933-cead-49f6-ba94-3a6f8cd3218a") 28 | ) 29 | 30 | val bytes = originalMessage.serialize() 31 | val newMessage = PebblePacket.deserialize(bytes) 32 | 33 | assertIs(newMessage) 34 | assertEquals(uuidFrom("30880933-cead-49f6-ba94-3a6f8cd3218a"), newMessage.uuid.get()) 35 | } 36 | 37 | @Test 38 | fun serializeDeserializeRequestMessage() { 39 | val originalMessage = AppRunStateMessage.AppRunStateRequest() 40 | 41 | val bytes = originalMessage.serialize() 42 | val newMessage = PebblePacket.deserialize(bytes) 43 | 44 | assertIs(newMessage) 45 | } 46 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/packets/SystemMessageTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import assertIs 4 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | 9 | internal class SystemMessageTest { 10 | @Test 11 | fun serializeDeserializeAppVersionRequest() { 12 | val originalMessage = PhoneAppVersion.AppVersionRequest() 13 | 14 | val bytes = originalMessage.serialize() 15 | val newMessage = PebblePacket.deserialize(bytes) 16 | 17 | assertIs(newMessage) 18 | } 19 | 20 | @Test 21 | fun serializeDeserializeAppVersionResponse() { 22 | val expectedPlatformFlags = listOf( 23 | PhoneAppVersion.PlatformFlag.Accelerometer, 24 | PhoneAppVersion.PlatformFlag.GPS, 25 | PhoneAppVersion.PlatformFlag.SMS, 26 | PhoneAppVersion.PlatformFlag.Telephony 27 | ) 28 | 29 | val expectedProtocolCaps = listOf( 30 | ProtocolCapsFlag.SupportsAppRunStateProtocol, 31 | ProtocolCapsFlag.SupportsHealthInsights, 32 | ProtocolCapsFlag.SupportsLocalization, 33 | ProtocolCapsFlag.SupportsUnreadCoreDump, 34 | ProtocolCapsFlag.SupportsWorkoutApp 35 | ) 36 | 37 | val originalMessage = PhoneAppVersion.AppVersionResponse( 38 | 70u, 39 | 0u, 40 | PhoneAppVersion.PlatformFlag.makeFlags( 41 | PhoneAppVersion.OSType.Linux, 42 | expectedPlatformFlags 43 | ), 44 | 7u, 45 | 12u, 46 | 15u, 47 | 1u, 48 | ProtocolCapsFlag.makeFlags(expectedProtocolCaps) 49 | ) 50 | 51 | val bytes = originalMessage.serialize() 52 | val newMessage = PebblePacket.deserialize(bytes) 53 | 54 | assertIs(newMessage) 55 | 56 | assertEquals(70u, newMessage.protocolVersion.get()) 57 | // Convert flags to set since we don't care about the order 58 | assertEquals( 59 | expectedPlatformFlags.toSet(), 60 | PhoneAppVersion.PlatformFlag.fromFlags(newMessage.platformFlags.get()).second.toSet() 61 | ) 62 | assertEquals( 63 | PhoneAppVersion.OSType.Linux, 64 | PhoneAppVersion.PlatformFlag.fromFlags(newMessage.platformFlags.get()).first 65 | ) 66 | assertEquals(7u, newMessage.responseVersion.get()) 67 | assertEquals(12u, newMessage.majorVersion.get()) 68 | assertEquals(15u, newMessage.minorVersion.get()) 69 | assertEquals( 70 | expectedProtocolCaps.toSet(), 71 | ProtocolCapsFlag.fromFlags(newMessage.protocolCaps.get()).toSet() 72 | ) 73 | } 74 | 75 | @Test 76 | fun serializeDeserializeSetTimeUtcMessage() { 77 | val originalMessage = TimeMessage.SetUTC( 78 | 12345678u, 79 | 2500, 80 | "Europe/Berlin" 81 | ) 82 | 83 | val bytes = originalMessage.serialize() 84 | val newMessage = PebblePacket.deserialize(bytes) 85 | 86 | assertIs(newMessage) 87 | 88 | assertEquals(12345678u, newMessage.unixTime.get()) 89 | assertEquals(2500, newMessage.utcOffset.get()) 90 | assertEquals("Europe/Berlin", newMessage.timeZoneName.get()) 91 | } 92 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/rebble/libpebblecommon/packets/blobdb/AppTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets.blobdb 2 | 3 | import assertUByteArrayEquals 4 | import io.rebble.libpebblecommon.util.DataBuffer 5 | import kotlin.test.Test 6 | 7 | internal class AppTest { 8 | @Test 9 | fun `AppMetadata flags should be little endian`() { 10 | val appMetadata = AppMetadata() 11 | appMetadata.flags.set(0x00FFu) 12 | val serialized = appMetadata.toBytes() 13 | val bytes = ubyteArrayOf(serialized[16], serialized[17]) 14 | assertUByteArrayEquals(ubyteArrayOf(0xFFu, 0x00u), bytes) 15 | } 16 | } -------------------------------------------------------------------------------- /src/iosMain/kotlin/io/rebble/libpebblecommon/util/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | actual class Bitmap { 4 | actual val width: Int 5 | get() = throw UnsupportedOperationException("Not supported on iOS yet") 6 | actual val height: Int 7 | get() = throw UnsupportedOperationException("Not supported on iOS yet") 8 | 9 | /** 10 | * Return pixel at the specified position at in AARRGGBB format. 11 | */ 12 | actual fun getPixel(x: Int, y: Int): Int { 13 | throw UnsupportedOperationException("Not supported on iOS yet") 14 | } 15 | } -------------------------------------------------------------------------------- /src/iosMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import io.rebble.libpebblecommon.packets.PhoneAppVersion 4 | 5 | actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.IOS -------------------------------------------------------------------------------- /src/iosMain/kotlin/util/DataBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import kotlinx.cinterop.* 4 | import platform.Foundation.* 5 | @OptIn(ExperimentalForeignApi::class) 6 | actual class DataBuffer { 7 | private val actualBuf: NSMutableData 8 | private var littleEndian = false 9 | 10 | /** 11 | * Total length of the buffer 12 | */ 13 | actual val length: Int 14 | get() = actualBuf.length.toInt() 15 | 16 | actual val remaining: Int 17 | get() = actualBuf.length().toInt()-_readPosition 18 | 19 | private var _readPosition: Int = 0 20 | 21 | /** 22 | * Current position in the buffer 23 | */ 24 | actual val readPosition: Int 25 | get() = _readPosition 26 | 27 | actual constructor(size: Int) { 28 | actualBuf = NSMutableData.dataWithCapacity(castToNativeSize(size))!! 29 | } 30 | 31 | actual constructor(bytes: UByteArray) { 32 | actualBuf = NSMutableData() 33 | memScoped { 34 | actualBuf.setData( 35 | NSData.create(bytes = allocArrayOf(bytes.asByteArray()), length = castToNativeSize(bytes.size)) 36 | ) 37 | } 38 | } 39 | 40 | private fun shouldReverse(): Boolean { 41 | return if (isPlatformBigEndian() && !littleEndian) { 42 | false 43 | }else if (isPlatformBigEndian() && littleEndian) { 44 | true 45 | }else !isPlatformBigEndian() && !littleEndian 46 | } 47 | 48 | actual fun putUShort(short: UShort) { 49 | memScoped { 50 | val pShort = alloc() 51 | pShort.value = if (shouldReverse()) reverseOrd(short) else short 52 | actualBuf.appendBytes(pShort.ptr, castToNativeSize(UShort.SIZE_BYTES)) 53 | } 54 | } 55 | actual fun getUShort(): UShort { 56 | memScoped { 57 | val pShort = alloc() 58 | actualBuf.getBytes(pShort.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(UShort.SIZE_BYTES))) 59 | _readPosition += UShort.SIZE_BYTES 60 | return if (shouldReverse()) reverseOrd(pShort.value) else pShort.value 61 | } 62 | } 63 | 64 | actual fun putShort(short: Short) { 65 | memScoped { 66 | val pShort = alloc() 67 | pShort.value = if (shouldReverse()) reverseOrd(short.toUShort()).toShort() else short 68 | actualBuf.appendBytes(pShort.ptr, castToNativeSize(Short.SIZE_BYTES)) 69 | } 70 | } 71 | actual fun getShort(): Short { 72 | memScoped { 73 | val pShort = alloc() 74 | actualBuf.getBytes(pShort.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(Short.SIZE_BYTES))) 75 | _readPosition += Short.SIZE_BYTES 76 | return if (shouldReverse()) reverseOrd(pShort.value.toUShort()).toShort() else pShort.value 77 | } 78 | } 79 | 80 | actual fun putUByte(byte: UByte) { 81 | memScoped { 82 | val pByte = alloc() 83 | pByte.value = byte 84 | actualBuf.appendBytes(pByte.ptr, castToNativeSize(UByte.SIZE_BYTES)) 85 | } 86 | } 87 | actual fun getUByte(): UByte { 88 | memScoped { 89 | val pByte = alloc() 90 | actualBuf.getBytes(pByte.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(UByte.SIZE_BYTES))) 91 | _readPosition += UByte.SIZE_BYTES 92 | return pByte.value 93 | } 94 | } 95 | 96 | actual fun putByte(byte: Byte) { 97 | memScoped { 98 | val pByte = alloc() 99 | pByte.value = byte 100 | actualBuf.appendBytes(pByte.ptr, castToNativeSize(Byte.SIZE_BYTES)) 101 | } 102 | } 103 | actual fun getByte(): Byte { 104 | memScoped { 105 | val pByte = alloc() 106 | actualBuf.getBytes(pByte.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(Byte.SIZE_BYTES))) 107 | _readPosition += Byte.SIZE_BYTES 108 | return pByte.value 109 | } 110 | } 111 | 112 | actual fun putBytes(bytes: UByteArray) { 113 | memScoped { 114 | val pBytes = allocArrayOf(bytes.toByteArray()) 115 | actualBuf.appendBytes(pBytes, castToNativeSize(bytes.size)) 116 | } 117 | } 118 | actual fun getBytes(count: Int): UByteArray { 119 | memScoped { 120 | val pBytes = allocArray(count) 121 | actualBuf.getBytes(pBytes.getPointer(this), NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(count))) 122 | _readPosition += count 123 | return pBytes.readBytes(count).toUByteArray() 124 | } 125 | } 126 | 127 | actual fun array(): UByteArray = getBytes(actualBuf.length.toInt()) 128 | 129 | actual fun setEndian(endian: Endian) { 130 | littleEndian = endian == Endian.Little 131 | } 132 | 133 | actual fun putUInt(uint: UInt) { 134 | memScoped { 135 | val pUInt = alloc() 136 | pUInt.value = if (shouldReverse()) reverseOrd(uint) else uint 137 | actualBuf.appendBytes(pUInt.ptr, castToNativeSize(UInt.SIZE_BYTES)) 138 | } 139 | } 140 | actual fun getUInt(): UInt { 141 | memScoped { 142 | val pUInt = alloc() 143 | actualBuf.getBytes(pUInt.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(UInt.SIZE_BYTES))) 144 | _readPosition += UInt.SIZE_BYTES 145 | return if (shouldReverse()) reverseOrd(pUInt.value) else pUInt.value 146 | } 147 | } 148 | 149 | actual fun putInt(int: Int) { 150 | memScoped { 151 | val pInt = alloc() 152 | pInt.value = if (shouldReverse()) reverseOrd(int.toUInt()).toInt() else int 153 | actualBuf.appendBytes(pInt.ptr, castToNativeSize(Int.SIZE_BYTES)) 154 | } 155 | } 156 | actual fun getInt(): Int { 157 | memScoped { 158 | val pInt = alloc() 159 | actualBuf.getBytes(pInt.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(Int.SIZE_BYTES))) 160 | _readPosition += Int.SIZE_BYTES 161 | return if (shouldReverse()) reverseOrd(pInt.value.toUInt()).toInt() else pInt.value 162 | } 163 | } 164 | 165 | actual fun putULong(ulong: ULong) { 166 | memScoped { 167 | val pULong = alloc() 168 | pULong.value = if (shouldReverse()) reverseOrd(ulong) else ulong 169 | actualBuf.appendBytes(pULong.ptr, castToNativeSize(ULong.SIZE_BYTES)) 170 | } 171 | } 172 | actual fun getULong(): ULong { 173 | memScoped { 174 | val pULong = alloc() 175 | actualBuf.getBytes(pULong.ptr, NSMakeRange(castToNativeSize(_readPosition), castToNativeSize(ULong.SIZE_BYTES))) 176 | _readPosition += ULong.SIZE_BYTES 177 | return if (shouldReverse()) reverseOrd(pULong.value) else pULong.value 178 | } 179 | } 180 | 181 | actual fun rewind() { 182 | _readPosition = 0 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/iosMain/kotlin/util/UtilFunctions.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | import kotlinx.cinterop.* 3 | import platform.Foundation.NSData 4 | import platform.Foundation.create 5 | import platform.posix.memcpy 6 | import platform.posix.size_t 7 | 8 | actual fun runBlocking(block: suspend () -> Unit) = kotlinx.coroutines.runBlocking{block()} 9 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) 10 | internal fun isPlatformBigEndian(): Boolean { 11 | memScoped { 12 | val i = alloc() 13 | i.value = 1 14 | val bytes = i.reinterpret() 15 | return bytes.value == 0.toByte() 16 | } 17 | } 18 | 19 | internal fun reverseOrd(varr: UShort): UShort = (((varr.toInt() and 0xff) shl 8) or ((varr.toInt() and 0xffff) ushr 8)).toUShort() 20 | 21 | internal fun reverseOrd(varr: UInt): UInt = ((reverseOrd((varr and 0xffffu).toUShort()).toInt() shl 16) or (reverseOrd((varr shr 16).toUShort()).toInt() and 0xffff)).toUInt() 22 | 23 | internal fun reverseOrd(varr: ULong): ULong = ((reverseOrd((varr and 0xffffffffu).toUInt()).toLong() shl 32) or (reverseOrd((varr shr 32).toUInt()).toLong() and 0xffffffff)).toULong() 24 | 25 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) 26 | fun ByteArray.toNative(): NSData = memScoped { 27 | NSData.create(bytes = allocArrayOf(this@toNative), length = castToNativeSize(this@toNative.size)) 28 | } 29 | 30 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) 31 | fun KUtil.byteArrayFromNative(arr: NSData): ByteArray = ByteArray(arr.length.toInt()).apply { 32 | if (this.isNotEmpty()) { 33 | usePinned { 34 | memcpy(it.addressOf(0), arr.bytes, arr.length) 35 | } 36 | } 37 | } 38 | 39 | @OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) 40 | internal fun castToNativeSize(v: Int): size_t { 41 | @Suppress("CAST_NEVER_SUCCEEDS", "USELESS_CAST") // Depending on the platform different side of if will trigger, lets ignore 42 | return if (size_t.SIZE_BITS == 32) { 43 | v.toUInt() as size_t 44 | }else { 45 | v.toULong() as size_t 46 | } 47 | } -------------------------------------------------------------------------------- /src/jvmMain/jvmMain.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/io/rebble/libpebblecommon/util/Bitmap.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | actual class Bitmap { 4 | actual val width: Int 5 | get() = throw UnsupportedOperationException("Not supported on generic JVM") 6 | actual val height: Int 7 | get() = throw UnsupportedOperationException("Not supported on generic JVM") 8 | 9 | /** 10 | * Return pixel at the specified position at in AARRGGBB format. 11 | */ 12 | actual fun getPixel(x: Int, y: Int): Int { 13 | throw UnsupportedOperationException("Not supported on generic JVM") 14 | } 15 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon 2 | 3 | import io.rebble.libpebblecommon.packets.PhoneAppVersion 4 | 5 | actual fun getPlatform(): PhoneAppVersion.OSType = PhoneAppVersion.OSType.Unknown -------------------------------------------------------------------------------- /src/jvmMain/kotlin/util/DataBuffer.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.ByteOrder 5 | 6 | actual class DataBuffer { 7 | private val actualBuf: ByteBuffer 8 | 9 | actual constructor(size: Int) { 10 | actualBuf = ByteBuffer.allocate(size) 11 | } 12 | 13 | actual constructor(bytes: UByteArray) { 14 | actualBuf = ByteBuffer.wrap(bytes.toByteArray()) 15 | } 16 | 17 | /** 18 | * Total length of the buffer 19 | */ 20 | actual val length: Int 21 | get() = actualBuf.capacity() 22 | 23 | /** 24 | * Current position in the buffer 25 | */ 26 | actual val readPosition: Int 27 | get() = actualBuf.position() 28 | 29 | actual val remaining: Int 30 | get() = actualBuf.remaining() 31 | 32 | 33 | actual fun putUShort(short: UShort) { 34 | actualBuf.putShort(short.toShort()) 35 | } 36 | 37 | actual fun getUShort(): UShort = actualBuf.short.toUShort() 38 | 39 | actual fun putShort(short: Short) { 40 | actualBuf.putShort(short) 41 | } 42 | 43 | actual fun getShort(): Short = actualBuf.short 44 | 45 | actual fun putUByte(byte: UByte) { 46 | actualBuf.put(byte.toByte()) 47 | } 48 | actual fun getUByte(): UByte = actualBuf.get().toUByte() 49 | 50 | actual fun putByte(byte: Byte) { 51 | actualBuf.put(byte) 52 | } 53 | actual fun getByte(): Byte = actualBuf.get() 54 | 55 | actual fun putBytes(bytes: UByteArray) { 56 | actualBuf.put(bytes.toByteArray()) 57 | } 58 | actual fun getBytes(count: Int): UByteArray { 59 | val tBuf = ByteArray(count) 60 | actualBuf.get(tBuf) 61 | return tBuf.toUByteArray() 62 | } 63 | 64 | actual fun array(): UByteArray = actualBuf.array().toUByteArray() 65 | 66 | actual fun setEndian(endian: Endian) { 67 | when (endian) { 68 | Endian.Big -> actualBuf.order(ByteOrder.BIG_ENDIAN) 69 | Endian.Little -> actualBuf.order(ByteOrder.LITTLE_ENDIAN) 70 | Endian.Unspecified -> {actualBuf.order(ByteOrder.BIG_ENDIAN)} 71 | } 72 | } 73 | 74 | actual fun putUInt(uint: UInt) { 75 | actualBuf.putInt(uint.toInt()) 76 | } 77 | actual fun getUInt(): UInt = actualBuf.int.toUInt() 78 | 79 | actual fun putInt(int: Int) { 80 | actualBuf.putInt(int) 81 | } 82 | actual fun getInt(): Int = actualBuf.int 83 | 84 | actual fun putULong(ulong: ULong) { 85 | actualBuf.putLong(ulong.toLong()) 86 | } 87 | actual fun getULong(): ULong = actualBuf.long.toULong() 88 | 89 | actual fun rewind() { 90 | actualBuf.rewind() 91 | } 92 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/util/UtilFunctionsJVM.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | actual fun runBlocking(block: suspend () -> Unit) = kotlinx.coroutines.runBlocking{block()} -------------------------------------------------------------------------------- /src/jvmTest/jvmTest.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/DeviceTest.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.client.* 2 | import io.ktor.client.features.websocket.* 3 | import io.ktor.http.* 4 | import io.ktor.http.cio.websocket.* 5 | import io.rebble.libpebblecommon.exceptions.PacketDecodeException 6 | import io.rebble.libpebblecommon.packets.PingPong 7 | import io.rebble.libpebblecommon.packets.blobdb.BlobResponse 8 | import io.rebble.libpebblecommon.packets.blobdb.NotificationSource 9 | import io.rebble.libpebblecommon.packets.blobdb.PushNotification 10 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 11 | import io.rebble.libpebblecommon.services.blobdb.BlobDBService 12 | import io.rebble.libpebblecommon.services.notification.NotificationService 13 | import kotlinx.coroutines.runBlocking 14 | import kotlinx.coroutines.withTimeout 15 | import kotlin.test.Ignore 16 | import kotlin.test.Test 17 | import kotlin.test.assertTrue 18 | import kotlin.text.String 19 | import kotlin.text.toCharArray 20 | 21 | @Ignore("This tests requires manual run.") 22 | class DeviceTests { 23 | private fun bytesToHex(bytes: UByteArray): String { 24 | val hexArray = "0123456789ABCDEF".toCharArray() 25 | val hexChars = CharArray(bytes.size * 2) 26 | for (j in bytes.indices) { 27 | val v = (bytes[j] and 0xFFu).toInt() 28 | 29 | hexChars[j * 2] = hexArray[v ushr 4] 30 | hexChars[j * 2 + 1] = hexArray[v and 0x0F] 31 | } 32 | return String(hexChars) 33 | } 34 | val phoneHost = "change-me" 35 | val phonePort = 9000 36 | 37 | val client = HttpClient { 38 | install(WebSockets) 39 | } 40 | 41 | private suspend fun sendWS(packet: PebblePacket, blockResponse: Boolean): PebblePacket? { 42 | var ret: PebblePacket? = null 43 | withTimeout(5_000) { 44 | client.ws( 45 | method = HttpMethod.Get, 46 | host = phoneHost, 47 | port = phonePort, path = "/" 48 | ) { 49 | send(Frame.Binary(true, byteArrayOf(0x01) + packet.serialize().toByteArray())) 50 | flush() 51 | while (blockResponse) { 52 | val frame = incoming.receive() 53 | if (frame is Frame.Binary) { 54 | try { 55 | ret = PebblePacket.deserialize( 56 | frame.data.slice(1 until frame.data.size).toByteArray() 57 | .toUByteArray() 58 | ) 59 | break 60 | } catch (e: PacketDecodeException) { 61 | println(e.toString()) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | return ret 68 | } 69 | 70 | @Test 71 | fun sendNotification() = runBlocking { 72 | val notif = PushNotification( 73 | sender = "Test Notif", 74 | subject = "This is a test notification!", 75 | message = "This is the notification body", 76 | backgroundColor = 0b11110011u, 77 | source = NotificationSource.Email 78 | ) 79 | 80 | val protocolHandler = TestProtocolHandler { receivePacket(sendWS(it, true)!!) } 81 | 82 | val notificationService = NotificationService(BlobDBService(protocolHandler)) 83 | val notificationResult = notificationService.send(notif) 84 | 85 | assertTrue( 86 | notificationResult is BlobResponse.Success, 87 | "Reply wasn't success from BlobDB when sending notif" 88 | ) 89 | } 90 | 91 | @Test 92 | fun sendPing() = runBlocking { 93 | val res = sendWS(PingPong.Ping(1337u), true) 94 | val gotPong = 95 | res?.endpoint == PingPong.endpoint && (res as? PingPong)?.cookie?.get() == 1337u 96 | assertTrue(gotPong, "Pong not received within sane amount of time") 97 | } 98 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/JvmTestUtils.kt: -------------------------------------------------------------------------------- 1 | import kotlinx.coroutines.runBlocking 2 | import kotlinx.coroutines.withTimeout 3 | import kotlin.contracts.ExperimentalContracts 4 | import kotlin.contracts.contract 5 | import kotlin.test.assertTrue 6 | 7 | /** 8 | * Combination of [runBlocking] and [withTimeout] for brevity. 9 | */ 10 | inline fun runBlockingWithTimeout(timeoutMs: Long = 5_000L, crossinline block: suspend () -> Unit) { 11 | runBlocking { 12 | withTimeout(timeoutMs) { 13 | block() 14 | } 15 | } 16 | } 17 | 18 | @OptIn(ExperimentalContracts::class) 19 | inline fun assertIs(obj: Any?, message: String? = null) { 20 | contract { returns() implies (obj is T) } 21 | assertTrue( 22 | obj is T, 23 | messagePrefix(message) + "Expected provided object to be <${T::class.java}>, " + 24 | "is <${obj?.javaClass}>." 25 | ) 26 | } 27 | 28 | fun messagePrefix(message: String?) = if (message == null) "" else "$message. " 29 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/io/rebble/libpebblecommon/packets/MusicControlTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.packets 2 | 3 | import assertUByteArrayEquals 4 | import io.rebble.libpebblecommon.protocolhelpers.PebblePacket 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | 9 | class MusicControlTest { 10 | @Test 11 | fun `serialize UpdateCurrentTrack with optional parameters`() { 12 | val packet = MusicControl.UpdateCurrentTrack( 13 | "A", 14 | "B", 15 | "C", 16 | 10, 17 | 20, 18 | 30 19 | ) 20 | 21 | val expectedData = ubyteArrayOf( 22 | 0u, 19u, 23 | 0u, 32u, 24 | 16u, 25 | 1u, 65u, 26 | 1u, 66u, 27 | 1u, 67u, 28 | 10u, 0u, 0u, 0u, 29 | 20u, 0u, 0u, 0u, 30 | 30u, 0u, 0u, 0u 31 | ) 32 | 33 | val actualData = packet.serialize() 34 | 35 | assertUByteArrayEquals(expectedData, actualData) 36 | } 37 | 38 | @Test 39 | fun `serialize UpdateCurrentTrack without optional parameters`() { 40 | val packet = MusicControl.UpdateCurrentTrack( 41 | "A", 42 | "B", 43 | "C" 44 | ) 45 | 46 | val expectedData = ubyteArrayOf( 47 | 0u, 7u, 48 | 0u, 32u, 49 | 16u, 50 | 1u, 65u, 51 | 1u, 66u, 52 | 1u, 67u 53 | ) 54 | 55 | val actualData = packet.serialize() 56 | 57 | assertUByteArrayEquals(expectedData, actualData) 58 | } 59 | 60 | @Test 61 | fun `deserialize UpdateCurrentTrack with optional parameters`() { 62 | val data = ubyteArrayOf( 63 | 0u, 19u, 64 | 0u, 32u, 65 | 16u, 66 | 1u, 65u, 67 | 1u, 66u, 68 | 1u, 67u, 69 | 10u, 0u, 0u, 0u, 70 | 20u, 0u, 0u, 0u, 71 | 30u, 0u, 0u, 0u 72 | ) 73 | 74 | val packet = PebblePacket.deserialize(data) as MusicControl.UpdateCurrentTrack 75 | 76 | assertEquals("A", packet.artist.get()) 77 | assertEquals("B", packet.album.get()) 78 | assertEquals("C", packet.title.get()) 79 | assertEquals(10u, packet.trackLength.get()) 80 | assertEquals(20u, packet.trackCount.get()) 81 | assertEquals(30u, packet.currentTrack.get()) 82 | } 83 | 84 | @Test 85 | fun `deserialize UpdateCurrentTrack without optional parameters`() { 86 | val data = ubyteArrayOf( 87 | 0u, 7u, 88 | 0u, 32u, 89 | 16u, 90 | 1u, 65u, 91 | 1u, 66u, 92 | 1u, 67u 93 | ) 94 | 95 | val packet = PebblePacket.deserialize(data) as MusicControl.UpdateCurrentTrack 96 | 97 | assertEquals("A", packet.artist.get()) 98 | assertEquals("B", packet.album.get()) 99 | assertEquals("C", packet.title.get()) 100 | assertEquals(null, packet.trackLength.get()) 101 | assertEquals(null, packet.trackCount.get()) 102 | assertEquals(null, packet.currentTrack.get()) 103 | } 104 | 105 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/io/rebble/libpebblecommon/services/notification/NotificationServiceTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.services.notification 2 | 3 | import TestProtocolHandler 4 | import assertIs 5 | import io.rebble.libpebblecommon.packets.blobdb.BlobCommand 6 | import io.rebble.libpebblecommon.packets.blobdb.BlobResponse 7 | import io.rebble.libpebblecommon.packets.blobdb.NotificationSource 8 | import io.rebble.libpebblecommon.packets.blobdb.PushNotification 9 | import io.rebble.libpebblecommon.services.blobdb.BlobDBService 10 | import runBlockingWithTimeout 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | 14 | class NotificationServiceTest { 15 | @Test 16 | fun `Forward notification success`() = runBlockingWithTimeout { 17 | val protocolHandler = TestProtocolHandler { receivedPacket -> 18 | if (receivedPacket is BlobCommand) { 19 | this.receivePacket(BlobResponse.Success().also { 20 | it.token.set(receivedPacket.token.get()) 21 | }) 22 | } 23 | } 24 | 25 | val notificationService = NotificationService(BlobDBService((protocolHandler))) 26 | val result = notificationService.send(TEST_NOTIFICATION) 27 | 28 | assertIs( 29 | result, 30 | "Reply wasn't success from BlobDB when sending notif" 31 | ) 32 | } 33 | 34 | @Test 35 | fun `Forward notification fail`() = runBlockingWithTimeout { 36 | val protocolHandler = TestProtocolHandler { receivedPacket -> 37 | if (receivedPacket is BlobCommand) { 38 | this.receivePacket(BlobResponse.GeneralFailure().also { 39 | it.token.set(receivedPacket.token.get()) 40 | }) 41 | } 42 | } 43 | 44 | val notificationService = NotificationService(BlobDBService((protocolHandler))) 45 | val result = notificationService.send(TEST_NOTIFICATION) 46 | 47 | assertIs( 48 | result, 49 | "Reply wasn't fail from BlobDB when sending notif" 50 | ) 51 | } 52 | 53 | @Test 54 | fun `Resend notification`() = runBlockingWithTimeout { 55 | val receivedTokens = ArrayList() 56 | val protocolHandler = TestProtocolHandler { receivedPacket -> 57 | if (receivedPacket is BlobCommand) { 58 | val nextPacket = if (receivedTokens.size == 0) { 59 | BlobResponse.TryLater() 60 | } else { 61 | BlobResponse.Success() 62 | } 63 | 64 | this.receivePacket(nextPacket.also { 65 | it.token.set(receivedPacket.token.get()) 66 | }) 67 | 68 | receivedTokens.add(receivedPacket.token.get()) 69 | 70 | } 71 | } 72 | 73 | val notificationService = NotificationService(BlobDBService((protocolHandler))) 74 | val result = notificationService.send(TEST_NOTIFICATION) 75 | 76 | assertIs( 77 | result, 78 | "Reply wasn't success from BlobDB when sending notif" 79 | ) 80 | 81 | assertEquals(2, receivedTokens.size) 82 | 83 | val uniqueTokens = receivedTokens.distinct() 84 | assertEquals( 85 | 2, 86 | uniqueTokens.size, 87 | "NotificationService should re-generate token every time." 88 | ) 89 | } 90 | } 91 | 92 | private val TEST_NOTIFICATION = PushNotification( 93 | sender = "Test Notif", 94 | subject = "This is a test notification!", 95 | message = "This is the notification body", 96 | backgroundColor = 0b11110011u, 97 | source = NotificationSource.Email 98 | ) 99 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/io/rebble/libpebblecommon/util/CrcCalculatorTest.kt: -------------------------------------------------------------------------------- 1 | package io.rebble.libpebblecommon.util 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class CrcCalculatorTest { 7 | @Test 8 | fun assertEmpty() { 9 | assertEquals( 10 | 0xffffffffu, 11 | calculateCrcOfBuffer(*ubyteArrayOf()) 12 | ) 13 | } 14 | 15 | @Test 16 | fun assertOneByte() { 17 | assertEquals( 18 | 0x1d604014u, 19 | calculateCrcOfBuffer(0xABu) 20 | ) 21 | } 22 | 23 | @Test 24 | fun assertFourBytes() { 25 | assertEquals( 26 | 0x1dabe74fu, 27 | calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u) 28 | ) 29 | } 30 | 31 | @Test 32 | fun assertSixBytes() { 33 | assertEquals( 34 | 0x205dbd4fu, 35 | calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u, 0x05u, 0x06u) 36 | ) 37 | } 38 | 39 | @Test 40 | fun assertEightBytesAtOnce() { 41 | assertEquals( 42 | 0x99f9e573u, 43 | calculateCrcOfBuffer(0x01u, 0x02u, 0x03u, 0x04u, 0x50u, 0x06u, 0x70u, 0x08u) 44 | ) 45 | } 46 | 47 | @Test 48 | fun assertEightBytesInChunks() { 49 | val calculator = Crc32Calculator() 50 | calculator.addBytes(ubyteArrayOf(0x01u)) 51 | calculator.addBytes(ubyteArrayOf(0x02u)) 52 | calculator.addBytes(ubyteArrayOf(0x03u, 0x04u, 0x50u, 0x06u)) 53 | calculator.addBytes(ubyteArrayOf(0x70u, 0x08u)) 54 | 55 | assertEquals( 56 | 0x99f9e573u, 57 | calculator.finalize() 58 | ) 59 | 60 | } 61 | 62 | private fun calculateCrcOfBuffer(vararg buffer: UByte): UInt { 63 | val calculator = Crc32Calculator() 64 | 65 | calculator.addBytes(buffer) 66 | 67 | return calculator.finalize() 68 | } 69 | } --------------------------------------------------------------------------------