├── .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 | 
--------------------------------------------------------------------------------
/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 |
14 |
15 |
18 |
21 |
22 |
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 |
16 |
17 |
20 |
23 |
24 |
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 | }
--------------------------------------------------------------------------------