From c492d74d90574c4090dc8abaec93dbbed02359fe Mon Sep 17 00:00:00 2001 From: Jukka Oikarinen Date: Tue, 26 Oct 2021 09:51:59 +0300 Subject: [PATCH] Android source: - fix to issue 208 - the PmdClient is converted to Kotlin code and new unit tests added --- .../android-communications/build.gradle | 4 +- .../library/build.gradle | 15 +- .../exceptions/BleControlPointCommandError.kt | 4 +- .../api/ble/model/gatt/BleGattFactory.java | 2 +- .../ble/model/gatt/client/BlePMDClient.java | 1355 ----------------- .../ble/model/gatt/client/pmd/BlePMDClient.kt | 560 +++++++ .../gatt/client/pmd/BlePMDClientUtils.kt | 52 + .../gatt/client/pmd/PmdControlPointCommand.kt | 9 + .../client/pmd/PmdControlPointResponse.kt | 48 + .../ble/model/gatt/client/pmd/PmdFeature.kt | 31 + .../gatt/client/pmd/PmdMeasurementType.kt | 26 + .../ble/model/gatt/client/pmd/PmdSetting.kt | 130 ++ .../model/gatt/client/pmd/model/AccData.kt | 114 ++ .../model/gatt/client/pmd/model/EcgData.kt | 93 ++ .../gatt/client/pmd/model/GnssLocationData.kt | 237 +++ .../model/gatt/client/pmd/model/GyrData.kt | 61 + .../model/gatt/client/pmd/model/MagData.kt | 76 + .../model/gatt/client/pmd/model/PpgData.kt | 154 ++ .../model/gatt/client/pmd/model/PpiData.kt | 72 + .../gatt/client/pmd/model/PressureData.kt | 44 + .../common/ble/TypeUtils.kt | 27 + .../bluedroid/host/BDDeviceSessionImpl.java | 13 +- .../ble/bluedroid/host/BDGattCallback.java | 15 +- .../polar/sdk/api/PolarBleApiDefaultImpl.kt | 2 + .../com/polar/sdk/api/model/PolarDataUtils.kt | 29 + .../com/polar/sdk/api/model/PolarOhrData.java | 10 +- .../sdk/api/model/PolarSensorSetting.java | 44 +- .../java/com/polar/sdk/impl/BDBleApiImpl.java | 88 +- .../model/gatt/client/BlePmdClientAccTest.kt | 555 ------- .../gatt/client/BlePmdClientParsersTest.java | 98 -- .../client/BlePmdClientPmdSettingsTest.java | 136 -- .../model/gatt/client/BlePmdClientPpgTest.kt | 355 ----- .../client/BlePmdClientThreeAxisTest.java | 65 - .../BlePmdClientControlPointResponseTest.kt | 14 +- .../client/pmd/BlePmdClientParsersTest.kt | 358 +++++ .../pmd/BlePmdClientPmdSettingsTest.java | 136 ++ .../model/gatt/client/pmd/BlePmdClientTest.kt | 484 ++++++ .../gatt/client/pmd/GnssLocationDataTest.kt | 189 +++ .../ble/model/gatt/client/pmd/PpgDataTest.kt | 372 +++++ .../PpiDataTest.kt} | 17 +- .../model/gatt/client/pmd/PressureDataTest.kt | 75 + .../gatt/client/pmd/model/AccDataTest.kt | 199 +++ .../model/EcgDataTest.kt} | 57 +- .../gatt/client/pmd/model/GyrDataTest.kt | 155 ++ .../gatt/client/pmd/model/MagDataTest.kt | 170 +++ .../common/ble/TypeUtilsTest.kt | 147 ++ 46 files changed, 4190 insertions(+), 2707 deletions(-) delete mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePMDClient.java create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClientUtils.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointCommand.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointResponse.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdSetting.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt create mode 100644 sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/common/ble/TypeUtils.kt create mode 100644 sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarDataUtils.kt delete mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientAccTest.kt delete mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientParsersTest.java delete mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPmdSettingsTest.java delete mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpgTest.kt delete mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientThreeAxisTest.java rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/{ => pmd}/BlePmdClientControlPointResponseTest.kt (87%) create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientParsersTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientPmdSettingsTest.java create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/{BlePmdClientPpiTest.kt => pmd/PpiDataTest.kt} (86%) create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccDataTest.kt rename sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/{BlePmdClientEcgTest.kt => pmd/model/EcgDataTest.kt} (73%) create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrDataTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagDataTest.kt create mode 100644 sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/common/ble/TypeUtilsTest.kt diff --git a/sources/Android/android-communications/build.gradle b/sources/Android/android-communications/build.gradle index f41a8411..e811e015 100755 --- a/sources/Android/android-communications/build.gradle +++ b/sources/Android/android-communications/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.5.31' ext.protobuf_version = '0.8.12' ext.dokka_version = '1.4.32' @@ -11,7 +11,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' + classpath 'com.android.tools.build:gradle:7.0.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.google.protobuf:protobuf-gradle-plugin:$protobuf_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" diff --git a/sources/Android/android-communications/library/build.gradle b/sources/Android/android-communications/library/build.gradle index 2423b0fd..e4216acf 100755 --- a/sources/Android/android-communications/library/build.gradle +++ b/sources/Android/android-communications/library/build.gradle @@ -56,13 +56,16 @@ android { } } - flavorDimensions "CommunicationsLibrary" + flavorDimensions 'library' productFlavors { androidCommunications { - dimension "CommunicationsLibrary" + dimension "library" } sdk { - dimension "CommunicationsLibrary" + dimension "library" + } + sdkProprietary { + dimension "library" } } @@ -157,13 +160,13 @@ dependencies { implementation 'androidx.annotation:annotation:1.2.0' implementation "androidx.core:core-ktx:1.6.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - sdkImplementation 'com.google.protobuf:protobuf-javalite:3.14.0' + sdkImplementation 'com.google.protobuf:protobuf-javalite:3.17.3' + sdkProprietaryImplementation 'com.google.protobuf:protobuf-javalite:3.17.3' testImplementation 'junit:junit:4.13.2' testImplementation "org.mockito:mockito-core:3.11.1" - testImplementation "io.mockk:mockk:1.10.4" + testImplementation "io.mockk:mockk:1.10.6" testImplementation 'androidx.test:runner:1.4.0' testImplementation 'androidx.test.espresso:espresso-core:3.4.0' - testImplementation 'org.robolectric:robolectric:4.0' androidTestImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:core:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleControlPointCommandError.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleControlPointCommandError.kt index a6a0e10d..6caff0d9 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleControlPointCommandError.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/exceptions/BleControlPointCommandError.kt @@ -1,12 +1,12 @@ package com.polar.androidcommunications.api.ble.exceptions -import com.polar.androidcommunications.api.ble.model.gatt.client.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdControlPointResponse /** * Error indicating that requested control point command operation failed with error code */ class BleControlPointCommandError( message: String, - val error: BlePMDClient.PmdControlPointResponse.PmdControlPointResponseCode + val error: PmdControlPointResponse.PmdControlPointResponseCode ) : Exception("$message failed with error: $error") \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattFactory.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattFactory.java index 274ecbab..9cf3a276 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattFactory.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/BleGattFactory.java @@ -7,10 +7,10 @@ import com.polar.androidcommunications.api.ble.model.gatt.client.BleDisClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BleH7SettingsClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BleHrClient; -import com.polar.androidcommunications.api.ble.model.gatt.client.BlePMDClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BlePfcClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BlePsdClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BleRscClient; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient; import com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpClient; import java.util.HashSet; diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePMDClient.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePMDClient.java deleted file mode 100644 index 5a81c46d..00000000 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePMDClient.java +++ /dev/null @@ -1,1355 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client; - -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.polar.androidcommunications.api.ble.BleLogger; -import com.polar.androidcommunications.api.ble.exceptions.BleAttributeError; -import com.polar.androidcommunications.api.ble.exceptions.BleCharacteristicNotificationNotEnabled; -import com.polar.androidcommunications.api.ble.exceptions.BleControlPointCommandError; -import com.polar.androidcommunications.api.ble.exceptions.BleDisconnected; -import com.polar.androidcommunications.api.ble.exceptions.BleOperationModeChange; -import com.polar.androidcommunications.api.ble.model.gatt.BleGattBase; -import com.polar.androidcommunications.api.ble.model.gatt.BleGattTxInterface; -import com.polar.androidcommunications.common.ble.AtomicSet; -import com.polar.androidcommunications.common.ble.BleUtils; -import com.polar.androidcommunications.common.ble.RxUtils; - -import java.io.ByteArrayOutputStream; -import java.nio.ByteBuffer; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; -import java.util.UUID; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.FlowableEmitter; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.core.SingleOnSubscribe; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public class BlePMDClient extends BleGattBase { - - private static final String TAG = BlePMDClient.class.getSimpleName(); - - public static final UUID PMD_DATA = UUID.fromString("FB005C82-02E7-F387-1CAD-8ACD2D8DF0C8"); - public static final UUID PMD_CP = UUID.fromString("FB005C81-02E7-F387-1CAD-8ACD2D8DF0C8"); - public static final UUID PMD_SERVICE = UUID.fromString("FB005C80-02E7-F387-1CAD-8ACD2D8DF0C8"); - - private final Object controlPointMutex = new Object(); - private final LinkedBlockingQueue> pmdCpInputQueue = new LinkedBlockingQueue<>(); - private final AtomicSet> ecgObservers = new AtomicSet<>(); - private final AtomicSet> accObservers = new AtomicSet<>(); - private final AtomicSet> gyroObservers = new AtomicSet<>(); - private final AtomicSet> magnetometerObservers = new AtomicSet<>(); - private final AtomicSet> ppgObservers = new AtomicSet<>(); - private final AtomicSet> ppiObservers = new AtomicSet<>(); - private final AtomicSet> autoGainAFE4404Observers = new AtomicSet<>(); - private final AtomicSet> autoGainAFE4410Observers = new AtomicSet<>(); - private final AtomicSet> autoGainADPD4000Observers = new AtomicSet<>(); - private final AtomicSet>> afeOperationModeObservers = new AtomicSet<>(); - private final AtomicSet>> sportIdObservers = new AtomicSet<>(); - private final AtomicSet> biozObservers = new AtomicSet<>(); - private final AtomicSet> rdObservers = new AtomicSet<>(); - private byte[] pmdFeatureData = null; - private final Object mutexFeature = new Object(); - private final Map currentSettings = new HashMap<>(); - private final AtomicInteger pmdCpEnabled; - private final AtomicInteger pmdDataEnabled; - - public enum PmdMeasurementType { - ECG(0), - PPG(1), - ACC(2), - PPI(3), - BIOZ(4), - GYRO(5), - MAGNETOMETER(6), - BAROMETER(7), - AMBIENT(8), - SDK_MODE(9), - UNKNOWN_TYPE(0xff); - - private final int numVal; - - PmdMeasurementType(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - - @NonNull - public static PmdMeasurementType fromId(byte id) { - for (PmdMeasurementType type : values()) { - if (type.numVal == id) { - return type; - } - } - return UNKNOWN_TYPE; - } - } - - public enum PmdEcgDataType { - ECG(0), - BS01(1), - MAX3000X(2); - private final int numVal; - - PmdEcgDataType(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - } - - public static class PmdFeature { - public final boolean ecgSupported; - public final boolean ppgSupported; - public final boolean accSupported; - public final boolean ppiSupported; - public final boolean bioZSupported; - public final boolean gyroSupported; - public final boolean magnetometerSupported; - public final boolean barometerSupported; - public final boolean ambientSupported; - public final boolean sdkModeSupported; - - public PmdFeature(final @NonNull byte[] data) { - ecgSupported = (data[1] & 0x01) != 0; - ppgSupported = (data[1] & 0x02) != 0; - accSupported = (data[1] & 0x04) != 0; - ppiSupported = (data[1] & 0x08) != 0; - bioZSupported = (data[1] & 0x10) != 0; - gyroSupported = (data[1] & 0x20) != 0; - magnetometerSupported = (data[1] & 0x40) != 0; - barometerSupported = (data[1] & 0x80) != 0; - ambientSupported = (data[2] & 0x01) != 0; - sdkModeSupported = (data[2] & 0x02) != 0; - } - } - - enum PmdControlPointCommand { - NULL_ITEM(0), // This fixes java enum bug - GET_MEASUREMENT_SETTINGS(1), - REQUEST_MEASUREMENT_START(2), - STOP_MEASUREMENT(3), - GET_SDK_MODE_MEASUREMENT_SETTINGS(4); - - private final int numVal; - - PmdControlPointCommand(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - } - - public static class PmdControlPointResponse { - public final byte responseCode; - public final PmdControlPointCommand opCode; - public final byte measurementType; - public final PmdControlPointResponseCode status; - public final ByteArrayOutputStream parameters = new ByteArrayOutputStream(); - public final boolean more; - - public enum PmdControlPointResponseCode { - SUCCESS(0), - ERROR_INVALID_OP_CODE(1), - ERROR_INVALID_MEASUREMENT_TYPE(2), - ERROR_NOT_SUPPORTED(3), - ERROR_INVALID_LENGTH(4), - ERROR_INVALID_PARAMETER(5), - ERROR_ALREADY_IN_STATE(6), - ERROR_INVALID_RESOLUTION(7), - ERROR_INVALID_SAMPLE_RATE(8), - ERROR_INVALID_RANGE(9), - ERROR_INVALID_MTU(10), - ERROR_INVALID_NUMBER_OF_CHANNELS(11), - ERROR_INVALID_STATE(12), - ERROR_DEVICE_IN_CHARGER(13); - private final int numVal; - - PmdControlPointResponseCode(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - } - - public PmdControlPointResponse(@NonNull byte[] data) { - responseCode = data[0]; - opCode = PmdControlPointCommand.values()[data[1]]; - measurementType = data[2]; - status = PmdControlPointResponseCode.values()[data[3]]; - if (status == PmdControlPointResponseCode.SUCCESS) { - more = data.length > 4 && data[4] != 0; - if (data.length > 5) { - parameters.write(data, 5, data.length - 5); - } - } else { - more = false; - } - } - } - - public static class PmdSetting { - public enum PmdSettingType { - SAMPLE_RATE(0), - RESOLUTION(1), - RANGE(2), - RANGE_MILLIUNIT(3), - CHANNELS(4), - FACTOR(5); - - private final int numVal; - - PmdSettingType(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - } - - private static final EnumMap typeToFieldSize = new EnumMap(PmdSettingType.class) {{ - put(PmdSettingType.SAMPLE_RATE, 2); - put(PmdSettingType.RESOLUTION, 2); - put(PmdSettingType.RANGE, 2); - put(PmdSettingType.RANGE_MILLIUNIT, 4); // not has range from min to max - put(PmdSettingType.CHANNELS, 1); - put(PmdSettingType.FACTOR, 4); - }}; - - // available settings - @Nullable - public Map> settings = new TreeMap<>(); - - // selected by client - @Nullable - public Map selected; - - public PmdSetting() { - } - - public PmdSetting(final @NonNull byte[] data) { - EnumMap> parsedSettings = parsePmdSettingsData(data); - validateSettings(parsedSettings); - this.settings = parsedSettings; - } - - public PmdSetting(@NonNull Map selected) { - PmdSetting.validateSelected(selected); - this.selected = selected; - } - - EnumMap> parsePmdSettingsData(final byte[] data) { - EnumMap> parsedSettings = new EnumMap<>(PmdSettingType.class); - if (data.length <= 1) { - return parsedSettings; - } - - int offset = 0; - while (offset < data.length) { - PmdSettingType type = PmdSetting.PmdSettingType.values()[data[offset++]]; - int count = data[offset++]; - Set items = new HashSet<>(); - while (count-- > 0) { - int fieldSize = Objects.requireNonNull(typeToFieldSize.get(type)); - int item = BleUtils.convertArrayToUnsignedInt(data, offset, fieldSize); - offset += fieldSize; - items.add(item); - } - parsedSettings.put(type, items); - } - return parsedSettings; - } - - void updateSelectedFromStartResponse(final byte[] data) { - EnumMap> settingsFromStartResponse = parsePmdSettingsData(data); - if (settingsFromStartResponse.containsKey(PmdSettingType.FACTOR)) { - selected.put(PmdSettingType.FACTOR, settingsFromStartResponse.get(PmdSettingType.FACTOR).iterator().next()); - } - } - - @NonNull - public byte[] serializeSelected() { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - if (selected != null) { - for (Map.Entry p : selected.entrySet()) { - if (p.getKey() == PmdSettingType.FACTOR) { - continue; - } - outputStream.write((byte) p.getKey().numVal); - outputStream.write((byte) 1); - int v = p.getValue(); - int fieldSize = Objects.requireNonNull(typeToFieldSize.get(p.getKey())); - for (int i = 0; i < fieldSize; ++i) { - outputStream.write((byte) (v >> (i * 8))); - } - } - } - return outputStream.toByteArray(); - } - - @NonNull - public PmdSetting maxSettings() { - Map set = new TreeMap<>(); - if (settings != null) { - for (Map.Entry> p : settings.entrySet()) { - set.put(p.getKey(), Collections.max(p.getValue())); - } - } - return new PmdSetting(set); - } - - private static void validateSettings(Map> settings) { - for (Map.Entry> setting : settings.entrySet()) { - PmdSettingType key = setting.getKey(); - for (Integer value : setting.getValue()) { - Map.Entry entry = new AbstractMap.SimpleEntry<>(key, value); - validateSetting(entry); - } - } - } - - private static void validateSelected(Map settings) { - for (Map.Entry setting : settings.entrySet()) { - validateSetting(setting); - } - } - - private static void validateSetting(Map.Entry setting) { - int fieldSize = typeToFieldSize.get(setting.getKey()); - int value = setting.getValue(); - if (fieldSize == 1 && (value < 0x0 || 0xFF < value)) { - throw new RuntimeException("PmdSetting not in valid range. Field size: " + fieldSize + " value: " + value); - } - if (fieldSize == 2 && (value < 0x0 || 0xFFFF < value)) { - throw new RuntimeException("PmdSetting not in valid range. Field size: " + fieldSize + " value: " + value); - } - if (fieldSize == 3 && (value < 0x0 || 0xFFFFFF < value)) { - throw new RuntimeException("PmdSetting not in valid range. Field size: " + fieldSize + " value: " + value); - } - } - } - - @NonNull - public static List> parseDeltaFrame(@NonNull byte[] bytes, int channels, int bitWidth, int totalBitLength) { - int offset = 0; - List bitSet = new ArrayList<>(); - for (byte b : bytes) { - for (int i = 0; i < 8; ++i) { - bitSet.add((b & (0x01 << i)) != 0); - } - } - List> samples = new ArrayList<>(); - int mask = Integer.MAX_VALUE << (bitWidth - 1); - while (offset < totalBitLength) { - List channelSamples = new ArrayList<>(); - int channelCount = 0; - while (channelCount++ < channels) { - List bits = bitSet.subList(offset, offset + bitWidth); - int val = 0; - for (int i = 0; i < bits.size(); ++i) { - val |= ((bits.get(i) ? 0x01 : 0x00) << i); - } - if ((val & mask) != 0) { - val |= mask; - } - offset += bitWidth; - channelSamples.add(val); - } - samples.add(channelSamples); - } - return samples; - } - - @NonNull - public static List parseDeltaFrameRefSamples(@NonNull byte[] bytes, int channels, int resolution) { - List samples = new ArrayList<>(); - int offset = 0; - int channelCount = 0; - int mask = 0xFFFFFFFF << (resolution - 1); - int resolutionInBytes = (int) Math.ceil(resolution / 8.0); - while (channelCount++ < channels) { - int sample = BleUtils.convertArrayToSignedInt(bytes, offset, resolutionInBytes); - if ((sample & mask) != 0) { - sample |= mask; - } - offset += resolutionInBytes; - samples.add(sample); - } - return samples; - } - - @NonNull - public static List> parseDeltaFramesAll(@NonNull byte[] value, - int channels, - int resolution) { - int offset = 0; - List refSamples = parseDeltaFrameRefSamples(value, channels, resolution); - offset += channels * Math.ceil(resolution / 8.0); - List> samples = new ArrayList<>(Collections.singleton(refSamples)); - BleUtils.validate(refSamples.size() == channels, "incorrect number of ref channels"); - while (offset < value.length) { - int deltaSize = value[offset++] & 0xFF; - int sampleCount = value[offset++] & 0xFF; - int bitLength = (sampleCount * deltaSize * channels); - int length = (int) Math.ceil(bitLength / 8.0); - final byte[] deltaFrame = new byte[length]; - System.arraycopy(value, offset, deltaFrame, 0, deltaFrame.length); - List> deltaSamples = parseDeltaFrame(deltaFrame, channels, deltaSize, bitLength); - for (List delta : deltaSamples) { - BleUtils.validate(delta.size() == channels, "incorrect number of delta channels"); - List lastSample = samples.get(samples.size() - 1); - List nextSamples = new ArrayList<>(); - for (int i = 0; i < channels; ++i) { - int sample = lastSample.get(i) + delta.get(i); - nextSamples.add(sample); - } - samples.addAll(Collections.singleton(nextSamples)); - } - offset += length; - } - - return samples; - } - - public static class EcgData { - public static class EcgSample { - // samples in signed microvolts - public PmdEcgDataType type; - public long timeStamp; - public int microVolts; - public boolean overSampling; - public byte skinContactBit; - public byte contactImpedance; - - public byte ecgDataTag; - public byte paceDataTag; - } - - public final long timeStamp; - public final List ecgSamples = new ArrayList<>(); - - public EcgData(byte type, @NonNull byte[] value, long timeStamp) { - int offset = 0; - this.timeStamp = timeStamp; - while (offset < value.length) { - EcgSample sample = new EcgSample(); - sample.type = PmdEcgDataType.values()[type]; - sample.timeStamp = timeStamp; - - if (type == 1) { // BS01 - sample.microVolts = (((value[offset] & 0xFF) | (value[offset + 1] & 0x3F) << 8) & 0x3FFF); - sample.overSampling = (value[offset + 2] & 0x01) != 0; - sample.skinContactBit = (byte) ((value[offset + 2] & 0x06) >> 1); - sample.contactImpedance = (byte) ((value[offset + 2] & 0x18) >> 3); - } else if (type == 2) { // MAX3000 - sample.microVolts = (((value[offset] & 0xFF) | ((value[offset + 1] & 0xFF) << 8) | ((value[offset + 2] & 0x03) << 16)) & 0x3FFFFF); - sample.ecgDataTag = (byte) ((value[offset + 2] & 0x1C) >> 2); - sample.paceDataTag = (byte) ((value[offset + 2] & 0xE0) >> 5); - } else if (type == 0) { // production - sample.microVolts = BleUtils.convertArrayToSignedInt(value, offset, 3); - } - offset += 3; - ecgSamples.add(sample); - } - } - } - - public static class AccData { - public static class AccSample { - // Sample contains signed x,y,z axis values in milliG - public final int x; - public final int y; - public final int z; - - AccSample(int x, int y, int z) { - this.x = x; - this.y = y; - this.z = z; - } - } - - public final List accSamples = new ArrayList<>(); - public final long timeStamp; - - public AccData(byte type, @NonNull byte[] value, long timeStamp) { - int offset = 0; - this.timeStamp = timeStamp; - int resolution = (type + 1) * 8; - int z, y, x, step = (int) Math.ceil((double) resolution / 8.0); - while (offset < value.length) { - x = BleUtils.convertArrayToSignedInt(value, offset, step); - offset += step; - y = BleUtils.convertArrayToSignedInt(value, offset, step); - offset += step; - z = BleUtils.convertArrayToSignedInt(value, offset, step); - offset += step; - accSamples.add(new AccSample(x, y, z)); - } - } - - /** - * ACC samples from delta frame - * - * @param value bytes - * @param factor relative to absolute multiplier - * @param resolution int bits - * @param timeStamp ns - */ - public AccData(@NonNull byte[] value, float factor, int resolution, long timeStamp) { - this.timeStamp = timeStamp; - float accFactor = factor * 1000; // Modify the factor to get data in milliG - ThreeAxisDeltaFramedData data = new ThreeAxisDeltaFramedData(value, accFactor, resolution, timeStamp); - for (ThreeAxisDeltaFramedData.ThreeAxisSample sample : data.axisSamples) { - this.accSamples.add(new AccSample((int) sample.x, (int) sample.y, (int) sample.z)); - } - } - } - - public static class MagData { - public static class MagSample { - // Sample contains signed x,y,z axis values in Gauss - public final float x; - public final float y; - public final float z; - - MagSample(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - } - - public final List magSamples = new ArrayList<>(); - public final long timeStamp; - - /** - * Magnetometer samples from delta frame - * - * @param value bytes - * @param factor relative to absolute multiplier - * @param resolution int bits - * @param timeStamp ns - */ - public MagData(@NonNull byte[] value, float factor, int resolution, long timeStamp) { - this.timeStamp = timeStamp; - ThreeAxisDeltaFramedData data = new ThreeAxisDeltaFramedData(value, factor, resolution, timeStamp); - for (ThreeAxisDeltaFramedData.ThreeAxisSample sample : data.axisSamples) { - this.magSamples.add(new MagSample(sample.x, sample.y, sample.z)); - } - } - } - - public static class GyrData { - public static class GyrSample { - // Sample contains signed x,y,z axis values in deg/sec - public final float x; - public final float y; - public final float z; - - GyrSample(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - } - - public final List gyrSamples = new ArrayList<>(); - public final long timeStamp; - - /** - * Magnetometer samples from delta frame - * - * @param value bytes - * @param factor relative to absolute multiplier - * @param resolution int bits - * @param timeStamp ns - */ - public GyrData(@NonNull byte[] value, float factor, int resolution, long timeStamp) { - this.timeStamp = timeStamp; - ThreeAxisDeltaFramedData data = new ThreeAxisDeltaFramedData(value, factor, resolution, timeStamp); - for (ThreeAxisDeltaFramedData.ThreeAxisSample sample : data.axisSamples) { - this.gyrSamples.add(new GyrSample(sample.x, sample.y, sample.z)); - } - } - } - - public static class PpgData { - public enum PpgFrameType { - PPG0_TYPE(0), - AFE4410(1), - AFE4404(2), - PPG1_TYPE(3), - ADPD4000(4), - AFE_OPERATION_MODE(5), - SPORT_ID(6), - DELTA_FRAME(128), - UNKNOWN_TYPE(0xff); - private final int numVal; - - PpgFrameType(int numVal) { - this.numVal = numVal; - } - - public int getNumVal() { - return numVal; - } - - @NonNull - public static PpgFrameType fromId(int id) { - for (PpgFrameType type : values()) { - if (type.numVal == id) { - return type; - } - } - return UNKNOWN_TYPE; - } - } - - public static class PpgSample { - public final List ppgDataSamples; - public final long status; - - public PpgSample(@NonNull List ppgDataSamples) { - this.ppgDataSamples = ppgDataSamples; - this.status = 0; - } - - public PpgSample(@NonNull List ppgDataSamples, long status) { - this.ppgDataSamples = ppgDataSamples; - this.status = status; - } - } - - public final List ppgSamples = new ArrayList<>(); - public final long timeStamp; - public final int channels; - - public PpgData(@NonNull byte[] value, long timeStamp, int type) { - this.timeStamp = timeStamp; - this.channels = type == 0 ? 4 : 18; - final int step = 3; - for (int i = 0; i < value.length; ) { - List samples = new ArrayList<>(); - for (int ch = 0; ch < this.channels; ++ch) { - samples.add(BleUtils.convertArrayToSignedInt(value, i, step)); - i += step; - } - long status = 0; - if (channels == 18) { - status = BleUtils.convertArrayToUnsignedLong(value, i, 4); - i += 4; - } - ppgSamples.add(new PpgSample(samples, status)); - } - } - - /** - * PPG samples from delta frame - * - * @param value bytes - * @param factor relative to absolute multiplier - * @param resolution int bits - * @param channels number of channels in one sample - * @param timeStamp ns - */ - public PpgData(@NonNull byte[] value, float factor, int resolution, int channels, long timeStamp) { - List> samples = parseDeltaFramesAll(value, channels, resolution); - for (List sample : samples) { - for (int i = 0; i < sample.size(); i++) { - int absoluteChannelValue = (int) ((float) sample.get(i) * factor); - sample.set(i, absoluteChannelValue); - } - ppgSamples.add(new PpgSample(sample)); - } - this.timeStamp = timeStamp; - this.channels = channels; - } - } - - public static class PpiData { - public static class PPSample { - public final int hr; - public final int ppInMs; - public final int ppErrorEstimate; - public final int blockerBit; - public final int skinContactStatus; - public final int skinContactSupported; - - public PPSample(@NonNull byte[] data) { - hr = (int) ((long) data[0] & 0xFFL); - ppInMs = (int) BleUtils.convertArrayToUnsignedLong(data, 1, 2); - ppErrorEstimate = (int) BleUtils.convertArrayToUnsignedLong(data, 3, 2); - blockerBit = data[5] & 0x01; - skinContactStatus = (data[5] & 0x02) >> 1; - skinContactSupported = (data[5] & 0x04) >> 2; - } - } - - public final List ppSamples = new ArrayList<>(); - public final long timeStamp; - - public PpiData(@NonNull byte[] data, long timeStamp) { - int offset = 0; - this.timeStamp = timeStamp; - while (offset < data.length) { - final int finalOffset = offset; - ppSamples.add(new PPSample(Arrays.copyOfRange(data, finalOffset, finalOffset + 6))); - offset += 6; - } - } - } - - public static class AutoGainAFE4404 { - public byte I_OFFDAC; - public byte TIA_GAIN; - public byte ILED; - public byte TIA_CF; - public long timeStamp; - - public AutoGainAFE4404(final byte[] data, long timeStamp) { - this.timeStamp = timeStamp; - this.I_OFFDAC = (byte) (data[0] & 0x1f); - this.TIA_GAIN = (byte) ((byte) (data[0] & 0xE0) >> 5); - this.ILED = data[1]; - this.TIA_CF = data[2]; - } - } - - public static class AutoGainAFE4410 { - public byte I_OFFDAC_1_MID; - public byte I_OFFDAC_2_MID; - public byte I_OFFDAC_3_MID; - public byte I_OFFDAC_AMB_MID; - public byte TIA_RF; - public byte TIA_CF; - public int ILED_1; - public int ILED_2; - public int ILED_3; - public long timeStamp; - - public AutoGainAFE4410(final byte[] data, long timeStamp) { - this.timeStamp = timeStamp; - this.I_OFFDAC_1_MID = data[0]; - this.I_OFFDAC_2_MID = data[1]; - this.I_OFFDAC_3_MID = data[2]; - this.I_OFFDAC_AMB_MID = data[3]; - this.TIA_RF = data[4]; - this.TIA_CF = data[5]; - this.ILED_1 = data[6] & 0xFF; - this.ILED_2 = data[7] & 0xFF; - this.ILED_3 = data[8] & 0xFF; - } - } - - public static class AutoGainADPD4000 { - public byte[] TIA_GAIN_CH1_TS; - public byte[] TIA_GAIN_CH2_TS; - public byte[] NUMINT_TS; - public long timeStamp; - - public AutoGainADPD4000(final byte[] data, long timeStamp) { - this.timeStamp = timeStamp; - this.TIA_GAIN_CH1_TS = Arrays.copyOfRange(data, 0, 12); - this.TIA_GAIN_CH2_TS = Arrays.copyOfRange(data, 12, 24); - this.NUMINT_TS = Arrays.copyOfRange(data, 24, data.length); - } - } - - public static class BiozData { - public long timeStamp; - public List samples = new ArrayList<>(); - public byte status; - public byte type; - - public BiozData(final byte[] data, long timeStamp, byte type) { - this.timeStamp = timeStamp; - this.type = type; - int offset = 0; - this.status = 0; - while (offset < data.length) { - if (type == 0) { - samples.add(BleUtils.convertArrayToSignedInt(data, offset, 3)); - offset += 3; - } else { - samples.add(BleUtils.convertArrayToSignedInt(data, offset, 3)); - offset += 3; - samples.add(BleUtils.convertArrayToSignedInt(data, offset, 3)); - offset += 3; - status = data[offset]; - offset += 1; - } - } - } - } - - public static class ThreeAxisDeltaFramedData { - public static class ThreeAxisSample { - public final float x; - public final float y; - public final float z; - - public ThreeAxisSample(float x, float y, float z) { - this.x = x; - this.y = y; - this.z = z; - } - } - - public final List axisSamples = new ArrayList<>(); - public final long timeStamp; - - /** - * Three axis samples from delta frame - * - * @param value bytes - * @param factor relative to absolute multiplier - * @param resolution int bits - * @param timeStamp ns - */ - public ThreeAxisDeltaFramedData(@NonNull byte[] value, float factor, int resolution, long timeStamp) { - this.timeStamp = timeStamp; - List> samples = parseDeltaFramesAll(value, 3, resolution); - for (List sample : samples) { - BleUtils.validate(sample.size() == 3, "delta samples invalid length"); - float channel0 = (float) sample.get(0) * factor; - float channel1 = (float) sample.get(1) * factor; - float channel2 = (float) sample.get(2) * factor; - axisSamples.add(new ThreeAxisSample(channel0, channel1, channel2)); - } - } - } - - public BlePMDClient(@NonNull BleGattTxInterface txInterface) { - super(txInterface, PMD_SERVICE); - addCharacteristicNotification(PMD_CP); - addCharacteristicRead(PMD_CP); - addCharacteristicNotification(PMD_DATA); - pmdCpEnabled = getNotificationAtomicInteger(PMD_CP); - pmdDataEnabled = getNotificationAtomicInteger(PMD_DATA); - } - - @Override - public void reset() { - super.reset(); - clearStreamObservers(new BleDisconnected()); - - synchronized (mutexFeature) { - pmdFeatureData = null; - mutexFeature.notifyAll(); - } - } - - private float fetchFactor(PmdMeasurementType type) { - BleUtils.validate(currentSettings.containsKey(type), type + " setting not stored"); - if (currentSettings.get(type).selected.containsKey(PmdSetting.PmdSettingType.FACTOR)) { - int ieee754 = currentSettings.get(type).selected.get(PmdSetting.PmdSettingType.FACTOR); - return Float.intBitsToFloat(ieee754); - } else { - BleLogger.e(TAG, "No factor found for type: " + type); - return 1.0f; - } - } - - private int fetchSetting(PmdMeasurementType type, PmdSetting.PmdSettingType setting) { - BleUtils.validate(currentSettings.containsKey(type), type.toString() + " setting not stored"); - BleUtils.validate(Objects.requireNonNull(currentSettings.get(type)).selected.containsKey(setting), type.toString() + " setting not stored"); - return currentSettings.get(type).selected.get(setting); - } - - @Override - public void processServiceData(@NonNull UUID characteristic, final @NonNull byte[] data, int status, boolean notifying) { - if (characteristic.equals(PMD_CP)) { - if (notifying) { - pmdCpInputQueue.add(new Pair<>(data, status)); - } else { - // feature read - synchronized (mutexFeature) { - pmdFeatureData = data; - mutexFeature.notifyAll(); - } - } - } else if (characteristic.equals(PMD_DATA)) { - if (status == 0) { - - BleLogger.d_hex(TAG, "pmd data: ", data); - - PmdMeasurementType type = PmdMeasurementType.fromId(data[0]); - final long timeStamp = BleUtils.convertArrayToUnsignedLong(data, 1, 8); - final long frameType = BleUtils.convertArrayToUnsignedLong(data, 9, 1); - final byte[] content = new byte[data.length - 10]; - System.arraycopy(data, 10, content, 0, content.length); - switch (type) { - case ECG: - if (frameType <= 2) { - RxUtils.emitNext(ecgObservers, object -> object.onNext(new EcgData((byte) frameType, content, timeStamp))); - } else { - BleLogger.w(TAG, "Unknown ECG frame type received"); - } - break; - case PPG: - switch (PpgData.PpgFrameType.fromId((int) frameType)) { - case PPG1_TYPE: - case PPG0_TYPE: { - RxUtils.emitNext(ppgObservers, object -> object.onNext(new PpgData(content, timeStamp, (int) frameType))); - break; - } - case AFE4410: { - RxUtils.emitNext(autoGainAFE4410Observers, object -> object.onNext(new AutoGainAFE4410(content, timeStamp))); - break; - } - case AFE4404: { - RxUtils.emitNext(autoGainAFE4404Observers, object -> object.onNext(new AutoGainAFE4404(content, timeStamp))); - break; - } - case ADPD4000: { - RxUtils.emitNext(autoGainADPD4000Observers, object -> object.onNext(new AutoGainADPD4000(content, timeStamp))); - break; - } - case AFE_OPERATION_MODE: { - RxUtils.emitNext(afeOperationModeObservers, object -> { - Long value = BleUtils.convertArrayToUnsignedLong(content, 0, content.length); - object.onNext(new Pair<>(timeStamp, value)); - }); - break; - } - case SPORT_ID: { - RxUtils.emitNext(sportIdObservers, object -> { - final long sportId = BleUtils.convertArrayToUnsignedLong(content, 0, 8); - object.onNext(new Pair<>(timeStamp, sportId)); - }); - break; - } - case DELTA_FRAME: { - float factor = fetchFactor(PmdMeasurementType.PPG); - int resolution = fetchSetting(PmdMeasurementType.PPG, PmdSetting.PmdSettingType.RESOLUTION); - int channels = fetchSetting(PmdMeasurementType.PPG, PmdSetting.PmdSettingType.CHANNELS); - RxUtils.emitNext(ppgObservers, object -> object.onNext( - new PpgData(content, factor, resolution, channels, timeStamp))); - break; - } - default: - BleLogger.w(TAG, "Unknown PPG frame type received"); - break; - } - break; - case ACC: - if (frameType <= 2) { - RxUtils.emitNext(accObservers, object -> object.onNext(new AccData((byte) frameType, content, timeStamp))); - } else if (frameType == 128) { - float factor = fetchFactor(PmdMeasurementType.ACC); - int resolution = fetchSetting(PmdMeasurementType.ACC, PmdSetting.PmdSettingType.RESOLUTION); - RxUtils.emitNext(accObservers, object -> object.onNext(new AccData(content, factor, resolution, timeStamp))); - - } else { - BleLogger.w(TAG, "Unknown ACC frame type received"); - } - break; - case PPI: - if (frameType == 0) { - RxUtils.emitNext(ppiObservers, object -> object.onNext(new PpiData(content, timeStamp))); - } else { - BleLogger.w(TAG, "Unknown PPI frame type received"); - } - break; - case BIOZ: - if (frameType == 0 || frameType == 1) { - RxUtils.emitNext(biozObservers, object -> object.onNext( - new BiozData(content, timeStamp, (byte) frameType))); - } else { - BleLogger.w(TAG, "Unknown BIOZ frame type received"); - } - break; - case GYRO: - if (frameType == 128) { - float factor = fetchFactor(PmdMeasurementType.GYRO); - int resolution = fetchSetting(PmdMeasurementType.GYRO, PmdSetting.PmdSettingType.RESOLUTION); - RxUtils.emitNext(gyroObservers, object -> object.onNext( - new GyrData(content, factor, resolution, timeStamp))); - } else { - BleLogger.w(TAG, "Unknown GYRO frame type received"); - } - break; - case MAGNETOMETER: - if (frameType == 128) { - float factor = fetchFactor(PmdMeasurementType.MAGNETOMETER); - int resolution = fetchSetting(PmdMeasurementType.MAGNETOMETER, PmdSetting.PmdSettingType.RESOLUTION); - RxUtils.emitNext(magnetometerObservers, object -> object.onNext( - new MagData(content, factor, resolution, timeStamp))); - } else { - BleLogger.w(TAG, "Unknown MAGNETOMETER frame type received"); - } - break; - default: - final byte[] rdData = new byte[data.length - 1]; - System.arraycopy(data, 1, content, 0, content.length); - RxUtils.emitNext(rdObservers, object -> object.onNext(rdData)); - break; - } - } else { - BleLogger.e(TAG, "pmd data attribute error"); - } - } - } - - @Override - public void processServiceDataWritten(UUID characteristic, int status) { - // do nothing - } - - @NonNull - @Override - public String toString() { - return "PMD Client"; - } - - private byte[] receiveControlPointPacket() throws Exception { - Pair pair = pmdCpInputQueue.poll(30, TimeUnit.SECONDS); - - if (pair != null) { - if (pair.second == 0) { - return pair.first; - } - throw new BleAttributeError("pmd cp attribute error: ", pair.second); - } - throw new Exception("Pmd response failed to receive in timeline"); - } - - private BlePMDClient.PmdControlPointResponse sendPmdCommand(byte[] packet) throws Exception { - txInterface.transmitMessage(BlePMDClient.this, BlePMDClient.PMD_SERVICE, BlePMDClient.PMD_CP, packet, true); - byte[] first = receiveControlPointPacket(); - BlePMDClient.PmdControlPointResponse response = new BlePMDClient.PmdControlPointResponse(first); - boolean more = response.more; - while (more) { - byte[] moreParameters = receiveControlPointPacket(); - more = moreParameters[0] != 0; - response.parameters.write(moreParameters, 1, moreParameters.length - 1); - } - return response; - } - - private Single sendControlPointCommand(final PmdControlPointCommand command, final byte value) { - return sendControlPointCommand(command, new byte[]{value}); - } - - private Single sendControlPointCommand(final PmdControlPointCommand command, final byte[] params) { - return Single.create((SingleOnSubscribe) subscriber -> { - synchronized (controlPointMutex) { - try { - if (pmdCpEnabled.get() == ATT_SUCCESS && pmdDataEnabled.get() == ATT_SUCCESS) { - ByteBuffer bb = ByteBuffer.allocate(1 + params.length); - bb.put(new byte[]{(byte) command.getNumVal()}); - bb.put(params); - PmdControlPointResponse response = sendPmdCommand(bb.array()); - if (response.status == PmdControlPointResponse.PmdControlPointResponseCode.SUCCESS) { - subscriber.onSuccess(response); - return; - } - throw new BleControlPointCommandError("pmd cp command error: ", response.status); - } - throw new BleCharacteristicNotificationNotEnabled(); - } catch (Throwable throwable) { - if (!subscriber.isDisposed()) { - subscriber.tryOnError(throwable); - } - } - } - }).subscribeOn(Schedulers.io()); - } - - /** - * Query settings by type - * - * @return Single stream - * - onSuccess settings query success, the queried settings emitted - * - onError settings query failed - */ - @NonNull - public Single querySettings(@NonNull PmdMeasurementType type) { - return sendControlPointCommand(PmdControlPointCommand.GET_MEASUREMENT_SETTINGS, (byte) type.getNumVal()) - .map(pmdControlPointResponse -> new PmdSetting(pmdControlPointResponse.parameters.toByteArray())); - } - - /** - * Query full settings by type - * - * @return Single stream - * - onSuccess full settings query success, the queried settings emitted - * - onError full settings query failed - */ - @NonNull - public Single queryFullSettings(@NonNull PmdMeasurementType type) { - return sendControlPointCommand(PmdControlPointCommand.GET_SDK_MODE_MEASUREMENT_SETTINGS, (byte) type.getNumVal()) - .map(pmdControlPointResponse -> new PmdSetting(pmdControlPointResponse.parameters.toByteArray())); - } - - /** - * @return Single stream - */ - @NonNull - public Single readFeature(final boolean checkConnection) { - return Single.create((SingleOnSubscribe) emitter -> { - try { - if (!checkConnection || txInterface.isConnected()) { - synchronized (mutexFeature) { - if (pmdFeatureData == null) { - mutexFeature.wait(); - } - if (pmdFeatureData != null) { - emitter.onSuccess(new PmdFeature(pmdFeatureData)); - return; - } else if (!txInterface.isConnected()) { - throw new BleDisconnected(); - } - throw new Exception("Undefined device error"); - } - } - throw new BleDisconnected(); - } catch (Exception ex) { - if (!emitter.isDisposed()) { - emitter.tryOnError(ex); - } - } - }).subscribeOn(Schedulers.io()); - } - - /** - * query bioz settings available on device - * - * @return Single stream - */ - public Single queryBiozSettings() { - return querySettings(PmdMeasurementType.BIOZ); - } - - /** - * request to start a specific measurement - * - * @param type measurement to start - * @param setting desired settings - * @return Completable stream - */ - @NonNull - public Completable startMeasurement(@NonNull final PmdMeasurementType type, @NonNull final PmdSetting setting) { - byte[] set = setting.serializeSelected(); - ByteBuffer bb = ByteBuffer.allocate(1 + set.length); - bb.put((byte) type.getNumVal()); - bb.put(set); - currentSettings.put(type, setting); - return sendControlPointCommand(PmdControlPointCommand.REQUEST_MEASUREMENT_START, bb.array()) - .doOnSuccess(pmdControlPointResponse -> - currentSettings.get(type).updateSelectedFromStartResponse(pmdControlPointResponse.parameters.toByteArray())) - .toObservable() - .ignoreElements(); - } - - /** - * Request to start SDK mode - * - * @return Completable stream - * - onComplete start SDK mode request completed successfully - * - onError start SDK mode request failed - */ - @NonNull - public Completable startSDKMode() { - return sendControlPointCommand(PmdControlPointCommand.REQUEST_MEASUREMENT_START, (byte) PmdMeasurementType.SDK_MODE.getNumVal()) - .toObservable() - .doOnComplete(() -> clearStreamObservers(new BleOperationModeChange("SDK mode enabled"))) - .ignoreElements(); - } - - /** - * Request to stop SDK mode - * - * @return Completable stream - * - onComplete stop SDK mode request completed successfully - * - onError stop SDK mode request failed - */ - @NonNull - public Completable stopSDKMode() { - return sendControlPointCommand(PmdControlPointCommand.STOP_MEASUREMENT, (byte) PmdMeasurementType.SDK_MODE.getNumVal()) - .toObservable() - .doOnComplete(() -> clearStreamObservers(new BleOperationModeChange("SDK mode disabled"))) - .ignoreElements(); - } - - /** - * Request to stop measurement - * - * @param type measurement to stop - * @return Completable stream - */ - @NonNull - public Completable stopMeasurement(final @NonNull PmdMeasurementType type) { - return sendControlPointCommand(PmdControlPointCommand.STOP_MEASUREMENT, new byte[]{(byte) type.numVal}) - .toObservable() - .ignoreElements(); - } - - /** - * start raw ecg monitoring - * - * @return Flowable stream Produces: - * - onNext for every air packet received
- * - onComplete non produced if stream is not further configured
- * - onError BleDisconnected produced on disconnection
- */ - @NonNull - public Flowable monitorEcgNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(ecgObservers, txInterface, checkConnection); - } - - /** - * start raw acc monitoring - * - * @return Flowable stream Produces: - * - onNext for every air packet received
- * - onComplete non produced if stream is not further configured
- * - onError BleDisconnected produced on disconnection
- */ - @NonNull - public Flowable monitorAccNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(accObservers, txInterface, checkConnection); - } - - /** - * start raw ppg monitoring - * - * @return Flowable stream Produces: - * - onNext for every air packet received
- * - onComplete non produced if stream is not further configured
- * - onError BleDisconnected produced on disconnection
- */ - @NonNull - public Flowable monitorPpgNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(ppgObservers, txInterface, checkConnection); - } - - /** - * start raw bioz monitoring - * - * @param checkConnection check initial connection - * @return Flowable stream - */ - @NonNull - public Flowable monitorBiozNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(biozObservers, txInterface, checkConnection); - } - - /** - * start raw ppi monitoring - * - * @return Flowable stream Produces: - * - onNext for every air packet received
- * - onComplete non produced if stream is not further configured
- * - onError BleDisconnected produced on disconnection
- */ - @NonNull - public Flowable monitorPpiNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(ppiObservers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorMagnetometerNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(magnetometerObservers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorGyroNotifications(final boolean checkConnection) { - return RxUtils.monitorNotifications(gyroObservers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorAutoGainAFE4404(final boolean checkConnection) { - return RxUtils.monitorNotifications(autoGainAFE4404Observers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorAutoGainAFE4410(final boolean checkConnection) { - return RxUtils.monitorNotifications(autoGainAFE4410Observers, txInterface, checkConnection); - } - - @NonNull - public Flowable> monitorAfeOperationMode(final boolean checkConnection) { - return RxUtils.monitorNotifications(afeOperationModeObservers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorAutoGainADPD4000(final boolean checkConnection) { - return RxUtils.monitorNotifications(autoGainADPD4000Observers, txInterface, checkConnection); - } - - @NonNull - public Flowable> monitorSportId(final boolean checkConnection) { - return RxUtils.monitorNotifications(sportIdObservers, txInterface, checkConnection); - } - - @NonNull - public Flowable monitorRDData(final boolean checkConnection) { - return RxUtils.monitorNotifications(rdObservers, txInterface, checkConnection); - } - - @NonNull - @Override - public Completable clientReady(boolean checkConnection) { - return Completable.concatArray(waitNotificationEnabled(PMD_CP, true), - waitNotificationEnabled(PMD_DATA, true)); - } - - /** - * @return current pmd feature or null if no features not available - */ - @Nullable - public PmdFeature getPmdFeatureData() { - synchronized (mutexFeature) { - if (pmdFeatureData != null) { - return new PmdFeature(pmdFeatureData); - } else { - return null; - } - } - } - - private void clearStreamObservers(@NonNull Throwable throwable) { - RxUtils.postExceptionAndClearList(ecgObservers, throwable); - RxUtils.postExceptionAndClearList(accObservers, throwable); - RxUtils.postExceptionAndClearList(ppgObservers, throwable); - RxUtils.postExceptionAndClearList(ppiObservers, throwable); - RxUtils.postExceptionAndClearList(autoGainAFE4404Observers, throwable); - RxUtils.postExceptionAndClearList(autoGainAFE4410Observers, throwable); - RxUtils.postExceptionAndClearList(autoGainADPD4000Observers, throwable); - RxUtils.postExceptionAndClearList(biozObservers, throwable); - RxUtils.postExceptionAndClearList(afeOperationModeObservers, throwable); - RxUtils.postExceptionAndClearList(sportIdObservers, throwable); - RxUtils.postExceptionAndClearList(rdObservers, throwable); - RxUtils.postExceptionAndClearList(gyroObservers, throwable); - RxUtils.postExceptionAndClearList(magnetometerObservers, throwable); - } -} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt new file mode 100644 index 00000000..81b2d3b4 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClient.kt @@ -0,0 +1,560 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import android.util.Pair +import androidx.annotation.VisibleForTesting +import com.polar.androidcommunications.api.ble.BleLogger +import com.polar.androidcommunications.api.ble.exceptions.* +import com.polar.androidcommunications.api.ble.model.gatt.BleGattBase +import com.polar.androidcommunications.api.ble.model.gatt.BleGattTxInterface +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils.getDataFrameType +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils.isCompressedFrame +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdMeasurementType.Companion.fromId +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdSetting.PmdSettingType +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.* +import com.polar.androidcommunications.common.ble.AtomicSet +import com.polar.androidcommunications.common.ble.BleUtils +import com.polar.androidcommunications.common.ble.RxUtils +import io.reactivex.rxjava3.core.* +import io.reactivex.rxjava3.schedulers.Schedulers +import java.nio.ByteBuffer +import java.util.* +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.math.ceil +/** +* BLE Client for Polar Measurement Data Service (aka. PMD service) +* */ +class BlePMDClient(txInterface: BleGattTxInterface) : BleGattBase(txInterface, PMD_SERVICE) { + private val pmdCpInputQueue = LinkedBlockingQueue>() + private val ecgObservers = AtomicSet>() + private val accObservers = AtomicSet>() + private val gyroObservers = AtomicSet>() + private val magnetometerObservers = AtomicSet>() + private val ppgObservers = AtomicSet>() + private val ppiObservers = AtomicSet>() + private val pressureObservers = AtomicSet>() + private val locationObservers = AtomicSet>() + private val rdObservers = AtomicSet>() + private var pmdFeatureData: ByteArray? = null + private val controlPointMutex = Object() + private val mutexFeature = Object() + + @VisibleForTesting + val currentSettings: MutableMap = HashMap() + private val pmdCpEnabled: AtomicInteger + private val pmdDataEnabled: AtomicInteger + + enum class PmdDataFieldEncoding { + FLOAT_IEEE754, + DOUBLE_IEEE754, + SIGNED_INT, + UNSIGNED_BYTE, + UNSIGNED_INT, + UNSIGNED_LONG, + BOOLEAN + } + + enum class PmdDataFrameType(val id: Int) { + TYPE_0(0), + TYPE_1(1), + TYPE_2(2), + TYPE_3(3), + TYPE_4(4), + TYPE_5(5), + TYPE_6(6), + TYPE_7(7); + + companion object { + fun getTypeById(id: Int): PmdDataFrameType { + for (type in values()) { + if (type.id == id) { + return type + } + } + throw BleNotImplemented("FrameType id:$id is not implemented") + } + } + } + + enum class PmdFrameCompressionType(val numVal: Int) { + DELTA_FRAME(0x80); + } + + override fun reset() { + super.reset() + clearStreamObservers(BleDisconnected()) + synchronized(mutexFeature) { + pmdFeatureData = null + mutexFeature.notifyAll() + } + } + + private fun fetchFactor(type: PmdMeasurementType): Float { + currentSettings[type]?.selected?.get(PmdSettingType.FACTOR)?.let { + val ieee754 = it + return java.lang.Float.intBitsToFloat(ieee754) + } + BleLogger.e(TAG, "No factor found for type: $type") + return 1.0f + } + + override fun processServiceData(characteristic: UUID, data: ByteArray, status: Int, notifying: Boolean) { + if (characteristic == PMD_CP) { + if (notifying) { + pmdCpInputQueue.add(Pair(data, status)) + } else { + // feature read + synchronized(mutexFeature) { + pmdFeatureData = data + mutexFeature.notifyAll() + } + } + } else if (characteristic == PMD_DATA) { + if (status == 0) { + BleLogger.d_hex(TAG, "pmd data: ", data) + val type = fromId(data[0]) + val timeStamp = BleUtils.convertArrayToUnsignedLong(data, 1, 8) + val frameTypeField = BleUtils.convertArrayToUnsignedLong(data, 9, 1) + val frameType = getDataFrameType(frameTypeField) + val isCompressedFrameType = isCompressedFrame(frameTypeField) + val content = ByteArray(data.size - 10) + System.arraycopy(data, 10, content, 0, content.size) + when (type) { + PmdMeasurementType.ECG -> { + val factor = fetchFactor(PmdMeasurementType.ECG) + RxUtils.emitNext(ecgObservers) { emitter: FlowableEmitter -> + emitter.onNext(EcgData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.PPG -> { + val factor = fetchFactor(PmdMeasurementType.PPG) + RxUtils.emitNext(ppgObservers) { emitter: FlowableEmitter -> + emitter.onNext(PpgData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.ACC -> { + val factor = fetchFactor(PmdMeasurementType.ACC) + RxUtils.emitNext(accObservers) { emitter: FlowableEmitter -> + emitter.onNext(AccData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.PPI -> { + val factor = fetchFactor(PmdMeasurementType.PPI) + RxUtils.emitNext(ppiObservers) { emitter: FlowableEmitter -> + emitter.onNext(PpiData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.GYRO -> { + val factor = fetchFactor(PmdMeasurementType.GYRO) + RxUtils.emitNext(gyroObservers) { emitter: FlowableEmitter -> + emitter.onNext(GyrData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.MAGNETOMETER -> { + val factor = fetchFactor(PmdMeasurementType.MAGNETOMETER) + RxUtils.emitNext(magnetometerObservers) { emitter: FlowableEmitter -> + emitter.onNext(MagData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.PRESSURE -> { + val factor = fetchFactor(PmdMeasurementType.PRESSURE) + RxUtils.emitNext(pressureObservers) { emitter: FlowableEmitter -> + emitter.onNext(PressureData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + PmdMeasurementType.LOCATION -> { + val factor = fetchFactor(PmdMeasurementType.LOCATION) + RxUtils.emitNext(locationObservers) { emitter: FlowableEmitter -> + emitter.onNext(GnssLocationData.parseDataFromDataFrame(isCompressedFrameType, frameType, content, factor, timeStamp)) + } + } + else -> { + val rdData = ByteArray(data.size - 1) + System.arraycopy(data, 1, content, 0, content.size) + RxUtils.emitNext(rdObservers) { emitter: FlowableEmitter -> emitter.onNext(rdData) } + } + } + } else { + BleLogger.e(TAG, "pmd data attribute error") + } + } + } + + override fun processServiceDataWritten(characteristic: UUID, status: Int) { + // do nothing + } + + override fun toString(): String { + return "PMD Client" + } + + + @Throws(Exception::class) + private fun receiveControlPointPacket(): ByteArray { + val pair = pmdCpInputQueue.poll(30, TimeUnit.SECONDS) + if (pair != null) { + if (pair.second == 0) { + return pair.first + } + throw BleAttributeError("pmd cp attribute error: ", pair.second) + } + throw Exception("Pmd response failed to receive in timeline") + } + + @Throws(Exception::class) + private fun sendPmdCommand(packet: ByteArray): PmdControlPointResponse { + txInterface.transmitMessage(this@BlePMDClient, PMD_SERVICE, PMD_CP, packet, true) + val first = receiveControlPointPacket() + val response = PmdControlPointResponse(first) + var more = response.more + while (more) { + val moreParameters = receiveControlPointPacket() + more = moreParameters[0].toInt() != 0 + response.parameters.write(moreParameters, 1, moreParameters.size - 1) + } + return response + } + + private fun sendControlPointCommand(command: PmdControlPointCommand, value: Byte): Single { + return sendControlPointCommand(command, byteArrayOf(value)) + } + + private fun sendControlPointCommand(command: PmdControlPointCommand, params: ByteArray): Single { + return Single.create(SingleOnSubscribe { subscriber: SingleEmitter -> + synchronized(controlPointMutex) { + try { + if (pmdCpEnabled.get() == ATT_SUCCESS && pmdDataEnabled.get() == ATT_SUCCESS) { + val bb = ByteBuffer.allocate(1 + params.size) + bb.put(byteArrayOf(command.numVal.toByte())) + bb.put(params) + val response = sendPmdCommand(bb.array()) + if (response.status === PmdControlPointResponse.PmdControlPointResponseCode.SUCCESS) { + subscriber.onSuccess(response) + return@SingleOnSubscribe + } + throw BleControlPointCommandError("pmd cp command error: ", response.status) + } + throw BleCharacteristicNotificationNotEnabled() + } catch (throwable: Throwable) { + if (!subscriber.isDisposed) { + subscriber.tryOnError(throwable) + } + } + } + } as SingleOnSubscribe) + .subscribeOn(Schedulers.io()) + } + + /** + * Query settings by type + * + * @return Single stream + * - onSuccess settings query success, the queried settings emitted + * - onError settings query failed + */ + fun querySettings(type: PmdMeasurementType): Single { + return sendControlPointCommand(PmdControlPointCommand.GET_MEASUREMENT_SETTINGS, type.numVal.toByte()) + .map { pmdControlPointResponse: PmdControlPointResponse -> PmdSetting(pmdControlPointResponse.parameters.toByteArray()) } + } + + /** + * Query full settings by type + * + * @return Single stream + * - onSuccess full settings query success, the queried settings emitted + * - onError full settings query failed + */ + fun queryFullSettings(type: PmdMeasurementType): Single { + return sendControlPointCommand(PmdControlPointCommand.GET_SDK_MODE_MEASUREMENT_SETTINGS, type.numVal.toByte()) + .map { pmdControlPointResponse: PmdControlPointResponse -> PmdSetting(pmdControlPointResponse.parameters.toByteArray()) } + } + + /** + * @return Single stream + */ + fun readFeature(checkConnection: Boolean): Single { + return Single.create(SingleOnSubscribe { emitter: SingleEmitter -> + try { + if (!checkConnection || txInterface.isConnected) { + synchronized(mutexFeature) { + if (pmdFeatureData == null) { + mutexFeature.wait() + } + if (pmdFeatureData != null) { + emitter.onSuccess(PmdFeature(pmdFeatureData!!)) + return@SingleOnSubscribe + } else if (!txInterface.isConnected) { + throw BleDisconnected() + } + throw Exception("Undefined device error") + } + } + throw BleDisconnected() + } catch (ex: Exception) { + if (!emitter.isDisposed) { + emitter.tryOnError(ex) + } + } + }).subscribeOn(Schedulers.io()) + } + + /** + * request to start a specific measurement + * + * @param type measurement to start + * @param setting desired settings + * @return Completable stream + */ + fun startMeasurement(type: PmdMeasurementType, setting: PmdSetting): Completable { + val set = setting.serializeSelected() + val bb = ByteBuffer.allocate(1 + set.size) + bb.put(type.numVal.toByte()) + bb.put(set) + currentSettings[type] = setting + + return sendControlPointCommand(PmdControlPointCommand.REQUEST_MEASUREMENT_START, bb.array()) + .doOnSuccess { pmdControlPointResponse: PmdControlPointResponse -> currentSettings[type]!!.updateSelectedFromStartResponse(pmdControlPointResponse.parameters.toByteArray()) } + .toObservable() + .ignoreElements() + } + + /** + * Request to start SDK mode + * + * @return Completable stream + * - onComplete start SDK mode request completed successfully + * - onError start SDK mode request failed + */ + fun startSDKMode(): Completable { + return sendControlPointCommand(PmdControlPointCommand.REQUEST_MEASUREMENT_START, PmdMeasurementType.SDK_MODE.numVal.toByte()) + .toObservable() + .doOnComplete { clearStreamObservers(BleOperationModeChange("SDK mode enabled")) } + .ignoreElements() + } + + /** + * Request to stop SDK mode + * + * @return Completable stream + * - onComplete stop SDK mode request completed successfully + * - onError stop SDK mode request failed + */ + fun stopSDKMode(): Completable { + return sendControlPointCommand(PmdControlPointCommand.STOP_MEASUREMENT, PmdMeasurementType.SDK_MODE.numVal.toByte()) + .toObservable() + .doOnComplete { clearStreamObservers(BleOperationModeChange("SDK mode disabled")) } + .ignoreElements() + } + + /** + * Request to stop measurement + * + * @param type measurement to stop + * @return Completable stream + */ + fun stopMeasurement(type: PmdMeasurementType): Completable { + return sendControlPointCommand(PmdControlPointCommand.STOP_MEASUREMENT, byteArrayOf(type.numVal.toByte())) + .toObservable() + .ignoreElements() + } + + /** + * start raw ecg monitoring + * + * @return Flowable stream Produces: + * - onNext for every air packet received

+ * - onComplete non produced if stream is not further configured

+ * - onError BleDisconnected produced on disconnection

+ */ + fun monitorEcgNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(ecgObservers, txInterface, checkConnection) + } + + /** + * start raw acc monitoring + * + * @return Flowable stream Produces: + * - onNext for every air packet received

+ * - onComplete non produced if stream is not further configured

+ * - onError BleDisconnected produced on disconnection

+ */ + fun monitorAccNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(accObservers, txInterface, checkConnection) + } + + /** + * start raw ppg monitoring + * + * @return Flowable stream Produces: + * - onNext for every air packet received

+ * - onComplete non produced if stream is not further configured

+ * - onError BleDisconnected produced on disconnection

+ */ + fun monitorPpgNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(ppgObservers, txInterface, checkConnection) + } + + /** + * start raw ppi monitoring + * + * @return Flowable stream Produces: + * - onNext for every air packet received

+ * - onComplete non produced if stream is not further configured

+ * - onError BleDisconnected produced on disconnection

+ */ + fun monitorPpiNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(ppiObservers, txInterface, checkConnection) + } + + fun monitorMagnetometerNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(magnetometerObservers, txInterface, checkConnection) + } + + fun monitorGyroNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(gyroObservers, txInterface, checkConnection) + } + + fun monitorPressureNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(pressureObservers, txInterface, checkConnection) + } + + fun monitorLocationNotifications(checkConnection: Boolean): Flowable { + return RxUtils.monitorNotifications(locationObservers, txInterface, checkConnection) + } + + override fun clientReady(checkConnection: Boolean): Completable { + return Completable.concatArray( + waitNotificationEnabled(PMD_CP, true), + waitNotificationEnabled(PMD_DATA, true) + ) + } + + /** + * @return current pmd feature or null if no features not available + */ + fun getPmdFeatureData(): PmdFeature? { + synchronized(mutexFeature) { + return if (pmdFeatureData != null) { + PmdFeature(pmdFeatureData!!) + } else { + null + } + } + } + + private fun clearStreamObservers(throwable: Throwable) { + RxUtils.postExceptionAndClearList(ecgObservers, throwable) + RxUtils.postExceptionAndClearList(accObservers, throwable) + RxUtils.postExceptionAndClearList(ppgObservers, throwable) + RxUtils.postExceptionAndClearList(ppiObservers, throwable) + RxUtils.postExceptionAndClearList(rdObservers, throwable) + RxUtils.postExceptionAndClearList(gyroObservers, throwable) + RxUtils.postExceptionAndClearList(magnetometerObservers, throwable) + RxUtils.postExceptionAndClearList(pressureObservers, throwable) + RxUtils.postExceptionAndClearList(locationObservers, throwable) + } + + companion object { + private const val TAG = "BlePMDClient" + + @JvmField + val PMD_DATA: UUID = UUID.fromString("FB005C82-02E7-F387-1CAD-8ACD2D8DF0C8") + + @JvmField + val PMD_CP: UUID = UUID.fromString("FB005C81-02E7-F387-1CAD-8ACD2D8DF0C8") + + @JvmField + val PMD_SERVICE: UUID = UUID.fromString("FB005C80-02E7-F387-1CAD-8ACD2D8DF0C8") + + private fun parseDeltaFrame(bytes: ByteArray, channels: Int, bitWidth: Int, totalBitLength: Int): List> { + var offset = 0 + val bitSet: MutableList = ArrayList() + for (b in bytes) { + for (i in 0..7) { + bitSet.add(b.toInt() and (0x01 shl i) != 0) + } + } + val samples: MutableList> = ArrayList() + val mask = Int.MAX_VALUE shl bitWidth - 1 + while (offset < totalBitLength) { + val channelSamples: MutableList = ArrayList() + var channelCount = 0 + while (channelCount++ < channels) { + val bits: List = bitSet.subList(offset, offset + bitWidth) + var value = 0 + for (i in bits.indices) { + value = value or ((if (bits[i]) 0x01 else 0x00) shl i) + } + if (value and mask != 0) { + value = value or mask + } + offset += bitWidth + channelSamples.add(value) + } + samples.add(channelSamples) + } + return samples + } + + @VisibleForTesting + fun parseDeltaFrameRefSamples(bytes: ByteArray, channels: Int, resolution: Int, type: PmdDataFieldEncoding): List { + val samples: MutableList = ArrayList() + var offset = 0 + var channelCount = 0 + val mask = -0x1 shl resolution - 1 + val resolutionInBytes = Math.ceil(resolution / 8.0).toInt() + while (channelCount++ < channels) { + var sample: Int + if (type == PmdDataFieldEncoding.SIGNED_INT) { + sample = BleUtils.convertArrayToSignedInt(bytes, offset, resolutionInBytes) + if (sample and mask != 0) { + sample = sample or mask + } + } else { + sample = BleUtils.convertArrayToUnsignedInt(bytes, offset, resolutionInBytes) + } + offset += resolutionInBytes + samples.add(sample) + } + return samples + } + + fun parseDeltaFramesAll(value: ByteArray, channels: Int, resolution: Int, type: PmdDataFieldEncoding): List> { + var offset = 0 + val refSamples = parseDeltaFrameRefSamples(value, channels, resolution, type) + offset += (channels * ceil(resolution / 8.0)).toInt() + val samples: MutableList> = ArrayList(setOf(refSamples)) + BleUtils.validate(refSamples.size == channels, "incorrect number of ref channels") + while (offset < value.size) { + val deltaSize: Int = value[offset++].toInt() and 0xFF + val sampleCount: Int = value[offset++].toInt() and 0xFF + val bitLength = sampleCount * deltaSize * channels + val length = ceil(bitLength / 8.0).toInt() + val deltaFrame = ByteArray(length) + System.arraycopy(value, offset, deltaFrame, 0, deltaFrame.size) + val deltaSamples = parseDeltaFrame(deltaFrame, channels, deltaSize, bitLength) + for (delta in deltaSamples) { + BleUtils.validate(delta.size == channels, "incorrect number of delta channels") + val lastSample = samples[samples.size - 1] + val nextSamples: MutableList = ArrayList() + for (i in 0 until channels) { + val sample = lastSample[i] + delta[i] + nextSamples.add(sample) + } + samples.addAll(setOf>(nextSamples)) + } + offset += length + } + return samples + } + } + + init { + addCharacteristicNotification(PMD_CP) + addCharacteristicRead(PMD_CP) + addCharacteristicNotification(PMD_DATA) + pmdCpEnabled = getNotificationAtomicInteger(PMD_CP) + pmdDataEnabled = getNotificationAtomicInteger(PMD_DATA) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClientUtils.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClientUtils.kt new file mode 100644 index 00000000..5dd702ff --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePMDClientUtils.kt @@ -0,0 +1,52 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.common.ble.BleUtils +import com.polar.androidcommunications.common.ble.TypeUtils +import java.lang.Double.longBitsToDouble +import java.lang.Float.intBitsToFloat + +object BlePMDClientUtils { + fun parseFrameDataField(data: ByteArray, coding: BlePMDClient.PmdDataFieldEncoding): Any { + when (coding) { + BlePMDClient.PmdDataFieldEncoding.FLOAT_IEEE754 -> { + BleUtils.validate(data.size == 4, "PMD parser expects data size 4 when FLOAT_IEEE754 parsed. Input data size was " + data.size) + val intIEEE754 = BleUtils.convertArrayToUnsignedInt(data, 0, data.size) + return intBitsToFloat(intIEEE754) + } + BlePMDClient.PmdDataFieldEncoding.DOUBLE_IEEE754 -> { + BleUtils.validate(data.size == 8, "PMD parser expects data size 8 when DOUBLE_IEEE754 parsed. Input data size was " + data.size) + val longIEEE754 = BleUtils.convertArrayToUnsignedLong(data, 0, data.size) + return longBitsToDouble(longIEEE754) + } + BlePMDClient.PmdDataFieldEncoding.SIGNED_INT -> { + BleUtils.validate(data.size <= 4, "PMD parser expects data size smaller than 4 when SIGNED_INT parsed. Input data size was " + data.size) + return BleUtils.convertArrayToSignedInt(data, 0, data.size) + } + BlePMDClient.PmdDataFieldEncoding.UNSIGNED_BYTE -> { + BleUtils.validate(data.size == 1, "PMD parser expects data size 1 when UNSIGNED_BYTE parsed. Input data size was " + data.size) + return TypeUtils.convertArrayToUnsignedByte(data) + } + + BlePMDClient.PmdDataFieldEncoding.UNSIGNED_INT -> { + BleUtils.validate(data.size <= 4, "PMD parser expects data size smaller than 4 when UNSIGNED_INT parsed. Input data size was " + data.size) + return TypeUtils.convertArrayToUnsignedInt(data) + } + BlePMDClient.PmdDataFieldEncoding.UNSIGNED_LONG -> { + BleUtils.validate(data.size <= 8, "PMD parser expects data size smaller than 8 when UNSIGNED_LONG parsed. Input data size was " + data.size) + return TypeUtils.convertArrayToUnsignedLong(data) + } + BlePMDClient.PmdDataFieldEncoding.BOOLEAN -> { + BleUtils.validate(data.size == 1, "PMD parser expects data size 1 when BOOLEAN parsed. Input data size was " + data.size) + return (data[0].toInt() != 0) + } + } + } + + fun isCompressedFrame(frameType: Long): Boolean { + return frameType and BlePMDClient.PmdFrameCompressionType.DELTA_FRAME.numVal.toLong() > 0 + } + + fun getDataFrameType(frameType: Long): BlePMDClient.PmdDataFrameType { + return BlePMDClient.PmdDataFrameType.getTypeById((frameType and 0x7F).toInt()) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointCommand.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointCommand.kt new file mode 100644 index 00000000..146140b7 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointCommand.kt @@ -0,0 +1,9 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +enum class PmdControlPointCommand(val numVal: Int) { + NULL_ITEM(0), + GET_MEASUREMENT_SETTINGS(1), + REQUEST_MEASUREMENT_START(2), + STOP_MEASUREMENT(3), + GET_SDK_MODE_MEASUREMENT_SETTINGS(4); +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointResponse.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointResponse.kt new file mode 100644 index 00000000..40ae2d2a --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdControlPointResponse.kt @@ -0,0 +1,48 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import java.io.ByteArrayOutputStream + +class PmdControlPointResponse(data: ByteArray) { + val responseCode: Byte = data[0] + val opCode: PmdControlPointCommand = PmdControlPointCommand.values()[data[1].toInt()] + val measurementType: Byte + + @JvmField + val status: PmdControlPointResponseCode + + @JvmField + val parameters = ByteArrayOutputStream() + + @JvmField + var more = false + + enum class PmdControlPointResponseCode(val numVal: Int) { + SUCCESS(0), + ERROR_INVALID_OP_CODE(1), + ERROR_INVALID_MEASUREMENT_TYPE(2), + ERROR_NOT_SUPPORTED(3), + ERROR_INVALID_LENGTH(4), + ERROR_INVALID_PARAMETER(5), + ERROR_ALREADY_IN_STATE(6), + ERROR_INVALID_RESOLUTION(7), + ERROR_INVALID_SAMPLE_RATE(8), + ERROR_INVALID_RANGE(9), + ERROR_INVALID_MTU(10), + ERROR_INVALID_NUMBER_OF_CHANNELS(11), + ERROR_INVALID_STATE(12), + ERROR_DEVICE_IN_CHARGER(13); + } + + init { + measurementType = data[2] + status = PmdControlPointResponseCode.values()[data[3].toInt()] + if (status == PmdControlPointResponseCode.SUCCESS) { + more = data.size > 4 && data[4] != 0.toByte() + if (data.size > 5) { + parameters.write(data, 5, data.size - 5) + } + } else { + more = false + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt new file mode 100644 index 00000000..140123a1 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdFeature.kt @@ -0,0 +1,31 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +class PmdFeature(data: ByteArray) { + @JvmField + val ecgSupported: Boolean = (data[1].toUInt() and 0x01u) != 0u + + @JvmField + val ppgSupported: Boolean = (data[1].toUInt() and 0x02u) != 0u + + @JvmField + val accSupported: Boolean = (data[1].toUInt() and 0x04u) != 0u + + @JvmField + val ppiSupported: Boolean = (data[1].toUInt() and 0x08u) != 0u + + @JvmField + val gyroSupported: Boolean = (data[1].toUInt() and 0x20u) != 0u + + @JvmField + val magnetometerSupported: Boolean = (data[1].toUInt() and 0x40u) != 0u + + @JvmField + val barometerSupported: Boolean = (data[2].toUInt() and 0x08u) != 0u + + @JvmField + val locationSupported: Boolean = (data[2].toUInt() and 0x04u) != 0u + + @JvmField + val sdkModeSupported: Boolean = (data[2].toUInt() and 0x02u) != 0u + +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt new file mode 100644 index 00000000..3a33ce4b --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdMeasurementType.kt @@ -0,0 +1,26 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +enum class PmdMeasurementType(val numVal: Int) { + ECG(0), + PPG(1), + ACC(2), + PPI(3), + GYRO(5), + MAGNETOMETER(6), + SDK_MODE(9), + LOCATION(10), + PRESSURE(11), + UNKNOWN_TYPE(0xff); + + companion object { + @JvmStatic + fun fromId(id: Byte): PmdMeasurementType { + for (type in values()) { + if (type.numVal == id.toInt()) { + return type + } + } + return UNKNOWN_TYPE + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdSetting.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdSetting.kt new file mode 100644 index 00000000..39892d72 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PmdSetting.kt @@ -0,0 +1,130 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.common.ble.BleUtils +import java.io.ByteArrayOutputStream +import java.util.* + +class PmdSetting { + enum class PmdSettingType(val numVal: Int) { + SAMPLE_RATE(0), + RESOLUTION(1), + RANGE(2), + RANGE_MILLIUNIT(3), + CHANNELS(4), + FACTOR(5); + } + + // available settings + @JvmField + var settings: Map> = emptyMap() + + // selected by client + @JvmField + var selected: MutableMap = mutableMapOf() + + constructor(data: ByteArray) { + val parsedSettings = parsePmdSettingsData(data) + validateSettings(parsedSettings) + settings = parsedSettings + } + + constructor(selected: MutableMap) { + validateSelected(selected) + this.selected = selected + } + + private fun parsePmdSettingsData(data: ByteArray): EnumMap> { + val parsedSettings = EnumMap>(PmdSettingType::class.java) + if (data.size <= 1) { + return parsedSettings + } + var offset = 0 + while (offset < data.size) { + val type = PmdSettingType.values()[data[offset++].toInt()] + var count = data[offset++].toInt() + val items: MutableSet = HashSet() + while (count-- > 0) { + val fieldSize = typeToFieldSize(type) + val item = BleUtils.convertArrayToUnsignedInt(data, offset, fieldSize) + offset += fieldSize + items.add(item) + } + parsedSettings[type] = items + } + return parsedSettings + } + + fun updateSelectedFromStartResponse(data: ByteArray) { + val settingsFromStartResponse = parsePmdSettingsData(data) + if (settingsFromStartResponse.containsKey(PmdSettingType.FACTOR)) { + selected[PmdSettingType.FACTOR] = settingsFromStartResponse[PmdSettingType.FACTOR]!!.iterator().next() + } + } + + fun serializeSelected(): ByteArray { + val outputStream = ByteArrayOutputStream() + for ((key, value) in selected) { + if (key == PmdSettingType.FACTOR) { + continue + } + outputStream.write(key.numVal) + outputStream.write(1) + val fieldSize = Objects.requireNonNull(typeToFieldSize(key)) + for (i in 0 until fieldSize) { + outputStream.write((value shr i * 8)) + } + } + + return outputStream.toByteArray() + } + + fun maxSettings(): PmdSetting { + val set: MutableMap = TreeMap() + for ((key, value) in settings) { + set[key] = Collections.max(value) + } + return PmdSetting(set) + } + + companion object { + private fun typeToFieldSize(type: PmdSettingType): Int { + return when (type) { + PmdSettingType.SAMPLE_RATE -> 2 + PmdSettingType.RESOLUTION -> 2 + PmdSettingType.RANGE -> 2 + PmdSettingType.RANGE_MILLIUNIT -> 4 + PmdSettingType.CHANNELS -> 1 + PmdSettingType.FACTOR -> 4 + } + } + + private fun validateSettings(settings: Map>) { + for ((key, value1) in settings) { + for (value in value1) { + val entry: Map.Entry = AbstractMap.SimpleEntry(key, value) + validateSetting(entry) + } + } + } + + private fun validateSelected(settings: Map) { + for (setting in settings.entries) { + validateSetting(setting) + } + } + + private fun validateSetting(setting: Map.Entry) { + val fieldSize = typeToFieldSize(setting.key) + val value = setting.value + if (fieldSize == 1 && (value < 0x0 || 0xFF < value)) { + throw RuntimeException("PmdSetting not in valid range. Field size: $fieldSize value: $value") + } + if (fieldSize == 2 && (value < 0x0 || 0xFFFF < value)) { + throw RuntimeException("PmdSetting not in valid range. Field size: $fieldSize value: $value") + } + if (fieldSize == 3 && (value < 0x0 || 0xFFFFFF < value)) { + throw RuntimeException("PmdSetting not in valid range. Field size: $fieldSize value: $value") + } + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccData.kt new file mode 100644 index 00000000..fc9a5726 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccData.kt @@ -0,0 +1,114 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.Companion.parseDeltaFramesAll +import com.polar.androidcommunications.common.ble.BleUtils +import java.util.* +import kotlin.math.ceil + +class AccData internal constructor(@JvmField val timeStamp: Long) { + data class AccSample internal constructor( + // Sample contains signed x,y,z axis values in milliG + val x: Int, + val y: Int, + val z: Int + ) + + @JvmField + val accSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): AccData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataCompressedType0(frame, factor, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_1 -> dataCompressedType1(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by ACC data parser") + } + } else { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromRawType0(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_1 -> dataFromRawType1(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_2 -> dataFromRawType2(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by ACC data parser") + } + } + } + + private fun dataFromRawType0(value: ByteArray, timeStamp: Long): AccData { + val accData = AccData(timeStamp) + var offset = 0 + val resolution = 8 + val step = ceil(resolution.toDouble() / 8.0).toInt() + while (offset < value.size) { + val x = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val y = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val z = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + accData.accSamples.add(AccSample(x, y, z)) + } + return accData + } + + private fun dataFromRawType1(value: ByteArray, timeStamp: Long): AccData { + val accData = AccData(timeStamp) + var offset = 0 + val resolution = 16 + val step = ceil(resolution.toDouble() / 8.0).toInt() + while (offset < value.size) { + val x = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val y = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val z = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + accData.accSamples.add(AccSample(x, y, z)) + } + return accData + } + + private fun dataFromRawType2(value: ByteArray, timeStamp: Long): AccData { + val accData = AccData(timeStamp) + var offset = 0 + val resolution = 24 + val step = ceil(resolution.toDouble() / 8.0).toInt() + while (offset < value.size) { + val x = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val y = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + val z = BleUtils.convertArrayToSignedInt(value, offset, step) + offset += step + accData.accSamples.add(AccSample(x, y, z)) + } + return accData + } + + private fun dataCompressedType0(value: ByteArray, factor: Float, timeStamp: Long): AccData { + val accData = AccData(timeStamp) + val accFactor = factor * 1000 // Modify the factor to get data in milliG + val samples = parseDeltaFramesAll(value, 3, 16, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + for (sample in samples) { + val x = (sample[0] * accFactor).toInt() + val y = (sample[1] * accFactor).toInt() + val z = (sample[2] * accFactor).toInt() + accData.accSamples.add(AccSample(x, y, z)) + } + return accData + } + + private fun dataCompressedType1(value: ByteArray, factor: Float, timeStamp: Long): AccData { + val accData = AccData(timeStamp) + val samples = parseDeltaFramesAll(value, 3, 16, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + for (sample in samples) { + val x = if (factor != 1.0f) (sample[0].toFloat() * factor).toInt() else sample[0] + val y = if (factor != 1.0f) (sample[1].toFloat() * factor).toInt() else sample[1] + val z = if (factor != 1.0f) (sample[2].toFloat() * factor).toInt() else sample[2] + accData.accSamples.add(AccSample(x, y, z)) + } + return accData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgData.kt new file mode 100644 index 00000000..4a64e4a3 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgData.kt @@ -0,0 +1,93 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFrameType +import com.polar.androidcommunications.common.ble.BleUtils +import java.util.* + +class EcgData internal constructor(@JvmField val timeStamp: Long) { + + data class EcgSample internal constructor( + // samples in signed microvolts + val timeStamp: Long, + + @JvmField + val microVolts: Int, + val overSampling: Boolean = false, + val skinContactBit: Byte = 0, + val contactImpedance: Byte = 0, + val ecgDataTag: Byte = 0, + val paceDataTag: Byte = 0, + ) + + + @JvmField + val ecgSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): EcgData { + return if (isCompressed) { + throw java.lang.Exception("Compressed FrameType: $frameType is not supported by EcgData data parser") + } else { + when (frameType) { + PmdDataFrameType.TYPE_0 -> dataFromRawType0(frame, timeStamp) + PmdDataFrameType.TYPE_1 -> dataFromRawType1(frame, timeStamp) + PmdDataFrameType.TYPE_2 -> dataFromRawType2(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by EcgData data parser") + } + } + } + + private fun dataFromRawType0(value: ByteArray, timeStamp: Long): EcgData { + val ecgData = EcgData(timeStamp) + var offset = 0 + while (offset < value.size) { + val microVolts = BleUtils.convertArrayToSignedInt(value, offset, 3) + offset += 3 + ecgData.ecgSamples.add(EcgSample(timeStamp = timeStamp, microVolts = microVolts)) + } + return ecgData + } + + private fun dataFromRawType1(value: ByteArray, timeStamp: Long): EcgData { + val ecgData = EcgData(timeStamp) + var offset = 0 + while (offset < value.size) { + val microVolts = (((value[offset]).toInt() and 0xFF) or (((value[offset + 1]).toInt() and 0x3F) shl 8)) and 0x3FFF + val overSampling = (value[offset + 2].toInt() and 0x01) != 0 + val skinContactBit = ((value[offset + 2].toInt() and 0x06) shr 1).toByte() + val contactImpedance = ((value[offset + 2].toInt() and 0x18) shr 3).toByte() + offset += 3 + ecgData.ecgSamples.add( + EcgSample( + timeStamp = timeStamp, + microVolts = microVolts, + overSampling = overSampling, + skinContactBit = skinContactBit, + contactImpedance = contactImpedance + ) + ) + } + return ecgData + } + + private fun dataFromRawType2(value: ByteArray, timeStamp: Long): EcgData { + val ecgData = EcgData(timeStamp) + var offset = 0 + while (offset < value.size) { + val microVolts = (value[offset].toInt() and 0xFF) or ((value[offset + 1].toInt() and 0xFF) shl 8) or ((value[offset + 2].toInt() and 0x03) shl 16) and 0x3FFFFF + val ecgDataTag = ((value[offset + 2].toInt() and 0x1C) shr 2).toByte() + val paceDataTag = ((value[offset + 2].toInt() and 0xE0) shr 5).toByte() + offset += 3 + ecgData.ecgSamples.add( + EcgSample( + timeStamp = timeStamp, + microVolts = microVolts, + ecgDataTag = ecgDataTag, + paceDataTag = paceDataTag + ) + ) + } + return ecgData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationData.kt new file mode 100644 index 00000000..6a2d67d2 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GnssLocationData.kt @@ -0,0 +1,237 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils.parseFrameDataField +import java.util.* + +/** + * Sealed class to represent Location data sample + */ +sealed class GnssLocationDataSample + +/** + * Location data + * @property timeStamp ns in epoch time. The [timeStamp] represent time of last sample in location data [gnssLocationDataSamples] list + */ +class GnssLocationData internal constructor(val timeStamp: Long) { + + /** + * GPS Coordinates, Speed, and Distance Data + */ + data class GnssCoordinateSample internal constructor( + val latitude: Double, + val longitude: Double, + // time in format "yyyy-MM-dd'T'HH:mm:ss.SSS" + val date: String, + // cumulative distance in m + val cumulativeDistance: Double, + // speed in km/h + val speed: Float, + val usedAccelerationSpeed: Float, + val coordinateSpeed: Float, + val accelerationSpeedFactor: Float, + // course in degrees + val course: Float, + // speed in knots + val gpsChipSpeed: Float, + val fix: Boolean, + val speedFlag: Int, + val fusionState: UInt + ) : GnssLocationDataSample() + + /** + * GPS Satellite Dilution, and Altitude Data + */ + data class GnssSatelliteDilutionSample internal constructor( + // dilution distance in 0.01 precision + val dilution: Float, + // altitude in meters + val altitude: Int, + val numberOfSatellites: UInt, + val fix: Boolean, + ) : GnssLocationDataSample() + + data class GnssSatelliteSummary internal constructor( + val sbasSnrTop5Avg: UByte, + val snrTop5Avg: UByte, + val sbasMaxSnr: UByte, + val sbasSat: UByte, + val glonassMaxSnr: UByte, + val glonassSat: UByte, + val gpsMaxSnr: UByte, + val gpsSat: UByte + ) + + /** + * GPS Satellite Summary Data + */ + data class GnssSatelliteSummarySample internal constructor( + val seenGnssSatelliteSummary: GnssSatelliteSummary, + val usedGnssSatelliteSummary: GnssSatelliteSummary + ) : GnssLocationDataSample() + + /** + * GPS NMEA Data + */ + data class GnssGpsNMEASample internal constructor( + val measurementPeriod: UInt, + val messageLength: UInt, + val statusFlags: UByte, + val nmeaMessage: String + ) : GnssLocationDataSample() + + val gnssLocationDataSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): GnssLocationData { + return if (isCompressed) { + throw Exception("Compressed FrameType: $frameType is not supported by Location data parser") + } else { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_1 -> dataFromType1(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_2 -> dataFromType2(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_3 -> dataFromType3(frame, timeStamp) + else -> throw Exception("Raw FrameType: $frameType is not supported by Location data parser") + } + } + } + + private fun dataFromType0(frame: ByteArray, timeStamp: Long): GnssLocationData { + val locationData = GnssLocationData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val latitude = parseFrameDataField(frame.sliceArray(offset..(offset + 7)), PmdDataFieldEncoding.DOUBLE_IEEE754) as Double + offset += 8 + val longitude = parseFrameDataField(frame.sliceArray(offset..(offset + 7)), PmdDataFieldEncoding.DOUBLE_IEEE754) as Double + offset += 8 + val year = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 2 + val month = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 1 + val day = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 1 + val time = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 4 + + val milliseconds = time and 0x3FFu + val hours = (time and 0x7C00u) shr 10 + val minutes = (time and 0x1F8000u) shr 15 + val seconds = (time and 0x7E00000u) shr 21 + + val date = "%04d".format(year.toInt()) + "-" + + "%02d".format(month.toInt()) + "-" + + "%02d".format(day.toInt()) + + "T" + + "%02d".format(hours.toInt()) + ":" + + "%02d".format(minutes.toInt()) + ":" + + "%02d".format(seconds.toInt()) + "." + + "%03d".format(milliseconds.toInt()) + + val cumulativeDistanceUInt = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + val cumulativeDistance = (cumulativeDistanceUInt.toDouble() / 10) + offset += 4 + val speed: Float = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + val usedAccelerationSpeed = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + val coordinateSpeed = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + val accelerationSpeedFactory = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.FLOAT_IEEE754) as Float + offset += 4 + val courseUInt = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + val course = (courseUInt.toFloat() / 100) + offset += 2 + val knotsSpeedUInt = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + val gpsChipSpeed = (knotsSpeedUInt.toFloat() / 100) + offset += 2 + val fix: Boolean = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.BOOLEAN) as Boolean + offset += 1 + val speedFlag = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.SIGNED_INT) as Int + offset += 1 + val fusionState = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 1 + + val sample = GnssCoordinateSample( + latitude = latitude, longitude = longitude, date = date, cumulativeDistance = cumulativeDistance, + speed = speed, usedAccelerationSpeed = usedAccelerationSpeed, coordinateSpeed = coordinateSpeed, accelerationSpeedFactor = accelerationSpeedFactory, + course = course, gpsChipSpeed = gpsChipSpeed, fix = fix, speedFlag = speedFlag, fusionState = fusionState + ) + locationData.gnssLocationDataSamples.add(sample) + } + return locationData + } + + private fun dataFromType1(frame: ByteArray, timeStamp: Long): GnssLocationData { + val locationData = GnssLocationData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val dilutionInt = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + val dilution = (dilutionInt.toFloat() / 100) + offset += 2 + val altitude = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.SIGNED_INT) as Int + offset += 2 + val numberOfSatellites = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 1 + val fix: Boolean = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.BOOLEAN) as Boolean + offset += 1 + + val sample = GnssSatelliteDilutionSample(dilution = dilution, altitude = altitude, numberOfSatellites = numberOfSatellites, fix = fix) + locationData.gnssLocationDataSamples.add(sample) + } + return locationData + } + + private fun dataFromType2(frame: ByteArray, timeStamp: Long): GnssLocationData { + val locationData = GnssLocationData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val seenSatelliteSummary = GnssSatelliteSummary( + gpsSat = frame[0].toUByte(), + gpsMaxSnr = frame[1].toUByte(), + glonassSat = frame[2].toUByte(), + glonassMaxSnr = frame[3].toUByte(), + sbasSat = frame[4].toUByte(), + sbasMaxSnr = frame[5].toUByte(), + snrTop5Avg = frame[6].toUByte(), + sbasSnrTop5Avg = frame[7].toUByte() + ) + offset += 8 + val usedSatelliteSummary = GnssSatelliteSummary( + gpsSat = frame[8].toUByte(), + gpsMaxSnr = frame[9].toUByte(), + glonassSat = frame[10].toUByte(), + glonassMaxSnr = frame[11].toUByte(), + sbasSat = frame[12].toUByte(), + sbasMaxSnr = frame[13].toUByte(), + snrTop5Avg = frame[14].toUByte(), + sbasSnrTop5Avg = frame[15].toUByte() + ) + offset += 8 + + val sample = GnssSatelliteSummarySample(seenGnssSatelliteSummary = seenSatelliteSummary, usedGnssSatelliteSummary = usedSatelliteSummary) + locationData.gnssLocationDataSamples.add(sample) + } + return locationData + } + + private fun dataFromType3(frame: ByteArray, timeStamp: Long): GnssLocationData { + val locationData = GnssLocationData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val measurementPeriod = parseFrameDataField(frame.sliceArray(offset..(offset + 3)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 4 + val messageLength = parseFrameDataField(frame.sliceArray(offset..(offset + 1)), PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 2 + val statusFlags = parseFrameDataField(frame.sliceArray(offset..offset), PmdDataFieldEncoding.UNSIGNED_BYTE) as UByte + offset += 1 + val nmeaMessage = String(frame.sliceArray(offset until (offset + messageLength.toInt())))//.filter { it != '\u0000' } + offset += messageLength.toInt() + val sample = GnssGpsNMEASample(measurementPeriod = measurementPeriod, messageLength = messageLength, statusFlags = statusFlags, nmeaMessage = nmeaMessage) + locationData.gnssLocationDataSamples.add(sample) + } + return locationData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrData.kt new file mode 100644 index 00000000..20b44523 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrData.kt @@ -0,0 +1,61 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import java.lang.Float.intBitsToFloat +import java.util.* + +/** + * Gyro data + * @param timeStamp ns in epoch time. The time stamp represent time of last sample in gyrSamples list + */ +class GyrData internal constructor(val timeStamp: Long) { + + data class GyrSample internal constructor( + // Sample contains signed x,y,z axis values in deg/sec + val x: Float, + val y: Float, + val z: Float + ) + + @JvmField + val gyrSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): GyrData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, factor, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_1 -> dataFromType1(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by Gyro data parser") + } + } else { + throw java.lang.Exception("Raw FrameType: $frameType is not supported by Gyro data parser") + } + } + + private fun dataFromType0(value: ByteArray, factor: Float, timeStamp: Long): GyrData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 3, 16, PmdDataFieldEncoding.SIGNED_INT) + val gyrData = GyrData(timeStamp) + for (sample in samples) { + val x = if (factor != 1.0f) (sample[0] * factor) else sample[0].toFloat() + val y = if (factor != 1.0f) (sample[1] * factor) else sample[1].toFloat() + val z = if (factor != 1.0f) (sample[2] * factor) else sample[2].toFloat() + gyrData.gyrSamples.add(GyrSample(x, y, z)) + } + return gyrData + } + + private fun dataFromType1(value: ByteArray, factor: Float, timeStamp: Long): GyrData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 3, 32, PmdDataFieldEncoding.FLOAT_IEEE754) + val gyrData = GyrData(timeStamp) + for (sample in samples) { + val x = if (factor != 1.0f) intBitsToFloat(sample[0]) * factor else intBitsToFloat(sample[0]) + val y = if (factor != 1.0f) intBitsToFloat(sample[1]) * factor else intBitsToFloat(sample[1]) + val z = if (factor != 1.0f) intBitsToFloat(sample[2]) * factor else intBitsToFloat(sample[2]) + gyrData.gyrSamples.add(GyrSample(x, y, z)) + } + return gyrData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagData.kt new file mode 100644 index 00000000..8f26708b --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagData.kt @@ -0,0 +1,76 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import java.util.* + +/** + * Magnetometer data + * @param timeStamp ns in epoch time. The time stamp represent time of last sample in magSamples list + */ +class MagData(val timeStamp: Long) { + + enum class CalibrationStatus(val id: Int) { + NOT_AVAILABLE(-1), + UNKNOWN(0), + POOR(1), + OK(2), + GOOD(3); + + companion object { + fun getById(id: Int): CalibrationStatus { + return values().first { it.id == id } + } + } + } + + data class MagSample internal constructor( + // Sample contains signed x,y,z axis values in Gauss + val x: Float, + val y: Float, + val z: Float, + val calibrationStatus: CalibrationStatus = CalibrationStatus.NOT_AVAILABLE + ) + + @JvmField + val magSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): MagData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, factor, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_1 -> dataFromType1(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by Magnetometer data parser") + } + } else { + throw java.lang.Exception("Raw FrameType: $frameType is not supported by Magnetometer data parser") + } + } + + private fun dataFromType0(value: ByteArray, factor: Float, timeStamp: Long): MagData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 3, 16, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + val magData = MagData(timeStamp) + for (sample in samples) { + val x = if (factor != 1.0f) sample[0] * factor else sample[0].toFloat() + val y = if (factor != 1.0f) sample[1] * factor else sample[1].toFloat() + val z = if (factor != 1.0f) sample[2] * factor else sample[2].toFloat() + magData.magSamples.add(MagSample(x, y, z)) + } + return magData + } + + private fun dataFromType1(value: ByteArray, factor: Float, timeStamp: Long): MagData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 4, 16, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + val magData = MagData(timeStamp) + val unitConversionFactor = 1000 // type 1 data arrives in milliGauss units + for (sample in samples) { + val x = (if (factor != 1.0f) sample[0] * factor else sample[0].toFloat()) / unitConversionFactor + val y = (if (factor != 1.0f) sample[1] * factor else sample[1].toFloat()) / unitConversionFactor + val z = (if (factor != 1.0f) sample[2] * factor else sample[2].toFloat()) / unitConversionFactor + val status = CalibrationStatus.getById(sample[3]) + magData.magSamples.add(MagSample(x = x, y = y, z = z, calibrationStatus = status)) + } + return magData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt new file mode 100644 index 00000000..bcfc0d1a --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpgData.kt @@ -0,0 +1,154 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.BleLogger +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClientUtils +import com.polar.androidcommunications.common.ble.BleUtils +import java.util.* +import kotlin.experimental.and + +/** + * Sealed class to represent Ppg data sample + */ +sealed class PpgDataSample + +class PpgData internal constructor(val timeStamp: Long) { + + // PPG Data Sample 0 + data class PpgDataSampleType0 internal constructor( + val ppgDataSamples: List, + val ambientSample: Int + ) : PpgDataSample() + + // PPG Data Sample 2 + data class PpgDataSampleType2 internal constructor( + val ppgDataSamples: List, + val status: UInt + ) : PpgDataSample() + + // PPG Data frame type 4 + data class PpgDataSampleFrameType4 internal constructor( + val channel1GainTs: List, + val channel2GainTs: List, + val numIntTs: List + ) : PpgDataSample() + + // PPG Data frame type 5 + data class PpgDataSampleFrameType5 internal constructor( + val operationMode: UInt + ) : PpgDataSample() + + // PPG Data Sport Id + data class PpgDataSampleSportId internal constructor( + val sportId: ULong + ) : PpgDataSample() + + val ppgSamples: MutableList = ArrayList() + + companion object { + const val TAG = "PpgData" + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): PpgData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromCompressedType0(frame, factor, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_7 -> dataFromCompressedType7(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by PPG data parser") + } + } else { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromRawType0(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_4 -> dataFromRawType4(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_5 -> dataFromRawType5(frame, timeStamp) + BlePMDClient.PmdDataFrameType.TYPE_6 -> dataFromRawType6(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by PPG data parser") + } + } + } + + private fun dataFromRawType0(value: ByteArray, timeStamp: Long): PpgData { + val ppgData = PpgData(timeStamp) + val step = 3 + var i = 0 + while (i < value.size) { + val samples: MutableList = ArrayList() + for (ch in 0 until 4) { + samples.add(BleUtils.convertArrayToSignedInt(value, i, step)) + i += step + } + ppgData.ppgSamples.add(PpgDataSampleType0(samples.subList(0, 3), samples[3])) + } + return ppgData + } + + var dataType4Counter = 0 + private fun dataFromRawType4(frame: ByteArray, timeStamp: Long): PpgData { + dataType4Counter++ + BleLogger.d("TESTING", "PPG Data4 received. Counter: $dataType4Counter timeStamp: $timeStamp") + + val ppgData = PpgData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val channel1GainTs = frame.sliceArray(offset..(offset + 11)).map { (it and 0x07).toInt() } + offset += 12 + val channel2GainTs = frame.sliceArray(offset..(offset + 11)).map { (it and 0x07).toInt() } + offset += 12 + val numIntTs = frame.sliceArray(offset..(offset + 11)).map { it.toUByte().toUInt() } + offset += 12 + + ppgData.ppgSamples.add( + PpgDataSampleFrameType4( + channel1GainTs = channel1GainTs, + channel2GainTs = channel2GainTs, + numIntTs = numIntTs + ) + ) + } + return ppgData + } + + private fun dataFromRawType5(frame: ByteArray, timeStamp: Long): PpgData { + val ppgData = PpgData(timeStamp) + var offset = 0 + while (offset < frame.size) { + val operationMode = BlePMDClientUtils.parseFrameDataField(frame.sliceArray(offset..(offset + 3)), BlePMDClient.PmdDataFieldEncoding.UNSIGNED_INT) as UInt + offset += 4 + ppgData.ppgSamples.add(PpgDataSampleFrameType5(operationMode = operationMode)) + } + return ppgData + } + + private fun dataFromCompressedType0(value: ByteArray, factor: Float, timeStamp: Long): PpgData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 4, 24, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + val ppgData = PpgData(timeStamp) + for (sample in samples) { + val ppg0 = (sample[0].toFloat() * factor).toInt() + val ppg1 = (sample[1].toFloat() * factor).toInt() + val ppg2 = (sample[2].toFloat() * factor).toInt() + val ambient = (sample[3].toFloat() * factor).toInt() + ppgData.ppgSamples.add(PpgDataSampleType0(ppgDataSamples = listOf(ppg0, ppg1, ppg2), ambient)) + } + return ppgData + } + + private fun dataFromRawType6(frame: ByteArray, timeStamp: Long): PpgData { + val ppgData = PpgData(timeStamp) + val sportId = BlePMDClientUtils.parseFrameDataField(frame.sliceArray(0..7), BlePMDClient.PmdDataFieldEncoding.UNSIGNED_LONG) as ULong + ppgData.ppgSamples.add(PpgDataSampleSportId(sportId = sportId)) + return ppgData + } + + private fun dataFromCompressedType7(value: ByteArray, factor: Float, timeStamp: Long): PpgData { + val samples = BlePMDClient.parseDeltaFramesAll(value, 17, 24, BlePMDClient.PmdDataFieldEncoding.SIGNED_INT) + val ppgData = PpgData(timeStamp) + for (sample in samples) { + val channels = sample.subList(0, 16).map { + if (factor != 1.0f) (it.toFloat() * factor).toInt() else it + } + val status = sample[16].toUInt() + + ppgData.ppgSamples.add(PpgDataSampleType2(ppgDataSamples = channels, status)) + } + return ppgData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiData.kt new file mode 100644 index 00000000..193023c3 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PpiData.kt @@ -0,0 +1,72 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.common.ble.BleUtils +import java.util.* + +class PpiData(@JvmField val timeStamp: Long) { + data class PPSample internal constructor( + @JvmField + val hr: Int, + + @JvmField + val ppInMs: Int, + + @JvmField + val ppErrorEstimate: Int, + + @JvmField + val blockerBit: Int, + + @JvmField + val skinContactStatus: Int, + + @JvmField + val skinContactSupported: Int, + ) + + @JvmField + val ppSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): PpiData { + return if (isCompressed) { + throw java.lang.Exception("Compressed FrameType: $frameType is not supported by PPI data parser") + } else { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, timeStamp) + else -> throw java.lang.Exception("Raw FrameType: $frameType is not supported by PPI data parser") + } + } + } + + private fun dataFromType0(value: ByteArray, timeStamp: Long): PpiData { + val ppiData = PpiData(timeStamp) + var offset = 0 + while (offset < value.size) { + val finalOffset = offset + val sample = value.copyOfRange(finalOffset, finalOffset + 6) + + val hr = sample[0].toInt() and 0xFF + val ppInMs = BleUtils.convertArrayToUnsignedLong(sample, 1, 2).toInt() + val ppErrorEstimate = BleUtils.convertArrayToUnsignedLong(sample, 3, 2).toInt() + val blockerBit: Int = sample[5].toInt() and 0x01 + val skinContactStatus: Int = sample[5].toInt() and 0x02 shr 1 + val skinContactSupported: Int = sample[5].toInt() and 0x04 shr 2 + + ppiData.ppSamples.add( + PPSample( + hr = hr, + ppInMs = ppInMs, + ppErrorEstimate = ppErrorEstimate, + blockerBit = blockerBit, + skinContactStatus = skinContactStatus, + skinContactSupported = skinContactSupported + ) + ) + offset += 6 + } + return ppiData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt new file mode 100644 index 00000000..48196e2f --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/PressureData.kt @@ -0,0 +1,44 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import java.lang.Float.intBitsToFloat +import java.util.* + +/** + * Pressure data + * @param timeStamp ns in epoch time. The time stamp represent time of last sample in pressureSamples list + */ +class PressureData internal constructor(val timeStamp: Long) { + + data class PressureSample internal constructor( + // Sample contains signed pressure value in bar + val pressure: Float + ) + + @JvmField + val pressureSamples: MutableList = ArrayList() + + companion object { + fun parseDataFromDataFrame(isCompressed: Boolean, frameType: BlePMDClient.PmdDataFrameType, frame: ByteArray, factor: Float, timeStamp: Long): PressureData { + return if (isCompressed) { + when (frameType) { + BlePMDClient.PmdDataFrameType.TYPE_0 -> dataFromType0(frame, factor, timeStamp) + else -> throw java.lang.Exception("Compressed FrameType: $frameType is not supported by Pressure data parser") + } + } else { + throw java.lang.Exception("Raw FrameType: $frameType is not supported by Pressure data parser") + } + } + + private fun dataFromType0(frame: ByteArray, factor: Float, timeStamp: Long): PressureData { + val samples = BlePMDClient.parseDeltaFramesAll(frame, 1, 32, PmdDataFieldEncoding.FLOAT_IEEE754) + val pressureData = PressureData(timeStamp) + for (sample in samples) { + val pressure = if (factor != 1.0f) intBitsToFloat(sample[0]) * factor else intBitsToFloat(sample[0]) + pressureData.pressureSamples.add(PressureSample(pressure)) + } + return pressureData + } + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/common/ble/TypeUtils.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/common/ble/TypeUtils.kt new file mode 100644 index 00000000..789facd1 --- /dev/null +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/common/ble/TypeUtils.kt @@ -0,0 +1,27 @@ +package com.polar.androidcommunications.common.ble + +object TypeUtils { + + fun convertArrayToUnsignedByte(data: ByteArray): UByte { + BleUtils.validate(data.size == 1, "Array other than 1 cannot be converted to UByte. Input data size was " + data.size) + return data[0].toUByte() + } + + fun convertArrayToUnsignedInt(data: ByteArray): UInt { + BleUtils.validate(data.size in 1..4, "Array bigger than 4 cannot be converted to UInt. Input data size was " + data.size) + var result = 0u + for (i in data.indices) { + result = result or (data[i].toUByte().toUInt() shl i * 8) + } + return result + } + + fun convertArrayToUnsignedLong(data: ByteArray): ULong { + BleUtils.validate(data.size in 1..8, "Array bigger than 8 cannot be converted to ULong. Input data size was " + data.size) + var result = 0u.toULong() + for (i in data.indices) { + result = result or (data[i].toUByte().toULong() shl i * 8) + } + return result + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java index b31a373b..e8d4f0d3 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDDeviceSessionImpl.java @@ -660,9 +660,14 @@ void processNextAttributeOperation(boolean remove) { void startAuthentication(Action complete) { // try next att operation anyway - subscriptions.add(authenticate().toObservable().delaySubscription(500, TimeUnit.MILLISECONDS).ignoreElements(). - observeOn(AndroidSchedulers.from(context.getMainLooper())).subscribe( - complete, - this::handleAuthenticationFailed)); + subscriptions.add( + authenticate().toObservable() + .delaySubscription(500, TimeUnit.MILLISECONDS) + .ignoreElements(). + observeOn(AndroidSchedulers.from(context.getMainLooper())) + .subscribe( + complete, + this::handleAuthenticationFailed) + ); } } \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDGattCallback.java b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDGattCallback.java index 6c79b939..4dfde215 100755 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDGattCallback.java +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/enpoints/ble/bluedroid/host/BDGattCallback.java @@ -53,11 +53,15 @@ public void onConnectionStateChange(final BluetoothGatt gatt, int status, int ne gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED); } if (smartPolarDeviceSession.isAuthenticated()) { - smartPolarDeviceSession.getSubscriptions().add(Observable.timer(600, TimeUnit.MILLISECONDS, Schedulers.newThread()).observeOn(scheduler).subscribe( - aLong -> { - }, - throwable -> BleLogger.e(TAG, "Wait encryption start failed: " + throwable.getLocalizedMessage()), - () -> startDiscovery(smartPolarDeviceSession, gatt))); + smartPolarDeviceSession.getSubscriptions().add( + Observable.timer(600, TimeUnit.MILLISECONDS, Schedulers.newThread()) + .observeOn(scheduler) + .subscribe( + aLong -> { + }, + throwable -> BleLogger.e(TAG, "Wait encryption start failed: " + throwable.getLocalizedMessage()), + () -> startDiscovery(smartPolarDeviceSession, gatt) + )); } else { handler.post(() -> startDiscovery(smartPolarDeviceSession, gatt)); } @@ -114,6 +118,7 @@ public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacte @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); + final BDDeviceSessionImpl session = sessions.getSession(gatt); if (session != null) { session.handleCharacteristicWrite(characteristic.getService(), characteristic, status); diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt index e457c53e..afdf4faf 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/PolarBleApiDefaultImpl.kt @@ -15,6 +15,7 @@ object PolarBleApiDefaultImpl { * @param features @see polar.com.sdk.api.PolarBleApi feature flags * @return default Polar API implementation */ + @JvmStatic fun defaultImplementation(context: Context, features: Int): PolarBleApi { return BDBleApiImpl.getInstance(context, features) } @@ -22,6 +23,7 @@ object PolarBleApiDefaultImpl { /** * @return SDK version number in format major.minor.patch */ + @JvmStatic fun versionInfo(): String { return "3.2.1" } diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarDataUtils.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarDataUtils.kt new file mode 100644 index 00000000..727fdab8 --- /dev/null +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarDataUtils.kt @@ -0,0 +1,29 @@ +package com.polar.sdk.api.model + +import com.polar.androidcommunications.api.ble.BleLogger +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpgData + +object PolarDataUtils { + const val TAG = "PolarDataUtils" + + @JvmStatic + fun mapPMDClientOhrDataToPolarOhr(ohrData: PpgData): PolarOhrData { + var type: PolarOhrData.OHR_DATA_TYPE = PolarOhrData.OHR_DATA_TYPE.UNKNOWN + val listOfSamples = mutableListOf() + for (sample in ohrData.ppgSamples) { + when (sample) { + is PpgData.PpgDataSampleType0 -> { + type = PolarOhrData.OHR_DATA_TYPE.PPG3_AMBIENT1 + val channelsData = mutableListOf() + channelsData.addAll(sample.ppgDataSamples) + channelsData.add(sample.ambientSample) + listOfSamples.add(PolarOhrData.PolarOhrSample(channelsData)) + } + else -> { + BleLogger.w(TAG, "Not supported PPG sample type: $sample") + } + } + } + return PolarOhrData(listOfSamples, type, ohrData.timeStamp) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarOhrData.java b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarOhrData.java index 34acdd25..8651c64d 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarOhrData.java +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarOhrData.java @@ -17,11 +17,15 @@ public static class PolarOhrSample { */ public final List channelSamples; - public final long status; + /** + * Status of OHR data + * @deprecated status of the OHR data doesn't contain any relevant information. Will be removed in future releases. + */ + @Deprecated + public final long status = 0L; - public PolarOhrSample(List channelSamples, long status) { + public PolarOhrSample(List channelSamples) { this.channelSamples = channelSamples; - this.status = status; } } diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarSensorSetting.java b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarSensorSetting.java index 25049528..9957f229 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarSensorSetting.java +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/api/model/PolarSensorSetting.java @@ -1,7 +1,8 @@ // Copyright © 2019 Polar Electro Oy. All rights reserved. package com.polar.sdk.api.model; -import com.polar.androidcommunications.api.ble.model.gatt.client.BlePMDClient; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdMeasurementType; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdSetting; import java.util.Collections; import java.util.HashMap; @@ -13,11 +14,26 @@ public class PolarSensorSetting { public enum SettingType { - SAMPLE_RATE(0), /*!< sample rate key in hz */ - RESOLUTION(1), /*!< resolution key in bits */ - RANGE(2), /*!< range key*/ - RANGE_MILLIUNIT(3), /*!< range key milliunit. Note Set contains range values from min to max */ - CHANNELS(4); /*!< amount of channels */ + /** + * sample rate key in hz + */ + SAMPLE_RATE(0), + /** + * resolution key in bits + */ + RESOLUTION(1), + /** + * range key + */ + RANGE(2), + /** + * range key milliunit. Note Set contains range values from min to max + */ + RANGE_MILLIUNIT(3), + /** + * amount of channels + */ + CHANNELS(4); private final int numVal; @@ -31,7 +47,7 @@ public int getNumVal() { } public final Map> settings; - private BlePMDClient.PmdMeasurementType type; + private PmdMeasurementType type; /** * Internal Constructor with PmdSetting and Type @@ -39,11 +55,11 @@ public int getNumVal() { * @param settings available settings * @param type measurement type */ - public PolarSensorSetting(Map> settings, - BlePMDClient.PmdMeasurementType type) { + public PolarSensorSetting(Map> settings, + PmdMeasurementType type) { this.settings = new HashMap<>(); this.type = type; - for (Map.Entry> e : settings.entrySet()) { + for (Map.Entry> e : settings.entrySet()) { this.settings.put(SettingType.values()[e.getKey().getNumVal()], e.getValue()); } } @@ -65,13 +81,13 @@ public PolarSensorSetting(Map settings) { * * @return PmdSetting */ - public BlePMDClient.PmdSetting map2PmdSettings() { - Map selected = new HashMap<>(); + public PmdSetting map2PmdSettings() { + Map selected = new HashMap<>(); for (Map.Entry> e : settings.entrySet()) { - selected.put(BlePMDClient.PmdSetting.PmdSettingType.values()[e.getKey().numVal], + selected.put(PmdSetting.PmdSettingType.values()[e.getKey().numVal], Collections.max(e.getValue())); } - return new BlePMDClient.PmdSetting(selected); + return new PmdSetting(selected); } /** diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java index 26e3a99e..ffb117bd 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.java @@ -4,7 +4,7 @@ import static com.polar.androidcommunications.api.ble.model.BleDeviceSession.DeviceSessionState.SESSION_CLOSED; import static com.polar.androidcommunications.api.ble.model.BleDeviceSession.DeviceSessionState.SESSION_OPEN; import static com.polar.androidcommunications.api.ble.model.BleDeviceSession.DeviceSessionState.SESSION_OPENING; -import static com.polar.androidcommunications.api.ble.model.gatt.client.BlePMDClient.PmdControlPointResponse.PmdControlPointResponseCode.ERROR_ALREADY_IN_STATE; +import static com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdControlPointResponse.PmdControlPointResponseCode.ERROR_ALREADY_IN_STATE; import android.annotation.SuppressLint; import android.bluetooth.le.ScanFilter; @@ -26,7 +26,14 @@ import com.polar.androidcommunications.api.ble.model.gatt.client.BleBattClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BleDisClient; import com.polar.androidcommunications.api.ble.model.gatt.client.BleHrClient; -import com.polar.androidcommunications.api.ble.model.gatt.client.BlePMDClient; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdMeasurementType; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.PmdSetting; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.AccData; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.EcgData; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.GyrData; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.MagData; +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpiData; import com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpClient; import com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpUtils; import com.polar.androidcommunications.api.ble.model.polar.BlePolarDeviceCapabilitiesUtility; @@ -42,6 +49,7 @@ import com.polar.sdk.api.errors.PolarOperationNotSupported; import com.polar.sdk.api.errors.PolarServiceNotAvailable; import com.polar.sdk.api.model.PolarAccelerometerData; +import com.polar.sdk.api.model.PolarDataUtils; import com.polar.sdk.api.model.PolarDeviceInfo; import com.polar.sdk.api.model.PolarEcgData; import com.polar.sdk.api.model.PolarExerciseData; @@ -304,17 +312,17 @@ public Single requestStreamSettings(@NonNull final String id @NonNull DeviceStreamingFeature feature) { switch (feature) { case ECG: - return querySettings(identifier, BlePMDClient.PmdMeasurementType.ECG); + return querySettings(identifier, PmdMeasurementType.ECG); case ACC: - return querySettings(identifier, BlePMDClient.PmdMeasurementType.ACC); + return querySettings(identifier, PmdMeasurementType.ACC); case PPG: - return querySettings(identifier, BlePMDClient.PmdMeasurementType.PPG); + return querySettings(identifier, PmdMeasurementType.PPG); case PPI: return Single.error(new PolarOperationNotSupported()); case GYRO: - return querySettings(identifier, BlePMDClient.PmdMeasurementType.GYRO); + return querySettings(identifier, PmdMeasurementType.GYRO); case MAGNETOMETER: - return querySettings(identifier, BlePMDClient.PmdMeasurementType.MAGNETOMETER); + return querySettings(identifier, PmdMeasurementType.MAGNETOMETER); default: return Single.error(new PolarInvalidArgument()); } @@ -326,39 +334,39 @@ public Single requestFullStreamSettings(@NonNull final Strin @NonNull DeviceStreamingFeature feature) { switch (feature) { case ECG: - return queryFullSettings(identifier, BlePMDClient.PmdMeasurementType.ECG); + return queryFullSettings(identifier, PmdMeasurementType.ECG); case ACC: - return queryFullSettings(identifier, BlePMDClient.PmdMeasurementType.ACC); + return queryFullSettings(identifier, PmdMeasurementType.ACC); case PPG: - return queryFullSettings(identifier, BlePMDClient.PmdMeasurementType.PPG); + return queryFullSettings(identifier, PmdMeasurementType.PPG); case PPI: return Single.error(new PolarOperationNotSupported()); case GYRO: - return queryFullSettings(identifier, BlePMDClient.PmdMeasurementType.GYRO); + return queryFullSettings(identifier, PmdMeasurementType.GYRO); case MAGNETOMETER: - return queryFullSettings(identifier, BlePMDClient.PmdMeasurementType.MAGNETOMETER); + return queryFullSettings(identifier, PmdMeasurementType.MAGNETOMETER); default: return Single.error(new PolarInvalidArgument()); } } - private Single querySettings(final String identifier, final BlePMDClient.PmdMeasurementType type) { + private Single querySettings(final String identifier, final PmdMeasurementType type) { try { final BleDeviceSession session = sessionPmdClientReady(identifier); final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE); return client.querySettings(type) - .map(setting -> new PolarSensorSetting(setting.settings, type)); + .map(setting -> new PolarSensorSetting((Map>) setting.settings, type)); } catch (Throwable e) { return Single.error(e); } } - private Single queryFullSettings(final String identifier, final BlePMDClient.PmdMeasurementType type) { + private Single queryFullSettings(final String identifier, final PmdMeasurementType type) { try { final BleDeviceSession session = sessionPmdClientReady(identifier); final BlePMDClient client = (BlePMDClient) session.fetchClient(BlePMDClient.PMD_SERVICE); return client.queryFullSettings(type) - .map(setting -> new PolarSensorSetting(setting.settings, type)); + .map(setting -> new PolarSensorSetting((Map>) setting.settings, type)); } catch (Throwable e) { return Single.error(e); } @@ -686,7 +694,7 @@ public Flowable startListenForPolarHrBroadcasts(@Nullable } private Flowable startStreaming(String identifier, - BlePMDClient.PmdMeasurementType type, + PmdMeasurementType type, PolarSensorSetting setting, Function> observer) { try { @@ -707,11 +715,11 @@ private Flowable startStreaming(String identifier, @Override public Flowable startEcgStreaming(@NonNull String identifier, @NonNull PolarSensorSetting setting) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.ECG, setting, + return startStreaming(identifier, PmdMeasurementType.ECG, setting, client -> client.monitorEcgNotifications(true) .map(ecgData -> { List samples = new ArrayList<>(); - for (BlePMDClient.EcgData.EcgSample s : ecgData.ecgSamples) { + for (EcgData.EcgSample s : ecgData.ecgSamples) { samples.add(s.microVolts); } return new PolarEcgData(samples, ecgData.timeStamp); @@ -722,11 +730,11 @@ public Flowable startEcgStreaming(@NonNull String identifier, @Override public Flowable startAccStreaming(@NonNull String identifier, @NonNull PolarSensorSetting setting) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.ACC, setting, client -> client.monitorAccNotifications(true) + return startStreaming(identifier, PmdMeasurementType.ACC, setting, client -> client.monitorAccNotifications(true) .map(accData -> { List samples = new ArrayList<>(); - for (BlePMDClient.AccData.AccSample s : accData.accSamples) { - samples.add(new PolarAccelerometerData.PolarAccelerometerDataSample(s.x, s.y, s.z)); + for (AccData.AccSample sample : accData.accSamples) { + samples.add(new PolarAccelerometerData.PolarAccelerometerDataSample(sample.getX(), sample.getY(), sample.getZ())); } return new PolarAccelerometerData(samples, accData.timeStamp); })); @@ -736,26 +744,16 @@ public Flowable startAccStreaming(@NonNull String identi @Override public Flowable startOhrStreaming(@NonNull String identifier, @NonNull PolarSensorSetting setting) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.PPG, setting, client -> client.monitorPpgNotifications(true) - .map(ppgData -> { - List samples = new ArrayList<>(); - PolarOhrData.OHR_DATA_TYPE type = PolarOhrData.OHR_DATA_TYPE.UNKNOWN; - if (ppgData.channels == 4) { - type = PolarOhrData.OHR_DATA_TYPE.PPG3_AMBIENT1; - } - for (BlePMDClient.PpgData.PpgSample s : ppgData.ppgSamples) { - samples.add(new PolarOhrData.PolarOhrSample(s.ppgDataSamples, s.status)); - } - return new PolarOhrData(samples, type, ppgData.timeStamp); - })); + return startStreaming(identifier, PmdMeasurementType.PPG, setting, client -> client.monitorPpgNotifications(true) + .map(PolarDataUtils::mapPMDClientOhrDataToPolarOhr)); } @NonNull @Override public Flowable startOhrPPIStreaming(@NonNull String identifier) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.PPI, new PolarSensorSetting(new HashMap<>()), client -> client.monitorPpiNotifications(true).map(ppiData -> { + return startStreaming(identifier, PmdMeasurementType.PPI, new PolarSensorSetting(new HashMap<>()), client -> client.monitorPpiNotifications(true).map(ppiData -> { List samples = new ArrayList<>(); - for (BlePMDClient.PpiData.PPSample ppSample : ppiData.ppSamples) { + for (PpiData.PPSample ppSample : ppiData.ppSamples) { samples.add(new PolarOhrPPIData.PolarOhrPPISample(ppSample.ppInMs, ppSample.ppErrorEstimate, ppSample.hr, @@ -771,13 +769,13 @@ public Flowable startOhrPPIStreaming(@NonNull String identifier @Override public Flowable startMagnetometerStreaming(@NonNull final String identifier, @NonNull PolarSensorSetting setting) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.MAGNETOMETER, setting, client -> client.monitorMagnetometerNotifications(true) + return startStreaming(identifier, PmdMeasurementType.MAGNETOMETER, setting, client -> client.monitorMagnetometerNotifications(true) .map(mgn -> { List samples = new ArrayList<>(); - for (BlePMDClient.MagData.MagSample sample : mgn.magSamples) { - samples.add(new PolarMagnetometerData.PolarMagnetometerDataSample(sample.x, sample.y, sample.z)); + for (MagData.MagSample sample : mgn.magSamples) { + samples.add(new PolarMagnetometerData.PolarMagnetometerDataSample(sample.getX(), sample.getY(), sample.getZ())); } - return new PolarMagnetometerData(samples, mgn.timeStamp); + return new PolarMagnetometerData(samples, mgn.getTimeStamp()); })); } @@ -785,13 +783,13 @@ public Flowable startMagnetometerStreaming(@NonNull final @Override public Flowable startGyroStreaming(@NonNull final String identifier, @NonNull PolarSensorSetting setting) { - return startStreaming(identifier, BlePMDClient.PmdMeasurementType.GYRO, setting, client -> client.monitorGyroNotifications(true) + return startStreaming(identifier, PmdMeasurementType.GYRO, setting, client -> client.monitorGyroNotifications(true) .map(gyro -> { List samples = new ArrayList<>(); - for (BlePMDClient.GyrData.GyrSample sample : gyro.gyrSamples) { - samples.add(new PolarGyroData.PolarGyroDataSample(sample.x, sample.y, sample.z)); + for (GyrData.GyrSample sample : gyro.gyrSamples) { + samples.add(new PolarGyroData.PolarGyroDataSample(sample.getX(), sample.getY(), sample.getZ())); } - return new PolarGyroData(samples, gyro.timeStamp); + return new PolarGyroData(samples, gyro.getTimeStamp()); })); } @@ -918,7 +916,7 @@ protected BleDeviceSession sessionPsFtpClientReady(final @NonNull String identif } @SuppressLint("CheckResult") - protected void stopPmdStreaming(@NonNull BleDeviceSession session, @NonNull BlePMDClient client, @NonNull BlePMDClient.PmdMeasurementType type) { + protected void stopPmdStreaming(@NonNull BleDeviceSession session, @NonNull BlePMDClient client, @NonNull PmdMeasurementType type) { if (session.getSessionState() == SESSION_OPEN) { // stop streaming client.stopMeasurement(type).subscribe( diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientAccTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientAccTest.kt deleted file mode 100644 index d1f0bece..00000000 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientAccTest.kt +++ /dev/null @@ -1,555 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client - -import org.junit.Assert -import org.junit.Test -import kotlin.math.abs - -class BlePmdClientAccTest { - @Test - fun test_parseAccDataDelta_withResolution16() { - // Arrange - // HEX: 71 07 F0 6A 9E 8D 0A 38 BE 5C BE BA 2F 96 B3 EE 4B E5 AD FB 42 B9 EB BE 4C FE BA 2F 92 BF EE 4B E4 B1 FB 12 B9 EC BD 3C 3E BB 2F 8F D3 DE 4B E3 B5 F7 D2 B8 ED BD 30 7E 7B 2F 8B E3 CE 8B E2 BA F7 A2 B8 EE BC 20 BE 7B 2F 88 F3 CE CB E1 BD EF 52 F8 EF BC 18 FE 3B 2F 84 03 BF CB E0 C2 EF 32 B8 F0 BB 04 4E BC 2E 81 13 AF 0B E0 C6 EF F2 F7 F1 B9 FC 7D BC 2E 7D 27 9F 4B DF CA EB C2 F7 F2 B8 EC CD 7C 2E 7B 37 8F 4B DE CE E3 92 F7 F3 B8 E0 0D FD 2D 77 4B 7F CB DD D2 DF 62 37 F5 B7 D4 4D BD 2D 74 5B 6F CB DC D7 D7 32 37 F6 B5 C8 8D 7D 2D 71 6B 4F 4B DC DC D3 F2 36 F7 B4 BC DD FD 2C 6F 7B 3F 4B DB E0 CF D2 36 F8 B2 B0 2D BE 2C 6C 8F 1F CB DA E3 C7 A2 76 F9 - // index type data: - // 0-5: Reference sample size 6: 0xC9 0xFF 0x12 0x00 0x11 0x00 - // Sample 0 (aka. reference sample): - // channel 0: 71 07 => 0x0771 => 1905 - val refSample0Channel0 = 1905 - // channel 1: F0 6A => 0x6AF0 => 27376 - val refSample0Channel1 = 27376 - // channel 2: 9E 8D => 0x8D9E => -29282 - val refSample0Channel2 = -29282 - // Delta dump: 0A 38 | BE 5C BE BA 2F 96 B3 EE 4B E5 AD ... - // 6: Delta size size 1: 0x0A (10 bits) - // 7: Sample amount size 1: 0x38 (Delta block contains 56 samples) - // 8: 0xBE (binary: 1011 1110) - // 9: 0x5C (binary: 0101 11 | 00) - // 10: 0xBE (binary: 1011 | 1110) - // Sample 1 - channel 0, size 10 bits: 00 1011 1110 - // Sample 1 - channel 1, size 10 bits: 11 1001 0111 - // 11: 0xBA (binary: 10 | 11 1010) - // Sample 1 - channel 2, size 10 bits: 11 1010 1011 - val refSample1Channel0 = 190 - val refSample1Channel1 = -105 - val refSample1Channel2 = -85 - val amountOfSamples = 1 + 56 // reference sample + delta samples - val measurementFrame = byteArrayOf( - 0x71.toByte(), - 0x07.toByte(), - 0xF0.toByte(), - 0x6A.toByte(), - 0x9E.toByte(), - 0x8D.toByte(), - 0x0A.toByte(), - 0x38.toByte(), - 0xBE.toByte(), - 0x5C.toByte(), - 0xBE.toByte(), - 0xBA.toByte(), - 0x2F.toByte(), - 0x96.toByte(), - 0xB3.toByte(), - 0xEE.toByte(), - 0x4B.toByte(), - 0xE5.toByte(), - 0xAD.toByte(), - 0xFB.toByte(), - 0x42.toByte(), - 0xB9.toByte(), - 0xEB.toByte(), - 0xBE.toByte(), - 0x4C.toByte(), - 0xFE.toByte(), - 0xBA.toByte(), - 0x2F.toByte(), - 0x92.toByte(), - 0xBF.toByte(), - 0xEE.toByte(), - 0x4B.toByte(), - 0xE4.toByte(), - 0xB1.toByte(), - 0xFB.toByte(), - 0x12.toByte(), - 0xB9.toByte(), - 0xEC.toByte(), - 0xBD.toByte(), - 0x3C.toByte(), - 0x3E.toByte(), - 0xBB.toByte(), - 0x2F.toByte(), - 0x8F.toByte(), - 0xD3.toByte(), - 0xDE.toByte(), - 0x4B.toByte(), - 0xE3.toByte(), - 0xB5.toByte(), - 0xF7.toByte(), - 0xD2.toByte(), - 0xB8.toByte(), - 0xED.toByte(), - 0xBD.toByte(), - 0x30.toByte(), - 0x7E.toByte(), - 0x7B.toByte(), - 0x2F.toByte(), - 0x8B.toByte(), - 0xE3.toByte(), - 0xCE.toByte(), - 0x8B.toByte(), - 0xE2.toByte(), - 0xBA.toByte(), - 0xF7.toByte(), - 0xA2.toByte(), - 0xB8.toByte(), - 0xEE.toByte(), - 0xBC.toByte(), - 0x20.toByte(), - 0xBE.toByte(), - 0x7B.toByte(), - 0x2F.toByte(), - 0x88.toByte(), - 0xF3.toByte(), - 0xCE.toByte(), - 0xCB.toByte(), - 0xE1.toByte(), - 0xBD.toByte(), - 0xEF.toByte(), - 0x52.toByte(), - 0xF8.toByte(), - 0xEF.toByte(), - 0xBC.toByte(), - 0x18.toByte(), - 0xFE.toByte(), - 0x3B.toByte(), - 0x2F.toByte(), - 0x84.toByte(), - 0x03.toByte(), - 0xBF.toByte(), - 0xCB.toByte(), - 0xE0.toByte(), - 0xC2.toByte(), - 0xEF.toByte(), - 0x32.toByte(), - 0xB8.toByte(), - 0xF0.toByte(), - 0xBB.toByte(), - 0x04.toByte(), - 0x4E.toByte(), - 0xBC.toByte(), - 0x2E.toByte(), - 0x81.toByte(), - 0x13.toByte(), - 0xAF.toByte(), - 0x0B.toByte(), - 0xE0.toByte(), - 0xC6.toByte(), - 0xEF.toByte(), - 0xF2.toByte(), - 0xF7.toByte(), - 0xF1.toByte(), - 0xB9.toByte(), - 0xFC.toByte(), - 0x7D.toByte(), - 0xBC.toByte(), - 0x2E.toByte(), - 0x7D.toByte(), - 0x27.toByte(), - 0x9F.toByte(), - 0x4B.toByte(), - 0xDF.toByte(), - 0xCA.toByte(), - 0xEB.toByte(), - 0xC2.toByte(), - 0xF7.toByte(), - 0xF2.toByte(), - 0xB8.toByte(), - 0xEC.toByte(), - 0xCD.toByte(), - 0x7C.toByte(), - 0x2E.toByte(), - 0x7B.toByte(), - 0x37.toByte(), - 0x8F.toByte(), - 0x4B.toByte(), - 0xDE.toByte(), - 0xCE.toByte(), - 0xE3.toByte(), - 0x92.toByte(), - 0xF7.toByte(), - 0xF3.toByte(), - 0xB8.toByte(), - 0xE0.toByte(), - 0x0D.toByte(), - 0xFD.toByte(), - 0x2D.toByte(), - 0x77.toByte(), - 0x4B.toByte(), - 0x7F.toByte(), - 0xCB.toByte(), - 0xDD.toByte(), - 0xD2.toByte(), - 0xDF.toByte(), - 0x62.toByte(), - 0x37.toByte(), - 0xF5.toByte(), - 0xB7.toByte(), - 0xD4.toByte(), - 0x4D.toByte(), - 0xBD.toByte(), - 0x2D.toByte(), - 0x74.toByte(), - 0x5B.toByte(), - 0x6F.toByte(), - 0xCB.toByte(), - 0xDC.toByte(), - 0xD7.toByte(), - 0xD7.toByte(), - 0x32.toByte(), - 0x37.toByte(), - 0xF6.toByte(), - 0xB5.toByte(), - 0xC8.toByte(), - 0x8D.toByte(), - 0x7D.toByte(), - 0x2D.toByte(), - 0x71.toByte(), - 0x6B.toByte(), - 0x4F.toByte(), - 0x4B.toByte(), - 0xDC.toByte(), - 0xDC.toByte(), - 0xD3.toByte(), - 0xF2.toByte(), - 0x36.toByte(), - 0xF7.toByte(), - 0xB4.toByte(), - 0xBC.toByte(), - 0xDD.toByte(), - 0xFD.toByte(), - 0x2C.toByte(), - 0x6F.toByte(), - 0x7B.toByte(), - 0x3F.toByte(), - 0x4B.toByte(), - 0xDB.toByte(), - 0xE0.toByte(), - 0xCF.toByte(), - 0xD2.toByte(), - 0x36.toByte(), - 0xF8.toByte(), - 0xB2.toByte(), - 0xB0.toByte(), - 0x2D.toByte(), - 0xBE.toByte(), - 0x2C.toByte(), - 0x6C.toByte(), - 0x8F.toByte(), - 0x1F.toByte(), - 0xCB.toByte(), - 0xDA.toByte(), - 0xE3.toByte(), - 0xC7.toByte(), - 0xA2.toByte(), - 0x76.toByte(), - 0xF9.toByte() - ) - val resolution = 16 - val range = 8 - val factor = 2.44E-4f - val timeStamp: Long = 0 - - // Act - val accData = BlePMDClient.AccData(measurementFrame, factor, resolution, timeStamp) - - // Assert - Assert.assertEquals( - (factor * refSample0Channel0.toFloat() * 1000f).toInt(), - accData.accSamples[0].x - ) - Assert.assertEquals( - (factor * refSample0Channel1.toFloat() * 1000f).toInt(), - accData.accSamples[0].y - ) - Assert.assertEquals( - (factor * refSample0Channel2.toFloat() * 1000f).toInt(), - accData.accSamples[0].z - ) - Assert.assertEquals( - (factor * (refSample0Channel0 + refSample1Channel0) * 1000f).toInt(), - accData.accSamples[1].x - ) - Assert.assertEquals( - (factor * (refSample0Channel1 + refSample1Channel1) * 1000f).toInt(), - accData.accSamples[1].y - ) - Assert.assertEquals( - (factor * (refSample0Channel2 + refSample1Channel2) * 1000f).toInt(), - accData.accSamples[1].z - ) - - // validate data in range - for (sample in accData.accSamples) { - Assert.assertTrue(abs(sample.x) <= range * 1000) - Assert.assertTrue(abs(sample.y) <= range * 1000) - Assert.assertTrue(abs(sample.z) <= range * 1000) - } - - // validate data size - Assert.assertEquals(amountOfSamples.toLong(), accData.accSamples.size.toLong()) - } - - @Test - fun test_parseAccData_withResolution16() { - // Arrange - // HEX: 02 7A B4 86 FF C7 87 52 08 01 F7 FF FF FF E7 03 F8 FF FE FF E5 03 F9 FF FF FF E5 03 FA FF FF FF E6 03 FA FF FE FF E6 03 F9 FF FF FF E5 03 F8 FF FF FF E6 03 F8 FF FE FF E6 03 FA FF FF FF E5 03 FA FF FF FF E7 03 FA FF FF FF E5 03 F8 FF FF FF E6 03 F7 FF FF FF E6 03 F8 FF FE FF E6 03 F9 FF FE FF E7 03 F9 FF 00 00 E6 03 F9 FF FF FF E6 03 F7 FF FE FF E5 03 F9 FF FF FF E5 03 F9 FF FF FF E5 03 FA FF 00 00 E6 03 F9 FF FE FF E6 03 F8 FF FF FF E6 03 F8 FF FF FF E5 03 F9 FF FF FF E6 03 F9 FF FF FF E5 03 FA FF FF FF E6 03 F9 FF FF FF E5 03 F9 FF FF FF E5 03 F8 FF FE FF E6 03 F9 FF FF FF E6 03 F9 FF FF FF E6 03 F9 FF 00 00 E5 03 F9 FF FE FF E6 03 F8 FF FE FF E6 03 F7 FF FE FF E6 03 - // index data: - // 0 type 01 - val type: Byte = 0x01 - // 1..2 x value F7 FF (-9) - val xValue1 = -9 - // 3..4 y value FF FF (-1) - val yValue1 = -1 - // 5..6 z value E7 03 (999) - val zValue1 = 999 - // 7..8 x value F8 FF (-8) - val xValue2 = -8 - // 9..10 y value FF FE (-2) - val yValue2 = -2 - // 11..12 z value E5 03 (997) - val zValue2 = 997 - val measurementFrame = byteArrayOf( - 0xF7.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE7.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE7.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF7.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE7.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0x00.toByte(), - 0x00.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF7.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0x00.toByte(), - 0x00.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xFA.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0x00.toByte(), - 0x00.toByte(), - 0xE5.toByte(), - 0x03.toByte(), - 0xF9.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF8.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte(), - 0xF7.toByte(), - 0xFF.toByte(), - 0xFE.toByte(), - 0xFF.toByte(), - 0xE6.toByte(), - 0x03.toByte() - ) - val timeStamp: Long = 0 - val amountOfSamples = - measurementFrame.size / 2 / 3 // measurement frame size / resolution in bytes / channels - - // Act - val accData = BlePMDClient.AccData(type, measurementFrame, timeStamp) - - // Assert - Assert.assertEquals(xValue1, accData.accSamples[0].x) - Assert.assertEquals(yValue1, accData.accSamples[0].y) - Assert.assertEquals(zValue1, accData.accSamples[0].z) - Assert.assertEquals(xValue2, accData.accSamples[1].x) - Assert.assertEquals(yValue2, accData.accSamples[1].y) - Assert.assertEquals(zValue2, accData.accSamples[1].z) - - // validate data size - Assert.assertEquals(amountOfSamples, accData.accSamples.size) - } -} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientParsersTest.java b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientParsersTest.java deleted file mode 100644 index 5e716cc4..00000000 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientParsersTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client; - -import org.junit.Test; - -import java.util.List; - -import static org.junit.Assert.assertEquals; - -public class BlePmdClientParsersTest { - - // Delta data dump: C9 FF 12 00 11 00 03 09 41 FE 2B 0F 9C 0B BF 15 00 4F 00 04 1E F1 EF 00 F0 C1 23 E4 ED F4 D1 F1 F1 F5 FF 22 DE 31 00 F1 FE 21 02 1F 0E 2B 1F 00 E2 20 00 0E 02 E1 1E 20 FF F1 F1 02 C5 D0 02 E0 E1 02 03 0A 31 2E FB BA 90 2B AA 0E 23 40 9E 03 04 14 E3 EF F3 0F 02 1F 01 E0 0F 04 9E 13 E2 D0 04 E2 22 E2 C2 0E 20 0F 20 02 FE 00 0F 1C 32 EE 03 0A 89 00 07 08 7C 00 CE 2F E8 3A 9E 03 04 1E 01 00 11 19 4F 00 2F 12 FD 13 FF 0E 10 00 00 F1 C0 12 E4 EF 21 00 00 01 F1 FF FF 02 10 10 2B 51 0B 4E 31 FC 2E BF 31 14 EC 0E 2F 52 EF 03 0A 06 9E 04 0E 02 A8 88 EE E0 07 9A 00 04 0A 1F 21 1E 4E 2E FE C6 C0 02 EF 03 01 02 EE 11 03 0A F8 13 00 00 F0 40 BF A5 E7 00 76 00 - // index type data: - // 0-5: Reference sample size 6: 0xC9 0xFF 0x12 0x00 0x11 0x00 - // Sample 0 (aka. reference sample): - // channel 0: C9 FF => 0xFFC9 => -55 - int refSample0Channel0 = -55; - // channel 1: 12 00 => 0x0012 => 18 - int refSample0Channel1 = 18; - // channel 2: 11 00 => 0x0011 => 17 - int refSample0Channel2 = 17; - // Delta dump: 03 09 | 41 FE 2B 0F 9C 0B BF 15 00 4F 00 - // 6: Delta size size 1: 0x03 (3 bits) - // 7: Sample amount size 1: 0x09 (Delta block contains 9 samples) - // 8: 0x41 (binary: 01 | 000 | 001) - // Sample 1 - channel 0, size 3 bits: 001 - // Sample 1 - channel 1, size 3 bits: 000 - // 9: 0xFE (binary: 1 | 111 | 111 | 0) - // Sample 1 - channel 2, size 3 bits: 001 - // Sample 2 - channel 0, size 3 bits: 111 - // Sample 2 - channel 1, size 3 bits: 111 - // 10: 0x2B (binary: 001 | 010 | 11) - // Sample 2 - channel 2, size 3 bits: 111 - int refSample1Channel0 = -54; - int refSample1Channel1 = 18; - int refSample1Channel2 = 18; - // ... - // Delta dump: 04 1E | F1 EF 00 F0 C1 23 E4 ED F4 D1 F1 F1 F5 FF 22 DE 31 00 F1 FE 21 02 1F 0E 2B 1F 00 E2 20 00 0E 02 E1 1E 20 FF F1 F1 02 C5 D0 02 E0 E1 02 - // 19: Delta size size 1: 0x04 (4 bits) - // 20: Sample amount size 1: 0x1E (Rest of the data contains 30 samples) - // ... - // Delta dump: 03 0A | 31 2E FB BA 90 2B AA 0E 23 40 9E 03 - // 66: Delta size size 1: 0x03 (3 bits) - // 67: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) - // ... - // Delta dump: 04 14 | E3 EF F3 0F 02 1F 01 E0 0F 04 9E 13 E2 D0 04 E2 22 E2 C2 0E 20 0F 20 02 FE 00 0F 1C 32 EE - // 80: Delta size size 1: 0x04 (4 bits) - // 81: Sample amount size 1: 0x14 (Rest of the data contains 20 samples) - // ... - // Delta dump: 03 0A | 89 00 07 08 7C 00 CE 2F E8 3A 9E 03 - // 112: Delta size size 1: 0x03 (3 bits) - // 113: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) - // ... - // Delta dump: 04 1E | 01 00 11 19 4F 00 2F 12 FD 13 FF 0E 10 00 00 F1 C0 12 E4 EF 21 00 00 01 F1 FF FF 02 10 10 2B 51 0B 4E 31 FC 2E BF 31 14 EC 0E 2F 52 EF - // 126: Delta size size 1: 0x04 (4 bits) - // 127: Sample amount size 1: 0x1E (Rest of the data contains 30 samples) - // ... - // Delta dump: 03 0A |06 9E 04 0E 02 A8 88 EE E0 07 9A 00 - // 173: Delta size size 1: 0x03 (3 bits) - // 174: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) - // ... - // Delta dump: 04 0A | 1F 21 1E 4E 2E FE C6 C0 02 EF 03 01 02 EE 11 - // 187: Delta size size 1: 0x04 (4 bits) - // 188: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) - // ... - // Delta dump: 03 0A | F8 13 00 00 F0 40 BF A5 E7 00 76 00 - // 204: Delta size size 1: 0x03 (3 bits) - // 205: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) - int totalSamples_size = 1 + 9 + 30 + 10 + 20 + 10 + 30 + 10 + 10 + 10; - byte[] measurementFrame = {(byte) 0xC9, (byte) 0xFF, (byte) 0x12, (byte) 0x00, (byte) 0x11, (byte) 0x00, (byte) 0x03, (byte) 0x09, (byte) 0x41, (byte) 0xFE, (byte) 0x2B, (byte) 0x0F, (byte) 0x9C, (byte) 0x0B, (byte) 0xBF, (byte) 0x15, (byte) 0x00, (byte) 0x4F, (byte) 0x00, (byte) 0x04, (byte) 0x1E, (byte) 0xF1, (byte) 0xEF, (byte) 0x00, (byte) 0xF0, (byte) 0xC1, (byte) 0x23, (byte) 0xE4, (byte) 0xED, (byte) 0xF4, (byte) 0xD1, (byte) 0xF1, (byte) 0xF1, (byte) 0xF5, (byte) 0xFF, (byte) 0x22, (byte) 0xDE, (byte) 0x31, (byte) 0x00, (byte) 0xF1, (byte) 0xFE, (byte) 0x21, (byte) 0x02, (byte) 0x1F, (byte) 0x0E, (byte) 0x2B, (byte) 0x1F, (byte) 0x00, (byte) 0xE2, (byte) 0x20, (byte) 0x00, (byte) 0x0E, (byte) 0x02, (byte) 0xE1, (byte) 0x1E, (byte) 0x20, (byte) 0xFF, (byte) 0xF1, (byte) 0xF1, (byte) 0x02, (byte) 0xC5, (byte) 0xD0, (byte) 0x02, (byte) 0xE0, (byte) 0xE1, (byte) 0x02, (byte) 0x03, (byte) 0x0A, (byte) 0x31, (byte) 0x2E, (byte) 0xFB, (byte) 0xBA, (byte) 0x90, (byte) 0x2B, (byte) 0xAA, (byte) 0x0E, (byte) 0x23, (byte) 0x40, (byte) 0x9E, (byte) 0x03, (byte) 0x04, (byte) 0x14, (byte) 0xE3, (byte) 0xEF, (byte) 0xF3, (byte) 0x0F, (byte) 0x02, (byte) 0x1F, (byte) 0x01, (byte) 0xE0, (byte) 0x0F, (byte) 0x04, (byte) 0x9E, (byte) 0x13, (byte) 0xE2, (byte) 0xD0, (byte) 0x04, (byte) 0xE2, (byte) 0x22, (byte) 0xE2, (byte) 0xC2, (byte) 0x0E, (byte) 0x20, (byte) 0x0F, (byte) 0x20, (byte) 0x02, (byte) 0xFE, (byte) 0x00, (byte) 0x0F, (byte) 0x1C, (byte) 0x32, (byte) 0xEE, (byte) 0x03, (byte) 0x0A, (byte) 0x89, (byte) 0x00, (byte) 0x07, (byte) 0x08, (byte) 0x7C, (byte) 0x00, (byte) 0xCE, (byte) 0x2F, (byte) 0xE8, (byte) 0x3A, (byte) 0x9E, (byte) 0x03, (byte) 0x04, (byte) 0x1E, (byte) 0x01, (byte) 0x00, (byte) 0x11, (byte) 0x19, (byte) 0x4F, (byte) 0x00, (byte) 0x2F, (byte) 0x12, (byte) 0xFD, (byte) 0x13, (byte) 0xFF, (byte) 0x0E, (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0xF1, (byte) 0xC0, (byte) 0x12, (byte) 0xE4, (byte) 0xEF, (byte) 0x21, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0xF1, (byte) 0xFF, (byte) 0xFF, (byte) 0x02, (byte) 0x10, (byte) 0x10, (byte) 0x2B, (byte) 0x51, (byte) 0x0B, (byte) 0x4E, (byte) 0x31, (byte) 0xFC, (byte) 0x2E, (byte) 0xBF, (byte) 0x31, (byte) 0x14, (byte) 0xEC, (byte) 0x0E, (byte) 0x2F, (byte) 0x52, (byte) 0xEF, (byte) 0x03, (byte) 0x0A, (byte) 0x06, (byte) 0x9E, (byte) 0x04, (byte) 0x0E, (byte) 0x02, (byte) 0xA8, (byte) 0x88, (byte) 0xEE, (byte) 0xE0, (byte) 0x07, (byte) 0x9A, (byte) 0x00, (byte) 0x04, (byte) 0x0A, (byte) 0x1F, (byte) 0x21, (byte) 0x1E, (byte) 0x4E, (byte) 0x2E, (byte) 0xFE, (byte) 0xC6, (byte) 0xC0, (byte) 0x02, (byte) 0xEF, (byte) 0x03, (byte) 0x01, (byte) 0x02, (byte) 0xEE, (byte) 0x11, (byte) 0x03, (byte) 0x0A, (byte) 0xF8, (byte) 0x13, (byte) 0x00, (byte) 0x00, (byte) 0xF0, (byte) 0x40, (byte) 0xBF, (byte) 0xA5, (byte) 0xE7, (byte) 0x00, (byte) 0x76, (byte) 0x00}; - int resolution = 16; - int channels = 3; - - @Test - public void test_parseDeltaFrameRefSamples() { - // Arrange && Act - List refSamples = BlePMDClient.parseDeltaFrameRefSamples(measurementFrame, channels, resolution); - // Assert - assertEquals(refSample0Channel0, (int) refSamples.get(0)); - assertEquals(refSample0Channel1, (int) refSamples.get(1)); - assertEquals(refSample0Channel2, (int) refSamples.get(2)); - } - - @Test - public void test_parseDeltaFrameAllSamples_multipleDeltas() { - // Arrange && Act - List> allSamples = BlePMDClient.parseDeltaFramesAll(measurementFrame, channels, resolution); - - // Assert - assertEquals(totalSamples_size, allSamples.size()); - assertEquals(3, allSamples.get(0).size()); - assertEquals(refSample0Channel0, (int) allSamples.get(0).get(0)); - assertEquals(refSample0Channel1, (int) allSamples.get(0).get(1)); - assertEquals(refSample0Channel2, (int) allSamples.get(0).get(2)); - assertEquals(refSample1Channel0, (int) allSamples.get(1).get(0)); - assertEquals(refSample1Channel1, (int) allSamples.get(1).get(1)); - assertEquals(refSample1Channel2, (int) allSamples.get(1).get(2)); - } -} diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPmdSettingsTest.java b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPmdSettingsTest.java deleted file mode 100644 index a7022945..00000000 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPmdSettingsTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -public class BlePmdClientPmdSettingsTest { - - @Test - public void testPmdSettingsWithRange() { - //Arrange - byte[] bytes = {(byte) 0x00, (byte) 0x01, (byte) 0x34, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x10, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0xF5, (byte) 0x00, (byte) 0xF4, (byte) 0x01, (byte) 0xE8, (byte) 0x03, (byte) 0xD0, (byte) 0x07, (byte) 0x04, (byte) 0x01, (byte) 0x03}; - // Parameters - // Setting Type : 00 (Sample Rate) - // array_length : 01 - // array of settings values: 34 00 (52Hz) - int sampleRate = 52; - //Setting Type : 01 (Resolution) - // array_length : 01 - // array of settings values: 10 00 (16) - int resolution = 16; - // Setting Type : 02 (Range) - // array_length : 04 - // array of settings values: F5 00 (245) - int range1 = 245; - // array of settings values: F4 01 (500) - int range2 = 500; - // array of settings values: E8 03 (1000) - int range3 = 1000; - // array of settings values: D0 07 (2000) - int range4 = 2000; - // Setting Type : 04 (Channels) - // array_length : 01 - // array of settings values: 03 (3 Channels) - int channels = 3; - int numberOfSettings = 4; - - //Act - BlePMDClient.PmdSetting pmdSetting = new BlePMDClient.PmdSetting(bytes); - - // Assert - Assert.assertEquals(numberOfSettings, pmdSetting.settings.size()); - - assertEquals(sampleRate, (int) pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.SAMPLE_RATE).iterator().next()); - assertEquals(1, pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.SAMPLE_RATE).size()); - - assertEquals(resolution, (int) pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION).iterator().next()); - assertEquals(1, pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION).size()); - - assertTrue(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).contains(range1)); - assertTrue(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).contains(range2)); - assertTrue(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).contains(range3)); - assertTrue(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).contains(range4)); - assertEquals(4, pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).size()); - - assertEquals(channels, (int) pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.CHANNELS).iterator().next()); - assertEquals(1, pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.CHANNELS).size()); - - assertNull(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT)); - assertNull(pmdSetting.settings.get(BlePMDClient.PmdSetting.PmdSettingType.FACTOR)); - } - - @Test - public void testPmdSettingWithRangeMilliUnit() { - //Arrange - byte[] bytes = new byte[]{(byte) BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT.getNumVal(), - (byte) 0x02, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION.getNumVal(), (byte) 0x01, (byte) 0x0E, 0x00}; - // Parameters - // Setting Type : 03 (Range milli unit) - // array_length : 02 - // array of settings values: FF FF FF FF(52Hz) - // array of settings values: FF 00 00 00(52Hz) - // Setting Type : 01 (Resolution) - // array_length : 01 - // array of settings values: 0E 00 (16) - int resolution = 14; - int numberOfSettings = 2; - - // Act - BlePMDClient.PmdSetting settings = new BlePMDClient.PmdSetting(bytes); - - // Assert - Assert.assertEquals(numberOfSettings, settings.settings.size()); - Assert.assertTrue(settings.settings.containsKey(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT)); - Assert.assertEquals(2, Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).size()); - Assert.assertTrue(Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).contains(-1)); - Assert.assertTrue(Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).contains(0xff)); - Assert.assertTrue(settings.settings.containsKey(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION)); - - Assert.assertEquals(1, Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION)).size()); - Assert.assertTrue(Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION)).contains(resolution)); - } - - @Test - public void testPmdSelectedSerialization() { - //Arrange - Map selected = new HashMap<>(); - int sampleRate = 0xFFFF; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.SAMPLE_RATE, sampleRate); - int resolution = 0; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION, resolution); - int range = 15; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.RANGE, range); - int rangeMilliUnit = Integer.MAX_VALUE; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT, rangeMilliUnit); - int channels = 4; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.CHANNELS, channels); - int factor = 15; - selected.put(BlePMDClient.PmdSetting.PmdSettingType.FACTOR, factor); - int numberOfSettings = 5; - - //Act - BlePMDClient.PmdSetting settingsFromSelected = new BlePMDClient.PmdSetting(selected); - byte[] serializedSelected = settingsFromSelected.serializeSelected(); - BlePMDClient.PmdSetting settings = new BlePMDClient.PmdSetting(serializedSelected); - - //Assert - Assert.assertEquals(numberOfSettings, settings.settings.size()); - assertTrue(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.SAMPLE_RATE).contains(sampleRate)); - Assert.assertEquals(1, Objects.requireNonNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.SAMPLE_RATE)).size()); - - assertTrue(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RESOLUTION).contains(resolution)); - assertTrue(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE).contains(range)); - assertTrue(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.RANGE_MILLIUNIT).contains(rangeMilliUnit)); - assertTrue(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.CHANNELS).contains(channels)); - assertNull(settings.settings.get(BlePMDClient.PmdSetting.PmdSettingType.FACTOR)); - } -} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpgTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpgTest.kt deleted file mode 100644 index 12ede196..00000000 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpgTest.kt +++ /dev/null @@ -1,355 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client - -import org.junit.Assert -import org.junit.Test - -class BlePmdClientPpgTest { - @Test - fun test_PPG_DataSample_Type0() { - // Arrange - val frameType: Byte = 0 //PPG Data Sample 0 - val timeStamp: Long = 0 - val measurementFrame = byteArrayOf( - 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), //PPG0 (197121) - 0x04.toByte(), 0x05.toByte(), 0x06.toByte(), //PPG1 (394500) - 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte(), //PPG2 (8388607) - 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), //ambient (0) - 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), //PPG0 (-1) - 0x0F.toByte(), 0xEF.toByte(), 0xEF.toByte(), //PPG1 (-1052913) - 0x00.toByte(), 0x00.toByte(), 0x80.toByte(), //PPG2 (-8388608) - 0x0F.toByte(), 0xEF.toByte(), 0xEF.toByte() - ) //ambient (-1052913) - val ppg0_0 = 197121 - val ppg1_0 = 394500 - val ppg2_0 = 8388607 - val ambient_0 = 0 - val ppg0_1 = -1 - val ppg1_1 = -1052913 - val ppg2_1 = -8388608 - val ambient_1 = -1052913 - - // Act - val ppgData = BlePMDClient.PpgData(measurementFrame, timeStamp, frameType.toInt()) - - // Assert - Assert.assertEquals(2, ppgData.ppgSamples.size) - Assert.assertEquals(4, ppgData.channels) - - Assert.assertEquals(4, ppgData.ppgSamples[0].ppgDataSamples.size) - Assert.assertEquals(0, ppgData.ppgSamples[0].status) - Assert.assertEquals(ppg0_0, ppgData.ppgSamples[0].ppgDataSamples[0]) - Assert.assertEquals(ppg1_0, ppgData.ppgSamples[0].ppgDataSamples[1]) - Assert.assertEquals(ppg2_0, ppgData.ppgSamples[0].ppgDataSamples[2]) - Assert.assertEquals(ambient_0, ppgData.ppgSamples[0].ppgDataSamples[3]) - - Assert.assertEquals(4, ppgData.ppgSamples[1].ppgDataSamples.size) - Assert.assertEquals(0, ppgData.ppgSamples[1].status) - Assert.assertEquals(ppg0_1, ppgData.ppgSamples[1].ppgDataSamples[0]) - Assert.assertEquals(ppg1_1, ppgData.ppgSamples[1].ppgDataSamples[1]) - Assert.assertEquals(ppg2_1, ppgData.ppgSamples[1].ppgDataSamples[2]) - Assert.assertEquals(ambient_1, ppgData.ppgSamples[1].ppgDataSamples[3]) - } - - @Test - fun test_PPG_DataSample_Delta() { - // Arrange - // HEX: 2C 2D 00 C2 77 00 D3 D2 FF 3D 88 FF 0A 29 B2 F0 EE 34 11 B2 EC EE 74 11 B1 E8 FE B4 11 B1 E8 FE B4 11 B1 E0 FE 34 12 B0 DC 0E 75 12 B0 D8 0E B5 12 AF D4 1E F5 12 AF D0 1E 35 13 AE CC 2E 75 13 AE C8 2E B5 13 AD C4 3E F5 13 AD BC 3E 75 14 AD BC 3E 75 14 AC B8 4E B5 14 AC B4 4E F5 14 AB B0 5E 35 15 AA AC 6E 75 15 AA A8 6E B5 15 AA A4 6E F5 15 A9 A0 7E 35 16 A9 9C 7E 75 16 A8 98 8E B5 16 A7 94 9E F5 16 A7 90 9E 35 17 A7 8C 9E 75 17 A6 88 AE B5 17 A5 88 BE B5 17 A5 80 BE 35 18 A4 7C CE 75 18 A4 78 CE B5 18 A3 78 DE B5 18 A2 70 EE 35 19 A2 6C EE 75 19 A2 6C EE 75 19 A1 68 FE B5 19 A0 60 0E 36 1A 9F 60 1E 36 1A 9F 5C 1E 76 1A 9F 58 1E B6 1A 9D 54 3E F6 1A - // index type data: - // 0-5: Reference sample size 6: 0x2C 0x2D 0x00 0xC2 0x77 0x00 0xD3 0xD2 0xFF 0x3D 0x88 0xFF - // Sample 0 (aka. reference sample): - // channel 0: 2C 2D 00 => 0x002D2C => 11564 - val refSample0Channel0 = 11564 - // channel 1: C2 77 00 => 0x0077C2 => 30658 - val refSample0Channel1 = 30658 - // channel 2: D3 D2 FF => 0xFFD2D3 => -11565 - val refSample0Channel2 = -11565 - // channel 3: 3D 88 FF => 0xFF883D => -30659 - val refSample0Channel3 = -30659 - // Delta dump: 0A 29 | B2 F0 EE 34 11 B2 EC EE 74 11 B1 E8 FE B4 11 B1 ... - // 6: Delta size size 1: 0x0A (10 bits) - // 7: Sample amount size 1: 0x29 (Delta block contains 41 samples) - // 8: 0xB2 (binary: 1011 0010) - // 9: 0xF0 (binary: 1111 00 | 00) - // 10: 0xEE (binary: 1110 | 1110) - // Sample 1 - channel 0, size 10 bits: 00 1011 0010 - // Sample 1 - channel 1, size 10 bits: 11 1011 1100 - // 11: 0x34 (binary: 00 | 11 0100) - // Sample 1 - channel 2, size 10 bits: 11 0100 1110 - // 12: - // Sample 1 - channel 3, size 10 bits: 00 0100 0100 0x11 (binary: 0001 0001) - val refSample1Channel0 = 178 - val refSample1Channel1 = -68 - val refSample1Channel2 = -178 - val refSample1Channel3 = 68 - val amountOfSamples = 1 + 41 // reference sample + delta samples - val measurementFrame = byteArrayOf( - 0x2C.toByte(), - 0x2D.toByte(), - 0x00.toByte(), - 0xC2.toByte(), - 0x77.toByte(), - 0x00.toByte(), - 0xD3.toByte(), - 0xD2.toByte(), - 0xFF.toByte(), - 0x3D.toByte(), - 0x88.toByte(), - 0xFF.toByte(), - 0x0A.toByte(), - 0x29.toByte(), - 0xB2.toByte(), - 0xF0.toByte(), - 0xEE.toByte(), - 0x34.toByte(), - 0x11.toByte(), - 0xB2.toByte(), - 0xEC.toByte(), - 0xEE.toByte(), - 0x74.toByte(), - 0x11.toByte(), - 0xB1.toByte(), - 0xE8.toByte(), - 0xFE.toByte(), - 0xB4.toByte(), - 0x11.toByte(), - 0xB1.toByte(), - 0xE8.toByte(), - 0xFE.toByte(), - 0xB4.toByte(), - 0x11.toByte(), - 0xB1.toByte(), - 0xE0.toByte(), - 0xFE.toByte(), - 0x34.toByte(), - 0x12.toByte(), - 0xB0.toByte(), - 0xDC.toByte(), - 0x0E.toByte(), - 0x75.toByte(), - 0x12.toByte(), - 0xB0.toByte(), - 0xD8.toByte(), - 0x0E.toByte(), - 0xB5.toByte(), - 0x12.toByte(), - 0xAF.toByte(), - 0xD4.toByte(), - 0x1E.toByte(), - 0xF5.toByte(), - 0x12.toByte(), - 0xAF.toByte(), - 0xD0.toByte(), - 0x1E.toByte(), - 0x35.toByte(), - 0x13.toByte(), - 0xAE.toByte(), - 0xCC.toByte(), - 0x2E.toByte(), - 0x75.toByte(), - 0x13.toByte(), - 0xAE.toByte(), - 0xC8.toByte(), - 0x2E.toByte(), - 0xB5.toByte(), - 0x13.toByte(), - 0xAD.toByte(), - 0xC4.toByte(), - 0x3E.toByte(), - 0xF5.toByte(), - 0x13.toByte(), - 0xAD.toByte(), - 0xBC.toByte(), - 0x3E.toByte(), - 0x75.toByte(), - 0x14.toByte(), - 0xAD.toByte(), - 0xBC.toByte(), - 0x3E.toByte(), - 0x75.toByte(), - 0x14.toByte(), - 0xAC.toByte(), - 0xB8.toByte(), - 0x4E.toByte(), - 0xB5.toByte(), - 0x14.toByte(), - 0xAC.toByte(), - 0xB4.toByte(), - 0x4E.toByte(), - 0xF5.toByte(), - 0x14.toByte(), - 0xAB.toByte(), - 0xB0.toByte(), - 0x5E.toByte(), - 0x35.toByte(), - 0x15.toByte(), - 0xAA.toByte(), - 0xAC.toByte(), - 0x6E.toByte(), - 0x75.toByte(), - 0x15.toByte(), - 0xAA.toByte(), - 0xA8.toByte(), - 0x6E.toByte(), - 0xB5.toByte(), - 0x15.toByte(), - 0xAA.toByte(), - 0xA4.toByte(), - 0x6E.toByte(), - 0xF5.toByte(), - 0x15.toByte(), - 0xA9.toByte(), - 0xA0.toByte(), - 0x7E.toByte(), - 0x35.toByte(), - 0x16.toByte(), - 0xA9.toByte(), - 0x9C.toByte(), - 0x7E.toByte(), - 0x75.toByte(), - 0x16.toByte(), - 0xA8.toByte(), - 0x98.toByte(), - 0x8E.toByte(), - 0xB5.toByte(), - 0x16.toByte(), - 0xA7.toByte(), - 0x94.toByte(), - 0x9E.toByte(), - 0xF5.toByte(), - 0x16.toByte(), - 0xA7.toByte(), - 0x90.toByte(), - 0x9E.toByte(), - 0x35.toByte(), - 0x17.toByte(), - 0xA7.toByte(), - 0x8C.toByte(), - 0x9E.toByte(), - 0x75.toByte(), - 0x17.toByte(), - 0xA6.toByte(), - 0x88.toByte(), - 0xAE.toByte(), - 0xB5.toByte(), - 0x17.toByte(), - 0xA5.toByte(), - 0x88.toByte(), - 0xBE.toByte(), - 0xB5.toByte(), - 0x17.toByte(), - 0xA5.toByte(), - 0x80.toByte(), - 0xBE.toByte(), - 0x35.toByte(), - 0x18.toByte(), - 0xA4.toByte(), - 0x7C.toByte(), - 0xCE.toByte(), - 0x75.toByte(), - 0x18.toByte(), - 0xA4.toByte(), - 0x78.toByte(), - 0xCE.toByte(), - 0xB5.toByte(), - 0x18.toByte(), - 0xA3.toByte(), - 0x78.toByte(), - 0xDE.toByte(), - 0xB5.toByte(), - 0x18.toByte(), - 0xA2.toByte(), - 0x70.toByte(), - 0xEE.toByte(), - 0x35.toByte(), - 0x19.toByte(), - 0xA2.toByte(), - 0x6C.toByte(), - 0xEE.toByte(), - 0x75.toByte(), - 0x19.toByte(), - 0xA2.toByte(), - 0x6C.toByte(), - 0xEE.toByte(), - 0x75.toByte(), - 0x19.toByte(), - 0xA1.toByte(), - 0x68.toByte(), - 0xFE.toByte(), - 0xB5.toByte(), - 0x19.toByte(), - 0xA0.toByte(), - 0x60.toByte(), - 0x0E.toByte(), - 0x36.toByte(), - 0x1A.toByte(), - 0x9F.toByte(), - 0x60.toByte(), - 0x1E.toByte(), - 0x36.toByte(), - 0x1A.toByte(), - 0x9F.toByte(), - 0x5C.toByte(), - 0x1E.toByte(), - 0x76.toByte(), - 0x1A.toByte(), - 0x9F.toByte(), - 0x58.toByte(), - 0x1E.toByte(), - 0xB6.toByte(), - 0x1A.toByte(), - 0x9D.toByte(), - 0x54.toByte(), - 0x3E.toByte(), - 0xF6.toByte(), - 0x1A.toByte() - ) - val factor = 1.0f - val channels = 4 - val resolution = 22 - val timeStamp: Long = 0 - - // Act - val ppgData = - BlePMDClient.PpgData(measurementFrame, factor, resolution, channels, timeStamp) - - // Assert - Assert.assertEquals(amountOfSamples, ppgData.ppgSamples.size) - Assert.assertEquals(4, ppgData.channels) - Assert.assertEquals(4, ppgData.ppgSamples[0].ppgDataSamples.size) - Assert.assertEquals(0, ppgData.ppgSamples[0].status) - Assert.assertEquals( - (factor * refSample0Channel0).toInt(), - ppgData.ppgSamples[0].ppgDataSamples[0] - ) - Assert.assertEquals( - (factor * refSample0Channel1).toInt(), - ppgData.ppgSamples[0].ppgDataSamples[1] - ) - Assert.assertEquals( - (factor * refSample0Channel2).toInt(), - ppgData.ppgSamples[0].ppgDataSamples[2] - ) - Assert.assertEquals( - (factor * refSample0Channel3).toInt(), - ppgData.ppgSamples[0].ppgDataSamples[3] - ) - Assert.assertEquals( - (factor * (refSample0Channel0 + refSample1Channel0)).toInt(), - ppgData.ppgSamples[1].ppgDataSamples[0] - ) - Assert.assertEquals( - (factor * (refSample0Channel1 + refSample1Channel1)).toInt(), - ppgData.ppgSamples[1].ppgDataSamples[1] - ) - Assert.assertEquals( - (factor * (refSample0Channel2 + refSample1Channel2)).toInt(), - ppgData.ppgSamples[1].ppgDataSamples[2] - ) - Assert.assertEquals( - (factor * (refSample0Channel3 + refSample1Channel3)).toInt(), - ppgData.ppgSamples[1].ppgDataSamples[3] - ) - Assert.assertEquals(4, ppgData.ppgSamples[1].ppgDataSamples.size) - Assert.assertEquals(0, ppgData.ppgSamples[1].status) - } -} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientThreeAxisTest.java b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientThreeAxisTest.java deleted file mode 100644 index 408a297b..00000000 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientThreeAxisTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class BlePmdClientThreeAxisTest { - - @Test - public void test_parseThreeAxisData_withResolution16() { - // Arrange - // HEX: 71 07 F0 6A 9E 8D 0A 38 BE 5C BE BA 2F 96 B3 EE 4B E5 AD FB 42 B9 EB BE 4C FE BA 2F 92 BF EE 4B E4 B1 FB 12 B9 EC BD 3C 3E BB 2F 8F D3 DE 4B E3 B5 F7 D2 B8 ED BD 30 7E 7B 2F 8B E3 CE 8B E2 BA F7 A2 B8 EE BC 20 BE 7B 2F 88 F3 CE CB E1 BD EF 52 F8 EF BC 18 FE 3B 2F 84 03 BF CB E0 C2 EF 32 B8 F0 BB 04 4E BC 2E 81 13 AF 0B E0 C6 EF F2 F7 F1 B9 FC 7D BC 2E 7D 27 9F 4B DF CA EB C2 F7 F2 B8 EC CD 7C 2E 7B 37 8F 4B DE CE E3 92 F7 F3 B8 E0 0D FD 2D 77 4B 7F CB DD D2 DF 62 37 F5 B7 D4 4D BD 2D 74 5B 6F CB DC D7 D7 32 37 F6 B5 C8 8D 7D 2D 71 6B 4F 4B DC DC D3 F2 36 F7 B4 BC DD FD 2C 6F 7B 3F 4B DB E0 CF D2 36 F8 B2 B0 2D BE 2C 6C 8F 1F CB DA E3 C7 A2 76 F9 - // index type data: - // 0-5: Reference sample size 6: 71 07 F0 6A 9E 8D - // Sample 0 (aka. reference sample): - // channel 0: 71 07 =>0x0771 => 1905 - int refSample0Channel0 = 1905; - // channel 1: F0 6A => 0x6AF0 => 27376 - int refSample0Channel1 = 27376; - // channel 2: 9E 8D => 0x8D9E => -29282 - int refSample0Channel2 = -29282; - // Delta dump: 0A 38 BE 5C BE BA 2F 96 B3 EE 4B E5 AD FB - // 6: Delta size size 1: 0x0A (10 bits) - // 7: Sample amount size 1: 0x38 (Delta block contains 56 samples) - // 8: 0xBE (binary: 1011 1110) - // 9: 0x5C (binary: 0101 11| 00) - // Sample 1 - channel 0, size 10 bits: 00 1011 1110 => 190 - // 10: 0xBE (binary: 1011 | 1110) - // Sample 1 - channel 1, size 10 bits: 11 1001 0111 => -105 - // 11: 0xBA (binary: 10 |11 1010) - // Sample 1 - channel 2, size 10 bits: 11 1010 1011 => -85 - int refSample1Channel0 = 190; - int refSample1Channel1 = -105; - int refSample1Channel2 = -85; - int amountOfSamples = 1 + 56; // reference sample + delta samples - byte[] measurementFrame = {(byte) 0x71, (byte) 0x07, (byte) 0xF0, (byte) 0x6A, (byte) 0x9E, (byte) 0x8D, (byte) 0x0A, (byte) 0x38, (byte) 0xBE, (byte) 0x5C, (byte) 0xBE, (byte) 0xBA, (byte) 0x2F, (byte) 0x96, (byte) 0xB3, (byte) 0xEE, (byte) 0x4B, (byte) 0xE5, (byte) 0xAD, (byte) 0xFB, (byte) 0x42, (byte) 0xB9, (byte) 0xEB, (byte) 0xBE, (byte) 0x4C, (byte) 0xFE, (byte) 0xBA, (byte) 0x2F, (byte) 0x92, (byte) 0xBF, (byte) 0xEE, (byte) 0x4B, (byte) 0xE4, (byte) 0xB1, (byte) 0xFB, (byte) 0x12, (byte) 0xB9, (byte) 0xEC, (byte) 0xBD, (byte) 0x3C, (byte) 0x3E, (byte) 0xBB, (byte) 0x2F, (byte) 0x8F, (byte) 0xD3, (byte) 0xDE, (byte) 0x4B, (byte) 0xE3, (byte) 0xB5, (byte) 0xF7, (byte) 0xD2, (byte) 0xB8, (byte) 0xED, (byte) 0xBD, (byte) 0x30, (byte) 0x7E, (byte) 0x7B, (byte) 0x2F, (byte) 0x8B, (byte) 0xE3, (byte) 0xCE, (byte) 0x8B, (byte) 0xE2, (byte) 0xBA, (byte) 0xF7, (byte) 0xA2, (byte) 0xB8, (byte) 0xEE, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0x7B, (byte) 0x2F, (byte) 0x88, (byte) 0xF3, (byte) 0xCE, (byte) 0xCB, (byte) 0xE1, (byte) 0xBD, (byte) 0xEF, (byte) 0x52, (byte) 0xF8, (byte) 0xEF, (byte) 0xBC, (byte) 0x18, (byte) 0xFE, (byte) 0x3B, (byte) 0x2F, (byte) 0x84, (byte) 0x03, (byte) 0xBF, (byte) 0xCB, (byte) 0xE0, (byte) 0xC2, (byte) 0xEF, (byte) 0x32, (byte) 0xB8, (byte) 0xF0, (byte) 0xBB, (byte) 0x04, (byte) 0x4E, (byte) 0xBC, (byte) 0x2E, (byte) 0x81, (byte) 0x13, (byte) 0xAF, (byte) 0x0B, (byte) 0xE0, (byte) 0xC6, (byte) 0xEF, (byte) 0xF2, (byte) 0xF7, (byte) 0xF1, (byte) 0xB9, (byte) 0xFC, (byte) 0x7D, (byte) 0xBC, (byte) 0x2E, (byte) 0x7D, (byte) 0x27, (byte) 0x9F, (byte) 0x4B, (byte) 0xDF, (byte) 0xCA, (byte) 0xEB, (byte) 0xC2, (byte) 0xF7, (byte) 0xF2, (byte) 0xB8, (byte) 0xEC, (byte) 0xCD, (byte) 0x7C, (byte) 0x2E, (byte) 0x7B, (byte) 0x37, (byte) 0x8F, (byte) 0x4B, (byte) 0xDE, (byte) 0xCE, (byte) 0xE3, (byte) 0x92, (byte) 0xF7, (byte) 0xF3, (byte) 0xB8, (byte) 0xE0, (byte) 0x0D, (byte) 0xFD, (byte) 0x2D, (byte) 0x77, (byte) 0x4B, (byte) 0x7F, (byte) 0xCB, (byte) 0xDD, (byte) 0xD2, (byte) 0xDF, (byte) 0x62, (byte) 0x37, (byte) 0xF5, (byte) 0xB7, (byte) 0xD4, (byte) 0x4D, (byte) 0xBD, (byte) 0x2D, (byte) 0x74, (byte) 0x5B, (byte) 0x6F, (byte) 0xCB, (byte) 0xDC, (byte) 0xD7, (byte) 0xD7, (byte) 0x32, (byte) 0x37, (byte) 0xF6, (byte) 0xB5, (byte) 0xC8, (byte) 0x8D, (byte) 0x7D, (byte) 0x2D, (byte) 0x71, (byte) 0x6B, (byte) 0x4F, (byte) 0x4B, (byte) 0xDC, (byte) 0xDC, (byte) 0xD3, (byte) 0xF2, (byte) 0x36, (byte) 0xF7, (byte) 0xB4, (byte) 0xBC, (byte) 0xDD, (byte) 0xFD, (byte) 0x2C, (byte) 0x6F, (byte) 0x7B, (byte) 0x3F, (byte) 0x4B, (byte) 0xDB, (byte) 0xE0, (byte) 0xCF, (byte) 0xD2, (byte) 0x36, (byte) 0xF8, (byte) 0xB2, (byte) 0xB0, (byte) 0x2D, (byte) 0xBE, (byte) 0x2C, (byte) 0x6C, (byte) 0x8F, (byte) 0x1F, (byte) 0xCB, (byte) 0xDA, (byte) 0xE3, (byte) 0xC7, (byte) 0xA2, (byte) 0x76, (byte) 0xF9}; - int resolution = 16; - int range = 8; - float factor = 2.44E-4f; - long timeStamp = 0; - - // Act - BlePMDClient.ThreeAxisDeltaFramedData threeAxisDeltaFramedData = new BlePMDClient.ThreeAxisDeltaFramedData(measurementFrame, factor, resolution, timeStamp); - - // Assert - assertEquals((factor * refSample0Channel0), threeAxisDeltaFramedData.axisSamples.get(0).x, 0.001); - assertEquals((factor * refSample0Channel1), threeAxisDeltaFramedData.axisSamples.get(0).y, 0.001); - assertEquals((factor * refSample0Channel2), threeAxisDeltaFramedData.axisSamples.get(0).z, 0.001); - - assertEquals((factor * (refSample0Channel0 + refSample1Channel0)), threeAxisDeltaFramedData.axisSamples.get(1).x, 0.000000001); - assertEquals((factor * (refSample0Channel1 + refSample1Channel1)), threeAxisDeltaFramedData.axisSamples.get(1).y, 0.000000001); - assertEquals((factor * (refSample0Channel2 + refSample1Channel2)), threeAxisDeltaFramedData.axisSamples.get(1).z, 0.000000001); - - // validate data in range - for (BlePMDClient.ThreeAxisDeltaFramedData.ThreeAxisSample sample : threeAxisDeltaFramedData.axisSamples) { - assertTrue(Math.abs(sample.x) <= range); - assertTrue(Math.abs(sample.y) <= range); - assertTrue(Math.abs(sample.z) <= range); - } - - // validate data size - assertEquals(amountOfSamples, threeAxisDeltaFramedData.axisSamples.size()); - } -} diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientControlPointResponseTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientControlPointResponseTest.kt similarity index 87% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientControlPointResponseTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientControlPointResponseTest.kt index be4c47f2..f4d61f77 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientControlPointResponseTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientControlPointResponseTest.kt @@ -1,4 +1,4 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd import io.mockk.MockKAnnotations import org.junit.Assert.assertEquals @@ -21,12 +21,12 @@ class BlePmdClientControlPointResponseTest { // 0: Response code size 1: 0xF0 val expectedResponseCode = 0xF0.toByte() // 1: Op code size 1: 0x01 (Request stream settings) - val expectedOpCode = BlePMDClient.PmdControlPointCommand.GET_MEASUREMENT_SETTINGS + val expectedOpCode = PmdControlPointCommand.GET_MEASUREMENT_SETTINGS // 2: Measurement Type size 1: 0x02 (Acc) val expectedMeasurementType = 0x02.toByte() // 3: Error Code size 1: 0x00 (Success) val expectedStatus = - BlePMDClient.PmdControlPointResponse.PmdControlPointResponseCode.SUCCESS + PmdControlPointResponse.PmdControlPointResponseCode.SUCCESS // 4: More size 1: 0x00 (No more) val expectedMore = false // 5..n: Parameters size 3: 0xFF 0xFF 0xFF (some data) @@ -45,7 +45,7 @@ class BlePmdClientControlPointResponseTest { ) //Act - val response = BlePMDClient.PmdControlPointResponse(cpResponse) + val response = PmdControlPointResponse(cpResponse) //Assert assertEquals(expectedResponseCode, response.responseCode) @@ -65,12 +65,12 @@ class BlePmdClientControlPointResponseTest { // 0: Response code size 1: 0xF0 val expectedResponseCode = 0xF0.toByte() // 1: Op code size 1: 0x01 (Request stream settings) - val expectedOpCode = BlePMDClient.PmdControlPointCommand.GET_MEASUREMENT_SETTINGS + val expectedOpCode = PmdControlPointCommand.GET_MEASUREMENT_SETTINGS // 2: Measurement Type size 1: 0x06 (mag) val expectedMeasurementType = 0x06.toByte() // 3: Error Code size 1: 0x07 (Failure) val expectedStatus = - BlePMDClient.PmdControlPointResponse.PmdControlPointResponseCode.ERROR_INVALID_RESOLUTION + PmdControlPointResponse.PmdControlPointResponseCode.ERROR_INVALID_RESOLUTION // 4: More size 1: 0x00 (No more) val expectedMore = false // 5..n: Parameters size 3: 0xFF 0xFF 0xFF (some data) @@ -81,7 +81,7 @@ class BlePmdClientControlPointResponseTest { byteArrayOf(0xF0.toByte(), 0x01.toByte(), 0x06.toByte(), 0x07.toByte(), 0x00.toByte()) //Act - val response = BlePMDClient.PmdControlPointResponse(cpResponse) + val response = PmdControlPointResponse(cpResponse) //Assert assertEquals(expectedResponseCode, response.responseCode) diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientParsersTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientParsersTest.kt new file mode 100644 index 00000000..1265383e --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientParsersTest.kt @@ -0,0 +1,358 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient.PmdDataFieldEncoding +import org.junit.Assert +import org.junit.Test + +class BlePmdClientParsersTest { + + @Test + fun `parse reference sample when resolution16 and type is unsigned int`() { + // Arrange + // HEX: FF FF 00 00 FF 7F 00 80 + // index type data: + // channel 0: FF FF => 0xFFFF => -1 + // channel 1: 00 00 => 0x0000 => 0 + // channel 2: FF 7F => 0x7FFF => 32767 + // channel 3: 00 80 => 0x8000 => -32768 + val dataBytes = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0x00.toByte(), 0x00.toByte(), 0xFF.toByte(), 0x7F.toByte(), 0x00.toByte(), 0x80.toByte()) + val expectedRefSample0 = -1 + val expectedRefSample1 = 0 + val expectedRefSample2 = 32767 + val expectedRefSample3 = -32768 + val resolution = 16 + val channels = 4 + + // Act + val refSamples = BlePMDClient.parseDeltaFrameRefSamples(dataBytes, channels, resolution, PmdDataFieldEncoding.SIGNED_INT) + + // Assert + Assert.assertEquals(expectedRefSample0, refSamples[0]) + Assert.assertEquals(expectedRefSample1, refSamples[1]) + Assert.assertEquals(expectedRefSample2, refSamples[2]) + Assert.assertEquals(expectedRefSample3, refSamples[3]) + } + + @Test + fun `parse reference sample when resolution32 and type is decimal IEEE 754`() { + // Arrange + // HEX: 00 00 00 00 FF FF FF FF 00 00 00 80 + val dataBytes = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x80.toByte() + ) + + // Reference sample size 8: 00 00 00 00 FF FF FF FF 00 00 00 80 + // Sample 0 - channel 0: 02 00 00 00 => 0x00000000 + // Sample 0 - channel 1: FF FF FF FF => 0xFFFFFFFF + // Sample 0 - channel 2: 00 00 FF FF => 0x80000000 + val expectedRefSample0 = 0x00000000 + val expectedRefSample1 = 0xFFFFFFFF.toInt() + val expectedRefSample2 = 0x80000000.toInt() + val resolution = 32 + val channels = 3 + + // Act + val refSamples = BlePMDClient.parseDeltaFrameRefSamples(dataBytes, channels, resolution, PmdDataFieldEncoding.FLOAT_IEEE754) + + // Assert + Assert.assertEquals(0x0, expectedRefSample0.xor(refSamples[0])) + Assert.assertEquals(0x0, expectedRefSample1.xor(refSamples[1])) + Assert.assertEquals(0x0, expectedRefSample2.xor(refSamples[2])) + } + + @Test + fun test_parseDeltaFrameAllSamples_withResolution32_realVector1() { + // Arrange + // HEX: 00 00 80 3F 00 00 20 41 00 00 A0 41 + // 1C (28 bits) + // 06 (6 samples => (6x3x28)/8 = 63 bytes) + // 00 00 A0 01 00 00 08 CD CC EC + // 0D 00 00 08 CD CC EC 3D 33 33 + // 1A CD CC EC 3D 33 33 1A 00 00 + // C0 30 33 33 1A 00 00 C0 A0 99 + // 99 DA 00 00 C0 A0 99 99 DA 66 + // 66 A6 A1 99 99 DA 66 66 A6 01 + // 00 00 0E + // 20 (32 bits) + // 03 (3 samples => (3x3x32)/8 = 36 bytes) + // 66 66 A6 01 + // 00 00 E0 00 + // 42 6C 45 0E + // 00 00 E0 00 + // 42 6C 45 0E + // 2B F8 ED 21 + // 00 00 60 FD + // BE 93 BA F0 + // 93 9B 4C CF + // 1C (28 bits) + // 06 (6 samples => (6x3x28)/8 = 63 bytes) + // 00 00 A0 01 00 00 08 CD CC EC + // 0D 00 00 08 CD CC EC 3D 33 33 + // 1A CD CC EC 3D 33 33 1A 00 00 + // C0 30 33 33 1A 00 00 C0 A0 99 + // 99 DA 00 00 C0 A0 99 99 DA 66 + // 66 A6 A1 99 99 DA 66 66 A6 01 + // 00 00 0E + // 20 (32 bits) + // 03 (3 samples => (3x3x32)/8 = 36 bytes) + // 66 66 A6 01 + // 00 00 E0 00 + // 42 6C 45 0E + // 00 00 E0 00 + // 42 6C 45 0E + // 2B F8 ED 21 + // 00 00 60 FD + // BE 93 BA F0 + // 93 9B 4C CF + + // index type data + // 0..3 Sample 0 - channel 0 (ref. sample) 00 00 80 3F (0x3F800000) + // 4..7 Sample 0 - channel 1 (ref. sample) 00 00 20 41 (0x41200000) + // 8..11 Sample 0 - channel 2 (ref. sample) 00 00 A0 41 (0x41A00000) + // 12 Delta size 1C (28 bit) + // 13 Sample amount 06 (6 samples) + // 14..25 delta data: 00 00 A0 01 00 00 08 CD CC EC 0D ... + // 14..17 Sample 1 - channel 0: (0x1A00000) + // 18..21 Sample 1 - channel 1: (0x0800000) + // 22..25 Sample 1 - channel 2 (0xDECCCCD) + + val expectedSamplesSize = 1 + 6 + 3 + 6 + 3 // reference sample + delta samples + val sample0Channel0 = 0x3F800000 + val sample0Channel1 = 0x41200000 + val sample0Channel2 = 0x41A00000 + + val sample1Channel0 = 0x3F800000 + 0x1A00000 + val sample1Channel1 = 0x41200000 + 0x0800000 + val sample1Channel2 = (0x41A00000 + 0xFDECCCCD).toInt() + + val deltaCodedDataFrame = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), 0x80.toByte(), 0x3F.toByte(), 0x00.toByte(), 0x00.toByte(), 0x20.toByte(), 0x41.toByte(), 0x00.toByte(), + 0x00.toByte(), 0xA0.toByte(), 0x41.toByte(), 0x1C.toByte(), 0x06.toByte(), 0x00.toByte(), 0x00.toByte(), 0xA0.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x08.toByte(), 0xCD.toByte(), 0xCC.toByte(), 0xEC.toByte(), 0x0D.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x08.toByte(), 0xCD.toByte(), 0xCC.toByte(), 0xEC.toByte(), 0x3D.toByte(), 0x33.toByte(), 0x33.toByte(), 0x1A.toByte(), 0xCD.toByte(), + 0xCC.toByte(), 0xEC.toByte(), 0x3D.toByte(), 0x33.toByte(), 0x33.toByte(), 0x1A.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC0.toByte(), + 0x30.toByte(), 0x33.toByte(), 0x33.toByte(), 0x1A.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC0.toByte(), 0xA0.toByte(), 0x99.toByte(), + 0x99.toByte(), 0xDA.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC0.toByte(), 0xA0.toByte(), 0x99.toByte(), 0x99.toByte(), 0xDA.toByte(), + 0x66.toByte(), 0x66.toByte(), 0xA6.toByte(), 0xA1.toByte(), 0x99.toByte(), 0x99.toByte(), 0xDA.toByte(), 0x66.toByte(), 0x66.toByte(), + 0xA6.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x0E.toByte(), 0x20.toByte(), 0x03.toByte(), 0x66.toByte(), 0x66.toByte(), + 0xA6.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE0.toByte(), 0x00.toByte(), 0x42.toByte(), 0x6C.toByte(), 0x45.toByte(), + 0x0E.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE0.toByte(), 0x00.toByte(), 0x42.toByte(), 0x6C.toByte(), 0x45.toByte(), 0x0E.toByte(), + 0x2B.toByte(), 0xF8.toByte(), 0xED.toByte(), 0x21.toByte(), 0x00.toByte(), 0x00.toByte(), 0x60.toByte(), 0xFD.toByte(), 0xBE.toByte(), + 0x93.toByte(), 0xBA.toByte(), 0xF0.toByte(), 0x93.toByte(), 0x9B.toByte(), 0x4C.toByte(), 0xCF.toByte(), 0x1C.toByte(), 0x06.toByte(), + 0x00.toByte(), 0x00.toByte(), 0xA0.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x08.toByte(), 0xCD.toByte(), 0xCC.toByte(), + 0xEC.toByte(), 0x0D.toByte(), 0x00.toByte(), 0x00.toByte(), 0x08.toByte(), 0xCD.toByte(), 0xCC.toByte(), 0xEC.toByte(), 0x3D.toByte(), + 0x33.toByte(), 0x33.toByte(), 0x1A.toByte(), 0xCD.toByte(), 0xCC.toByte(), 0xEC.toByte(), 0x3D.toByte(), 0x33.toByte(), 0x33.toByte(), + 0x1A.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC0.toByte(), 0x30.toByte(), 0x33.toByte(), 0x33.toByte(), 0x1A.toByte(), 0x00.toByte(), + 0x00.toByte(), 0xC0.toByte(), 0xA0.toByte(), 0x99.toByte(), 0x99.toByte(), 0xDA.toByte(), 0x00.toByte(), 0x00.toByte(), 0xC0.toByte(), + 0xA0.toByte(), 0x99.toByte(), 0x99.toByte(), 0xDA.toByte(), 0x66.toByte(), 0x66.toByte(), 0xA6.toByte(), 0xA1.toByte(), 0x99.toByte(), + 0x99.toByte(), 0xDA.toByte(), 0x66.toByte(), 0x66.toByte(), 0xA6.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x0E.toByte(), + 0x20.toByte(), 0x03.toByte(), 0x66.toByte(), 0x66.toByte(), 0xA6.toByte(), 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE0.toByte(), + 0x00.toByte(), 0x42.toByte(), 0x6C.toByte(), 0x45.toByte(), 0x0E.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE0.toByte(), 0x00.toByte(), + 0x42.toByte(), 0x6C.toByte(), 0x45.toByte(), 0x0E.toByte(), 0x2B.toByte(), 0xF8.toByte(), 0xED.toByte(), 0x21.toByte(), 0x00.toByte(), + 0x00.toByte(), 0x60.toByte(), 0xFD.toByte(), 0xBE.toByte(), 0x93.toByte(), 0xBA.toByte(), 0xF0.toByte(), 0x93.toByte(), 0x9B.toByte(), + 0x4C.toByte(), 0xCF.toByte() + ) + val resolution = 32 + val channels = 3 + + // Act + val allSamples = BlePMDClient.parseDeltaFramesAll(deltaCodedDataFrame, channels, resolution, PmdDataFieldEncoding.FLOAT_IEEE754) + + // Assert + Assert.assertEquals(expectedSamplesSize, allSamples.size) + Assert.assertEquals(3, allSamples[0].size) + + Assert.assertEquals(0x0, sample0Channel0.xor(allSamples[0][0])) + Assert.assertEquals(0x0, sample0Channel1.xor(allSamples[0][1])) + Assert.assertEquals(0x0, sample0Channel2.xor(allSamples[0][2])) + + Assert.assertEquals(0x0, sample1Channel0.xor(allSamples[1][0])) + Assert.assertEquals(0x0, sample1Channel1.xor(allSamples[1][1])) + Assert.assertEquals(0x0, sample1Channel2.xor(allSamples[1][2])) + } + + @Test + fun test_parseDeltaFrameAllSamples_withResolution32() { + // Arrange + // HEX: 02 00 00 00 19 32 99 E9 00 DA FE FF 20 11 17 32 99 E9 E7 A7 65 16 38 DB 06 46 E7 A7 65 16 38 DB 06 46 73 41 3B B8 7A 26 04 01 C9 0A FF 59 77 07 D1 32 87 BF 01 9F 21 3E 0D 91 E5 01 76 AD 21 3E 0D 91 E5 01 76 AD 92 01 38 CD E5 01 76 AD 92 01 38 CD 00 54 84 87 92 01 38 CD 00 54 84 87 78 CB EE 40 00 54 84 87 78 CB EE 40 F1 DE CC 8B 78 CB EE 40 F1 DE CC 8B 17 32 99 E9 F1 DE CC 8B 17 32 99 E9 E7 A7 65 16 17 32 99 E9 E7 A7 65 16 38 DB 06 46 E7 A7 65 16 38 DB 06 46 73 41 3B B8 7A 26 04 01 C9 0A FF 59 77 07 D1 32 87 BF 01 9F 21 3E 0D 91 E5 01 76 AD 21 3E 0D 91 E5 01 76 AD 92 01 38 CD E5 01 76 AD 92 01 38 CD 00 54 84 87 92 01 38 CD 00 54 84 87 78 CB EE 40 + // index type data: + // 0-5: Reference sample size 6: 02 00 00 00 19 32 99 E9 00 DA FE FF + // Sample 0 (aka. reference sample): + // Sample 0 - channel 0: 02 00 00 00 => 0x00000002 + // Sample 0 - channel 1: 19 32 99 E9 => 0xE9993219 + // Sample 0 - channel 2: 00 DA FE FF => 0xFFFEDA00 + // 6: Delta size size 1: 0x20 (32 bits) + // 7: Sample amount size 1: 0x11 (Delta block contains 17 samples) + // 8-11: Sample 1 - channel 0 17 32 99 E9 => 0xE9993217 + // 12-15: Sample 1 - channel 1 E7 A7 65 16 => 0x1665A7E7 + // 16-19: Sample 1 - channel 2 38 DB 06 46 => 0x4606DB38 + + val sample0Channel0 = 0x00000002 + val sample0Channel1 = 0xE9993219.toInt() + val sample0Channel2 = 0xFFFEDA00.toInt() + val sample1Channel0 = sample0Channel0 + 0xE9993217.toInt() + val sample1Channel1 = sample0Channel1 + 0x1665A7E7 + val sample1Channel2 = sample0Channel2 + 0x4606DB38 + + val measurementFrame = byteArrayOf( + 0x02.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x19.toByte(), 0x32.toByte(), 0x99.toByte(), 0xE9.toByte(), + 0x00.toByte(), 0xDA.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0x20.toByte(), 0x11.toByte(), 0x17.toByte(), 0x32.toByte(), + 0x99.toByte(), 0xE9.toByte(), 0xE7.toByte(), 0xA7.toByte(), 0x65.toByte(), 0x16.toByte(), 0x38.toByte(), 0xDB.toByte(), + 0x06.toByte(), 0x46.toByte(), 0xE7.toByte(), 0xA7.toByte(), 0x65.toByte(), 0x16.toByte(), 0x38.toByte(), 0xDB.toByte(), + 0x06.toByte(), 0x46.toByte(), 0x73.toByte(), 0x41.toByte(), 0x3B.toByte(), 0xB8.toByte(), 0x7A.toByte(), 0x26.toByte(), + 0x04.toByte(), 0x01.toByte(), 0xC9.toByte(), 0x0A.toByte(), 0xFF.toByte(), 0x59.toByte(), 0x77.toByte(), 0x07.toByte(), + 0xD1.toByte(), 0x32.toByte(), 0x87.toByte(), 0xBF.toByte(), 0x01.toByte(), 0x9F.toByte(), 0x21.toByte(), 0x3E.toByte(), + 0x0D.toByte(), 0x91.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x21.toByte(), 0x3E.toByte(), + 0x0D.toByte(), 0x91.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0x00.toByte(), 0x54.toByte(), 0x84.toByte(), 0x87.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0x00.toByte(), 0x54.toByte(), 0x84.toByte(), 0x87.toByte(), 0x78.toByte(), 0xCB.toByte(), + 0xEE.toByte(), 0x40.toByte(), 0x00.toByte(), 0x54.toByte(), 0x84.toByte(), 0x87.toByte(), 0x78.toByte(), 0xCB.toByte(), + 0xEE.toByte(), 0x40.toByte(), 0xF1.toByte(), 0xDE.toByte(), 0xCC.toByte(), 0x8B.toByte(), 0x78.toByte(), 0xCB.toByte(), + 0xEE.toByte(), 0x40.toByte(), 0xF1.toByte(), 0xDE.toByte(), 0xCC.toByte(), 0x8B.toByte(), 0x17.toByte(), 0x32.toByte(), + 0x99.toByte(), 0xE9.toByte(), 0xF1.toByte(), 0xDE.toByte(), 0xCC.toByte(), 0x8B.toByte(), 0x17.toByte(), 0x32.toByte(), + 0x99.toByte(), 0xE9.toByte(), 0xE7.toByte(), 0xA7.toByte(), 0x65.toByte(), 0x16.toByte(), 0x17.toByte(), 0x32.toByte(), + 0x99.toByte(), 0xE9.toByte(), 0xE7.toByte(), 0xA7.toByte(), 0x65.toByte(), 0x16.toByte(), 0x38.toByte(), 0xDB.toByte(), + 0x06.toByte(), 0x46.toByte(), 0xE7.toByte(), 0xA7.toByte(), 0x65.toByte(), 0x16.toByte(), 0x38.toByte(), 0xDB.toByte(), + 0x06.toByte(), 0x46.toByte(), 0x73.toByte(), 0x41.toByte(), 0x3B.toByte(), 0xB8.toByte(), 0x7A.toByte(), 0x26.toByte(), + 0x04.toByte(), 0x01.toByte(), 0xC9.toByte(), 0x0A.toByte(), 0xFF.toByte(), 0x59.toByte(), 0x77.toByte(), 0x07.toByte(), + 0xD1.toByte(), 0x32.toByte(), 0x87.toByte(), 0xBF.toByte(), 0x01.toByte(), 0x9F.toByte(), 0x21.toByte(), 0x3E.toByte(), + 0x0D.toByte(), 0x91.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x21.toByte(), 0x3E.toByte(), + 0x0D.toByte(), 0x91.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0xE5.toByte(), 0x01.toByte(), 0x76.toByte(), 0xAD.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0x00.toByte(), 0x54.toByte(), 0x84.toByte(), 0x87.toByte(), 0x92.toByte(), 0x01.toByte(), + 0x38.toByte(), 0xCD.toByte(), 0x00.toByte(), 0x54.toByte(), 0x84.toByte(), 0x87.toByte(), 0x78.toByte(), 0xCB.toByte(), + 0xEE.toByte(), 0x40.toByte() + ) + + val amountOfSamples = 18 // reference sample + delta samples + val resolution = 32 + val channels = 3 + + // Act + val allSamples = BlePMDClient.parseDeltaFramesAll(measurementFrame, channels, resolution, PmdDataFieldEncoding.FLOAT_IEEE754) + + // Assert + Assert.assertEquals(amountOfSamples, allSamples.size) + Assert.assertEquals(3, allSamples[0].size) + + Assert.assertEquals(0x0, sample0Channel0.xor(allSamples[0][0])) + Assert.assertEquals(0x0, sample0Channel1.xor(allSamples[0][1])) + Assert.assertEquals(0x0, sample0Channel2.xor(allSamples[0][2])) + + Assert.assertEquals(0x0, sample1Channel0.xor(allSamples[1][0])) + Assert.assertEquals(0x0, sample1Channel1.xor(allSamples[1][1])) + Assert.assertEquals(0x0, sample1Channel2.xor(allSamples[1][2])) + } + + @Test + fun test_parseDeltaFrameAllSamples_multipleDeltas() { + // Arrange + // Delta data dump: C9 FF 12 00 11 00 03 09 41 FE 2B 0F 9C 0B BF 15 00 4F 00 04 1E F1 EF 00 F0 C1 23 E4 ED F4 D1 F1 F1 F5 FF 22 DE 31 00 F1 FE 21 02 1F 0E 2B 1F 00 E2 20 00 0E 02 E1 1E 20 FF F1 F1 02 C5 D0 02 E0 E1 02 03 0A 31 2E FB BA 90 2B AA 0E 23 40 9E 03 04 14 E3 EF F3 0F 02 1F 01 E0 0F 04 9E 13 E2 D0 04 E2 22 E2 C2 0E 20 0F 20 02 FE 00 0F 1C 32 EE 03 0A 89 00 07 08 7C 00 CE 2F E8 3A 9E 03 04 1E 01 00 11 19 4F 00 2F 12 FD 13 FF 0E 10 00 00 F1 C0 12 E4 EF 21 00 00 01 F1 FF FF 02 10 10 2B 51 0B 4E 31 FC 2E BF 31 14 EC 0E 2F 52 EF 03 0A 06 9E 04 0E 02 A8 88 EE E0 07 9A 00 04 0A 1F 21 1E 4E 2E FE C6 C0 02 EF 03 01 02 EE 11 03 0A F8 13 00 00 F0 40 BF A5 E7 00 76 00 + // index type data: + // 0-5: Reference sample size 6: 0xC9 0xFF 0x12 0x00 0x11 0x00 + // Sample 0 (aka. reference sample): + // channel 0: C9 FF => 0xFFC9 => -55 + val refSample0Channel0 = -55 + // channel 1: 12 00 => 0x0012 => 18 + val refSample0Channel1 = 18 + // channel 2: 11 00 => 0x0011 => 17 + val refSample0Channel2 = 17 + // Delta dump: 03 09 | 41 FE 2B 0F 9C 0B BF 15 00 4F 00 + // 6: Delta size size 1: 0x03 (3 bits) + // 7: Sample amount size 1: 0x09 (Delta block contains 9 samples) + // 8: 0x41 (binary: 01 | 000 | 001) + // Sample 1 - channel 0, size 3 bits: 001 + // Sample 1 - channel 1, size 3 bits: 000 + // 9: 0xFE (binary: 1 | 111 | 111 | 0) + // Sample 1 - channel 2, size 3 bits: 001 + // Sample 2 - channel 0, size 3 bits: 111 + // Sample 2 - channel 1, size 3 bits: 111 + // 10: 0x2B (binary: 001 | 010 | 11) + // Sample 2 - channel 2, size 3 bits: 111 + val refSample1Channel0 = -54 + val refSample1Channel1 = 18 + val refSample1Channel2 = 18 + + // ... + // Delta dump: 04 1E | F1 EF 00 F0 C1 23 E4 ED F4 D1 F1 F1 F5 FF 22 DE 31 00 F1 FE 21 02 1F 0E 2B 1F 00 E2 20 00 0E 02 E1 1E 20 FF F1 F1 02 C5 D0 02 E0 E1 02 + // 19: Delta size size 1: 0x04 (4 bits) + // 20: Sample amount size 1: 0x1E (Rest of the data contains 30 samples) + // ... + // Delta dump: 03 0A | 31 2E FB BA 90 2B AA 0E 23 40 9E 03 + // 66: Delta size size 1: 0x03 (3 bits) + // 67: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) + // ... + // Delta dump: 04 14 | E3 EF F3 0F 02 1F 01 E0 0F 04 9E 13 E2 D0 04 E2 22 E2 C2 0E 20 0F 20 02 FE 00 0F 1C 32 EE + // 80: Delta size size 1: 0x04 (4 bits) + // 81: Sample amount size 1: 0x14 (Rest of the data contains 20 samples) + // ... + // Delta dump: 03 0A | 89 00 07 08 7C 00 CE 2F E8 3A 9E 03 + // 112: Delta size size 1: 0x03 (3 bits) + // 113: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) + // ... + // Delta dump: 04 1E | 01 00 11 19 4F 00 2F 12 FD 13 FF 0E 10 00 00 F1 C0 12 E4 EF 21 00 00 01 F1 FF FF 02 10 10 2B 51 0B 4E 31 FC 2E BF 31 14 EC 0E 2F 52 EF + // 126: Delta size size 1: 0x04 (4 bits) + // 127: Sample amount size 1: 0x1E (Rest of the data contains 30 samples) + // ... + // Delta dump: 03 0A |06 9E 04 0E 02 A8 88 EE E0 07 9A 00 + // 173: Delta size size 1: 0x03 (3 bits) + // 174: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) + // ... + // Delta dump: 04 0A | 1F 21 1E 4E 2E FE C6 C0 02 EF 03 01 02 EE 11 + // 187: Delta size size 1: 0x04 (4 bits) + // 188: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) + // ... + // Delta dump: 03 0A | F8 13 00 00 F0 40 BF A5 E7 00 76 00 + // 204: Delta size size 1: 0x03 (3 bits) + // 205: Sample amount size 1: 0x0A (Rest of the data contains 10 samples) + val expectedSampleSize = 1 + 9 + 30 + 10 + 20 + 10 + 30 + 10 + 10 + 10 + val resolution = 16 + val channels = 3 + val dataFrame = byteArrayOf( + 0xC9.toByte(), 0xFF.toByte(), 0x12.toByte(), 0x00.toByte(), 0x11.toByte(), 0x00.toByte(), 0x03.toByte(), 0x09.toByte(), + 0x41.toByte(), 0xFE.toByte(), 0x2B.toByte(), 0x0F.toByte(), 0x9C.toByte(), 0x0B.toByte(), 0xBF.toByte(), 0x15.toByte(), + 0x00.toByte(), 0x4F.toByte(), 0x00.toByte(), 0x04.toByte(), 0x1E.toByte(), 0xF1.toByte(), 0xEF.toByte(), 0x00.toByte(), + 0xF0.toByte(), 0xC1.toByte(), 0x23.toByte(), 0xE4.toByte(), 0xED.toByte(), 0xF4.toByte(), 0xD1.toByte(), 0xF1.toByte(), + 0xF1.toByte(), 0xF5.toByte(), 0xFF.toByte(), 0x22.toByte(), 0xDE.toByte(), 0x31.toByte(), 0x00.toByte(), 0xF1.toByte(), + 0xFE.toByte(), 0x21.toByte(), 0x02.toByte(), 0x1F.toByte(), 0x0E.toByte(), 0x2B.toByte(), 0x1F.toByte(), 0x00.toByte(), + 0xE2.toByte(), 0x20.toByte(), 0x00.toByte(), 0x0E.toByte(), 0x02.toByte(), 0xE1.toByte(), 0x1E.toByte(), 0x20.toByte(), + 0xFF.toByte(), 0xF1.toByte(), 0xF1.toByte(), 0x02.toByte(), 0xC5.toByte(), 0xD0.toByte(), 0x02.toByte(), 0xE0.toByte(), + 0xE1.toByte(), 0x02.toByte(), 0x03.toByte(), 0x0A.toByte(), 0x31.toByte(), 0x2E.toByte(), 0xFB.toByte(), 0xBA.toByte(), + 0x90.toByte(), 0x2B.toByte(), 0xAA.toByte(), 0x0E.toByte(), 0x23.toByte(), 0x40.toByte(), 0x9E.toByte(), 0x03.toByte(), + 0x04.toByte(), 0x14.toByte(), 0xE3.toByte(), 0xEF.toByte(), 0xF3.toByte(), 0x0F.toByte(), 0x02.toByte(), 0x1F.toByte(), + 0x01.toByte(), 0xE0.toByte(), 0x0F.toByte(), 0x04.toByte(), 0x9E.toByte(), 0x13.toByte(), 0xE2.toByte(), 0xD0.toByte(), + 0x04.toByte(), 0xE2.toByte(), 0x22.toByte(), 0xE2.toByte(), 0xC2.toByte(), 0x0E.toByte(), 0x20.toByte(), 0x0F.toByte(), + 0x20.toByte(), 0x02.toByte(), 0xFE.toByte(), 0x00.toByte(), 0x0F.toByte(), 0x1C.toByte(), 0x32.toByte(), 0xEE.toByte(), + 0x03.toByte(), 0x0A.toByte(), 0x89.toByte(), 0x00.toByte(), 0x07.toByte(), 0x08.toByte(), 0x7C.toByte(), 0x00.toByte(), + 0xCE.toByte(), 0x2F.toByte(), 0xE8.toByte(), 0x3A.toByte(), 0x9E.toByte(), 0x03.toByte(), 0x04.toByte(), 0x1E.toByte(), + 0x01.toByte(), 0x00.toByte(), 0x11.toByte(), 0x19.toByte(), 0x4F.toByte(), 0x00.toByte(), 0x2F.toByte(), 0x12.toByte(), + 0xFD.toByte(), 0x13.toByte(), 0xFF.toByte(), 0x0E.toByte(), 0x10.toByte(), 0x00.toByte(), 0x00.toByte(), 0xF1.toByte(), + 0xC0.toByte(), 0x12.toByte(), 0xE4.toByte(), 0xEF.toByte(), 0x21.toByte(), 0x00.toByte(), 0x00.toByte(), 0x01.toByte(), + 0xF1.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x02.toByte(), 0x10.toByte(), 0x10.toByte(), 0x2B.toByte(), 0x51.toByte(), + 0x0B.toByte(), 0x4E.toByte(), 0x31.toByte(), 0xFC.toByte(), 0x2E.toByte(), 0xBF.toByte(), 0x31.toByte(), 0x14.toByte(), + 0xEC.toByte(), 0x0E.toByte(), 0x2F.toByte(), 0x52.toByte(), 0xEF.toByte(), 0x03.toByte(), 0x0A.toByte(), 0x06.toByte(), + 0x9E.toByte(), 0x04.toByte(), 0x0E.toByte(), 0x02.toByte(), 0xA8.toByte(), 0x88.toByte(), 0xEE.toByte(), 0xE0.toByte(), + 0x07.toByte(), 0x9A.toByte(), 0x00.toByte(), 0x04.toByte(), 0x0A.toByte(), 0x1F.toByte(), 0x21.toByte(), 0x1E.toByte(), + 0x4E.toByte(), 0x2E.toByte(), 0xFE.toByte(), 0xC6.toByte(), 0xC0.toByte(), 0x02.toByte(), 0xEF.toByte(), 0x03.toByte(), + 0x01.toByte(), 0x02.toByte(), 0xEE.toByte(), 0x11.toByte(), 0x03.toByte(), 0x0A.toByte(), 0xF8.toByte(), 0x13.toByte(), + 0x00.toByte(), 0x00.toByte(), 0xF0.toByte(), 0x40.toByte(), 0xBF.toByte(), 0xA5.toByte(), 0xE7.toByte(), 0x00.toByte(), + 0x76.toByte(), 0x00.toByte() + ) + + // Act + val allSamples = BlePMDClient.parseDeltaFramesAll(dataFrame, channels, resolution, PmdDataFieldEncoding.SIGNED_INT) + + // Assert + Assert.assertEquals(expectedSampleSize, allSamples.size) + Assert.assertEquals(3, allSamples[0].size) + Assert.assertEquals(refSample0Channel0, allSamples[0][0]) + Assert.assertEquals(refSample0Channel1, allSamples[0][1]) + Assert.assertEquals(refSample0Channel2, allSamples[0][2]) + Assert.assertEquals(refSample1Channel0, allSamples[1][0]) + Assert.assertEquals(refSample1Channel1, allSamples[1][1]) + Assert.assertEquals(refSample1Channel2, allSamples[1][2]) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientPmdSettingsTest.java b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientPmdSettingsTest.java new file mode 100644 index 00000000..86d67a39 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientPmdSettingsTest.java @@ -0,0 +1,136 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BlePmdClientPmdSettingsTest { + + @Test + public void testPmdSettingsWithRange() { + //Arrange + byte[] bytes = {(byte) 0x00, (byte) 0x01, (byte) 0x34, (byte) 0x00, (byte) 0x01, (byte) 0x01, (byte) 0x10, (byte) 0x00, (byte) 0x02, (byte) 0x04, (byte) 0xF5, (byte) 0x00, (byte) 0xF4, (byte) 0x01, (byte) 0xE8, (byte) 0x03, (byte) 0xD0, (byte) 0x07, (byte) 0x04, (byte) 0x01, (byte) 0x03}; + // Parameters + // Setting Type : 00 (Sample Rate) + // array_length : 01 + // array of settings values: 34 00 (52Hz) + int sampleRate = 52; + //Setting Type : 01 (Resolution) + // array_length : 01 + // array of settings values: 10 00 (16) + int resolution = 16; + // Setting Type : 02 (Range) + // array_length : 04 + // array of settings values: F5 00 (245) + int range1 = 245; + // array of settings values: F4 01 (500) + int range2 = 500; + // array of settings values: E8 03 (1000) + int range3 = 1000; + // array of settings values: D0 07 (2000) + int range4 = 2000; + // Setting Type : 04 (Channels) + // array_length : 01 + // array of settings values: 03 (3 Channels) + int channels = 3; + int numberOfSettings = 4; + + //Act + PmdSetting pmdSetting = new PmdSetting(bytes); + + // Assert + Assert.assertEquals(numberOfSettings, pmdSetting.settings.size()); + + assertEquals(sampleRate, (int) pmdSetting.settings.get(PmdSetting.PmdSettingType.SAMPLE_RATE).iterator().next()); + assertEquals(1, pmdSetting.settings.get(PmdSetting.PmdSettingType.SAMPLE_RATE).size()); + + assertEquals(resolution, (int) pmdSetting.settings.get(PmdSetting.PmdSettingType.RESOLUTION).iterator().next()); + assertEquals(1, pmdSetting.settings.get(PmdSetting.PmdSettingType.RESOLUTION).size()); + + assertTrue(pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE).contains(range1)); + assertTrue(pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE).contains(range2)); + assertTrue(pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE).contains(range3)); + assertTrue(pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE).contains(range4)); + assertEquals(4, pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE).size()); + + assertEquals(channels, (int) pmdSetting.settings.get(PmdSetting.PmdSettingType.CHANNELS).iterator().next()); + assertEquals(1, pmdSetting.settings.get(PmdSetting.PmdSettingType.CHANNELS).size()); + + assertNull(pmdSetting.settings.get(PmdSetting.PmdSettingType.RANGE_MILLIUNIT)); + assertNull(pmdSetting.settings.get(PmdSetting.PmdSettingType.FACTOR)); + } + + @Test + public void testPmdSettingWithRangeMilliUnit() { + //Arrange + byte[] bytes = new byte[]{(byte) PmdSetting.PmdSettingType.RANGE_MILLIUNIT.getNumVal(), + (byte) 0x02, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) PmdSetting.PmdSettingType.RESOLUTION.getNumVal(), (byte) 0x01, (byte) 0x0E, 0x00}; + // Parameters + // Setting Type : 03 (Range milli unit) + // array_length : 02 + // array of settings values: FF FF FF FF(52Hz) + // array of settings values: FF 00 00 00(52Hz) + // Setting Type : 01 (Resolution) + // array_length : 01 + // array of settings values: 0E 00 (16) + int resolution = 14; + int numberOfSettings = 2; + + // Act + PmdSetting settings = new PmdSetting(bytes); + + // Assert + Assert.assertEquals(numberOfSettings, settings.settings.size()); + Assert.assertTrue(settings.settings.containsKey(PmdSetting.PmdSettingType.RANGE_MILLIUNIT)); + Assert.assertEquals(2, Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).size()); + Assert.assertTrue(Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).contains(-1)); + Assert.assertTrue(Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.RANGE_MILLIUNIT)).contains(0xff)); + Assert.assertTrue(settings.settings.containsKey(PmdSetting.PmdSettingType.RESOLUTION)); + + Assert.assertEquals(1, Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.RESOLUTION)).size()); + Assert.assertTrue(Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.RESOLUTION)).contains(resolution)); + } + + @Test + public void testPmdSelectedSerialization() { + //Arrange + Map selected = new HashMap<>(); + int sampleRate = 0xFFFF; + selected.put(PmdSetting.PmdSettingType.SAMPLE_RATE, sampleRate); + int resolution = 0; + selected.put(PmdSetting.PmdSettingType.RESOLUTION, resolution); + int range = 15; + selected.put(PmdSetting.PmdSettingType.RANGE, range); + int rangeMilliUnit = Integer.MAX_VALUE; + selected.put(PmdSetting.PmdSettingType.RANGE_MILLIUNIT, rangeMilliUnit); + int channels = 4; + selected.put(PmdSetting.PmdSettingType.CHANNELS, channels); + int factor = 15; + selected.put(PmdSetting.PmdSettingType.FACTOR, factor); + int numberOfSettings = 5; + + //Act + PmdSetting settingsFromSelected = new PmdSetting(selected); + byte[] serializedSelected = settingsFromSelected.serializeSelected(); + PmdSetting settings = new PmdSetting(serializedSelected); + + //Assert + Assert.assertEquals(numberOfSettings, settings.settings.size()); + assertTrue(settings.settings.get(PmdSetting.PmdSettingType.SAMPLE_RATE).contains(sampleRate)); + Assert.assertEquals(1, Objects.requireNonNull(settings.settings.get(PmdSetting.PmdSettingType.SAMPLE_RATE)).size()); + + assertTrue(settings.settings.get(PmdSetting.PmdSettingType.RESOLUTION).contains(resolution)); + assertTrue(settings.settings.get(PmdSetting.PmdSettingType.RANGE).contains(range)); + assertTrue(settings.settings.get(PmdSetting.PmdSettingType.RANGE_MILLIUNIT).contains(rangeMilliUnit)); + assertTrue(settings.settings.get(PmdSetting.PmdSettingType.CHANNELS).contains(channels)); + assertNull(settings.settings.get(PmdSetting.PmdSettingType.FACTOR)); + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientTest.kt new file mode 100644 index 00000000..6493e06a --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/BlePmdClientTest.kt @@ -0,0 +1,484 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.api.ble.exceptions.BleNotImplemented +import com.polar.androidcommunications.api.ble.model.gatt.BleGattTxInterface +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.* +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.reactivex.rxjava3.subscribers.TestSubscriber +import org.junit.After +import org.junit.Assert +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test + +class BlePmdClientTest { + + @MockK + lateinit var mockGattTxInterface: BleGattTxInterface + + private lateinit var blePmdClient: BlePMDClient + + @Before + fun setUp() { + MockKAnnotations.init(this) + blePmdClient = BlePMDClient(mockGattTxInterface) + every { mockGattTxInterface.isConnected } returns true + + mockkObject(AccData) + mockkObject(EcgData) + mockkObject(GnssLocationData) + mockkObject(GyrData) + mockkObject(MagData) + mockkObject(PpiData) + mockkObject(PpgData) + mockkObject(PressureData) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `process not supported frame type`() { + // Arrange + // HEX: 01 01 00 00 00 00 00 00 70 FF + // index type data + // 0: Measurement type 01 (ppg data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 70 (0x7000000000000000 = 8070450532247928832) + // 9: Frame type FF (compressed, frame type 0x7F) + + val locationDataFromService = byteArrayOf( + 0x01.toByte(), + 0x38.toByte(), 0x6C.toByte(), 0x31.toByte(), 0x72.toByte(), 0xA4.toByte(), 0xD3.toByte(), 0x23.toByte(), 0x0D.toByte(), + 0x7F.toByte(), + ) + + // Act && Assert + assertThrows(BleNotImplemented::class.java) { + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + } + } + + @Test + fun `process ecg data`() { + // Arrange + // HEX: 00 38 6C 31 72 A4 D3 23 0D 03 FF + // index type data + // 0: Measurement type 00 (Ecg data) + // 1..8: 64-bit Timestamp 38 6C 31 72 A4 D3 23 0D (0x0D23D3A472316C38 = 946833049921875000) + // 9: Frame type 03 (raw, frame type 3) + // 10: Data FF + val expectedTimeStamp = 946833049921875000L + val expectedIsCompressed = false + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_3 + val expectedFrameContent = byteArrayOf(0xFF.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x00.toByte(), + 0x38.toByte(), 0x6C.toByte(), 0x31.toByte(), 0x72.toByte(), 0xA4.toByte(), 0xD3.toByte(), 0x23.toByte(), 0x0D.toByte(), + 0x03.toByte(), + 0xFF.toByte() + ) + + val result = blePmdClient.monitorEcgNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + EcgData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + EcgData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val ecgData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, ecgData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process ppg data`() { + // Arrange + // HEX: 01 01 00 00 00 00 00 00 70 80 FF + // index type data + // 0: Measurement type 01 (ppg data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 70 (0x7000000000000000 = 8070450532247928832) + // 9: Frame type 80 (compressed, frame type 0) + // 10: Data FF + val expectedTimeStamp = 8070450532247928832L + val expectedIsCompressed = true + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val expectedFrameContent = byteArrayOf(0xFF.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x70.toByte(), + 0x80.toByte(), + 0xFF.toByte() + ) + + val result = blePmdClient.monitorPpgNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + PpgData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + PpgData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val ppgData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, ppgData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process acc data`() { + // Arrange + // HEX: 02 00 00 00 00 00 00 00 00 83 00 + // index type data + // 0: Measurement type 02 (acc data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 00 (0x0000000000000000 = 0) + // 9: Frame type 83 (compressed, frame type 3) + // 10: Data 00 + val expectedTimeStamp = 0L + val expectedIsCompressed = true + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_3 + val expectedFrameContent = byteArrayOf(0x00.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x02.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x83.toByte(), + 0x00.toByte() + ) + + val result = blePmdClient.monitorAccNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + AccData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + AccData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val accData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, accData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process ppi data`() { + // Arrange + // HEX: 03 00 00 00 00 00 00 00 00 03 00 + // index type data + // 0: Measurement type 03 (ppi data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 00 (0x0000000000000000 = 0) + // 9: Frame type 03 (raw, frame type 3) + // 10: Data 00 + val expectedTimeStamp = 0L + val expectedIsCompressed = false + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_3 + val expectedFrameContent = byteArrayOf(0x00.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x03.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x03.toByte(), + 0x00.toByte() + ) + + val result = blePmdClient.monitorPpiNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + PpiData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + PpiData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val ppiData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, ppiData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process gyro data`() { + // Arrange + // HEX: 05 01 00 00 00 00 00 00 00 80 FF + // index type data + // 0: Measurement type 05 (gyro data) + // 1..8: 64-bit Timestamp 01 00 00 00 00 00 00 00 (0x0000000000000001 = 1) + // 9: Frame type 80 (compressed, frame type 0) + // 10: Data FF + val expectedTimeStamp = 1L + val expectedIsCompressed = true + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val expectedFrameContent = byteArrayOf(0xFF.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x05.toByte(), + 0x01.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x80.toByte(), + 0xFF.toByte() + ) + + val result = blePmdClient.monitorGyroNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + GyrData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + GyrData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val gyroData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, gyroData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process magnetometer data`() { + // Arrange + // HEX: 06 01 00 00 00 00 00 00 70 80 FF + // index type data + // 0: Measurement type 06 (magnetometer data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 70 (0x7000000000000000 = 8070450532247928832) + // 9: Frame type 80 (compressed, frame type 0) + // 10: Data FF + val expectedTimeStamp = 8070450532247928832L + val expectedIsCompressed = true + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val expectedFrameContent = byteArrayOf(0xFF.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x06.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x70.toByte(), + 0x80.toByte(), + 0xFF.toByte() + ) + + val result = blePmdClient.monitorMagnetometerNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + MagData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + MagData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val magData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, magData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process location data`() { + // Arrange + // HEX: 0A 38 6C 31 72 A4 D3 23 0D 00 12 + // index type data + // 0: Measurement type 0A (Location data) + // 1..8: 64-bit Timestamp 38 6C 31 72 A4 D3 23 0D (0x0D23D3A472316C38 = 946833049921875000) + // 9: Frame type 00 (raw, frame type 0) + // 10: Data 12 + val expectedTimeStamp = 946833049921875000L + val expectedIsCompressed = false + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val expectedFrameContent = byteArrayOf(0x12) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x0A.toByte(), 0x38.toByte(), 0x6C.toByte(), 0x31.toByte(), 0x72.toByte(), 0xA4.toByte(), 0xD3.toByte(), 0x23.toByte(), 0x0D.toByte(), + 0x00.toByte(), 0x12.toByte() + ) + + val result = blePmdClient.monitorLocationNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + GnssLocationData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + GnssLocationData(timeStamp = capturedTimeStamp.captured) + } + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val locationData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, locationData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } + + @Test + fun `process pressure data`() { + // Arrange + // HEX: 0B 00 00 00 00 00 00 00 00 03 00 + // index type data + // 0: Measurement type 0B (pressure data) + // 1..8: 64-bit Timestamp 00 00 00 00 00 00 00 00 (0x0000000000000000 = 0) + // 9: Frame type 03 (raw, frame type 3) + // 10: Data 00 + val expectedTimeStamp = 0L + val expectedIsCompressed = false + val expectedFrameType = BlePMDClient.PmdDataFrameType.TYPE_3 + val expectedFrameContent = byteArrayOf(0x00.toByte()) + val expectedFactor = 1.0f + + val locationDataFromService = byteArrayOf( + 0x0B.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x03.toByte(), + 0x00.toByte() + ) + + val result = blePmdClient.monitorPressureNotifications(true) + val testObserver = TestSubscriber() + result.subscribe(testObserver) + + val capturedCompression = slot() + val capturedFrameType = slot() + val capturedData = slot() + val capturedFactor = slot() + val capturedTimeStamp = slot() + + every { + PressureData.parseDataFromDataFrame(capture(capturedCompression), capture(capturedFrameType), capture(capturedData), capture(capturedFactor), capture(capturedTimeStamp)) + } answers { + PressureData(timeStamp = capturedTimeStamp.captured) + } + + // Act + blePmdClient.processServiceData(BlePMDClient.PMD_DATA, locationDataFromService, 0, false) + + // Assert + testObserver.assertNoErrors() + testObserver.assertValueCount(1) + val pressureData = testObserver.values()[0] + + Assert.assertEquals(expectedTimeStamp, pressureData.timeStamp) + Assert.assertEquals(expectedIsCompressed, capturedCompression.captured) + Assert.assertEquals(expectedFrameType, capturedFrameType.captured) + Assert.assertEquals(expectedFactor, capturedFactor.captured) + Assert.assertEquals(expectedFrameContent[0], capturedData.captured[0]) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt new file mode 100644 index 00000000..e1581241 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/GnssLocationDataTest.kt @@ -0,0 +1,189 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.GnssLocationData +import org.junit.Assert +import org.junit.Test +import java.lang.Double.longBitsToDouble +import java.lang.Float.intBitsToFloat + +class GnssLocationDataTest { + + @Test + fun `process location data type 0`() { + // Arrange + // HEX: 00 00 00 00 4C E2 81 42 00 00 00 00 FB 8F CB 41 E5 07 0A 01 00 14 8D 03 FF FF FF 0F E1 FA C9 42 CD CC CC 3D 48 61 8B 42 00 00 02 00 03 8D FF FF 01 FF FF + // index type data + // 0..7 Latitude 00 00 00 00 4C E2 81 42 (0x4281E24C) + // 8..15 Longitude 00 00 00 00 FB 8F CB 41 (0x41cb8ffb) + // 16..19 Date E5 07 0A 01 (year = 2021, month = 10, date = 1) + // 20..23 Time 00 2C 47 0C (0x0C472C00 => hours = 11 , minutes = 14, seconds = 34, milliseconds = 000 trusted = true) + // 24..27 Cumulative distance FF FF FF 0F (0x0FFFFFFF = 268435455) + // 28..31 Speed E1 FA C9 42 (0x42C9FAE1 => 100.99km/h) + // 32..35 Acceleration Speed CD CC CC 3D (0x3DCCCCCD => 0.1) + // 36..39 Coordinate Speed 48 61 8B 42 (0x428B6148 => 69.69) + // 40..43 Acceleration Speed Factor 00 00 02 00 (0x00000200) + // 44..45 Course 03 8D (0x8CA0 = 360.99 degrees ) + // 46..47 Knots speed FF FF (0xFFFF = 655.35 ) + // 48 Fix 01 (true) + // 49 Speed flag FF + // 50 Fusion state FF + val expectedSamplesSize = 1 + val expectedTimeStamp = 946833049921875000L + val latitude = longBitsToDouble(0x4281e24c00000000) + val longitude = longBitsToDouble(0x41cb8ffb00000000) + val date = "2021-10-01T11:14:34.000" + val cumulativeDistance = 26843545.5 + val speed = intBitsToFloat(0x42C9FAE1) + val accelerationSpeed = intBitsToFloat(0x3DCCCCCD) + val coordinateSpeed = intBitsToFloat(0x428B6148) + val accelerationSpeedFactor = intBitsToFloat(0x00000200) + val course = 360.99f + val gpsChipSpeed = 655.35f + val speedFlag = -1 + val fusionState = 0xFFu + + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x4C.toByte(), 0xE2.toByte(), 0x81.toByte(), 0x42.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xFB.toByte(), 0x8F.toByte(), 0xCB.toByte(), 0x41.toByte(), + 0xE5.toByte(), 0x07.toByte(), 0x0A.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x2C.toByte(), 0x47.toByte(), 0x0C.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x0F.toByte(), 0xE1.toByte(), 0xFA.toByte(), 0xC9.toByte(), 0x42.toByte(), + 0xCD.toByte(), 0xCC.toByte(), 0xCC.toByte(), 0x3D.toByte(), 0x48.toByte(), 0x61.toByte(), 0x8B.toByte(), 0x42.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x02.toByte(), 0x00.toByte(), 0x03.toByte(), 0x8D.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0x01.toByte(), 0xFF.toByte(), 0xFF.toByte() + ) + + // Act + val gnssData = GnssLocationData.parseDataFromDataFrame(isCompressed = false, frameType = BlePMDClient.PmdDataFrameType.TYPE_0, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + Assert.assertEquals(expectedTimeStamp, gnssData.timeStamp) + Assert.assertEquals(expectedSamplesSize, gnssData.gnssLocationDataSamples.size) + Assert.assertTrue(gnssData.gnssLocationDataSamples[0] is GnssLocationData.GnssCoordinateSample) + Assert.assertEquals(latitude, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).latitude, 0.00001) + Assert.assertEquals(longitude, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).longitude, 0.00001) + Assert.assertEquals(date, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).date) + Assert.assertEquals(cumulativeDistance, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).cumulativeDistance, 0.00001) + Assert.assertEquals(speed, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).speed) + Assert.assertEquals(accelerationSpeed, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).usedAccelerationSpeed) + Assert.assertEquals(coordinateSpeed, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).coordinateSpeed) + Assert.assertEquals(accelerationSpeedFactor, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).accelerationSpeedFactor, 0.00001f) + Assert.assertEquals(course, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).course) + Assert.assertEquals(gpsChipSpeed, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).gpsChipSpeed) + Assert.assertTrue((gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).fix) + Assert.assertEquals(speedFlag, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).speedFlag) + Assert.assertEquals(fusionState, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssCoordinateSample).fusionState) + } + + @Test + fun `process location data type 1`() { + // Arrange + // HEX: F5 07 00 80 FF 07 + // index type data + // 0..1: Dilution F5 07 (0x7F5 = 20.37) + // 2..3: Altitude 00 80 (0xFFFF = -32768) + // 4: Number of satellites FF + // 5: Fix 07 + val expectedSamplesSize = 1 + val expectedTimeStamp = 946784976788085937L + val dilution = 20.37f + val altitude = -32768 + val numberOfSatellites = 255u + + val measurementFrame = byteArrayOf(0xF5.toByte(), 0x07.toByte(), 0x00.toByte(), 0x80.toByte(), 0xFF.toByte(), 0x07.toByte()) + + // Act + val gnssData = GnssLocationData.parseDataFromDataFrame(isCompressed = false, frameType = BlePMDClient.PmdDataFrameType.TYPE_1, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, gnssData.timeStamp) + Assert.assertEquals(expectedSamplesSize, gnssData.gnssLocationDataSamples.size) + Assert.assertEquals(dilution, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteDilutionSample).dilution) + Assert.assertEquals(altitude, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteDilutionSample).altitude) + Assert.assertEquals(numberOfSatellites, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteDilutionSample).numberOfSatellites) + Assert.assertTrue((gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteDilutionSample).fix) + } + + @Test + fun `process location data type 2`() { + // Arrange + // HEX: 00 01 02 03 04 05 06 07 FF EF 80 00 01 02 03 04 + // index type data + // 0..7: Seen satellite summary 00 01 02 03 04 05 06 07 + // 8..15: Used satellite summary FF EF 80 00 01 02 03 04 + + val expectedSamplesSize = 1 + val expectedTimeStamp = 946784976788085937L + val seenSatelliteSummary = GnssLocationData.GnssSatelliteSummary( + gpsSat = 0u, + gpsMaxSnr = 1u, + glonassSat = 2u, + glonassMaxSnr = 3u, + sbasSat = 4u, + sbasMaxSnr = 5u, + snrTop5Avg = 6u, + sbasSnrTop5Avg = 7u + ) + val usedSatelliteSummary = GnssLocationData.GnssSatelliteSummary( + gpsSat = 0xFFu, + gpsMaxSnr = 0xEFu, + glonassSat = 0x80u, + glonassMaxSnr = 0x00u, + sbasSat = 1u, + sbasMaxSnr = 2u, + snrTop5Avg = 3u, + sbasSnrTop5Avg = 4u + ) + + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte(), 0x05.toByte(), 0x06.toByte(), 0x07.toByte(), + 0xFF.toByte(), 0xEF.toByte(), 0x80.toByte(), 0x00.toByte(), 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte(), + ) + + // Act + val gnssData = GnssLocationData.parseDataFromDataFrame(isCompressed = false, frameType = BlePMDClient.PmdDataFrameType.TYPE_2, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, gnssData.timeStamp) + Assert.assertEquals(expectedSamplesSize, gnssData.gnssLocationDataSamples.size) + Assert.assertEquals(seenSatelliteSummary, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteSummarySample).seenGnssSatelliteSummary) + Assert.assertEquals(usedSatelliteSummary, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssSatelliteSummarySample).usedGnssSatelliteSummary) + } + + @Test + fun `process location data type 3`() { + // Arrange + // HEX: E8 03 00 00 1C 00 00 1A 80 47 50 41 41 4D 2C 41 2C 41 2C 30 2E 31 30 2C 4E 2C 57 50 54 4E 4D 45 2A 33 32 + // index type data + // 0..3: Measurement period 00 00 1C 00 (0x001C0000 = 1835008) + // 4..5: NMEA Message length 1A 00 (0x1A = 26) + // 6: Status flags 80 + // 7.. NMEA message 47 50 41 41 4D 2C 41 2C 41 2C 30 2E 31 30 2C 4E 2C 57 50 54 4E 4D 45 2A 33 32 + + val expectedSamplesSize = 1 + val expectedTimeStamp = 946871122724609375L + val measurementPeriod = 1835008u + val nmeaMessageLength = 26u + val statusFlags = 0x80u.toUByte() + val nmeaMessage = "GPAAM,A,A,0.10,N,WPTNME*32" + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), 0x1C.toByte(), 0x00.toByte(), 0x1A.toByte(), 0x00.toByte(), 0x80.toByte(), 0x47.toByte(), + 0x50.toByte(), 0x41.toByte(), 0x41.toByte(), 0x4D.toByte(), 0x2C.toByte(), 0x41.toByte(), 0x2C.toByte(), 0x41.toByte(), + 0x2C.toByte(), 0x30.toByte(), 0x2E.toByte(), 0x31.toByte(), 0x30.toByte(), 0x2C.toByte(), 0x4E.toByte(), 0x2C.toByte(), + 0x57.toByte(), 0x50.toByte(), 0x54.toByte(), 0x4E.toByte(), 0x4D.toByte(), 0x45.toByte(), 0x2A.toByte(), 0x33.toByte(), + 0x32.toByte() + ) + + // Act + val gnssData = GnssLocationData.parseDataFromDataFrame(isCompressed = false, frameType = BlePMDClient.PmdDataFrameType.TYPE_3, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, gnssData.timeStamp) + Assert.assertEquals(expectedSamplesSize, gnssData.gnssLocationDataSamples.size) + Assert.assertEquals(measurementPeriod, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssGpsNMEASample).measurementPeriod) + Assert.assertEquals(nmeaMessageLength, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssGpsNMEASample).messageLength) + Assert.assertEquals(statusFlags, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssGpsNMEASample).statusFlags) + Assert.assertEquals(nmeaMessage, (gnssData.gnssLocationDataSamples[0] as GnssLocationData.GnssGpsNMEASample).nmeaMessage) + } + + +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt new file mode 100644 index 00000000..564a65e6 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpgDataTest.kt @@ -0,0 +1,372 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpgData +import org.junit.Assert +import org.junit.Test + +class PpgDataTest { + @Test + fun `test raw PPG frame type 0`() { + // Arrange + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val timeStamp: Long = 0 + val measurementFrame = byteArrayOf( + 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), //PPG0 (197121) + 0x04.toByte(), 0x05.toByte(), 0x06.toByte(), //PPG1 (394500) + 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte(), //PPG2 (8388607) + 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), //ambient (0) + 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), //PPG0 (-1) + 0x0F.toByte(), 0xEF.toByte(), 0xEF.toByte(), //PPG1 (-1052913) + 0x00.toByte(), 0x00.toByte(), 0x80.toByte(), //PPG2 (-8388608) + 0x0F.toByte(), 0xEF.toByte(), 0xEF.toByte() //ambient (-1052913) + ) + val ppg0Sample0 = 197121 + val ppg1Sample0 = 394500 + val ppg2Sample0 = 8388607 + val ambientSample0 = 0 + val ppg0Sample1 = -1 + val ppg1Sample1 = -1052913 + val ppg2Sample1 = -8388608 + val ambientSample1 = -1052913 + + // Act + val ppgData = PpgData.parseDataFromDataFrame( + isCompressed = false, + frameType = frameType, + frame = measurementFrame, + factor = 1.0f, + timeStamp = timeStamp + ) + + // Assert + Assert.assertEquals(2, ppgData.ppgSamples.size) + Assert.assertEquals(3, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples.size) + Assert.assertEquals(ppg0Sample0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[0]) + Assert.assertEquals(ppg1Sample0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[1]) + Assert.assertEquals(ppg2Sample0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[2]) + Assert.assertEquals(ambientSample0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ambientSample) + + Assert.assertEquals(3, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples.size) + Assert.assertEquals(ppg0Sample1, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[0]) + Assert.assertEquals(ppg1Sample1, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[1]) + Assert.assertEquals(ppg2Sample1, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[2]) + Assert.assertEquals(ambientSample1, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ambientSample) + } + + @Test + fun `test raw PPG frame type 4`() { + // Arrange + // HEX: 00 01 02 03 04 05 06 07 08 09 0A 0B + // F8 FF 00 01 00 02 02 00 02 03 00 07 + // 06 02 06 02 06 03 03 04 02 03 01 01 + // index type data: + // 0..11: channel1 Gain Ts 00 01 02 03 04 05 06 07 08 09 0A 0B + // 12..23: channel2 Gain Ts F8 FF 00 01 00 02 02 00 02 03 00 07 + // 24..35: num Int Ts 00 7F 00 00 00 02 02 00 02 03 00 FF + + val frameType = BlePMDClient.PmdDataFrameType.TYPE_4 + val isCompressed = false + val timeStamp: Long = 0 + val expectedChannel1GainTs0 = 0 + val expectedChannel1GainTs11 = 0x3 + val expectedChannel2GainTs0 = 0 + val expectedChannel2GainTs1 = 7 + val expectedChannel2GainTs11 = 7 + val expectedNumIntTs0 = 0u + val expectedNumIntTs1 = 127u + val expectedNumIntTs11 = 0xFFu + + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x01.toByte(), 0x02.toByte(), 0x03.toByte(), 0x04.toByte(), 0x05.toByte(), 0x06.toByte(), + 0x07.toByte(), 0x08.toByte(), 0x09.toByte(), 0x0A.toByte(), 0x0B.toByte(), + 0xF8.toByte(), 0xFF.toByte(), 0x00.toByte(), 0x01.toByte(), 0x00.toByte(), 0x02.toByte(), 0x02.toByte(), + 0x00.toByte(), 0x02.toByte(), 0x03.toByte(), 0x00.toByte(), 0x07.toByte(), + 0x00.toByte(), 0x7F.toByte(), 0x06.toByte(), 0x02.toByte(), 0x06.toByte(), 0x03.toByte(), 0x03.toByte(), + 0x04.toByte(), 0x02.toByte(), 0x03.toByte(), 0x01.toByte(), 0xFF.toByte() + ) + + // Act + val ppgData = PpgData.parseDataFromDataFrame( + isCompressed = isCompressed, + frameType = frameType, + frame = measurementFrame, + factor = 1.0f, + timeStamp = timeStamp + ) + + // Assert + Assert.assertEquals(1, ppgData.ppgSamples.size) + Assert.assertEquals(12, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel1GainTs.size) + Assert.assertEquals(expectedChannel1GainTs0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel1GainTs[0]) + Assert.assertEquals(expectedChannel1GainTs11, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel1GainTs[11]) + + Assert.assertEquals(12, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel2GainTs.size) + Assert.assertEquals(expectedChannel2GainTs0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel2GainTs[0]) + Assert.assertEquals(expectedChannel2GainTs1, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel2GainTs[1]) + Assert.assertEquals(expectedChannel2GainTs11, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).channel2GainTs[11]) + + Assert.assertEquals(12, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).numIntTs.size) + Assert.assertEquals(expectedNumIntTs0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).numIntTs[0]) + Assert.assertEquals(expectedNumIntTs1, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).numIntTs[1]) + Assert.assertEquals(expectedNumIntTs11, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType4).numIntTs[11]) + } + + @Test + fun `test raw PPG frame type 5`() { + // Arrange + // HEX: FF FF FF FF + // index type data: + // 0..3: operation mode FF FF FF FF + + val frameType = BlePMDClient.PmdDataFrameType.TYPE_5 + val isCompressed = false + val timeStamp: Long = 0 + val expectedOperationMode = 0xFFFFFFFFu + + val measurementFrame = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(),) + + // Act + val ppgData = PpgData.parseDataFromDataFrame( + isCompressed = isCompressed, + frameType = frameType, + frame = measurementFrame, + factor = 1.0f, + timeStamp = timeStamp + ) + + // Assert + Assert.assertEquals(1, ppgData.ppgSamples.size) + Assert.assertEquals(expectedOperationMode, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleFrameType5).operationMode) + } + + @Test + fun `test compressed PPG frame type 0`() { + // Arrange + // HEX: 2C 2D 00 C2 77 00 D3 D2 FF 3D 88 FF 0A 29 B2 F0 EE 34 11 B2 EC EE 74 11 B1 E8 FE B4 11 B1 E8 FE B4 11 B1 E0 FE 34 12 B0 DC 0E 75 12 B0 D8 0E B5 12 AF D4 1E F5 12 AF D0 1E 35 13 AE CC 2E 75 13 AE C8 2E B5 13 AD C4 3E F5 13 AD BC 3E 75 14 AD BC 3E 75 14 AC B8 4E B5 14 AC B4 4E F5 14 AB B0 5E 35 15 AA AC 6E 75 15 AA A8 6E B5 15 AA A4 6E F5 15 A9 A0 7E 35 16 A9 9C 7E 75 16 A8 98 8E B5 16 A7 94 9E F5 16 A7 90 9E 35 17 A7 8C 9E 75 17 A6 88 AE B5 17 A5 88 BE B5 17 A5 80 BE 35 18 A4 7C CE 75 18 A4 78 CE B5 18 A3 78 DE B5 18 A2 70 EE 35 19 A2 6C EE 75 19 A2 6C EE 75 19 A1 68 FE B5 19 A0 60 0E 36 1A 9F 60 1E 36 1A 9F 5C 1E 76 1A 9F 58 1E B6 1A 9D 54 3E F6 1A + // index type data: + // 0-11: Reference sample 0x2C 0x2D 0x00 0xC2 0x77 0x00 0xD3 0xD2 0xFF 0x3D 0x88 0xFF + // Sample 0 (aka. reference sample): + // channel 0: 2C 2D 00 => 0x002D2C => 11564 + val refSample0Channel0 = 11564 + // channel 1: C2 77 00 => 0x0077C2 => 30658 + val refSample0Channel1 = 30658 + // channel 2: D3 D2 FF => 0xFFD2D3 => -11565 + val refSample0Channel2 = -11565 + // channel 3: 3D 88 FF => 0xFF883D => -30659 + val refSample0Channel3 = -30659 + // Delta dump: 0A 29 | B2 F0 EE 34 11 B2 EC EE 74 11 B1 E8 FE B4 11 B1 ... + // 12: Delta size size 1: 0x0A (10 bits) + // 13: Sample amount size 1: 0x29 (Delta block contains 41 samples) + // 14: 0xB2 (binary: 1011 0010) + // 15: 0xF0 (binary: 1111 00 | 00) + // 16: 0xEE (binary: 1110 | 1110) + // Sample 1 - channel 0, size 10 bits: 00 1011 0010 + // Sample 1 - channel 1, size 10 bits: 11 1011 1100 + // 17: 0x34 (binary: 00 | 11 0100) + // Sample 1 - channel 2, size 10 bits: 11 0100 1110 + // 18: + // Sample 1 - channel 3, size 10 bits: 00 0100 0100 0x11 (binary: 0001 0001) + val refSample1Channel0 = 178 + val refSample1Channel1 = -68 + val refSample1Channel2 = -178 + val refSample1Channel3 = 68 + val amountOfSamples = 1 + 41 // reference sample + delta samples + val measurementFrame = byteArrayOf( + 0x2C.toByte(), 0x2D.toByte(), 0x00.toByte(), 0xC2.toByte(), 0x77.toByte(), 0x00.toByte(), 0xD3.toByte(), 0xD2.toByte(), 0xFF.toByte(), + 0x3D.toByte(), 0x88.toByte(), 0xFF.toByte(), 0x0A.toByte(), 0x29.toByte(), 0xB2.toByte(), 0xF0.toByte(), 0xEE.toByte(), 0x34.toByte(), + 0x11.toByte(), 0xB2.toByte(), 0xEC.toByte(), 0xEE.toByte(), 0x74.toByte(), 0x11.toByte(), 0xB1.toByte(), 0xE8.toByte(), 0xFE.toByte(), + 0xB4.toByte(), 0x11.toByte(), 0xB1.toByte(), 0xE8.toByte(), 0xFE.toByte(), 0xB4.toByte(), 0x11.toByte(), 0xB1.toByte(), 0xE0.toByte(), + 0xFE.toByte(), 0x34.toByte(), 0x12.toByte(), 0xB0.toByte(), 0xDC.toByte(), 0x0E.toByte(), 0x75.toByte(), 0x12.toByte(), 0xB0.toByte(), + 0xD8.toByte(), 0x0E.toByte(), 0xB5.toByte(), 0x12.toByte(), 0xAF.toByte(), 0xD4.toByte(), 0x1E.toByte(), 0xF5.toByte(), 0x12.toByte(), + 0xAF.toByte(), 0xD0.toByte(), 0x1E.toByte(), 0x35.toByte(), 0x13.toByte(), 0xAE.toByte(), 0xCC.toByte(), 0x2E.toByte(), 0x75.toByte(), + 0x13.toByte(), 0xAE.toByte(), 0xC8.toByte(), 0x2E.toByte(), 0xB5.toByte(), 0x13.toByte(), 0xAD.toByte(), 0xC4.toByte(), 0x3E.toByte(), + 0xF5.toByte(), 0x13.toByte(), 0xAD.toByte(), 0xBC.toByte(), 0x3E.toByte(), 0x75.toByte(), 0x14.toByte(), 0xAD.toByte(), 0xBC.toByte(), + 0x3E.toByte(), 0x75.toByte(), 0x14.toByte(), 0xAC.toByte(), 0xB8.toByte(), 0x4E.toByte(), 0xB5.toByte(), 0x14.toByte(), 0xAC.toByte(), + 0xB4.toByte(), 0x4E.toByte(), 0xF5.toByte(), 0x14.toByte(), 0xAB.toByte(), 0xB0.toByte(), 0x5E.toByte(), 0x35.toByte(), 0x15.toByte(), + 0xAA.toByte(), 0xAC.toByte(), 0x6E.toByte(), 0x75.toByte(), 0x15.toByte(), 0xAA.toByte(), 0xA8.toByte(), 0x6E.toByte(), 0xB5.toByte(), + 0x15.toByte(), 0xAA.toByte(), 0xA4.toByte(), 0x6E.toByte(), 0xF5.toByte(), 0x15.toByte(), 0xA9.toByte(), 0xA0.toByte(), 0x7E.toByte(), + 0x35.toByte(), 0x16.toByte(), 0xA9.toByte(), 0x9C.toByte(), 0x7E.toByte(), 0x75.toByte(), 0x16.toByte(), 0xA8.toByte(), 0x98.toByte(), + 0x8E.toByte(), 0xB5.toByte(), 0x16.toByte(), 0xA7.toByte(), 0x94.toByte(), 0x9E.toByte(), 0xF5.toByte(), 0x16.toByte(), 0xA7.toByte(), + 0x90.toByte(), 0x9E.toByte(), 0x35.toByte(), 0x17.toByte(), 0xA7.toByte(), 0x8C.toByte(), 0x9E.toByte(), 0x75.toByte(), 0x17.toByte(), + 0xA6.toByte(), 0x88.toByte(), 0xAE.toByte(), 0xB5.toByte(), 0x17.toByte(), 0xA5.toByte(), 0x88.toByte(), 0xBE.toByte(), 0xB5.toByte(), + 0x17.toByte(), 0xA5.toByte(), 0x80.toByte(), 0xBE.toByte(), 0x35.toByte(), 0x18.toByte(), 0xA4.toByte(), 0x7C.toByte(), 0xCE.toByte(), + 0x75.toByte(), 0x18.toByte(), 0xA4.toByte(), 0x78.toByte(), 0xCE.toByte(), 0xB5.toByte(), 0x18.toByte(), 0xA3.toByte(), 0x78.toByte(), + 0xDE.toByte(), 0xB5.toByte(), 0x18.toByte(), 0xA2.toByte(), 0x70.toByte(), 0xEE.toByte(), 0x35.toByte(), 0x19.toByte(), 0xA2.toByte(), + 0x6C.toByte(), 0xEE.toByte(), 0x75.toByte(), 0x19.toByte(), 0xA2.toByte(), 0x6C.toByte(), 0xEE.toByte(), 0x75.toByte(), 0x19.toByte(), + 0xA1.toByte(), 0x68.toByte(), 0xFE.toByte(), 0xB5.toByte(), 0x19.toByte(), 0xA0.toByte(), 0x60.toByte(), 0x0E.toByte(), 0x36.toByte(), + 0x1A.toByte(), 0x9F.toByte(), 0x60.toByte(), 0x1E.toByte(), 0x36.toByte(), 0x1A.toByte(), 0x9F.toByte(), 0x5C.toByte(), 0x1E.toByte(), + 0x76.toByte(), 0x1A.toByte(), 0x9F.toByte(), 0x58.toByte(), 0x1E.toByte(), 0xB6.toByte(), 0x1A.toByte(), 0x9D.toByte(), 0x54.toByte(), + 0x3E.toByte(), 0xF6.toByte(), 0x1A.toByte() + ) + val factor = 1.0f + val timeStamp: Long = 0 + + // Act + val ppgData = PpgData.parseDataFromDataFrame(true, BlePMDClient.PmdDataFrameType.TYPE_0, measurementFrame, factor, timeStamp) + + // Assert + Assert.assertEquals(amountOfSamples, ppgData.ppgSamples.size) + Assert.assertEquals(3, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples.size) + Assert.assertEquals((factor * refSample0Channel0).toInt(), (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[0]) + Assert.assertEquals((factor * refSample0Channel1).toInt(), (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[1]) + Assert.assertEquals((factor * refSample0Channel2).toInt(), (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ppgDataSamples[2]) + Assert.assertEquals((factor * refSample0Channel3).toInt(), (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType0).ambientSample) + + Assert.assertEquals(3, (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples.size) + Assert.assertEquals((factor * (refSample0Channel0 + refSample1Channel0)).toInt(), (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[0]) + Assert.assertEquals((factor * (refSample0Channel1 + refSample1Channel1)).toInt(), (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[1]) + Assert.assertEquals((factor * (refSample0Channel2 + refSample1Channel2)).toInt(), (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ppgDataSamples[2]) + Assert.assertEquals((factor * (refSample0Channel3 + refSample1Channel3)).toInt(), (ppgData.ppgSamples[1] as PpgData.PpgDataSampleType0).ambientSample) + } + + @Test + fun `test compressed PPG frame type 7`() { + //HEX: 09 AE 20 + // 8A 7C 23 0 + // 58 3B 19 1 + // 80 B9 18 2 + // DB 2E 22 3 + // FA 88 1D 4 + // D7 BB 18 5 + // C2 B8 1F 6 + // 7A 44 26 7 + // 48 DF 23 8 + // A1 F2 17 9 + // 3A 37 1B 10 + // FF FF 00 11 + // 7C 08 00 12 + // 70 F5 02 13 + // FF FF 00 15 + // 28 C4 11 16 + // 18 + // 01 + // 87 + // FE FF EC 02 00 01 FE FF FB 00 00 8D F9 FF B1 FF FF 0C FE FF 26 FE FF EE FE FF 89 02 00 DF 02 00 51 FE FF 00 00 00 00 00 00 00 00 00 00 00 00 AE A6 8F + // index type data: + // 0..3 Sample 0 - channel 0 (ref. sample) 09 AE 20 (0x20AE09 = 2141705) + // 0..3 Sample 0 - channel 15 (ref. sample) FF FF 00 (0x00FFFF = 65535) + // 0..3 Sample 0 - status (ref. sample) 28 C4 11 (0x11C428 = 1164328) + val amountOfSamples = 1 + 1 // reference sample + delta samples + val refSample0Channel0 = 2141705 + val refSample0Channel15 = 65535 + val refSample0ChannelStatus = 1164328u + + val factor = 1.0f + val timeStamp = 0xFFFL + val measurementFrame = byteArrayOf( + 0x09.toByte(), + 0xAE.toByte(), + 0x20.toByte(), + 0x8A.toByte(), + 0x7C.toByte(), + 0x23.toByte(), + 0x58.toByte(), + 0x3B.toByte(), + 0x19.toByte(), + 0x80.toByte(), + 0xB9.toByte(), + 0x18.toByte(), + 0xDB.toByte(), + 0x2E.toByte(), + 0x22.toByte(), + 0xFA.toByte(), + 0x88.toByte(), + 0x1D.toByte(), + 0xD7.toByte(), + 0xBB.toByte(), + 0x18.toByte(), + 0xC2.toByte(), + 0xB8.toByte(), + 0x1F.toByte(), + 0x7A.toByte(), + 0x44.toByte(), + 0x26.toByte(), + 0x48.toByte(), + 0xDF.toByte(), + 0x23.toByte(), + 0xA1.toByte(), + 0xF2.toByte(), + 0x17.toByte(), + 0x3A.toByte(), + 0x37.toByte(), + 0x1B.toByte(), + 0xFF.toByte(), + 0xFF.toByte(), + 0x00.toByte(), + 0x7C.toByte(), + 0x08.toByte(), + 0x00.toByte(), + 0x70.toByte(), + 0xF5.toByte(), + 0x02.toByte(), + 0xFF.toByte(), + 0xFF.toByte(), + 0x00.toByte(), + 0x28.toByte(), + 0xC4.toByte(), + 0x11.toByte(), + 0x18.toByte(), + 0x01.toByte(), + 0x87.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0xEC.toByte(), + 0x02.toByte(), + 0x00.toByte(), + 0x01.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0xFB.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x8D.toByte(), + 0xF9.toByte(), + 0xFF.toByte(), + 0xB1.toByte(), + 0xFF.toByte(), + 0xFF.toByte(), + 0x0C.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0x26.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0xEE.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0x89.toByte(), + 0x02.toByte(), + 0x00.toByte(), + 0xDF.toByte(), + 0x02.toByte(), + 0x00.toByte(), + 0x51.toByte(), + 0xFE.toByte(), + 0xFF.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0x00.toByte(), + 0xAE.toByte(), + 0xA6.toByte(), + 0x8F.toByte() + ) + // Act + val ppgData = PpgData.parseDataFromDataFrame(true, BlePMDClient.PmdDataFrameType.TYPE_7, measurementFrame, factor, timeStamp) + + // Assert + Assert.assertEquals(amountOfSamples, ppgData.ppgSamples.size) + Assert.assertEquals(16, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType2).ppgDataSamples.size) + Assert.assertEquals(refSample0Channel0, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType2).ppgDataSamples[0]) + Assert.assertEquals(refSample0Channel15, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType2).ppgDataSamples[15]) + Assert.assertEquals(refSample0ChannelStatus, (ppgData.ppgSamples[0] as PpgData.PpgDataSampleType2).status) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpiTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt similarity index 86% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpiTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt index bf493124..80b63257 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientPpiTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PpiDataTest.kt @@ -1,9 +1,10 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PpiData import org.junit.Assert.assertEquals import org.junit.Test -class BlePmdClientPpiTest { +class PpiDataTest { @Test fun test_PPI_DataSample() { // Arrange @@ -50,7 +51,7 @@ class BlePmdClientPpiTest { val timeStamp: Long = Long.MAX_VALUE // Act - val ppiData = BlePMDClient.PpiData(measurementFrame, timeStamp) + val ppiData = PpiData.parseDataFromDataFrame(isCompressed = false, BlePMDClient.PmdDataFrameType.TYPE_0, measurementFrame, 1.0f, timeStamp) // Assert assertEquals(heartRate.toLong(), ppiData.ppSamples[0].hr.toLong()) @@ -58,20 +59,14 @@ class BlePmdClientPpiTest { assertEquals(errorEstimate.toLong(), ppiData.ppSamples[0].ppErrorEstimate.toLong()) assertEquals(blockerBit.toLong(), ppiData.ppSamples[0].blockerBit.toLong()) assertEquals(skinContactStatus.toLong(), ppiData.ppSamples[0].skinContactStatus.toLong()) - assertEquals( - skinContactSupported.toLong(), - ppiData.ppSamples[0].skinContactSupported.toLong() - ) + assertEquals(skinContactSupported.toLong(), ppiData.ppSamples[0].skinContactSupported.toLong()) assertEquals(heartRate2.toLong(), ppiData.ppSamples[1].hr.toLong()) assertEquals(intervalInMs2.toLong(), ppiData.ppSamples[1].ppInMs.toLong()) assertEquals(errorEstimate2.toLong(), ppiData.ppSamples[1].ppErrorEstimate.toLong()) assertEquals(blockerBit2.toLong(), ppiData.ppSamples[1].blockerBit.toLong()) assertEquals(skinContactStatus2.toLong(), ppiData.ppSamples[1].skinContactStatus.toLong()) - assertEquals( - skinContactSupported2.toLong(), - ppiData.ppSamples[1].skinContactSupported.toLong() - ) + assertEquals(skinContactSupported2.toLong(), ppiData.ppSamples[1].skinContactSupported.toLong()) assertEquals(2, ppiData.ppSamples.size) } diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt new file mode 100644 index 00000000..8aa2d693 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/PressureDataTest.kt @@ -0,0 +1,75 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model.PressureData +import org.junit.Assert +import org.junit.Test +import java.lang.Float.intBitsToFloat + +class PressureDataTest { + + @Test + fun `process pressure data type 0`() { + // Arrange + // HEX: C2 87 80 44 0A 01 1F BF + // index type data + // 0..3 Sample 1 (ref. sample) C2 87 80 44 (0x448087C2) + // 4 Delta size 0A (10 bit) + // 5 Sample amount 01 (1 samples) + // 6.. Delta data 1F BF + // Delta sample 1 11 0001 1111b (- 0xE1) + + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 578437695752307201L + val sample0 = intBitsToFloat(0x448087C2) + val sample1 = intBitsToFloat(0x448087C2 - 0xE1) + + val measurementFrame = byteArrayOf( + 0xC2.toByte(), 0x87.toByte(), 0x80.toByte(), 0x44.toByte(), + 0x0A.toByte(), 0x01.toByte(), 0x1F.toByte(), 0xBF.toByte(), + ) + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val factor = 1.0f + + // Act + val pressureData = PressureData.parseDataFromDataFrame(isCompressed = true, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, pressureData.timeStamp) + Assert.assertEquals(expectedSamplesSize, pressureData.pressureSamples.size) + Assert.assertEquals(sample0, pressureData.pressureSamples[0].pressure) + Assert.assertEquals(sample1, pressureData.pressureSamples[1].pressure) + } + + @Test + fun `process pressure data type 0 with factor`() { + // Arrange + // HEX: C2 87 80 44 0A 01 1F BF + // index type data + // 0..3 Sample 1 (ref. sample) C2 87 80 44 (0x448087C2) + // 4 Delta size 0A (10 bit) + // 5 Sample amount 01 (1 samples) + // 6.. Delta data 1F BF + // Delta sample 1 11 0001 1111b (- 0xE1) + + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 578437695752307201L + val sample0 = intBitsToFloat(0x448087C2) + val sample1 = intBitsToFloat(0x448087C2 - 0xE1) + + val measurementFrame = byteArrayOf( + 0xC2.toByte(), 0x87.toByte(), 0x80.toByte(), 0x44.toByte(), + 0x0A.toByte(), 0x01.toByte(), 0x1F.toByte(), 0xBF.toByte(), + ) + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val factor = 2.0f + + // Act + val pressureData = PressureData.parseDataFromDataFrame(isCompressed = true, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, pressureData.timeStamp) + Assert.assertEquals(expectedSamplesSize, pressureData.pressureSamples.size) + Assert.assertEquals(factor * sample0, pressureData.pressureSamples[0].pressure) + Assert.assertEquals(factor * sample1, pressureData.pressureSamples[1].pressure) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccDataTest.kt new file mode 100644 index 00000000..70af7d97 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/AccDataTest.kt @@ -0,0 +1,199 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import org.junit.Assert +import org.junit.Test +import kotlin.math.abs + +class AccDataTest { + + @Test + fun `process acc raw data type 1`() { + // Arrange + // HEX: 01 F7 FF FF FF E7 03 F8 FF FE FF E5 03 F9 FF FF FF E5 03 FA FF FF FF E6 03 FA FF FE FF E6 03 F9 FF FF FF E5 03 F8 FF FF FF E6 03 F8 FF FE FF E6 03 FA FF FF FF E5 03 FA FF FF FF E7 03 FA FF FF FF E5 03 F8 FF FF FF E6 03 F7 FF FF FF E6 03 F8 FF FE FF E6 03 F9 FF FE FF E7 03 F9 FF 00 00 E6 03 F9 FF FF FF E6 03 F7 FF FE FF E5 03 F9 FF FF FF E5 03 F9 FF FF FF E5 03 FA FF 00 00 E6 03 F9 FF FE FF E6 03 F8 FF FF FF E6 03 F8 FF FF FF E5 03 F9 FF FF FF E6 03 F9 FF FF FF E5 03 FA FF FF FF E6 03 F9 FF FF FF E5 03 F9 FF FF FF E5 03 F8 FF FE FF E6 03 F9 FF FF FF E6 03 F9 FF FF FF E6 03 F9 FF 00 00 E5 03 F9 FF FE FF E6 03 F8 FF FE FF E6 03 F7 FF FE FF E6 03 + // index data: + // 0 type 01 + val frameType = BlePMDClient.PmdDataFrameType.TYPE_1 + // 1..2 x value F7 FF (-9) + val xValue1 = -9 + // 3..4 y value FF FF (-1) + val yValue1 = -1 + // 5..6 z value E7 03 (999) + val zValue1 = 999 + // 7..8 x value F8 FF (-8) + val xValue2 = -8 + // 9..10 y value FF FE (-2) + val yValue2 = -2 + // 11..12 z value E5 03 (997) + val zValue2 = 997 + val measurementFrame = byteArrayOf( + 0xF7.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE7.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), + 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xFA.toByte(), 0xFF.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xFA.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), + 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xFA.toByte(), 0xFF.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xFA.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE7.toByte(), 0x03.toByte(), + 0xFA.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF7.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), + 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE7.toByte(), 0x03.toByte(), + 0xF9.toByte(), 0xFF.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF7.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), + 0xFA.toByte(), 0xFF.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), + 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xFA.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), + 0xFF.toByte(), 0xFF.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), + 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), + 0xE6.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), 0x00.toByte(), 0x00.toByte(), 0xE5.toByte(), 0x03.toByte(), 0xF9.toByte(), 0xFF.toByte(), + 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), 0xF8.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte(), + 0xF7.toByte(), 0xFF.toByte(), 0xFE.toByte(), 0xFF.toByte(), 0xE6.toByte(), 0x03.toByte() + ) + val isCompressed = false + val timeStamp: Long = 0 + val amountOfSamples = measurementFrame.size / 2 / 3 // measurement frame size / resolution in bytes / channels + + // Act + val accData = AccData.parseDataFromDataFrame(isCompressed, frameType, measurementFrame, 1.0f, timeStamp) + + // Assert + Assert.assertEquals(xValue1, accData.accSamples[0].x) + Assert.assertEquals(yValue1, accData.accSamples[0].y) + Assert.assertEquals(zValue1, accData.accSamples[0].z) + Assert.assertEquals(xValue2, accData.accSamples[1].x) + Assert.assertEquals(yValue2, accData.accSamples[1].y) + Assert.assertEquals(zValue2, accData.accSamples[1].z) + + // validate data size + Assert.assertEquals(amountOfSamples, accData.accSamples.size) + } + + @Test + fun `process acc compressed data type 0`() { + // Arrange + // HEX: 71 07 F0 6A 9E 8D 0A 38 BE 5C BE BA 2F 96 B3 EE 4B E5 AD FB 42 B9 EB BE 4C FE BA 2F 92 BF EE 4B E4 B1 FB 12 B9 EC BD 3C 3E BB 2F 8F D3 DE 4B E3 B5 F7 D2 B8 ED BD 30 7E 7B 2F 8B E3 CE 8B E2 BA F7 A2 B8 EE BC 20 BE 7B 2F 88 F3 CE CB E1 BD EF 52 F8 EF BC 18 FE 3B 2F 84 03 BF CB E0 C2 EF 32 B8 F0 BB 04 4E BC 2E 81 13 AF 0B E0 C6 EF F2 F7 F1 B9 FC 7D BC 2E 7D 27 9F 4B DF CA EB C2 F7 F2 B8 EC CD 7C 2E 7B 37 8F 4B DE CE E3 92 F7 F3 B8 E0 0D FD 2D 77 4B 7F CB DD D2 DF 62 37 F5 B7 D4 4D BD 2D 74 5B 6F CB DC D7 D7 32 37 F6 B5 C8 8D 7D 2D 71 6B 4F 4B DC DC D3 F2 36 F7 B4 BC DD FD 2C 6F 7B 3F 4B DB E0 CF D2 36 F8 B2 B0 2D BE 2C 6C 8F 1F CB DA E3 C7 A2 76 F9 + // index type data: + // 0-5: Reference sample 0xC9 0xFF 0x12 0x00 0x11 0x00 + // Sample 0 (aka. reference sample): + // channel 0: 71 07 => 0x0771 => 1905 + val refSample0Channel0 = 1905 + // channel 1: F0 6A => 0x6AF0 => 27376 + val refSample0Channel1 = 27376 + // channel 2: 9E 8D => 0x8D9E => -29282 + val refSample0Channel2 = -29282 + // Delta dump: 0A 38 | BE 5C BE BA 2F 96 B3 EE 4B E5 AD ... + // 6: Delta size size 1: 0x0A (10 bits) + // 7: Sample amount size 1: 0x38 (Delta block contains 56 samples) + // 8: 0xBE (binary: 1011 1110) + // 9: 0x5C (binary: 0101 11 | 00) + // 10: 0xBE (binary: 1011 | 1110) + // Sample 1 - channel 0, size 10 bits: 00 1011 1110 + // Sample 1 - channel 1, size 10 bits: 11 1001 0111 + // 11: 0xBA (binary: 10 | 11 1010) + // Sample 1 - channel 2, size 10 bits: 11 1010 1011 + val refSample1Channel0 = 190 + val refSample1Channel1 = -105 + val refSample1Channel2 = -85 + val amountOfSamples = 1 + 56 // reference sample + delta samples + val measurementFrame = byteArrayOf( + 0x71.toByte(), 0x07.toByte(), 0xF0.toByte(), 0x6A.toByte(), 0x9E.toByte(), 0x8D.toByte(), 0x0A.toByte(), 0x38.toByte(), + 0xBE.toByte(), 0x5C.toByte(), 0xBE.toByte(), 0xBA.toByte(), 0x2F.toByte(), 0x96.toByte(), 0xB3.toByte(), 0xEE.toByte(), + 0x4B.toByte(), 0xE5.toByte(), 0xAD.toByte(), 0xFB.toByte(), 0x42.toByte(), 0xB9.toByte(), 0xEB.toByte(), 0xBE.toByte(), + 0x4C.toByte(), 0xFE.toByte(), 0xBA.toByte(), 0x2F.toByte(), 0x92.toByte(), 0xBF.toByte(), 0xEE.toByte(), 0x4B.toByte(), + 0xE4.toByte(), 0xB1.toByte(), 0xFB.toByte(), 0x12.toByte(), 0xB9.toByte(), 0xEC.toByte(), 0xBD.toByte(), 0x3C.toByte(), + 0x3E.toByte(), 0xBB.toByte(), 0x2F.toByte(), 0x8F.toByte(), 0xD3.toByte(), 0xDE.toByte(), 0x4B.toByte(), 0xE3.toByte(), + 0xB5.toByte(), 0xF7.toByte(), 0xD2.toByte(), 0xB8.toByte(), 0xED.toByte(), 0xBD.toByte(), 0x30.toByte(), 0x7E.toByte(), + 0x7B.toByte(), 0x2F.toByte(), 0x8B.toByte(), 0xE3.toByte(), 0xCE.toByte(), 0x8B.toByte(), 0xE2.toByte(), 0xBA.toByte(), + 0xF7.toByte(), 0xA2.toByte(), 0xB8.toByte(), 0xEE.toByte(), 0xBC.toByte(), 0x20.toByte(), 0xBE.toByte(), 0x7B.toByte(), + 0x2F.toByte(), 0x88.toByte(), 0xF3.toByte(), 0xCE.toByte(), 0xCB.toByte(), 0xE1.toByte(), 0xBD.toByte(), 0xEF.toByte(), + 0x52.toByte(), 0xF8.toByte(), 0xEF.toByte(), 0xBC.toByte(), 0x18.toByte(), 0xFE.toByte(), 0x3B.toByte(), 0x2F.toByte(), + 0x84.toByte(), 0x03.toByte(), 0xBF.toByte(), 0xCB.toByte(), 0xE0.toByte(), 0xC2.toByte(), 0xEF.toByte(), 0x32.toByte(), + 0xB8.toByte(), 0xF0.toByte(), 0xBB.toByte(), 0x04.toByte(), 0x4E.toByte(), 0xBC.toByte(), 0x2E.toByte(), 0x81.toByte(), + 0x13.toByte(), 0xAF.toByte(), 0x0B.toByte(), 0xE0.toByte(), 0xC6.toByte(), 0xEF.toByte(), 0xF2.toByte(), 0xF7.toByte(), + 0xF1.toByte(), 0xB9.toByte(), 0xFC.toByte(), 0x7D.toByte(), 0xBC.toByte(), 0x2E.toByte(), 0x7D.toByte(), 0x27.toByte(), + 0x9F.toByte(), 0x4B.toByte(), 0xDF.toByte(), 0xCA.toByte(), 0xEB.toByte(), 0xC2.toByte(), 0xF7.toByte(), 0xF2.toByte(), + 0xB8.toByte(), 0xEC.toByte(), 0xCD.toByte(), 0x7C.toByte(), 0x2E.toByte(), 0x7B.toByte(), 0x37.toByte(), 0x8F.toByte(), + 0x4B.toByte(), 0xDE.toByte(), 0xCE.toByte(), 0xE3.toByte(), 0x92.toByte(), 0xF7.toByte(), 0xF3.toByte(), 0xB8.toByte(), + 0xE0.toByte(), 0x0D.toByte(), 0xFD.toByte(), 0x2D.toByte(), 0x77.toByte(), 0x4B.toByte(), 0x7F.toByte(), 0xCB.toByte(), + 0xDD.toByte(), 0xD2.toByte(), 0xDF.toByte(), 0x62.toByte(), 0x37.toByte(), 0xF5.toByte(), 0xB7.toByte(), 0xD4.toByte(), + 0x4D.toByte(), 0xBD.toByte(), 0x2D.toByte(), 0x74.toByte(), 0x5B.toByte(), 0x6F.toByte(), 0xCB.toByte(), 0xDC.toByte(), + 0xD7.toByte(), 0xD7.toByte(), 0x32.toByte(), 0x37.toByte(), 0xF6.toByte(), 0xB5.toByte(), 0xC8.toByte(), 0x8D.toByte(), + 0x7D.toByte(), 0x2D.toByte(), 0x71.toByte(), 0x6B.toByte(), 0x4F.toByte(), 0x4B.toByte(), 0xDC.toByte(), 0xDC.toByte(), + 0xD3.toByte(), 0xF2.toByte(), 0x36.toByte(), 0xF7.toByte(), 0xB4.toByte(), 0xBC.toByte(), 0xDD.toByte(), 0xFD.toByte(), + 0x2C.toByte(), 0x6F.toByte(), 0x7B.toByte(), 0x3F.toByte(), 0x4B.toByte(), 0xDB.toByte(), 0xE0.toByte(), 0xCF.toByte(), + 0xD2.toByte(), 0x36.toByte(), 0xF8.toByte(), 0xB2.toByte(), 0xB0.toByte(), 0x2D.toByte(), 0xBE.toByte(), 0x2C.toByte(), + 0x6C.toByte(), 0x8F.toByte(), 0x1F.toByte(), 0xCB.toByte(), 0xDA.toByte(), 0xE3.toByte(), 0xC7.toByte(), 0xA2.toByte(), + 0x76.toByte(), 0xF9.toByte() + ) + val range = 8 + val factor = 2.44E-4f + val timeStamp: Long = 0 + val isCompressed = true + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + + // Act + val accData = AccData.parseDataFromDataFrame(isCompressed, frameType, measurementFrame, factor, timeStamp) + + // Assert + Assert.assertEquals((factor * refSample0Channel0.toFloat() * 1000f).toInt(), accData.accSamples[0].x) + Assert.assertEquals((factor * refSample0Channel1.toFloat() * 1000f).toInt(), accData.accSamples[0].y) + Assert.assertEquals((factor * refSample0Channel2.toFloat() * 1000f).toInt(), accData.accSamples[0].z) + Assert.assertEquals((factor * (refSample0Channel0 + refSample1Channel0) * 1000f).toInt(), accData.accSamples[1].x) + Assert.assertEquals((factor * (refSample0Channel1 + refSample1Channel1) * 1000f).toInt(), accData.accSamples[1].y) + Assert.assertEquals((factor * (refSample0Channel2 + refSample1Channel2) * 1000f).toInt(), accData.accSamples[1].z) + + // validate data in range + for (sample in accData.accSamples) { + Assert.assertTrue(abs(sample.x) <= range * 1000) + Assert.assertTrue(abs(sample.y) <= range * 1000) + Assert.assertTrue(abs(sample.z) <= range * 1000) + } + + // validate data size + Assert.assertEquals(amountOfSamples.toLong(), accData.accSamples.size.toLong()) + } + + @Test + fun `process acc compressed data type 1`() { + // HEX: F1 FF 14 00 F0 03 06 01 7B 0F 08 + // index type data + // 0..1 Sample 0 - channel 0 (ref. sample) F1 FF (0xFFF1 = -22) + // 2..3 Sample 0 - channel 1 (ref. sample) 14 00 (0x0014 = 20) + // 4..5 Sample 0 - channel 2 (ref. sample) F0 03 (0x03F0 = 1008) + // 6 Delta size 06 (6 bit) + // 7 Sample amount 01 (10 samples) + // 8.. Delta data 7B (binary: 01 111011) 0F (binary: 0000 1111) 08 (binary: 0000 1000) + // Delta channel 0 111011b + // Delta channel 1 111101b + // Delta channel 2 000000b + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val sample0channel0 = -15 + val sample0channel1 = 20 + val sample0channel2 = 1008 + val sample1channel0 = sample0channel0 - 5 + val sample1channel1 = sample0channel1 -3 + val sample1channel2 = sample0channel2 + 0 + + val isCompressed = true + val frameType = BlePMDClient.PmdDataFrameType.TYPE_1 + val timeStamp: Long = 0 + val measurementFrame = byteArrayOf( + 0xF1.toByte(), 0xFF.toByte(), 0x14.toByte(), 0x00.toByte(), 0xF0.toByte(), 0x03.toByte(), 0x06.toByte(), + 0x01.toByte(), 0x7B.toByte(), 0x0F.toByte(), 0x08.toByte() + ) + // Act + val accData = AccData.parseDataFromDataFrame(isCompressed, frameType, measurementFrame, 1.0f, timeStamp) + + // Assert + Assert.assertEquals(expectedSamplesSize, accData.accSamples.size) + + Assert.assertEquals(sample0channel0, accData.accSamples[0].x) + Assert.assertEquals(sample0channel1, accData.accSamples[0].y) + Assert.assertEquals(sample0channel2, accData.accSamples[0].z) + + Assert.assertEquals(sample1channel0, accData.accSamples[1].x) + Assert.assertEquals(sample1channel1, accData.accSamples[1].y) + Assert.assertEquals(sample1channel2, accData.accSamples[1].z) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientEcgTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgDataTest.kt similarity index 73% rename from sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientEcgTest.kt rename to sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgDataTest.kt index 369e75bd..122d4f97 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/BlePmdClientEcgTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/EcgDataTest.kt @@ -1,36 +1,29 @@ -package com.polar.androidcommunications.api.ble.model.gatt.client +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient import org.junit.Assert import org.junit.Test -class BlePmdClientEcgTest { +class EcgDataTest { @Test fun test_Ecg_DataSample_type0() { // Arrange // HEX: 02 08 FF 02 80 00 // index type data: // 0 - val type: Byte = 0 + val type = BlePMDClient.PmdDataFrameType.TYPE_0 // 1..3 uVolts 02 80 FF (-32766) val ecgValue1 = -32766 // 4..6 uVolts 02 80 00 (32770) val ecgValue2 = 32770 - val measurementFrame = byteArrayOf( - 0x02.toByte(), - 0x80.toByte(), - 0xFF.toByte(), - 0x02.toByte(), - 0x80.toByte(), - 0x00.toByte() - ) + val measurementFrame = byteArrayOf(0x02.toByte(), 0x80.toByte(), 0xFF.toByte(), 0x02.toByte(), 0x80.toByte(), 0x00.toByte()) val timeStamp = Long.MAX_VALUE // Act - val ecgData = BlePMDClient.EcgData(type, measurementFrame, timeStamp) + val ecgData = EcgData.parseDataFromDataFrame(false, type, measurementFrame, 1.0f, timeStamp) // Assert Assert.assertEquals(timeStamp, ecgData.ecgSamples[0].timeStamp) - Assert.assertEquals(type.toLong(), ecgData.ecgSamples[0].type.numVal.toLong()) Assert.assertEquals(ecgValue1.toLong(), ecgData.ecgSamples[0].microVolts.toLong()) Assert.assertEquals(ecgValue2.toLong(), ecgData.ecgSamples[1].microVolts.toLong()) Assert.assertEquals(2, ecgData.ecgSamples.size.toLong()) @@ -42,7 +35,7 @@ class BlePmdClientEcgTest { // HEX: 80 02 FF 02 80 00 // index type data: // 0 - val type: Byte = 1 + val type = BlePMDClient.PmdDataFrameType.TYPE_1 // 1..2 uVolts 80 02 (2) val ecgValue1 = 2 // 3 FF @@ -55,35 +48,21 @@ class BlePmdClientEcgTest { val sampleBit2 = 0x00 val skinContact2 = (sampleBit2 and 0x06 shr 1).toByte() val contactImpedance2 = (sampleBit2 and 0x18 shr 3).toByte() - val measurementFrame = byteArrayOf( - 0x02.toByte(), - 0x80.toByte(), - 0xFF.toByte(), - 0x80.toByte(), - 0x02.toByte(), - 0x00.toByte() - ) + val measurementFrame = byteArrayOf(0x02.toByte(), 0x80.toByte(), 0xFF.toByte(), 0x80.toByte(), 0x02.toByte(), 0x00.toByte()) val timeStamp = Long.MAX_VALUE // Act - val ecgData = BlePMDClient.EcgData(type, measurementFrame, timeStamp) + val ecgData = EcgData.parseDataFromDataFrame(false, type, measurementFrame, 1.0f, timeStamp) // Assert Assert.assertEquals(timeStamp, ecgData.ecgSamples[0].timeStamp) - Assert.assertEquals(type.toLong(), ecgData.ecgSamples[0].type.numVal.toLong()) Assert.assertEquals(ecgValue1.toLong(), ecgData.ecgSamples[0].microVolts.toLong()) Assert.assertTrue(ecgData.ecgSamples[0].overSampling) Assert.assertEquals(skinContact1.toLong(), ecgData.ecgSamples[0].skinContactBit.toLong()) - Assert.assertEquals( - contactImpedance1.toLong(), - ecgData.ecgSamples[0].contactImpedance.toLong() - ) + Assert.assertEquals(contactImpedance1.toLong(), ecgData.ecgSamples[0].contactImpedance.toLong()) Assert.assertFalse(ecgData.ecgSamples[1].overSampling) Assert.assertEquals(skinContact2.toLong(), ecgData.ecgSamples[1].skinContactBit.toLong()) - Assert.assertEquals( - contactImpedance2.toLong(), - ecgData.ecgSamples[1].contactImpedance.toLong() - ) + Assert.assertEquals(contactImpedance2.toLong(), ecgData.ecgSamples[1].contactImpedance.toLong()) Assert.assertEquals(ecgValue2.toLong(), ecgData.ecgSamples[1].microVolts.toLong()) Assert.assertEquals(2, ecgData.ecgSamples.size.toLong()) } @@ -94,7 +73,7 @@ class BlePmdClientEcgTest { // HEX: 80 80 FC FF FF 03 // index type data: // 0 - val type: Byte = 2 + val type = BlePMDClient.PmdDataFrameType.TYPE_2 // 1..3 uVolts 80 80 FC (32896) val ecgValue1 = 32896 // 3 FC @@ -107,22 +86,14 @@ class BlePmdClientEcgTest { val sampleBit2 = 0x00 val ecgDataTag2 = (sampleBit2 and 0x1C shr 2) val paceDataTag2 = (sampleBit2 and 0xE0 shr 5) - val measurementFrame = byteArrayOf( - 0x80.toByte(), - 0x80.toByte(), - 0xFC.toByte(), - 0xFF.toByte(), - 0xFF.toByte(), - 0x03.toByte() - ) + val measurementFrame = byteArrayOf(0x80.toByte(), 0x80.toByte(), 0xFC.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x03.toByte()) val timeStamp = Long.MAX_VALUE // Act - val ecgData = BlePMDClient.EcgData(type, measurementFrame, timeStamp) + val ecgData = EcgData.parseDataFromDataFrame(false, type, measurementFrame, 1.0f, timeStamp) // Assert Assert.assertEquals(timeStamp, ecgData.ecgSamples[0].timeStamp) - Assert.assertEquals(type.toLong(), ecgData.ecgSamples[0].type.numVal.toLong()) Assert.assertEquals(ecgValue1.toLong(), ecgData.ecgSamples[0].microVolts.toLong()) Assert.assertEquals(ecgDataTag1.toLong(), ecgData.ecgSamples[0].ecgDataTag.toLong()) Assert.assertEquals(paceDataTag1.toLong(), ecgData.ecgSamples[0].paceDataTag.toLong()) diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrDataTest.kt new file mode 100644 index 00000000..0fb8c753 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/GyrDataTest.kt @@ -0,0 +1,155 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import org.junit.Assert +import org.junit.Test +import java.lang.Float.intBitsToFloat + +class GyrDataTest { + + @Test + fun `process gyro compressed data type 0`() { + // Arrange + // HEX: EA FF 08 00 0D 00 03 01 DF 00 + // index type data + // 0..1 Sample 0 - channel 0 (ref. sample) EA FF (0xFFEA = -22) + // 2..3 Sample 0 - channel 1 (ref. sample) 08 00 (0x0008 = 8) + // 4..5 Sample 0 - channel 2 (ref. sample) 0D 00 (0x000D = 13) + // 6 Delta size 03 (3 bit) + // 7 Sample amount 01 (1 samples) + // 8.. Delta data DF (binary: 11 011 111) 00 (binary: 0000000 0) + // Delta channel 0 111b + // Delta channel 1 011b + // Delta channel 2 011b + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 9223372036854775807L + val sample0channel0 = -22.0f + val sample0channel1 = 8.0f + val sample0channel2 = 13.0f + + val sample1channel0 = sample0channel0 - 0x1 + val sample1channel1 = sample0channel1 + 0x3 + val sample1channel2 = sample0channel2 + 0x3 + + val measurementFrame = byteArrayOf( + 0xEA.toByte(), 0xFF.toByte(), + 0x08.toByte(), 0x00.toByte(), 0x0D.toByte(), 0x00.toByte(), + 0x03.toByte(), 0x01.toByte(), 0xDF.toByte(), 0x00.toByte() + ) + + // Act + val gyroData = GyrData.parseDataFromDataFrame(isCompressed = true, frameType = BlePMDClient.PmdDataFrameType.TYPE_0, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, gyroData.timeStamp) + Assert.assertEquals(expectedSamplesSize, gyroData.gyrSamples.size) + Assert.assertEquals(sample0channel0, gyroData.gyrSamples[0].x) + Assert.assertEquals(sample0channel1, gyroData.gyrSamples[0].y) + Assert.assertEquals(sample0channel2, gyroData.gyrSamples[0].z) + + Assert.assertEquals(sample1channel0, gyroData.gyrSamples[1].x) + Assert.assertEquals(sample1channel1, gyroData.gyrSamples[1].y) + Assert.assertEquals(sample1channel2, gyroData.gyrSamples[1].z) + } + + @Test + fun `process gyro data type 1`() { + // Arrange + // HEX: 00 00 80 3F 00 00 20 41 00 00 A0 41 1C 01 00 00 A0 01 00 00 08 CD CC EC 0D + // index type data + // 10..13 Sample 0 - channel 0 (ref. sample) 00 00 80 3F (0x3F800000) + // 14..17 Sample 0 - channel 1 (ref. sample) 00 00 20 41 (0x41200000) + // 18..21 Sample 0 - channel 2 (ref. sample) 00 00 A0 41 (0x41A00000) + // 22 Delta size 1C (28 bit) + // 23 Sample amount 01 (1 samples) + // 24..35 delta data: 00 00 A0 01 00 00 08 CD CC EC 0D + // Sample 1 - channel 0: (0x01A00000) + // Sample 1 - channel 1: (0x00800000) + // Sample 1 - channel 2 (0xFDECCCCD) + + val expectedTimeStamp = 2509038777 + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val sample0channel0 = intBitsToFloat(0x3F800000) + val sample0channel1 = intBitsToFloat(0x41200000) + val sample0channel2 = intBitsToFloat(0x41A00000) + + val sample1channel0 = intBitsToFloat(0x3F800000 + 0x1A00000) + val sample1channel1 = intBitsToFloat(0x41200000 + 0x0800000) + val sample1channel2 = intBitsToFloat((0x41A00000 + 0xFDECCCCD).toInt()) + + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), + 0x80.toByte(), 0x3F.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x20.toByte(), 0x41.toByte(), 0x00.toByte(), 0x00.toByte(), + 0xA0.toByte(), 0x41.toByte(), 0x1C.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0xA0.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x08.toByte(), 0xCD.toByte(), + 0xCC.toByte(), 0xEC.toByte(), 0x0D.toByte() + ) + + // Act + val gyroData = GyrData.parseDataFromDataFrame(isCompressed = true, frameType = BlePMDClient.PmdDataFrameType.TYPE_1, frame = measurementFrame, factor = 1.0f, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedSamplesSize, gyroData.gyrSamples.size) + Assert.assertEquals(expectedTimeStamp, gyroData.timeStamp) + Assert.assertEquals(sample0channel0, gyroData.gyrSamples[0].x) + Assert.assertEquals(sample0channel1, gyroData.gyrSamples[0].y) + Assert.assertEquals(sample0channel2, gyroData.gyrSamples[0].z) + + Assert.assertEquals(sample1channel0, gyroData.gyrSamples[1].x) + Assert.assertEquals(sample1channel1, gyroData.gyrSamples[1].y) + Assert.assertEquals(sample1channel2, gyroData.gyrSamples[1].z) + } + + @Test + fun `process gyro data type 1 with factor`() { + // Arrange + // HEX: 00 00 80 3F 00 00 20 41 00 00 A0 41 1C 01 00 00 A0 01 00 00 08 CD CC EC 0D + // index type data + // 10..13 Sample 0 - channel 0 (ref. sample) 00 00 80 3F (0x3F800000) + // 14..17 Sample 0 - channel 1 (ref. sample) 00 00 20 41 (0x41200000) + // 18..21 Sample 0 - channel 2 (ref. sample) 00 00 A0 41 (0x41A00000) + // 22 Delta size 1C (28 bit) + // 23 Sample amount 01 (1 samples) + // 24..35 delta data: 00 00 A0 01 00 00 08 CD CC EC 0D + // Sample 1 - channel 0: (0x01A00000) + // Sample 1 - channel 1: (0x00800000) + // Sample 1 - channel 2 (0xFDECCCCD) + + val expectedTimeStamp = 2509038777 + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val sample0channel0 = intBitsToFloat(0x3F800000) + val sample0channel1 = intBitsToFloat(0x41200000) + val sample0channel2 = intBitsToFloat(0x41A00000) + + val sample1channel0 = intBitsToFloat(0x3F800000 + 0x1A00000) + val sample1channel1 = intBitsToFloat(0x41200000 + 0x0800000) + val sample1channel2 = intBitsToFloat((0x41A00000 + 0xFDECCCCD).toInt()) + + val measurementFrame = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), + 0x80.toByte(), 0x3F.toByte(), 0x00.toByte(), 0x00.toByte(), + 0x20.toByte(), 0x41.toByte(), 0x00.toByte(), 0x00.toByte(), + 0xA0.toByte(), 0x41.toByte(), 0x1C.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0xA0.toByte(), 0x01.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x08.toByte(), 0xCD.toByte(), + 0xCC.toByte(), 0xEC.toByte(), 0x0D.toByte() + ) + val factor = 0.5f + + // Act + val gyroData = GyrData.parseDataFromDataFrame(isCompressed = true, frameType = BlePMDClient.PmdDataFrameType.TYPE_1, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedSamplesSize, gyroData.gyrSamples.size) + Assert.assertEquals(expectedTimeStamp, gyroData.timeStamp) + Assert.assertEquals(factor * sample0channel0, gyroData.gyrSamples[0].x) + Assert.assertEquals(factor * sample0channel1, gyroData.gyrSamples[0].y) + Assert.assertEquals(factor * sample0channel2, gyroData.gyrSamples[0].z) + + Assert.assertEquals(factor * sample1channel0, gyroData.gyrSamples[1].x) + Assert.assertEquals(factor * sample1channel1, gyroData.gyrSamples[1].y) + Assert.assertEquals(factor * sample1channel2, gyroData.gyrSamples[1].z) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagDataTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagDataTest.kt new file mode 100644 index 00000000..ea4669a2 --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/api/ble/model/gatt/client/pmd/model/MagDataTest.kt @@ -0,0 +1,170 @@ +package com.polar.androidcommunications.api.ble.model.gatt.client.pmd.model + +import com.polar.androidcommunications.api.ble.model.gatt.client.pmd.BlePMDClient +import org.junit.Assert +import org.junit.Test + +class MagDataTest { + + @Test + fun `process magnetometer compressed data type 0`() { + // Arrange + // HEX: E2 E6 FA 15 49 0A 06 01 7F 20 FC + // index type data + // 0..1 Sample 0 - channel 0 (ref. sample) E2 E6 (0xE6E2 = -6430) + // 1..2 Sample 0 - channel 1 (ref. sample) FA 15 (0x15FA = 5626) + // 3..4 Sample 0 - channel 2 (ref. sample) 49 0A (0x0A49 = 2633) + // 5 Delta size 06 (6 bit) + // 6 Sample amount 01 (1 samples) + // 7.. Delta data 7F (binary: 01 111111) 20 (binary: 0010 0000) FC (binary: 111111 00) + // Delta channel 0 111111b + // Delta channel 1 000001b + // Delta channel 2 000010b + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 4294967295L + val measurementFrame = byteArrayOf( + 0xE2.toByte(), 0xE6.toByte(), 0xFA.toByte(), 0x15.toByte(), 0x49.toByte(), 0x0A.toByte(), + 0x06.toByte(), 0x01.toByte(), 0x7F.toByte(), 0x20.toByte(), 0xFC.toByte() + ) + + val sample0channel0 = -6430.0f + val sample0channel1 = 5626.0f + val sample0channel2 = 2633.0f + val sample0status = MagData.CalibrationStatus.NOT_AVAILABLE + + val sample1channel0 = sample0channel0 - 0x01 + val sample1channel1 = sample0channel1 + 0x01 + val sample1channel2 = sample0channel2 + 0x02 + val sample1status = MagData.CalibrationStatus.NOT_AVAILABLE + + val frameType = BlePMDClient.PmdDataFrameType.TYPE_0 + val factor = 1.0f + + // Act + val magData = MagData.parseDataFromDataFrame(isCompressed = true, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + Assert.assertEquals(expectedTimeStamp, magData.timeStamp) + Assert.assertEquals(expectedSamplesSize, magData.magSamples.size) + + Assert.assertEquals(sample0channel0, magData.magSamples[0].x) + Assert.assertEquals(sample0channel1, magData.magSamples[0].y) + Assert.assertEquals(sample0channel2, magData.magSamples[0].z) + Assert.assertEquals(sample0status, magData.magSamples[0].calibrationStatus) + + Assert.assertEquals(sample1channel0, magData.magSamples[1].x) + Assert.assertEquals(sample1channel1, magData.magSamples[1].y) + Assert.assertEquals(sample1channel2, magData.magSamples[1].z) + Assert.assertEquals(sample1status, magData.magSamples[1].calibrationStatus) + } + + @Test + fun `process magnetometer data type 1`() { + // Arrange + // HEX: 37 FF 51 FD 6C F6 00 00 03 01 F8 02 + // index type data + // 0..1 Sample 0 - channel 0 (ref. sample) 37 FF (0xFF37 = -201) + // 2..3 Sample 0 - channel 1 (ref. sample) 51 FD (0xFD51 = -687) + // 4..5 Sample 0 - channel 2 (ref. sample) 6C F6 (0xF66C = -2452) + // 6..7 Status (ref. sample) 00 00 (0x0000 = 0) + // 8 Delta size 03 (3 bit) + // 9 Sample amount 01 (1 samples) + // 10.. Delta data F8 (binary: 11 111 000) 02 (binary: 0000 0010) + // Delta channel 0 000b + // Delta channel 1 111b + // Delta channel 2 011b + // Delta status 001b + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 1632803681L + val measurementFrame = byteArrayOf( + 0x37.toByte(), 0xFF.toByte(), 0x51.toByte(), 0xFD.toByte(), 0x6C.toByte(), 0xF6.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x03.toByte(), 0x01.toByte(), 0xF8.toByte(), 0x02.toByte() + ) + + val sample0channel0 = -201.0f / 1000 + val sample0channel1 = -687.0f / 1000 + val sample0channel2 = -2452.0f / 1000 + val sample0status = MagData.CalibrationStatus.getById(0x00) + + val sample1channel0 = (-201.0f + 0x00) / 1000 + val sample1channel1 = (-687.0f - 0x01) / 1000 + val sample1channel2 = (-2452.0f + 0x3) / 1000 + val sample1status = MagData.CalibrationStatus.getById(0x00 + 0x01) + + val frameType = BlePMDClient.PmdDataFrameType.TYPE_1 + val factor = 1.0f + + // Act + val magData = MagData.parseDataFromDataFrame(isCompressed = true, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + + Assert.assertEquals(expectedTimeStamp, magData.timeStamp) + Assert.assertEquals(expectedSamplesSize, magData.magSamples.size) + + Assert.assertEquals(sample0channel0, magData.magSamples[0].x, 0.00001f) + Assert.assertEquals(sample0channel1, magData.magSamples[0].y, 0.00001f) + Assert.assertEquals(sample0channel2, magData.magSamples[0].z, 0.00001f) + Assert.assertEquals(sample0status, magData.magSamples[0].calibrationStatus) + + Assert.assertEquals(sample1channel0, magData.magSamples[1].x, 0.00001f) + Assert.assertEquals(sample1channel1, magData.magSamples[1].y, 0.00001f) + Assert.assertEquals(sample1channel2, magData.magSamples[1].z, 0.00001f) + Assert.assertEquals(sample1status, magData.magSamples[1].calibrationStatus) + } + + @Test + fun `process magnetometer data type 1 with factor`() { + // Arrange + // HEX: 37 FF 51 FD 6C F6 00 00 03 01 F8 02 + // index type data + // 0..1 Sample 0 - channel 0 (ref. sample) 37 FF (0xFF37 = -201) + // 2..3 Sample 0 - channel 1 (ref. sample) 51 FD (0xFD51 = -687) + // 4..5 Sample 0 - channel 2 (ref. sample) 6C F6 (0xF66C = -2452) + // 6..7 Status (ref. sample) 00 00 (0x0000 = 0) + // 8 Delta size 03 (3 bit) + // 9 Sample amount 01 (1 samples) + // 10.. Delta data F8 (binary: 11 111 000) 02 (binary: 0000 0010) + // Delta channel 0 000b + // Delta channel 1 111b + // Delta channel 2 011b + // Delta status 001b + val expectedSamplesSize = 1 + 1 // reference sample + delta samples + val expectedTimeStamp = 1632803681L + val measurementFrame = byteArrayOf( + 0x37.toByte(), 0xFF.toByte(), 0x51.toByte(), 0xFD.toByte(), 0x6C.toByte(), 0xF6.toByte(), + 0x00.toByte(), 0x00.toByte(), 0x03.toByte(), 0x01.toByte(), 0xF8.toByte(), 0x02.toByte() + ) + + val sample0channel0 = -201.0f / 1000 + val sample0channel1 = -687.0f / 1000 + val sample0channel2 = -2452.0f / 1000 + val sample0status = MagData.CalibrationStatus.getById(0x00) + + val sample1channel0 = (-201.0f + 0x00) / 1000 + val sample1channel1 = (-687.0f - 0x01) / 1000 + val sample1channel2 = (-2452.0f + 0x3) / 1000 + val sample1status = MagData.CalibrationStatus.getById(0x00 + 0x01) + + val frameType = BlePMDClient.PmdDataFrameType.TYPE_1 + val factor = 1.1f + + // Act + val magData = MagData.parseDataFromDataFrame(isCompressed = true, frameType = frameType, frame = measurementFrame, factor = factor, timeStamp = expectedTimeStamp) + + // Assert + + Assert.assertEquals(expectedTimeStamp, magData.timeStamp) + Assert.assertEquals(expectedSamplesSize, magData.magSamples.size) + + Assert.assertEquals(factor * sample0channel0, magData.magSamples[0].x, 0.00001f) + Assert.assertEquals(factor * sample0channel1, magData.magSamples[0].y, 0.00001f) + Assert.assertEquals(factor * sample0channel2, magData.magSamples[0].z, 0.00001f) + Assert.assertEquals(sample0status, magData.magSamples[0].calibrationStatus) + + Assert.assertEquals(factor * sample1channel0, magData.magSamples[1].x, 0.00001f) + Assert.assertEquals(factor * sample1channel1, magData.magSamples[1].y, 0.00001f) + Assert.assertEquals(factor * sample1channel2, magData.magSamples[1].z, 0.00001f) + Assert.assertEquals(sample1status, magData.magSamples[1].calibrationStatus) + } +} \ No newline at end of file diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/common/ble/TypeUtilsTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/common/ble/TypeUtilsTest.kt new file mode 100644 index 00000000..e5abaeac --- /dev/null +++ b/sources/Android/android-communications/library/src/test/java/com/polar/androidcommunications/common/ble/TypeUtilsTest.kt @@ -0,0 +1,147 @@ +package com.polar.androidcommunications.common.ble + +import org.junit.Assert +import org.junit.Assert.assertThrows +import org.junit.Test + +class TypeUtilsTest { + + @Test + fun `test array conversion to unsigned byte max value`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte()) + val expectedValue = 0xFFu.toUByte() + + // Act + val result = TypeUtils.convertArrayToUnsignedByte(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned int max value`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) + val expectedValue = 0xFFFFFFFFu + + // Act + val result = TypeUtils.convertArrayToUnsignedInt(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned int min value`() { + // Arrange + val byteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00) + val expectedValue = 0x00000000u + + // Act + val result = TypeUtils.convertArrayToUnsignedInt(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned int max positive int`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte()) + val expectedValue = 0x7FFFFFFFu + + // Act + val result = TypeUtils.convertArrayToUnsignedInt(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned int small array`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte()) + val expectedValue = 0xFFFFu + + // Act + val result = TypeUtils.convertArrayToUnsignedInt(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned int too big array`() { + // Arrange + val byteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00) + + // Act & Assert + assertThrows(AssertionError::class.java) { + TypeUtils.convertArrayToUnsignedInt(byteArray) + } + } + + @Test + fun `test array conversion to unsigned long max value`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()) + val expectedValue = 0xFFFFFFFFFFFFFFFFu + + // Act + val result = TypeUtils.convertArrayToUnsignedLong(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned long min value`() { + // Arrange + val byteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + val expectedValue = 0x0000000000000000u.toULong() + + // Act + val result = TypeUtils.convertArrayToUnsignedLong(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned long max positive int`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte()) + val expectedValue = 0x7FFFFFFFFFFFFFFFu + + // Act + val result = TypeUtils.convertArrayToUnsignedLong(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned long small array`() { + // Arrange + val byteArray = byteArrayOf(0xFF.toByte(), 0xFF.toByte()) + val expectedValue = 0xFFFFu.toULong() + + // Act + val result = TypeUtils.convertArrayToUnsignedLong(byteArray) + + // Assert + Assert.assertEquals(expectedValue, result) + } + + @Test + fun `test array conversion to unsigned long too big array`() { + // Arrange + val byteArray = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + + // Act & Assert + assertThrows(AssertionError::class.java) { + TypeUtils.convertArrayToUnsignedLong(byteArray) + } + } +} \ No newline at end of file